Migrate to TypeScript

This commit is contained in:
Giuseppe Criscione 2024-02-24 17:29:32 +01:00
parent a437035a53
commit 8355115c0b
52 changed files with 1320 additions and 901 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,11 @@
import eslintConfigPrettier from "eslint-config-prettier";
import globals from "globals";
import js from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
languageOptions: {
ecmaVersion: 13,
@ -41,6 +43,13 @@ export default [
allowSeparatedGroups: true,
},
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/typedef": [
"warn",
{
parameter: true,
},
],
},
},
eslintConfigPrettier,

View File

@ -14,12 +14,12 @@
"scripts": {
"build": "yarn build:css && yarn build:js",
"build:css": "sass ./src/scss/panel.scss:./assets/css/panel.min.css ./src/scss/panel-dark.scss:./assets/css/panel-dark.min.css --style=compressed --no-source-map",
"build:js": "esbuild ./src/js/app.js --outfile=./assets/js/app.min.js --bundle --format=iife --global-name=Formwork --target=es6 --minify",
"build:js": "tsc && esbuild ./src/ts/app.ts --outfile=./assets/js/app.min.js --bundle --format=iife --global-name=Formwork --target=es6 --minify",
"watch:css": "yarn build:css --watch",
"watch:js": "yarn build:js --watch",
"lint": "yarn lint:css && yarn lint:js",
"lint": "yarn lint:css && yarn lint:ts",
"lint:css": "prettier './src/scss/**/*.scss' --write && stylelint './src/scss/**/*.scss' --fix",
"lint:js": "prettier './src/js/**/*.js' --write && eslint './src/js/**/*.js' --fix"
"lint:ts": "prettier './src/ts/**/*.ts' --write && eslint './src/ts/**/*.ts' --fix"
},
"dependencies": {
"chartist": "^1.3.0",
@ -28,6 +28,8 @@
},
"devDependencies": {
"@eslint/js": "^8.56.0",
"@types/codemirror": "^5.60.15",
"@types/sortablejs": "^1.15.8",
"esbuild": "^0.20.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
@ -38,7 +40,9 @@
"stylelint": "^15.11.0",
"stylelint-config-standard-scss": "^11.1.0",
"stylelint-order": "^6.0.4",
"stylelint-scss": "^5.3.1"
"stylelint-scss": "^5.3.1",
"typescript": "^5.3.3",
"typescript-eslint": "^7.0.2"
},
"packageManager": "yarn@4.0.2"
}

View File

@ -1,24 +0,0 @@
import { $, $$ } from "../utils/selectors";
export class Files {
constructor() {
$$(".files-list").forEach((filesList) => {
const toggle = $(".form-togglegroup", filesList);
const viewAs = window.localStorage.getItem("formwork.filesListViewAs");
if (viewAs) {
$$("input", toggle).forEach((input) => (input.checked = false));
$(`input[value=${viewAs}]`, filesList).checked = true;
filesList.classList.toggle("is-thumbnails", viewAs === "thumbnails");
}
$$("input", toggle).forEach((input) => {
input.addEventListener("input", () => {
filesList.classList.toggle("is-thumbnails", input.value === "thumbnails");
window.localStorage.setItem("formwork.filesListViewAs", input.value);
});
});
});
}
}

View File

@ -1,8 +0,0 @@
import { $$ } from "../utils/selectors";
import { Form } from "./form";
export class Forms {
constructor() {
$$("[data-form]").forEach((element) => (this[element.dataset.form] = new Form(element)));
}
}

View File

@ -1,58 +0,0 @@
import { $, $$ } from "../utils/selectors";
import { app } from "../app";
import { ArrayInput } from "./inputs/array-input";
import { DateInput } from "./inputs/date-input";
import { DurationInput } from "./inputs/duration-input";
import { EditorInput } from "./inputs/editor-input";
import { FileInput } from "./inputs/file-input";
import { ImageInput } from "./inputs/image-input";
import { ImagePicker } from "./inputs/image-picker";
import { RangeInput } from "./inputs/range-input";
import { SelectInput } from "./inputs/select-input";
import { TagInput } from "./inputs/tag-input";
export class Inputs {
constructor(parent) {
$$(".form-input-date", parent).forEach((element) => (this[element.name] = new DateInput(element, app.config.DateInput)));
$$(".form-input-image", parent).forEach((element) => (this[element.name] = new ImageInput(element)));
$$(".image-picker", parent).forEach((element) => (this[element.name] = new ImagePicker(element)));
$$(".editor-textarea", parent).forEach((element) => (this[element.name] = new EditorInput(element)));
$$("input[type=file]", parent).forEach((element) => (this[element.name] = new FileInput(element)));
$$("input[data-field=tags]", parent).forEach((element) => (this[element.name] = new TagInput(element)));
$$("input[data-field=duration]", parent).forEach((element) => (this[element.name] = new DurationInput(element, app.config.DurationInput)));
$$("input[type=range]", parent).forEach((element) => (this[element.name] = new RangeInput(element)));
$$(".form-input-array", parent).forEach((element) => (this[element.name] = new ArrayInput(element)));
$$("select:not([hidden])", parent).forEach((element) => (this[element.name] = new SelectInput(element, app.config.SelectInput)));
$$(".form-input-reset", parent).forEach((element) => {
element.addEventListener("click", () => {
const target = document.getElementById(element.dataset.reset);
target.value = "";
target.dispatchEvent(new Event("change"));
});
});
$$("input[data-enable]", parent).forEach((element) => {
element.addEventListener("change", () => {
const inputs = element.dataset.enable.split(",");
for (const name of inputs) {
const input = $(`input[name="${name}"]`);
if (!element.checked) {
input.disabled = true;
} else {
input.disabled = false;
}
}
});
});
}
}

View File

@ -1,58 +0,0 @@
import { $, $$ } from "../../utils/selectors";
export class ImagePicker {
constructor(element) {
const options = $$("option", element);
const confirmCommand = $(".image-picker-confirm", element.parentNode.parentNode);
const uploadCommand = $("[data-command=upload]", element.parentNode.parentNode);
element.hidden = true;
if (options.length > 0) {
const container = document.createElement("div");
container.className = "image-picker-thumbnails";
for (const option of options) {
const thumbnail = document.createElement("div");
thumbnail.className = "image-picker-thumbnail";
thumbnail.style.backgroundImage = `url(${option.value})`;
thumbnail.dataset.uri = option.value;
thumbnail.dataset.filename = option.text;
thumbnail.addEventListener("click", handleThumbnailClick);
thumbnail.addEventListener("dblclick", handleThumbnailDblclick);
container.appendChild(thumbnail);
}
element.parentNode.insertBefore(container, element);
$(".image-picker-empty-state").style.display = "none";
}
confirmCommand.addEventListener("click", function () {
const selectedThumbnail = $(".image-picker-thumbnail.selected");
const target = document.getElementById(this.dataset.target);
if (selectedThumbnail && target) {
target.value = selectedThumbnail.dataset.filename;
}
});
uploadCommand.addEventListener("click", function () {
document.getElementById(this.dataset.uploadTarget).click();
});
function handleThumbnailClick() {
const target = document.getElementById($(".image-picker-confirm").dataset.target);
if (target) {
target.value = this.dataset.filename;
}
$$(".image-picker-thumbnail").forEach((element) => {
element.classList.remove("selected");
});
this.classList.add("selected");
}
function handleThumbnailDblclick() {
this.click();
$(".image-picker-confirm").click();
}
}
}

View File

@ -1,59 +0,0 @@
import { LineChart } from "chartist";
import { passIcon } from "./icons";
import { Tooltip } from "./tooltip";
export class StatisticsChart {
constructor(element, data) {
const spacing = 100;
const options = {
showArea: true,
fullWidth: true,
scaleMinSpace: 20,
divisor: 5,
chartPadding: 20,
lineSmooth: false,
low: 0,
axisX: {
showGrid: false,
labelOffset: {
x: 0,
y: 10,
},
labelInterpolationFnc: (value, index, labels) => (index % Math.floor(labels.length / (element.clientWidth / spacing)) ? null : value),
},
axisY: {
onlyInteger: true,
offset: 15,
labelOffset: {
x: 0,
y: 5,
},
},
};
const chart = new LineChart(element, data, options);
chart.on("draw", (event) => {
if (event.type === "point") {
event.element.attr({ "ct:index": event.index });
}
});
chart.container.addEventListener("mouseover", (event) => {
if (event.target.getAttribute("class") === "ct-point") {
const strokeWidth = parseFloat(getComputedStyle(event.target)["stroke-width"]);
const index = event.target.getAttribute("ct:index");
passIcon("circle-small-fill", (icon) => {
const text = `${data.labels[index]}<br><span class="text-color-blue">${icon}</span> ${data.series[0][index]} <span class="text-color-amber ml-2">${icon}</span>${data.series[1][index]}`;
const tooltip = new Tooltip(text, {
referenceElement: event.target,
offset: { x: 0, y: -strokeWidth },
});
tooltip.show();
});
}
});
}
}

View File

@ -1,7 +0,0 @@
export function $(selector, parent = document) {
return parent.querySelector(selector);
}
export function $$(selector, parent = document) {
return parent.querySelectorAll(selector);
}

View File

@ -14,14 +14,34 @@ import { Pages } from "./components/views/pages";
import { Statistics } from "./components/views/statistics";
import { Updates } from "./components/views/updates";
interface AppConfig {
baseUri: string;
DateInput?: any;
DurationInput?: any;
SelectInput?: any;
Backups?: any;
}
interface Component {
new (app: App): void;
}
interface ComponentConfig {
globalAlias?: string;
}
class App {
config = {};
config: AppConfig = {
baseUri: "/",
};
modals = {};
modals: Modals = {};
forms = {};
forms: Forms = {};
load(config) {
[alias: string]: any;
load(config: AppConfig) {
this.loadConfig(config);
this.loadComponent(Modals, {
@ -47,14 +67,14 @@ class App {
this.loadComponent(Updates);
}
loadConfig(config) {
loadConfig(config: AppConfig) {
Object.assign(this.config, config);
}
loadComponent(
component,
options = {
globalAlias: null,
component: Component,
options: ComponentConfig = {
globalAlias: undefined,
},
) {
const instance = new component(this);

View File

@ -7,7 +7,7 @@ export class ColorScheme {
const cookies = getCookies();
const cookieName = "formwork_preferred_color_scheme";
const oldValue = cookieName in cookies ? cookies[cookieName] : null;
let value = null;
let value: "light" | "dark" = "light";
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
value = "light";
@ -15,7 +15,7 @@ export class ColorScheme {
value = "dark";
}
if (value !== oldValue) {
if (value && value !== oldValue) {
setCookie(cookieName, value, {
"max-age": 2592000, // 1 month
path: app.config.baseUri,

View File

@ -8,11 +8,11 @@ export class Dropdowns {
document.addEventListener("click", (event) => {
$$(".dropdown-menu").forEach((element) => (element.style.display = ""));
const button = event.target.closest(".dropdown-button");
const button = (event.target as HTMLDivElement).closest(".dropdown-button") as HTMLButtonElement;
if (button) {
const dropdown = document.getElementById(button.dataset.dropdown);
const isVisible = getComputedStyle(dropdown).display !== "none";
const dropdown = document.getElementById(button.dataset.dropdown as string) as HTMLElement;
const isVisible = getComputedStyle(dropdown as HTMLElement).display !== "none";
event.preventDefault();
const resizeHandler = throttle(() => setDropdownPosition(dropdown), 100);
@ -30,8 +30,8 @@ export class Dropdowns {
}
}
function setDropdownPosition(dropdown) {
dropdown.style.left = 0;
function setDropdownPosition(dropdown: HTMLElement) {
dropdown.style.left = "0";
dropdown.style.right = "";
const dropdownRect = dropdown.getBoundingClientRect();
@ -45,7 +45,7 @@ function setDropdownPosition(dropdown) {
if (dropdownLeft + dropdownWidth > windowWidth) {
dropdown.style.left = "auto";
dropdown.style.right = 0;
dropdown.style.right = "0";
}
if (dropdownTop < window.scrollY || window.scrollY < dropdownTop + dropdownHeight - windowHeight) {

View File

@ -0,0 +1,26 @@
import { $, $$ } from "../utils/selectors";
export class Files {
constructor() {
$$(".files-list").forEach((filesList) => {
const toggle = $(".form-togglegroup", filesList);
if (toggle) {
const viewAs = window.localStorage.getItem("formwork.filesListViewAs");
if (viewAs) {
$$("input", toggle).forEach((input: HTMLInputElement) => (input.checked = false));
($(`input[value=${viewAs}]`, filesList) as HTMLInputElement).checked = true;
filesList.classList.toggle("is-thumbnails", viewAs === "thumbnails");
}
$$("input", toggle).forEach((input: HTMLInputElement) => {
input.addEventListener("input", () => {
filesList.classList.toggle("is-thumbnails", input.value === "thumbnails");
window.localStorage.setItem("formwork.filesListViewAs", input.value);
});
});
}
});
}
}

View File

@ -5,7 +5,10 @@ import { Inputs } from "./inputs";
import { serializeForm } from "../utils/forms";
export class Form {
constructor(form) {
inputs: Inputs;
originalData: string;
constructor(form: HTMLFormElement) {
this.inputs = new Inputs(form);
// Serialize after inputs are loaded
@ -16,11 +19,11 @@ export class Form {
form.addEventListener("submit", removeBeforeUnload);
const hasChanged = (checkFileInputs = true) => {
const fileInputs = $$("input[type=file]", form);
const fileInputs = $$("input[type=file]", form) as NodeListOf<HTMLInputElement>;
if (checkFileInputs === true && fileInputs.length > 0) {
for (const fileInput of fileInputs) {
if (fileInput.files.length > 0) {
for (const fileInput of Array.from(fileInputs)) {
if (fileInput.files && fileInput.files.length > 0) {
return true;
}
}
@ -29,12 +32,15 @@ export class Form {
return serializeForm(form) !== this.originalData;
};
$$('a[href]:not([href^="#"]):not([target="_blank"]):not([target^="formwork-"])').forEach((element) => {
$$('a[href]:not([href^="#"]):not([target="_blank"]):not([target^="formwork-"])').forEach((element: HTMLAnchorElement) => {
element.addEventListener("click", (event) => {
if (hasChanged()) {
event.preventDefault();
app.modals["changesModal"].show(null, (modal) => {
$("[data-command=continue]", modal.element).dataset.href = element.href;
app.modals["changesModal"].show(undefined, (modal) => {
const continueCommand = $("[data-command=continue]", modal.element);
if (continueCommand) {
continueCommand.dataset.href = element.href;
}
});
}
});
@ -50,10 +56,10 @@ export class Form {
registerModalExceptions();
function handleBeforeunload(event) {
function handleBeforeunload(event: Event) {
if (hasChanged()) {
event.preventDefault();
event.returnValue = "";
event.returnValue = false;
}
}
@ -67,18 +73,29 @@ export class Form {
const deleteUserModal = document.getElementById("deleteUserModal");
if (changesModal) {
$("[data-command=continue]", changesModal).addEventListener("click", function () {
removeBeforeUnload();
window.location.href = this.dataset.href;
});
const continueCommand = $("[data-command=continue]", changesModal);
if (continueCommand) {
continueCommand.addEventListener("click", function () {
removeBeforeUnload();
if (this.dataset.href) {
window.location.href = this.dataset.href;
}
});
}
}
if (deletePageModal) {
$("[data-command=delete]", deletePageModal).addEventListener("click", removeBeforeUnload);
const deleteCommand = $("[data-command=delete]", deletePageModal);
if (deleteCommand) {
deleteCommand.addEventListener("click", removeBeforeUnload);
}
}
if (deleteUserModal) {
$("[data-command=delete]", deleteUserModal).addEventListener("click", removeBeforeUnload);
const deleteCommand = $("[data-command=delete]", deleteUserModal);
if (deleteCommand) {
deleteCommand.addEventListener("click", removeBeforeUnload);
}
}
}
}

View File

@ -0,0 +1,14 @@
import { $$ } from "../utils/selectors";
import { Form } from "./form";
export class Forms {
[name: string]: Form;
constructor() {
$$("[data-form]").forEach((element: HTMLFormElement) => {
if (element.dataset.form) {
this[element.dataset.form] = new Form(element);
}
});
}
}

View File

@ -2,7 +2,7 @@ import { app } from "../app";
const cache = new Map();
export function passIcon(icon, callback) {
export function passIcon(icon: string, callback: (iconData: string) => void) {
if (cache.has(icon)) {
callback(cache.get(icon));
return;
@ -22,6 +22,6 @@ export function passIcon(icon, callback) {
request.send();
}
export function insertIcon(icon, element, position = "afterBegin") {
export function insertIcon(icon: string, element: HTMLElement, position: InsertPosition = "afterbegin") {
passIcon(icon, (data) => element.insertAdjacentHTML(position, data));
}

View File

@ -0,0 +1,66 @@
import { $, $$ } from "../utils/selectors";
import { app } from "../app";
import { ArrayInput } from "./inputs/array-input";
import { DateInput } from "./inputs/date-input";
import { DurationInput } from "./inputs/duration-input";
import { EditorInput } from "./inputs/editor-input";
import { FileInput } from "./inputs/file-input";
import { ImageInput } from "./inputs/image-input";
import { ImagePicker } from "./inputs/image-picker";
import { RangeInput } from "./inputs/range-input";
import { SelectInput } from "./inputs/select-input";
import { TagInput } from "./inputs/tag-input";
export class Inputs {
[name: string]: object;
constructor(parent: HTMLElement) {
$$(".form-input-date", parent).forEach((element: HTMLInputElement) => (this[element.name] = new DateInput(element, app.config.DateInput)));
$$(".form-input-image", parent).forEach((element: HTMLInputElement) => (this[element.name] = new ImageInput(element)));
$$(".image-picker", parent).forEach((element: HTMLSelectElement) => (this[element.name] = new ImagePicker(element)));
$$(".editor-textarea", parent).forEach((element: HTMLTextAreaElement) => (this[element.name] = new EditorInput(element)));
$$("input[type=file]", parent).forEach((element: HTMLInputElement) => (this[element.name] = new FileInput(element)));
$$("input[data-field=tags]", parent).forEach((element: HTMLInputElement) => (this[element.name] = new TagInput(element)));
$$("input[data-field=duration]", parent).forEach((element: HTMLInputElement) => (this[element.name] = new DurationInput(element, app.config.DurationInput)));
$$("input[type=range]", parent).forEach((element: HTMLInputElement) => (this[element.name] = new RangeInput(element)));
$$(".form-input-array", parent).forEach((element: HTMLInputElement) => (this[element.name] = new ArrayInput(element)));
$$("select:not([hidden])", parent).forEach((element: HTMLSelectElement) => (this[element.name] = new SelectInput(element, app.config.SelectInput)));
$$(".form-input-reset", parent).forEach((element) => {
const targetId = element.dataset.reset;
if (targetId) {
element.addEventListener("click", () => {
const target = document.getElementById(targetId) as HTMLInputElement;
target.value = "";
target.dispatchEvent(new Event("change"));
});
}
});
$$("input[data-enable]", parent).forEach((element: HTMLInputElement) => {
element.addEventListener("change", () => {
const targetId = element.dataset.enable;
if (targetId) {
const inputs = targetId.split(",");
for (const name of inputs) {
const input = $(`input[name="${name}"]`) as HTMLInputElement;
if (!element.checked) {
input.disabled = true;
} else {
input.disabled = false;
}
}
}
});
});
}
}

View File

@ -2,7 +2,7 @@ import { $, $$ } from "../../utils/selectors";
import Sortable from "sortablejs";
export class ArrayInput {
constructor(input) {
constructor(input: HTMLInputElement) {
const isAssociative = input.classList.contains("form-input-array-associative");
const inputName = input.dataset.name;
@ -13,53 +13,55 @@ export class ArrayInput {
forceFallback: true,
});
function addRow(row) {
const clone = row.cloneNode(true);
function addRow(row: HTMLElement) {
const clone = row.cloneNode(true) as HTMLElement;
const parent = row.parentNode as ParentNode;
clearRow(clone);
bindRowEvents(clone);
if (row.nextSibling) {
row.parentNode.insertBefore(clone, row.nextSibling);
parent.insertBefore(clone, row.nextSibling);
} else {
row.parentNode.appendChild(clone);
parent.appendChild(clone);
}
}
function removeRow(row) {
if ($$(".form-input-array-row", row.parentNode).length > 1) {
row.parentNode.removeChild(row);
function removeRow(row: HTMLElement) {
const parent = row.parentNode as ParentNode;
if ($$(".form-input-array-row", parent).length > 1) {
parent.removeChild(row);
} else {
clearRow(row);
}
}
function clearRow(row) {
function clearRow(row: HTMLElement) {
if (isAssociative) {
const inputKey = $(".form-input-array-key", row);
const inputKey = $(".form-input-array-key", row) as HTMLInputElement;
inputKey.value = "";
inputKey.removeAttribute("value");
}
const inputValue = $(".form-input-array-value", row);
const inputValue = $(".form-input-array-value", row) as HTMLInputElement;
inputValue.value = "";
inputValue.removeAttribute("value");
inputValue.name = `${inputName}[]`;
}
function updateAssociativeRow(row) {
const inputKey = $(".form-input-array-key", row);
const inputValue = $(".form-input-array-value", row);
function updateAssociativeRow(row: HTMLElement) {
const inputKey = $(".form-input-array-key", row) as HTMLInputElement;
const inputValue = $(".form-input-array-value", row) as HTMLInputElement;
inputValue.name = `${inputName}[${inputKey.value.trim()}]`;
}
function bindRowEvents(row) {
const inputAdd = $(".form-input-array-add", row);
const inputRemove = $(".form-input-array-remove", row);
function bindRowEvents(row: HTMLElement) {
const inputAdd = $(".form-input-array-add", row) as HTMLButtonElement;
const inputRemove = $(".form-input-array-remove", row) as HTMLButtonElement;
inputAdd.addEventListener("click", addRow.bind(inputAdd, row));
inputRemove.addEventListener("click", removeRow.bind(inputRemove, row));
if (isAssociative) {
const inputKey = $(".form-input-array-key", row);
const inputValue = $(".form-input-array-value", row);
const inputKey = $(".form-input-array-key", row) as HTMLInputElement;
const inputValue = $(".form-input-array-value", row) as HTMLInputElement;
inputKey.addEventListener("keyup", updateAssociativeRow.bind(inputKey, row));
inputValue.addEventListener("keyup", updateAssociativeRow.bind(inputValue, row));
}

View File

@ -3,14 +3,17 @@ import { getOuterHeight, getOuterWidth } from "../../utils/dimensions";
import { insertIcon } from "../icons";
import { throttle } from "../../utils/events";
const inputValues = {};
const inputValues: {
[id: string]: Date;
} = {};
function handleLongClick(element, callback, timeout, interval) {
let timer;
function handleLongClick(element: HTMLElement, callback: (event: MouseEvent) => void, timeout: number, interval: number) {
let timer: number;
function clear() {
clearTimeout(timer);
}
element.addEventListener("mousedown", function (event) {
element.addEventListener("mousedown", function (event: MouseEvent) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this;
if (event.button !== 0) {
clear();
@ -23,8 +26,26 @@ function handleLongClick(element, callback, timeout, interval) {
window.addEventListener("mouseup", clear);
}
interface DateInputOptions {
weekStarts: number;
format: string;
time: boolean;
labels: {
today: string;
weekdays: {
long: string[];
short: string[];
};
months: {
long: string[];
short: string[];
};
};
onChange: (date: Date) => void;
}
export class DateInput {
constructor(input, options) {
constructor(input: HTMLInputElement, userOptions: Partial<DateInputOptions> = {}) {
const defaults = {
weekStarts: 0,
format: "YYYY-MM-DD",
@ -40,21 +61,20 @@ export class DateInput {
short: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
},
},
};
onChange(date: Date) {
const dateInput = getCurrentInput();
if (dateInput !== null) {
inputValues[dateInput.id] = date;
dateInput.value = formatDateTime(date);
}
},
} satisfies DateInputOptions;
options = Object.assign({}, defaults, options);
const options = Object.assign({}, defaults, userOptions);
inputValues[input.id] = new Date();
const calendar = new Calendar($(".calendar"), inputValues[input.id]);
options.onChange = (date) => {
const dateInput = getCurrentInput();
if (dateInput !== null) {
inputValues[dateInput.id] = date;
dateInput.value = formatDateTime(date);
}
};
const calendar = Calendar($(".calendar") as HTMLElement, inputValues[input.id]);
initInput();
@ -78,7 +98,7 @@ export class DateInput {
calendar.hide();
});
input.addEventListener("keydown", (event) => {
input.addEventListener("keydown", (event: KeyboardEvent) => {
switch (event.key) {
case "Backspace":
input.value = "";
@ -96,18 +116,18 @@ export class DateInput {
}
function getCurrentInput() {
const currentElement = document.activeElement;
const currentElement = document.activeElement as HTMLInputElement;
return currentElement.matches(".form-input-date") ? currentElement : null;
}
function Calendar(element, date) {
let year, month, day, hours, minutes, seconds;
function Calendar(element: HTMLElement, date: Date) {
let year: number, month: number, day: number, hours: number, minutes: number, seconds: number;
element = element || createElement();
setDate(date);
function setDate(date) {
function setDate(date: Date) {
year = date.getFullYear();
month = date.getMonth();
day = date.getDate();
@ -116,7 +136,7 @@ export class DateInput {
seconds = date.getSeconds();
}
function gotoDate(date) {
function gotoDate(date: Date) {
setDate(date);
update();
}
@ -333,7 +353,7 @@ export class DateInput {
}
function update() {
$(".calendar-table", element).innerHTML = getInnerHTML();
($(".calendar-table", element) as HTMLElement).innerHTML = getInnerHTML();
setEvents();
@ -394,7 +414,7 @@ export class DateInput {
event.preventDefault();
});
element.addEventListener("click", () => {
day = parseInt(element.textContent);
day = parseInt(`${element.textContent}`);
update();
options.onChange(getDate());
});
@ -402,9 +422,9 @@ export class DateInput {
}
function updateTime() {
$(".calendar-hours", element).innerHTML = pad(has12HourFormat(options.format) ? mod(hours, 12) || 12 : hours, 2);
$(".calendar-minutes", element).innerHTML = pad(minutes, 2);
$(".calendar-meridiem", element).innerHTML = has12HourFormat(options.format) ? (hours < 12 ? "AM" : "PM") : "";
($(".calendar-hours", element) as HTMLElement).innerHTML = pad(has12HourFormat(options.format) ? mod(hours, 12) || 12 : hours, 2);
($(".calendar-minutes", element) as HTMLElement).innerHTML = pad(minutes, 2);
($(".calendar-meridiem", element) as HTMLElement).innerHTML = has12HourFormat(options.format) ? (hours < 12 ? "AM" : "PM") : "";
}
}
@ -416,26 +436,26 @@ export class DateInput {
if (options.time) {
element.innerHTML += '<div class="calendar-separator"></div><table class="calendar-time"><tr><td><button type="button" class="nextHour"></button></td><td></td><td><button type="button" class="nextMinute"></button></td></tr><tr><td class="calendar-hours"></td><td>:</td><td class="calendar-minutes"></td><td class="calendar-meridiem"></td></tr><tr><td><button type="button" class="prevHour"></button></td><td></td><td><button type="button" class="prevMinute"></button></td></tr></table></div>';
insertIcon("chevron-down", $(".prevHour", element));
insertIcon("chevron-up", $(".nextHour", element));
insertIcon("chevron-down", $(".prevHour", element) as HTMLElement);
insertIcon("chevron-up", $(".nextHour", element) as HTMLElement);
insertIcon("chevron-down", $(".prevMinute", element));
insertIcon("chevron-up", $(".nextMinute", element));
insertIcon("chevron-down", $(".prevMinute", element) as HTMLElement);
insertIcon("chevron-up", $(".nextMinute", element) as HTMLElement);
}
insertIcon("calendar-clock", $(".currentMonth", element));
insertIcon("calendar-clock", $(".currentMonth", element) as HTMLElement);
insertIcon("chevron-left", $(".prevMonth", element));
insertIcon("chevron-right", $(".nextMonth", element));
insertIcon("chevron-left", $(".prevMonth", element) as HTMLElement);
insertIcon("chevron-right", $(".nextMonth", element) as HTMLElement);
$(".currentMonth", element).addEventListener("mousedown", (event) => {
($(".currentMonth", element) as HTMLElement).addEventListener("mousedown", (event) => {
now();
options.onChange(getDate());
event.preventDefault();
});
handleLongClick(
$(".prevMonth", element),
$(".prevMonth", element) as HTMLElement,
(event) => {
prevMonth();
options.onChange(getDate());
@ -446,7 +466,7 @@ export class DateInput {
);
handleLongClick(
$(".nextMonth", element),
$(".nextMonth", element) as HTMLElement,
(event) => {
nextMonth();
options.onChange(getDate());
@ -458,7 +478,7 @@ export class DateInput {
if (options.time) {
handleLongClick(
$(".nextHour", element),
$(".nextHour", element) as HTMLElement,
(event) => {
nextHour();
options.onChange(getDate());
@ -469,7 +489,7 @@ export class DateInput {
);
handleLongClick(
$(".prevHour", element),
$(".prevHour", element) as HTMLElement,
(event) => {
prevHour();
options.onChange(getDate());
@ -480,7 +500,7 @@ export class DateInput {
);
handleLongClick(
$(".nextMinute", element),
$(".nextMinute", element) as HTMLElement,
(event) => {
nextMinute();
options.onChange(getDate());
@ -491,7 +511,7 @@ export class DateInput {
);
handleLongClick(
$(".prevMinute", element),
$(".prevMinute", element) as HTMLElement,
(event) => {
prevMinute();
options.onChange(getDate());
@ -506,7 +526,7 @@ export class DateInput {
window.addEventListener("mousedown", (event) => {
if (element.style.display !== "none") {
if (event.target.closest(".calendar")) {
if ((event.target as HTMLElement).closest(".calendar")) {
event.preventDefault();
}
}
@ -518,7 +538,7 @@ export class DateInput {
}
switch (event.key) {
case "Enter":
$(".calendar-day.selected", element).click();
($(".calendar-day.selected", element) as HTMLElement).click();
hide();
break;
case "Backspace":
@ -636,12 +656,12 @@ export class DateInput {
}
}
function mod(x, y) {
function mod(x: number, y: number) {
// Return x mod y (always rounded downwards, differs from x % y which is the remainder)
return x - y * Math.floor(x / y);
}
function pad(num, length) {
function pad(num: number, length: number) {
let result = num.toString();
while (result.length < length) {
result = `0${result}`;
@ -649,26 +669,26 @@ export class DateInput {
return result;
}
function isValidDate(date) {
function isValidDate(date: string) {
return date && !isNaN(Date.parse(date));
}
function isLeapYear(year) {
function isLeapYear(year: number) {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}
function daysInMonth(month, year) {
function daysInMonth(month: number, year: number) {
const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return month === 1 && isLeapYear(year) ? 29 : daysInMonth[month];
}
function weekStart(date, firstDay = options.weekStarts) {
function weekStart(date: Date, firstDay: number = options.weekStarts) {
let day = date.getDate();
day -= mod(date.getDay() - firstDay, 7);
return new Date(date.getFullYear(), date.getMonth(), day);
}
function weekNumberingYear(date) {
function weekNumberingYear(date: Date) {
const year = date.getFullYear();
const thisYearFirstWeekStart = weekStart(new Date(year, 0, 4), 1);
const nextYearFirstWeekStart = weekStart(new Date(year + 1, 0, 4), 1);
@ -680,22 +700,22 @@ export class DateInput {
return year - 1;
}
function weekOfYear(date) {
const weekNumberingYear = weekNumberingYear(date);
const firstWeekStart = weekStart(new Date(weekNumberingYear, 0, 4), 1);
const weekStart = weekStart(date, 1);
return Math.round((weekStart.getTime() - firstWeekStart.getTime()) / 604800000) + 1;
function weekOfYear(date: Date) {
const dateWeekNumberingYear = weekNumberingYear(date);
const dateFirstWeekStart = weekStart(new Date(dateWeekNumberingYear, 0, 4), 1);
const dateWeekStart = weekStart(date, 1);
return Math.round((dateWeekStart.getTime() - dateFirstWeekStart.getTime()) / 604800000) + 1;
}
function has12HourFormat(format) {
function has12HourFormat(format: string) {
const match = format.match(/\[([^\]]*)\]|H{1,2}/);
return match !== null && match[0][0] === "H";
}
function formatDateTime(date, format = options.format) {
function formatDateTime(date: Date, format: string = options.format) {
const regex = /\[([^\]]*)\]|[YR]{4}|uuu|[YR]{2}|[MD]{1,4}|[WHhms]{1,2}|[AaZz]/g;
function splitTimezoneOffset(offset) {
function splitTimezoneOffset(offset: number) {
// Note that the offset returned by Date.getTimezoneOffset()
// is positive if behind UTC and negative if ahead UTC
const sign = offset > 0 ? "-" : "+";
@ -704,7 +724,7 @@ export class DateInput {
return [sign + pad(hours, 2), pad(minutes, 2)];
}
return format.replace(regex, (match, $1) => {
return format.replace(regex, (match: string, $1) => {
switch (match) {
case "YY":
return date.getFullYear().toString().substr(-2);

View File

@ -1,6 +1,6 @@
import { $ } from "../../utils/selectors";
function getSafeInteger(value) {
function getSafeInteger(value: number) {
const max = Number.MAX_SAFE_INTEGER;
const min = -max;
if (value > max) {
@ -9,12 +9,31 @@ function getSafeInteger(value) {
if (value < min) {
return min;
}
return parseInt(value, 10) || 0;
return value;
}
const TIME_INTERVALS = {
years: 60 * 60 * 24 * 365,
months: 60 * 60 * 24 * 30,
weeks: 60 * 60 * 24 * 7,
days: 60 * 60 * 24,
hours: 60 * 60,
minutes: 60,
seconds: 1,
};
type TimeInterval = keyof typeof TIME_INTERVALS;
type TimeIntervalLabel = [singular: string, plural: string];
interface DurationInputOptions {
unit: TimeInterval;
intervals: TimeInterval[];
labels: Record<TimeInterval, TimeIntervalLabel>;
}
export class DurationInput {
constructor(input, options) {
const defaults = {
constructor(input: HTMLInputElement, userOptions: Partial<DurationInputOptions>) {
const defaults: DurationInputOptions = {
unit: "seconds",
intervals: ["years", "months", "weeks", "days", "hours", "minutes", "seconds"],
labels: {
@ -28,91 +47,81 @@ export class DurationInput {
},
};
const TIME_INTERVALS = {
years: 60 * 60 * 24 * 365,
months: 60 * 60 * 24 * 30,
weeks: 60 * 60 * 24 * 7,
days: 60 * 60 * 24,
hours: 60 * 60,
minutes: 60,
seconds: 1,
};
let field: HTMLElement, hiddenInput: HTMLInputElement;
let field, hiddenInput;
const innerInputs: Partial<Record<TimeInterval, HTMLInputElement>> = {};
const innerInputs = {};
const labels: Partial<Record<TimeInterval, HTMLLabelElement>> = {};
const labels = {};
options = Object.assign({}, defaults, options);
const options = Object.assign({}, defaults, userOptions);
createField();
function secondsToIntervals(seconds, intervalNames = options.intervals) {
const intervals = {};
function secondsToIntervals(seconds: number, intervalNames: TimeInterval[] = options.intervals) {
const intervals: Partial<Record<TimeInterval, number>> = {};
seconds = getSafeInteger(seconds);
for (const t in TIME_INTERVALS) {
Object.keys(TIME_INTERVALS).forEach((t: TimeInterval) => {
if (intervalNames.includes(t)) {
intervals[t] = Math.floor(seconds / TIME_INTERVALS[t]);
seconds -= intervals[t] * TIME_INTERVALS[t];
seconds -= (intervals[t] as number) * TIME_INTERVALS[t];
}
}
});
return intervals;
}
function intervalsToSeconds(intervals) {
function intervalsToSeconds(intervals: Partial<Record<TimeInterval, number>>) {
let seconds = 0;
for (const interval in intervals) {
seconds += intervals[interval] * TIME_INTERVALS[interval];
}
Object.entries(intervals).forEach(([interval, value]: [TimeInterval, number]) => {
seconds += value * TIME_INTERVALS[interval];
});
return getSafeInteger(seconds);
}
function updateHiddenInput() {
const intervals = {};
const intervals: Partial<Record<TimeInterval, number>> = {};
let seconds = 0;
let step = 0;
for (const i in innerInputs) {
intervals[i] = innerInputs[i].value;
}
Object.entries(innerInputs).forEach(([i, input]: [TimeInterval, HTMLInputElement]) => {
intervals[i] = parseInt(input.value);
});
seconds = intervalsToSeconds(intervals);
if (hiddenInput.step) {
step = hiddenInput.step * TIME_INTERVALS[options.unit];
step = parseInt(hiddenInput.step) * TIME_INTERVALS[options.unit];
seconds = Math.floor(seconds / step) * step;
}
if (hiddenInput.min) {
seconds = Math.max(seconds, hiddenInput.min);
seconds = Math.max(seconds, parseInt(hiddenInput.min));
}
if (hiddenInput.max) {
seconds = Math.min(seconds, hiddenInput.max);
seconds = Math.min(seconds, parseInt(hiddenInput.max));
}
hiddenInput.value = Math.round(seconds / TIME_INTERVALS[options.unit]);
hiddenInput.value = `${Math.round(seconds / TIME_INTERVALS[options.unit])}`;
}
function updateInnerInputs() {
const intervals = secondsToIntervals(hiddenInput.value * TIME_INTERVALS[options.unit]);
for (const i in innerInputs) {
innerInputs[i].value = intervals[i];
}
const intervals = secondsToIntervals(parseInt(hiddenInput.value) * TIME_INTERVALS[options.unit]);
Object.entries(innerInputs).forEach(([i, input]: [TimeInterval, HTMLInputElement]) => {
input.value = `${intervals[i] || 0}`;
});
}
function updateInnerInputsLength() {
for (const i in innerInputs) {
innerInputs[i].style.width = `${Math.max(3, innerInputs[i].value.length + 2)}ch`;
}
Object.values(innerInputs).forEach((input) => {
input.style.width = `${Math.max(3, input.value.length + 2)}ch`;
});
}
function updateLabels() {
for (const i in innerInputs) {
labels[i].innerHTML = options.labels[i][parseInt(innerInputs[i].value) === 1 ? 0 : 1];
}
Object.entries(innerInputs).forEach(([i, input]: [TimeInterval, HTMLInputElement]) => {
(labels[i] as HTMLLabelElement).innerHTML = options.labels[i][parseInt(input.value) === 1 ? 0 : 1];
});
}
function createInnerInputs(intervals, steps) {
function createInnerInputs(intervals: Partial<Record<TimeInterval, number>>, steps: Partial<Record<TimeInterval, number>>) {
field = document.createElement("div");
field.className = "form-input-duration";
let innerInput;
let innerInput: HTMLInputElement;
for (const name of options.intervals) {
innerInput = document.createElement("input");
@ -120,10 +129,10 @@ export class DurationInput {
const wrap = document.createElement("span");
wrap.className = `duration-${name}`;
innerInput.type = "number";
innerInput.value = intervals[name] || 0;
innerInput.value = `${intervals[name] || 0}`;
innerInput.style.width = `${Math.max(3, innerInput.value.length + 2)}ch`;
if (steps[name] > 1) {
innerInput.step = steps[name];
if ((steps[name] as number) > 1) {
innerInput.step = `${steps[name]}`;
}
if (input.disabled) {
innerInput.disabled = true;
@ -133,7 +142,7 @@ export class DurationInput {
while (this.value.charAt(0) === "0" && this.value.length > 1 && !this.value.charAt(1).match(/[.,]/)) {
this.value = this.value.slice(1);
}
while (this.value > Number.MAX_SAFE_INTEGER) {
while (parseInt(this.value) > Number.MAX_SAFE_INTEGER) {
this.value = this.value.slice(0, -1);
}
updateInnerInputsLength();
@ -151,7 +160,7 @@ export class DurationInput {
innerInput.addEventListener("blur", () => field.classList.remove("focused"));
wrap.addEventListener("mousedown", function (event) {
wrap.addEventListener("mousedown", function (event: MouseEvent) {
const input = $("input", this);
if (input && event.target !== input) {
input.focus();
@ -167,7 +176,7 @@ export class DurationInput {
field.appendChild(wrap);
}
field.addEventListener("mousedown", function (event) {
field.addEventListener("mousedown", function (event: MouseEvent) {
if (event.target === this) {
innerInput.focus();
event.preventDefault();
@ -202,15 +211,15 @@ export class DurationInput {
hiddenInput.disabled = true;
}
if ("intervals" in input.dataset) {
options.intervals = input.dataset.intervals.split(", ");
options.intervals = (input.dataset.intervals as string).split(", ") as TimeInterval[];
}
if ("unit" in input.dataset) {
options.unit = input.dataset.unit;
options.unit = input.dataset.unit as TimeInterval;
}
const valueSeconds = input.value * TIME_INTERVALS[options.unit];
const stepSeconds = input.step * TIME_INTERVALS[options.unit];
const valueSeconds = parseInt(input.value) * TIME_INTERVALS[options.unit];
const stepSeconds = parseInt(input.step) * TIME_INTERVALS[options.unit];
const field = createInnerInputs(secondsToIntervals(valueSeconds || 0), secondsToIntervals(stepSeconds || 1));
input.parentNode.replaceChild(field, input);
(input.parentNode as ParentNode).replaceChild(field, input);
field.appendChild(hiddenInput);
}
}

View File

@ -1,16 +1,16 @@
import CodeMirror from "codemirror/lib/codemirror.js";
import { $ } from "../../utils/selectors";
import { app } from "../../app";
import { arrayEquals } from "../../utils/arrays";
import { debounce } from "../../utils/events";
import CodeMirror from "codemirror";
import "codemirror/mode/markdown/markdown.js";
import "codemirror/addon/display/placeholder.js";
import "codemirror/addon/edit/continuelist.js";
export class EditorInput {
constructor(textarea) {
constructor(textarea: HTMLTextAreaElement) {
const height = textarea.offsetHeight;
const editor = CodeMirror.fromTextArea(textarea, {
@ -29,40 +29,40 @@ export class EditorInput {
}),
});
const toolbar = $(`.editor-toolbar[data-for=${textarea.id}]`);
const toolbar = $(`.editor-toolbar[data-for=${textarea.id}]`) as HTMLElement;
const wrap = textarea.parentNode.classList.contains("editor-wrap") ? textarea.parentNode : null;
const wrap = (textarea.parentNode as HTMLElement).classList.contains("editor-wrap") ? (textarea.parentNode as HTMLElement) : null;
let activeLines = [];
let activeLines: number[] = [];
editor.getWrapperElement().style.height = `${height}px`;
$("[data-command=bold]", toolbar).addEventListener("click", () => {
$("[data-command=bold]", toolbar)?.addEventListener("click", () => {
insertAtCursor("**");
});
$("[data-command=italic]", toolbar).addEventListener("click", () => {
$("[data-command=italic]", toolbar)?.addEventListener("click", () => {
insertAtCursor("_");
});
$("[data-command=ul]", toolbar).addEventListener("click", () => {
$("[data-command=ul]", toolbar)?.addEventListener("click", () => {
insertAtCursor(`${prependSequence()}- `, "");
});
$("[data-command=ol]", toolbar).addEventListener("click", () => {
const num = /^\d+\./.exec(lastLine(editor.getValue()));
$("[data-command=ol]", toolbar)?.addEventListener("click", () => {
const num = /^(\d+)\./.exec(lastLine(editor.getValue()));
if (num) {
insertAtCursor(`\n${parseInt(num) + 1}. `, "");
insertAtCursor(`\n${parseInt(num[1]) + 1}. `, "");
} else {
insertAtCursor(`${prependSequence()}1. `, "");
}
});
$("[data-command=quote]", toolbar).addEventListener("click", () => {
$("[data-command=quote]", toolbar)?.addEventListener("click", () => {
insertAtCursor(`${prependSequence()}> `, "");
});
$("[data-command=link]", toolbar).addEventListener("click", () => {
$("[data-command=link]", toolbar)?.addEventListener("click", () => {
const selection = editor.getSelection();
if (/^(https?:\/\/|mailto:)/i.test(selection)) {
insertAtCursor("[", `](${selection})`, true);
@ -73,13 +73,13 @@ export class EditorInput {
}
});
$("[data-command=image]", toolbar).addEventListener("click", () => {
app.modals["imagesModal"].show(null, (modal) => {
$("[data-command=image]", toolbar)?.addEventListener("click", () => {
app.modals["imagesModal"].show(undefined, (modal) => {
const selected = $(".image-picker-thumbnail.selected", modal.element);
if (selected) {
selected.classList.remove("selected");
}
function confirmImage() {
function confirmImage(this: HTMLElement) {
if (selected) {
const filename = selected.dataset.filename;
insertAtCursor(`${prependSequence()}![`, `](${filename})`);
@ -87,16 +87,16 @@ export class EditorInput {
modal.hide();
this.removeEventListener("click", confirmImage);
}
$(".image-picker-confirm", modal.element).addEventListener("click", confirmImage);
($(".image-picker-confirm", modal.element) as HTMLElement).addEventListener("click", confirmImage);
});
});
$("[data-command=undo]", toolbar).addEventListener("click", () => {
$("[data-command=undo]", toolbar)?.addEventListener("click", () => {
editor.undo();
editor.focus();
});
$("[data-command=redo]", toolbar).addEventListener("click", () => {
$("[data-command=redo]", toolbar)?.addEventListener("click", () => {
editor.redo();
editor.focus();
});
@ -106,14 +106,14 @@ export class EditorInput {
debounce(() => {
textarea.value = editor.getValue();
if (editor.historySize().undo < 1) {
$("[data-command=undo]").disabled = true;
($("[data-command=undo]") as HTMLButtonElement).disabled = true;
} else {
$("[data-command=undo]").disabled = false;
($("[data-command=undo]") as HTMLButtonElement).disabled = false;
}
if (editor.historySize().redo < 1) {
$("[data-command=redo]").disabled = true;
($("[data-command=redo]") as HTMLButtonElement).disabled = true;
} else {
$("[data-command=redo]").disabled = false;
($("[data-command=redo]") as HTMLButtonElement).disabled = false;
}
}, 500),
);
@ -148,22 +148,22 @@ export class EditorInput {
if (!event.altKey && (event.ctrlKey || event.metaKey)) {
switch (event.key) {
case "b":
$("[data-command=bold]", toolbar).click();
$("[data-command=bold]", toolbar)?.click();
event.preventDefault();
break;
case "i":
$("[data-command=italic]", toolbar).click();
$("[data-command=italic]", toolbar)?.click();
event.preventDefault();
break;
case "k":
$("[data-command=link]", toolbar).click();
$("[data-command=link]", toolbar)?.click();
event.preventDefault();
break;
}
}
});
function lastLine(text) {
function lastLine(text: string) {
const index = text.lastIndexOf("\n");
if (index === -1) {
return text;
@ -187,7 +187,7 @@ export class EditorInput {
}
}
function insertAtCursor(leftValue, rightValue, dropSelection) {
function insertAtCursor(leftValue: string, rightValue?: string, dropSelection: boolean = false) {
if (rightValue === undefined) {
rightValue = leftValue;
}
@ -199,21 +199,21 @@ export class EditorInput {
editor.focus();
}
function getLinesFromRange(ranges) {
const lines = [];
function getLinesFromRange(ranges: CodeMirror.Range[]) {
const lines: number[] = [];
for (const range of ranges) {
lines.push(range.head.line);
}
return lines;
}
function removeActiveLines(editor, lines) {
function removeActiveLines(editor: CodeMirror.Editor, lines: number[]) {
for (const line of lines) {
editor.removeLineClass(line, "wrap", "CodeMirror-activeline");
}
}
function addActiveLines(editor, lines) {
function addActiveLines(editor: CodeMirror.Editor, lines: number[]) {
for (const line of lines) {
editor.addLineClass(line, "wrap", "CodeMirror-activeline");
}

View File

@ -1,18 +1,18 @@
import { $ } from "../../utils/selectors";
export class FileInput {
constructor(input) {
const label = $(`label[for="${input.id}"]`);
const span = $("span", label);
constructor(input: HTMLInputElement) {
const label = $(`label[for="${input.id}"]`) as HTMLElement;
const span = $("span", label) as HTMLElement;
let isSubmitted = false;
input.dataset.label = $(`label[for="${input.id}"] span`).innerHTML;
input.dataset.label = $(`label[for="${input.id}"] span`)?.innerHTML;
input.addEventListener("change", updateLabel);
input.addEventListener("input", updateLabel);
input.form.addEventListener("submit", () => {
if (input.files.length > 0) {
input.form?.addEventListener("submit", () => {
if (input.files && input.files.length > 0) {
span.innerHTML += ' <span class="spinner"></span>';
}
isSubmitted = true;
@ -30,9 +30,11 @@ export class FileInput {
if (isSubmitted) {
return;
}
input.files = event.dataTransfer.files;
// Firefox won't trigger a change event, so we explicitly do that
input.dispatchEvent(new Event("change"));
if (event.dataTransfer) {
input.files = event.dataTransfer.files;
// Firefox won't trigger a change event, so we explicitly do that
input.dispatchEvent(new Event("change"));
}
});
label.addEventListener("click", (event) => {
@ -41,28 +43,28 @@ export class FileInput {
}
});
function updateLabel() {
if (this.files.length > 0) {
const filenames = [];
for (const file of this.files) {
function updateLabel(this: HTMLInputElement) {
if (this.files && this.files.length > 0) {
const filenames: string[] = [];
for (const file of Array.from(this.files)) {
filenames.push(file.name);
}
span.innerHTML = filenames.join(", ");
} else {
span.innerHTML = this.dataset.label;
span.innerHTML = this.dataset.label as string;
}
}
function preventDefault(event) {
function preventDefault(event: Event) {
event.preventDefault();
}
function handleDragenter(event) {
function handleDragenter(this: HTMLInputElement, event: DragEvent) {
this.classList.add("drag");
event.preventDefault();
}
function handleDragleave(event) {
function handleDragleave(this: HTMLInputElement, event: DragEvent) {
this.classList.remove("drag");
event.preventDefault();
}

View File

@ -2,9 +2,9 @@ import { $ } from "../../utils/selectors";
import { app } from "../../app";
export class ImageInput {
constructor(element) {
constructor(element: HTMLInputElement) {
element.addEventListener("click", () => {
app.modals["imagesModal"].show(null, (modal) => {
app.modals["imagesModal"].show(undefined, (modal) => {
const selected = $(".image-picker-thumbnail.selected", modal.element);
if (selected) {
selected.classList.remove("selected");
@ -15,8 +15,9 @@ export class ImageInput {
thumbnail.classList.add("selected");
}
}
$(".image-picker-confirm", modal.element).dataset.target = element.id;
$(".image-picker-confirm", modal.element).addEventListener("click", () => modal.hide());
const confirm = $(".image-picker-confirm", modal.element) as HTMLElement;
confirm.dataset.target = element.id;
confirm.addEventListener("click", () => modal.hide());
});
});
}

View File

@ -0,0 +1,69 @@
import { $, $$ } from "../../utils/selectors";
export class ImagePicker {
constructor(element: HTMLSelectElement) {
const options = $$("option", element);
const confirmCommand = $(".image-picker-confirm", (element.parentNode as ParentNode).parentNode ?? document);
const uploadCommand = $("[data-command=upload]", (element.parentNode as ParentNode).parentNode ?? document);
element.hidden = true;
if (options.length > 0) {
const container = document.createElement("div");
container.className = "image-picker-thumbnails";
for (const option of Array.from(options) as HTMLOptionElement[]) {
const thumbnail = document.createElement("div");
thumbnail.className = "image-picker-thumbnail";
thumbnail.style.backgroundImage = `url(${option.value})`;
thumbnail.dataset.uri = option.value;
thumbnail.dataset.filename = option.text;
thumbnail.addEventListener("click", handleThumbnailClick);
thumbnail.addEventListener("dblclick", handleThumbnailDblclick);
container.appendChild(thumbnail);
}
(element.parentNode as ParentNode).insertBefore(container, element);
($(".image-picker-empty-state") as HTMLElement).style.display = "none";
}
confirmCommand?.addEventListener("click", function () {
const selectedThumbnail = $(".image-picker-thumbnail.selected");
const targetId = this.dataset.target;
if (selectedThumbnail && targetId) {
const target = document.getElementById(targetId) as HTMLSelectElement;
const selectedThumbnailFilename = selectedThumbnail.dataset.filename;
if (target && selectedThumbnailFilename) {
target.value = selectedThumbnailFilename;
}
}
});
uploadCommand?.addEventListener("click", function () {
const uploadTargetId = this.dataset.uploadTarget;
if (uploadTargetId) {
const uploadTarget = document.getElementById(uploadTargetId);
uploadTarget && uploadTarget.click();
}
});
function handleThumbnailClick(this: HTMLElement) {
const targetId = ($(".image-picker-confirm") as HTMLElement).dataset.target;
if (targetId) {
const target = document.getElementById(targetId) as HTMLSelectElement;
if (target) {
target.value = this.dataset.filename as string;
}
$$(".image-picker-thumbnail").forEach((element) => {
element.classList.remove("selected");
});
this.classList.add("selected");
}
}
function handleThumbnailDblclick(this: HTMLElement) {
this.click();
$(".image-picker-confirm")?.click();
}
}
}

View File

@ -1,22 +1,22 @@
import { $ } from "../../utils/selectors";
export class RangeInput {
constructor(input) {
constructor(input: HTMLInputElement) {
input.addEventListener("change", updateValueLabel);
input.addEventListener("input", updateValueLabel);
updateValueLabel.call(input);
if ("ticks" in input.dataset) {
const count = input.dataset.ticks;
const count = input.dataset.ticks as string;
switch (count) {
case 0:
case "0":
break;
case "true":
case "":
addTicks((input.max - input.min) / (input.step || 1) + 1);
addTicks((parseInt(input.max) - parseInt(input.min)) / (parseInt(input.step) || 1) + 1);
break;
default:
@ -25,16 +25,19 @@ export class RangeInput {
}
}
function updateValueLabel() {
this.style.setProperty("--progress", `${Math.round((this.value / (this.max - this.min)) * 100)}%`);
$(`output[for="${this.id}"]`).innerHTML = this.value;
function updateValueLabel(this: HTMLInputElement) {
this.style.setProperty("--progress", `${Math.round((parseInt(this.value) / (parseInt(this.max) - parseInt(this.min))) * 100)}%`);
const outputElement = $(`output[for="${this.id}"]`);
if (outputElement) {
outputElement.innerHTML = this.value;
}
}
function addTicks(count) {
function addTicks(count: number) {
const ticks = document.createElement("div");
ticks.className = "form-input-range-ticks";
ticks.dataset.for = input.id;
input.parentElement.insertBefore(ticks, input.nextSibling);
(input.parentElement as ParentNode).insertBefore(ticks, input.nextSibling);
for (let i = 0; i < count; i++) {
const tick = document.createElement("div");

View File

@ -1,13 +1,27 @@
import { $, $$ } from "../../utils/selectors";
import { escapeRegExp, makeDiacriticsRegExp } from "../../utils/validation";
type SelectInputListItem = {
label: string;
value: string;
selected: boolean;
disabled: boolean;
dataset: Record<string, string>;
};
interface SelectInputOptions {
labels: {
empty: string;
};
}
export class SelectInput {
constructor(select, options) {
const defaults = { labels: { empty: "No matching options" } };
constructor(select: HTMLSelectElement, userOptions: Partial<SelectInputOptions>) {
const defaults: SelectInputOptions = { labels: { empty: "No matching options" } };
options = Object.assign({}, defaults, options);
const options = Object.assign({}, defaults, userOptions);
let dropdown;
let dropdown: HTMLElement;
const labelInput = document.createElement("input");
@ -40,13 +54,13 @@ export class SelectInput {
labelInput.dataset[key] = select.dataset[key];
}
const list = [];
const list: SelectInputListItem[] = [];
$$("option", select).forEach((option) => {
const dataset = {};
$$("option", select).forEach((option: HTMLOptionElement) => {
const dataset: Record<string, string> = {};
for (const key in option.dataset) {
dataset[key] = option.dataset[key];
dataset[key] = option.dataset[key] as string;
}
list.push({
@ -62,7 +76,7 @@ export class SelectInput {
}
});
select.parentNode.insertBefore(wrap, select.nextSibling);
(select.parentNode as ParentNode).insertBefore(wrap, select.nextSibling);
wrap.appendChild(select);
@ -71,7 +85,7 @@ export class SelectInput {
createDropdown(list, wrap);
}
function createDropdown(list, wrap) {
function createDropdown(list: SelectInputListItem[], wrap: HTMLElement) {
dropdown = document.createElement("div");
dropdown.className = "dropdown-list";
@ -227,9 +241,9 @@ export class SelectInput {
}
}
function filterDropdown(value) {
const filter = (element) => {
const text = element.textContent;
function filterDropdown(value: string) {
const filter = (element: HTMLElement) => {
const text = `${element.textContent}`;
const regexp = new RegExp(makeDiacriticsRegExp(escapeRegExp(value)), "i");
return regexp.test(text);
};
@ -251,7 +265,7 @@ export class SelectInput {
}
}
function scrollToDropdownItem(item) {
function scrollToDropdownItem(item: HTMLElement) {
const dropdownScrollTop = dropdown.scrollTop;
const dropdownHeight = dropdown.clientHeight;
const dropdownScrollBottom = dropdownScrollTop + dropdownHeight;
@ -268,7 +282,7 @@ export class SelectInput {
}
}
function selectDropdownItem(item) {
function selectDropdownItem(item: HTMLElement) {
const selectedItem = $(".dropdown-item.selected", dropdown);
if (selectedItem) {
selectedItem.classList.remove("selected");
@ -303,26 +317,26 @@ export class SelectInput {
}
function selectPrevDropdownItem() {
const selectedItem = $(".dropdown-item.selected", dropdown);
const selectedItem = $(".dropdown-item.selected", dropdown) as HTMLElement;
if (selectedItem) {
let previousItem = selectedItem.previousSibling;
let previousItem = selectedItem.previousSibling as HTMLElement;
while (previousItem && (previousItem.style.display === "none" || previousItem.classList.contains("disabled"))) {
previousItem = previousItem.previousSibling;
previousItem = previousItem.previousSibling as HTMLElement;
}
if (previousItem) {
return selectDropdownItem(previousItem);
}
selectDropdownItem(selectedItem.previousSibling);
selectDropdownItem(selectedItem.previousSibling as HTMLElement);
}
selectLastDropdownItem();
}
function selectNextDropdownItem() {
const selectedItem = $(".dropdown-item.selected", dropdown);
const selectedItem = $(".dropdown-item.selected", dropdown) as HTMLElement;
if (selectedItem) {
let nextItem = selectedItem.nextSibling;
let nextItem = selectedItem.nextSibling as HTMLElement;
while (nextItem && (nextItem.style.display === "none" || nextItem.classList.contains("disabled"))) {
nextItem = nextItem.nextSibling;
nextItem = nextItem.nextSibling as HTMLElement;
}
if (nextItem) {
return selectDropdownItem(nextItem);
@ -331,14 +345,14 @@ export class SelectInput {
selectFirstDropdownItem();
}
function setCurrent(item) {
select.value = item.dataset.value;
function setCurrent(item: HTMLElement) {
select.value = item.dataset.value as string;
labelInput.value = item.innerText;
select.dispatchEvent(new Event("change"));
}
function getCurrent() {
return $(`[data-value="${select.value}"]`, dropdown);
return $(`[data-value="${select.value}"]`, dropdown) as HTMLElement;
}
function getCurrentLabel() {
@ -347,7 +361,7 @@ export class SelectInput {
function selectCurrent() {
if (getComputedStyle(dropdown).display === "none") {
filterDropdown(null);
filterDropdown("");
updateDropdown();
selectDropdownItem(getCurrent());
dropdown.style.display = "block";
@ -355,7 +369,7 @@ export class SelectInput {
}
}
function validateDropdownItem(value) {
function validateDropdownItem(value: string) {
const items = $$(".dropdown-item", dropdown);
for (let i = 0; i < items.length; i++) {
if (items[i].innerText === value) {

View File

@ -3,10 +3,10 @@ import { escapeRegExp, makeDiacriticsRegExp } from "../../utils/validation";
import { debounce } from "../../utils/events";
export class TagInput {
constructor(input) {
constructor(input: HTMLInputElement) {
const options = { addKeyCodes: ["Space"] };
let tags = [];
let placeholder, dropdown;
let tags: string[] = [];
let placeholder: string, dropdown: HTMLElement;
const field = document.createElement("div");
const innerInput = document.createElement("input");
@ -40,12 +40,12 @@ export class TagInput {
}
if (isDisabled) {
field.disabled = true;
field.setAttribute("disabled", "disabled");
innerInput.disabled = true;
hiddenInput.disabled = true;
}
input.parentNode.replaceChild(field, input);
(input.parentNode as ParentNode).replaceChild(field, input);
field.appendChild(innerInput);
field.appendChild(hiddenInput);
@ -73,7 +73,7 @@ export class TagInput {
function createDropdown() {
if ("options" in input.dataset) {
const list = JSON.parse(input.dataset.options);
const list = JSON.parse(input.dataset.options ?? "{}");
dropdown = document.createElement("div");
dropdown.className = "dropdown-list";
@ -84,7 +84,7 @@ export class TagInput {
item.innerHTML = list[key];
item.dataset.value = key;
item.addEventListener("click", function () {
addTag(this.dataset.value);
this.dataset.value && addTag(this.dataset.value);
});
dropdown.appendChild(item);
}
@ -139,7 +139,7 @@ export class TagInput {
innerInput.addEventListener(
"keyup",
debounce((event) => {
debounce((event: KeyboardEvent) => {
const value = innerInput.value.trim();
switch (event.key) {
case "Escape":
@ -178,7 +178,7 @@ export class TagInput {
if (value === "") {
removeTag(tags[tags.length - 1]);
if (innerInput.previousSibling) {
innerInput.parentNode.removeChild(innerInput.previousSibling);
(innerInput.parentNode as ParentNode).removeChild(innerInput.previousSibling);
}
event.preventDefault();
} else {
@ -228,7 +228,7 @@ export class TagInput {
}
}
function validateTag(value) {
function validateTag(value: string) {
if (!tags.includes(value)) {
if (dropdown) {
return $(`[data-value="${value}"]`, dropdown) !== null;
@ -238,25 +238,25 @@ export class TagInput {
return false;
}
function insertTag(value) {
function insertTag(value: string) {
const tag = document.createElement("span");
const tagRemove = document.createElement("i");
tag.className = "tag";
tag.innerHTML = value;
tag.style.marginRight = ".25rem";
innerInput.parentNode.insertBefore(tag, innerInput);
(innerInput.parentNode as ParentNode).insertBefore(tag, innerInput);
tagRemove.className = "tag-remove";
tagRemove.setAttribute("role", "button");
tagRemove.addEventListener("mousedown", (event) => {
removeTag(value);
tag.parentNode.removeChild(tag);
(tag.parentNode as ParentNode).removeChild(tag);
event.preventDefault();
});
tag.appendChild(tagRemove);
}
function addTag(value) {
function addTag(value: string) {
if (validateTag(value)) {
tags.push(value);
insertTag(value);
@ -270,7 +270,7 @@ export class TagInput {
}
}
function removeTag(value) {
function removeTag(value: string) {
const index = tags.indexOf(value);
if (index > -1) {
tags.splice(index, 1);
@ -292,7 +292,7 @@ export class TagInput {
if (getComputedStyle(element).display !== "none") {
visibleItems++;
}
if (!tags.includes(element.dataset.value)) {
if (!tags.includes(element.dataset.value as string)) {
element.style.display = "block";
} else {
element.style.display = "none";
@ -306,11 +306,11 @@ export class TagInput {
}
}
function filterDropdown(value) {
function filterDropdown(value: string) {
let visibleItems = 0;
dropdown.style.display = "block";
$$(".dropdown-item", dropdown).forEach((element) => {
const text = element.textContent;
const text = `${element.textContent}`;
const regexp = new RegExp(makeDiacriticsRegExp(escapeRegExp(value)), "i");
if (text.match(regexp) !== null && element.style.display !== "none") {
element.style.display = "block";
@ -326,7 +326,7 @@ export class TagInput {
}
}
function scrollToDropdownItem(item) {
function scrollToDropdownItem(item: HTMLElement) {
const dropdownScrollTop = dropdown.scrollTop;
const dropdownHeight = dropdown.clientHeight;
const dropdownScrollBottom = dropdownScrollTop + dropdownHeight;
@ -345,12 +345,12 @@ export class TagInput {
function addTagFromSelectedDropdownItem() {
const selectedItem = $(".dropdown-item.selected", dropdown);
if (getComputedStyle(selectedItem).display !== "none") {
innerInput.value = selectedItem.dataset.value;
if (selectedItem && getComputedStyle(selectedItem).display !== "none") {
innerInput.value = selectedItem.dataset.value as string;
}
}
function selectDropdownItem(item) {
function selectDropdownItem(item: HTMLElement) {
const selectedItem = $(".dropdown-item.selected", dropdown);
if (selectedItem) {
selectedItem.classList.remove("selected");
@ -384,14 +384,14 @@ export class TagInput {
function selectPrevDropdownItem() {
const selectedItem = $(".dropdown-item.selected", dropdown);
if (selectedItem) {
let previousItem = selectedItem.previousSibling;
let previousItem = selectedItem.previousSibling as HTMLElement;
while (previousItem && previousItem.style.display === "none") {
previousItem = previousItem.previousSibling;
previousItem = previousItem.previousSibling as HTMLElement;
}
if (previousItem) {
return selectDropdownItem(previousItem);
}
selectDropdownItem(selectedItem.previousSibling);
selectDropdownItem(selectedItem.previousSibling as HTMLElement);
}
selectLastDropdownItem();
}
@ -399,12 +399,12 @@ export class TagInput {
function selectNextDropdownItem() {
const selectedItem = $(".dropdown-item.selected", dropdown);
if (selectedItem) {
let nextItem = selectedItem.nextSibling;
let nextItem = selectedItem.nextSibling as HTMLElement;
while (nextItem && nextItem.style.display === "none") {
nextItem = nextItem.nextSibling;
nextItem = nextItem.nextSibling as HTMLElement;
}
if (nextItem) {
return selectDropdownItem(nextItem);
return selectDropdownItem(nextItem as HTMLElement);
}
}
selectFirstDropdownItem();

View File

@ -1,12 +1,15 @@
import { $, $$ } from "../utils/selectors";
import { Inputs } from "./inputs";
function getFirstFocusableElement(parent = document.body) {
function getFirstFocusableElement(parent: HTMLElement = document.body): HTMLElement {
return parent.querySelector("button, .button, input:not([type=hidden]), select, textarea") || parent;
}
export class Modal {
constructor(element) {
element: HTMLElement;
inputs: Inputs;
constructor(element: HTMLElement) {
this.element = element;
document.addEventListener("keyup", (event) => {
@ -19,7 +22,7 @@ export class Modal {
this.inputs = new Inputs(this.element);
$("[data-dismiss]", element).addEventListener("click", () => this.hide());
$("[data-dismiss]", element)?.addEventListener("click", () => this.hide());
let mousedownTriggered = false;
element.addEventListener("mousedown", () => (mousedownTriggered = true));
@ -31,7 +34,7 @@ export class Modal {
});
document.addEventListener("click", (event) => {
const target = event.target.closest("[data-modal]");
const target = (event.target as HTMLElement).closest("[data-modal]") as HTMLDivElement;
if (target && target.dataset.modal === element.id) {
const modalAction = target.dataset.modalAction;
if (modalAction) {
@ -43,24 +46,24 @@ export class Modal {
});
}
show(action, callback) {
show(action?: string, callback?: (modal: this) => void) {
const modal = this.element;
modal.setAttribute("role", "dialog");
modal.setAttribute("aria-modal", "true");
modal.classList.add("show");
if (action) {
$("form", modal).action = action;
($("form", modal) as HTMLFormElement).action = action;
}
document.activeElement.blur(); // Don't retain focus on any element
(document.activeElement as HTMLElement).blur(); // Don't retain focus on any element
if ($("[autofocus]", modal)) {
$("[autofocus]", modal).focus(); // Firefox bug
($("[autofocus]", modal) as HTMLFormElement).focus(); // Firefox bug
} else {
getFirstFocusableElement(modal).focus();
}
if (typeof callback === "function") {
callback(this);
}
$$(".tooltip").forEach((element) => element.parentNode.removeChild(element));
$$(".tooltip").forEach((element) => element.parentNode && element.parentNode.removeChild(element));
this.createBackdrop();
}
@ -82,7 +85,7 @@ export class Modal {
removeBackdrop() {
const backdrop = $(".modal-backdrop");
if (backdrop) {
if (backdrop && backdrop.parentNode) {
backdrop.parentNode.removeChild(backdrop);
}
}

View File

@ -2,7 +2,8 @@ import { $$ } from "../utils/selectors";
import { Modal } from "./modal";
export class Modals {
[id: string]: Modal;
constructor() {
$$(".modal").forEach((element) => (this[element.id] = new Modal(element)));
$$(".modal").forEach((element: HTMLElement) => (this[element.id] = new Modal(element)));
}
}

View File

@ -3,8 +3,8 @@ import { $ } from "../utils/selectors";
export class Navigation {
constructor() {
if ($(".sidebar-toggle")) {
$(".sidebar-toggle").addEventListener("click", () => {
if ($(".sidebar").classList.toggle("show")) {
$(".sidebar-toggle")?.addEventListener("click", () => {
if (($(".sidebar") as HTMLElement).classList.toggle("show")) {
if (!$(".sidebar-backdrop")) {
const backdrop = document.createElement("div");
backdrop.className = "sidebar-backdrop hide-from-s";
@ -13,7 +13,7 @@ export class Navigation {
} else {
const backdrop = $(".sidebar-backdrop");
if (backdrop) {
backdrop.parentNode.removeChild(backdrop);
(backdrop.parentNode as ParentNode).removeChild(backdrop);
}
}
});
@ -23,7 +23,7 @@ export class Navigation {
document.addEventListener("keydown", (event) => {
if (!event.altKey && (event.ctrlKey || event.metaKey)) {
if (event.key === "s") {
$("[data-command=save]").click();
$("[data-command=save]")?.click();
event.preventDefault();
}
}

View File

@ -1,8 +1,22 @@
import { $ } from "../utils/selectors";
import { passIcon } from "./icons";
type NotificationOptions = {
interval: number;
icon?: string;
newestOnTop: boolean;
fadeOutDelay: number;
mouseleaveDelay: number;
};
export class Notification {
constructor(text, type, options) {
text: string;
type: string;
options: NotificationOptions;
containerElement: HTMLElement | null;
notificationElement: HTMLElement;
constructor(text: string, type: string, options: Partial<NotificationOptions>) {
const defaults = {
interval: 5000,
icon: null,
@ -14,13 +28,13 @@ export class Notification {
this.text = text;
this.type = type;
this.options = Object.assign({}, defaults, options);
this.options = Object.assign({}, defaults, options) as NotificationOptions;
this.containerElement = $(".notification-container");
this.containerElement = $(".notification-container") as HTMLElement;
}
show() {
const create = (text, type, interval) => {
const create = (text: string, type: string, interval: number) => {
if (!this.containerElement) {
this.containerElement = document.createElement("div");
this.containerElement.className = "notification-container";
@ -48,10 +62,10 @@ export class Notification {
return notification;
};
if (this.options.icon !== null) {
if (this.options.icon) {
passIcon(this.options.icon, (icon) => {
this.notificationElement = create(this.text, this.type, this.options.interval);
this.notificationElement.insertAdjacentHTML("afterBegin", icon);
this.notificationElement.insertAdjacentHTML("afterbegin", icon);
});
} else {
this.notificationElement = create(this.text, this.type, this.options.interval);
@ -62,7 +76,7 @@ export class Notification {
this.notificationElement.classList.add("fadeout");
setTimeout(() => {
if (this.notificationElement && this.notificationElement.parentNode) {
if (this.containerElement && this.notificationElement && this.notificationElement.parentNode) {
this.containerElement.removeChild(this.notificationElement);
}
if (this.containerElement && this.containerElement.childNodes.length < 1) {

View File

@ -5,7 +5,7 @@ export class Notifications {
constructor() {
let delay = 0;
$$("meta[name=notification]").forEach((element) => {
$$("meta[name=notification]").forEach((element: HTMLMetaElement) => {
setTimeout(() => {
const data = JSON.parse(element.content);
const notification = new Notification(data.text, data.type, {
@ -15,7 +15,7 @@ export class Notifications {
notification.show();
}, delay);
delay += 500;
element.parentNode.removeChild(element);
(element.parentNode as ParentNode).removeChild(element);
});
}
}

View File

@ -4,7 +4,7 @@ export class Sections {
constructor() {
$$(".collapsible .section-header").forEach((element) => {
element.addEventListener("click", () => {
const section = element.parentNode;
const section = element.parentNode as HTMLElement;
section.classList.toggle("collapsed");
});
});

View File

@ -0,0 +1,63 @@
import { LineChart, LineChartData } from "chartist";
import { passIcon } from "./icons";
import { Tooltip } from "./tooltip";
export class StatisticsChart {
constructor(element: HTMLElement, data: LineChartData) {
const spacing = 100;
const options = {
showArea: true,
fullWidth: true,
scaleMinSpace: 20,
divisor: 5,
chartPadding: 20,
lineSmooth: false,
low: 0,
axisX: {
showGrid: false,
labelOffset: {
x: 0,
y: 10,
},
labelInterpolationFnc: (value: string | number, index: number, labels?: any) => (index % Math.floor(labels.length / (element.clientWidth / spacing)) ? null : value),
},
axisY: {
onlyInteger: true,
offset: 15,
labelOffset: {
x: 0,
y: 5,
},
},
};
const chart = new LineChart(element, data, options);
chart.on("draw", (event) => {
if (event.type === "point") {
event.element.attr({ "ct:index": event.index });
}
});
// @ts-expect-error We need to access this property even if it's protected
chart.container.addEventListener("mouseover", (event) => {
const target = event.target as SVGElement;
if (target.getAttribute("class") === "ct-point") {
const strokeWidth = parseFloat(getComputedStyle(target).strokeWidth);
const index = target.getAttribute("ct:index");
if (index) {
passIcon("circle-small-fill", (icon) => {
// @ts-expect-error TODO
const text = `${data.labels[index]}<br><span class="text-color-blue">${icon}</span> ${data.series[0][index]} <span class="text-color-amber ml-2">${icon}</span>${data.series[1][index]}`;
const tooltip = new Tooltip(text, {
referenceElement: event.target as HTMLElement,
offset: { x: 0, y: -strokeWidth },
});
tooltip.show();
});
}
}
});
}
}

View File

@ -1,5 +1,25 @@
interface TooltipOptions {
container: HTMLElement;
referenceElement: HTMLElement;
position: "top" | "right" | "bottom" | "left" | "center";
offset: {
x: number;
y: number;
};
delay: number;
timeout: number | null;
removeOnMouseout: boolean;
removeOnClick: boolean;
}
export class Tooltip {
constructor(text, options) {
text: string;
options: TooltipOptions;
delayTimer: number;
timeoutTimer: number;
tooltipElement: HTMLElement;
constructor(text: string, options: Partial<TooltipOptions> = {}) {
const defaults = {
container: document.body,
referenceElement: document.body,
@ -29,7 +49,7 @@ export class Tooltip {
tooltip.style.display = "block";
tooltip.innerHTML = this.text;
const getTooltipPosition = (tooltip) => {
const getTooltipPosition = (tooltip: HTMLElement) => {
const referenceElement = options.referenceElement;
const offset = options.offset;
const rect = referenceElement.getBoundingClientRect();

View File

@ -11,7 +11,7 @@ export class Tooltips {
$$("[data-tooltip]").forEach((element) => {
element.addEventListener("mouseover", () => {
const tooltip = new Tooltip(element.dataset.tooltip, {
const tooltip = new Tooltip(element.dataset.tooltip as string, {
referenceElement: element,
position: "bottom",
offset: {
@ -25,7 +25,7 @@ export class Tooltips {
// Immediately show tooltip on focused buttons
if (element.tagName.toLowerCase() === "button" || element.classList.contains("button")) {
element.addEventListener("focus", () => {
const tooltip = new Tooltip(element.dataset.tooltip, {
const tooltip = new Tooltip(element.dataset.tooltip as string, {
referenceElement: element,
position: "bottom",
offset: {

View File

@ -11,7 +11,7 @@ export class Backups {
if (makeBackupCommand) {
makeBackupCommand.addEventListener("click", function () {
const button = this;
const button = this as HTMLButtonElement;
const getSpinner = () => {
let spinner = $(".spinner");
@ -35,7 +35,7 @@ export class Backups {
{
method: "POST",
url: `${app.config.baseUri}backup/make/`,
data: { "csrf-token": $("meta[name=csrf-token]").content },
data: { "csrf-token": ($("meta[name=csrf-token]") as HTMLMetaElement).content },
},
(response) => {
if (response.status === "success") {
@ -44,22 +44,22 @@ export class Backups {
spinner.classList.add("spinner-success");
insertIcon("check", spinner);
const template = $("#backups-row");
const template = $("#backups-row") as HTMLTemplateElement;
if (template) {
const table = $("#backups-table");
const table = $("#backups-table") as HTMLTableElement;
const node = template.content.cloneNode(true);
const node = template.content.cloneNode(true) as HTMLElement;
$(".backup-uri", node).href = response.data.uri;
$(".backup-uri", node).innerHTML = response.data.filename;
($(".backup-uri", node) as HTMLAnchorElement).href = response.data.uri;
($(".backup-uri", node) as HTMLElement).innerHTML = response.data.filename;
$(".backup-date", node).innerHTML = response.data.date;
$(".backup-size", node).innerHTML = response.data.size;
$(".backup-delete", node).dataset.modalAction = response.data.deleteUri;
($(".backup-date", node) as HTMLElement).innerHTML = response.data.date;
($(".backup-size", node) as HTMLElement).innerHTML = response.data.size;
($(".backup-delete", node) as HTMLElement).dataset.modalAction = response.data.deleteUri;
$(".backup-last-time").innerHTML = app.config.Backups.labels.now;
($(".backup-last-time") as HTMLElement).innerHTML = app.config.Backups.labels.now;
$("tbody", table).prepend(node);
($("tbody", table) as HTMLElement).prepend(node);
const limit = response.data.maxFiles;
@ -82,7 +82,7 @@ export class Backups {
if (response.status === "success") {
setTimeout(() => {
triggerDownload(response.data.uri, $("meta[name=csrf-token]").content);
triggerDownload(response.data.uri, ($("meta[name=csrf-token]") as HTMLMetaElement).content);
}, 1000);
}
},

View File

@ -17,7 +17,7 @@ export class Dashboard {
{
method: "POST",
url: `${app.config.baseUri}cache/clear/`,
data: { "csrf-token": $("meta[name=csrf-token]").content },
data: { "csrf-token": ($("meta[name=csrf-token]") as HTMLMetaElement).content },
},
(response) => {
const notification = new Notification(response.message, response.status, { icon: "check-circle" });
@ -29,14 +29,14 @@ export class Dashboard {
if (makeBackupCommand) {
makeBackupCommand.addEventListener("click", function () {
const button = this;
const button = this as HTMLButtonElement;
button.disabled = true;
new Request(
{
method: "POST",
url: `${app.config.baseUri}backup/make/`,
data: { "csrf-token": $("meta[name=csrf-token]").content },
data: { "csrf-token": ($("meta[name=csrf-token]") as HTMLMetaElement).content },
},
(response) => {
const notification = new Notification(response.message, response.status, { icon: "check-circle" });
@ -45,7 +45,7 @@ export class Dashboard {
if (response.status === "success") {
setTimeout(() => {
button.disabled = false;
triggerDownload(response.data.uri, $("meta[name=csrf-token]").content);
triggerDownload(response.data.uri, ($("meta[name=csrf-token]") as HTMLMetaElement).content);
}, 1000);
}
@ -58,7 +58,10 @@ export class Dashboard {
}
if (chart) {
new StatisticsChart(chart, JSON.parse(chart.dataset.chartData));
const chartData = chart.dataset.chartData;
if (chartData) {
new StatisticsChart(chart, JSON.parse(chartData));
}
}
}
}

View File

@ -4,7 +4,7 @@ import { app } from "../../app";
import { debounce } from "../../utils/events";
import { Notification } from "../notification";
import { Request } from "../../utils/request";
import { Sortable } from "sortablejs";
import Sortable from "sortablejs";
export class Pages {
constructor() {
@ -62,7 +62,7 @@ export class Pages {
if (commandReorderPages) {
commandReorderPages.addEventListener("click", () => {
commandReorderPages.classList.toggle("active");
$(".pages-tree").classList.toggle("is-reordering");
($(".pages-tree") as HTMLElement).classList.toggle("is-reordering");
commandReorderPages.blur();
});
}
@ -74,26 +74,26 @@ export class Pages {
});
});
const handleSearch = (event) => {
const value = event.target.value;
const handleSearch = (event: Event) => {
const value = (event.target as HTMLInputElement).value;
if (value.length === 0) {
$(".pages-tree-root").classList.remove("is-filtered");
($(".pages-tree-root") as HTMLElement).classList.remove("is-filtered");
$$(".pages-tree-item").forEach((element) => {
const title = $(".page-title a", element);
title.innerHTML = title.textContent;
$(".pages-tree-row", element).style.display = "";
const title = $(".page-title a", element) as HTMLElement;
title.innerHTML = title.textContent as string;
($(".pages-tree-row", element) as HTMLElement).style.display = "";
element.classList.toggle("is-expanded", element.dataset.expanded === "true");
});
} else {
$(".pages-tree-root").classList.add("is-filtered");
($(".pages-tree-root") as HTMLElement).classList.add("is-filtered");
const regexp = new RegExp(makeDiacriticsRegExp(escapeRegExp(value)), "gi");
$$(".pages-tree-item").forEach((element) => {
const title = $(".page-title a", element);
const text = title.textContent;
const pagesItem = $(".pages-tree-row", element);
const title = $(".page-title a", element) as HTMLElement;
const text = title.textContent as string;
const pagesItem = $(".pages-tree-row", element) as HTMLElement;
if (text.match(regexp) !== null) {
title.innerHTML = text.replace(regexp, "<mark>$&</mark>");
@ -121,45 +121,44 @@ export class Pages {
}
if (newPageModal) {
$("#page-title", newPageModal).addEventListener("keyup", (event) => {
$("#page-slug", newPageModal).value = makeSlug(event.target.value);
($("#page-title", newPageModal) as HTMLElement).addEventListener("keyup", (event) => {
($("#page-slug", newPageModal) as HTMLInputElement).value = makeSlug((event.target as HTMLInputElement).value);
});
const handleSlugChange = (event) => {
event.target.value = validateSlug(event.target.value);
const handleSlugChange = (event: Event) => {
const target = event.target as HTMLInputElement;
target.value = validateSlug(target.value);
};
$("#page-slug", newPageModal).addEventListener("keyup", handleSlugChange);
$("#page-slug", newPageModal).addEventListener("blur", handleSlugChange);
($("#page-slug", newPageModal) as HTMLElement).addEventListener("keyup", handleSlugChange);
($("#page-slug", newPageModal) as HTMLElement).addEventListener("blur", handleSlugChange);
$("#page-parent", newPageModal).addEventListener("change", () => {
($("#page-parent", newPageModal) as HTMLElement).addEventListener("change", () => {
const option = $('.dropdown-list[data-for="page-parent"] .selected');
if (!option) {
return;
}
let allowedTemplates = option.dataset.allowedTemplates;
const allowedTemplates = (option.dataset.allowedTemplates as string).split(", ");
const pageTemplate = $("#page-template", newPageModal);
if (allowedTemplates) {
allowedTemplates = allowedTemplates.split(", ");
const pageTemplate = $("#page-template", newPageModal) as HTMLInputElement;
if (allowedTemplates.length > 0) {
pageTemplate.dataset.previousValue = pageTemplate.value;
pageTemplate.value = allowedTemplates[0];
$('.select[data-for="page-template"').value = $(`.dropdown-list[data-for="page-template"] .dropdown-item[data-value="${pageTemplate.value}"]`).innerText;
($('.select[data-for="page-template"') as HTMLInputElement).value = ($(`.dropdown-list[data-for="page-template"] .dropdown-item[data-value="${pageTemplate.value}"]`) as HTMLElement).innerText;
$$('.dropdown-list[data-for="page-template"] .dropdown-item').forEach((option) => {
if (!allowedTemplates.includes(option.dataset.value)) {
if (!allowedTemplates.includes(option.dataset.value as string)) {
option.classList.add("disabled");
}
});
} else {
if ("previousValue" in pageTemplate.dataset) {
pageTemplate.value = pageTemplate.dataset.previousValue;
pageTemplate.value = pageTemplate.dataset.previousValue as string;
delete pageTemplate.dataset.previousValue;
$('.select[data-for="page-template"').value = $(`.dropdown-list[data-for="page-template"] .dropdown-item[data-value="${pageTemplate.value}"]`).innerText;
($('.select[data-for="page-template"') as HTMLInputElement).value = ($(`.dropdown-list[data-for="page-template"] .dropdown-item[data-value="${pageTemplate.value}"]`) as HTMLElement).innerText;
}
$$('.dropdown-list[data-for="page-template"] .dropdown-item').forEach((option) => {
@ -171,55 +170,56 @@ export class Pages {
if (slugModal && commandChangeSlug) {
commandChangeSlug.addEventListener("click", () => {
app.modals["slugModal"].show(null, (modal) => {
const slug = document.getElementById("slug").value;
const slugInput = $("#page-slug", modal.element);
app.modals["slugModal"].show(undefined, (modal) => {
const slug = (document.getElementById("slug") as HTMLInputElement).value;
const slugInput = $("#page-slug", modal.element) as HTMLInputElement;
slugInput.value = slug;
slugInput.placeholder = slug;
});
});
$("#page-slug", slugModal).addEventListener("keydown", (event) => {
($("#page-slug", slugModal) as HTMLElement).addEventListener("keydown", (event) => {
if (event.key === "Enter") {
$("[data-command=continue]", slugModal).click();
($("[data-command=continue]", slugModal) as HTMLElement).click();
}
});
const handleSlugChange = (event) => {
event.target.value = validateSlug(event.target.value);
const handleSlugChange = (event: Event) => {
const target = event.target as HTMLInputElement;
target.value = validateSlug(target.value);
};
$("#page-slug", slugModal).addEventListener("keyup", handleSlugChange);
$("#page-slug", slugModal).addEventListener("blur", handleSlugChange);
($("#page-slug", slugModal) as HTMLElement).addEventListener("keyup", handleSlugChange);
($("#page-slug", slugModal) as HTMLElement).addEventListener("blur", handleSlugChange);
$("[data-command=generate-slug]", slugModal).addEventListener("click", () => {
const slug = makeSlug(document.getElementById("title").value);
$("#page-slug", slugModal).value = slug;
$("#page-slug", slugModal).focus();
($("[data-command=generate-slug]", slugModal) as HTMLElement).addEventListener("click", () => {
const slug = makeSlug((document.getElementById("title") as HTMLInputElement).value);
($("#page-slug", slugModal) as HTMLInputElement).value = slug;
($("#page-slug", slugModal) as HTMLElement).focus();
});
$("[data-command=continue]", slugModal).addEventListener("click", () => {
const slug = $("#page-slug", slugModal).value.replace(/^-+|-+$/, "");
($("[data-command=continue]", slugModal) as HTMLElement).addEventListener("click", () => {
const slug = ($("#page-slug", slugModal) as HTMLInputElement).value.replace(/^-+|-+$/, "");
if (slug.length > 0) {
const route = $(".page-route-inner").innerHTML;
$$("#page-slug, #slug").forEach((element) => {
const route = ($(".page-route-inner") as HTMLElement).innerHTML;
$$("#page-slug, #slug").forEach((element: HTMLInputElement) => {
element.value = slug;
});
$("#page-slug", slugModal).value = slug;
document.getElementById("slug").value = slug;
$(".page-route-inner").innerHTML = route.replace(/\/[a-z0-9-]+\/$/, `/${slug}/`);
($("#page-slug", slugModal) as HTMLInputElement).value = slug;
(document.getElementById("slug") as HTMLInputElement).value = slug;
($(".page-route-inner") as HTMLElement).innerHTML = route.replace(/\/[a-z0-9-]+\/$/, `/${slug}/`);
}
app.modals["slugModal"].hide();
});
}
$$(["[data-modal=renameFileModal]"]).forEach((element) => {
$$("[data-modal=renameFileModal]").forEach((element) => {
element.addEventListener("click", () => {
const modal = document.getElementById("renameFileModal");
const input = $("#file-name", modal);
input.value = element.dataset.filename;
const modal = document.getElementById("renameFileModal") as HTMLElement;
const input = $("#file-name", modal) as HTMLInputElement;
input.value = element.dataset.filename as string;
input.setSelectionRange(0, input.value.lastIndexOf("."));
});
});
@ -236,13 +236,13 @@ export class Pages {
});
}
function togglePageItem(list) {
function togglePageItem(list: HTMLElement) {
const element = list.closest(".pages-tree-item");
element.classList.toggle("is-expanded");
element?.classList.toggle("is-expanded");
}
function initSortable(element) {
let originalOrder = [];
function initSortable(element: HTMLElement) {
let originalOrder: string[] = [];
const sortable = Sortable.create(element, {
handle: ".sortable-handle",
@ -256,23 +256,24 @@ export class Pages {
const height = document.body.offsetHeight;
document.body.style.height = `${height}px`;
const e = window.addEventListener("scroll", () => {
const e = () => {
window.document.body.style.height = "";
window.removeEventListener("scroll", e);
});
};
window.addEventListener("scroll", e);
},
onStart() {
element.classList.add("is-dragging");
},
onMove(event) {
onMove(event: Sortable.MoveEvent) {
if (event.related.classList.contains("is-not-orderable")) {
return false;
}
},
onEnd(event) {
onEnd(event: Sortable.SortableEvent) {
element.classList.remove("is-dragging");
document.body.style.height = "";
@ -284,9 +285,9 @@ export class Pages {
sortable.option("disabled", true);
const data = {
"csrf-token": $("meta[name=csrf-token]").content,
page: element.children[event.newIndex].dataset.route,
before: element.children[event.oldIndex].dataset.route,
"csrf-token": ($("meta[name=csrf-token]") as HTMLMetaElement).content,
page: (element.children[event.newIndex as number] as HTMLElement).dataset.route,
before: (element.children[event.oldIndex as number] as HTMLElement).dataset.route,
parent: element.dataset.parent,
};

View File

@ -4,9 +4,11 @@ import { StatisticsChart } from "../statistics-chart";
export class Statistics {
constructor() {
const chart = $(".statistics-chart");
if (chart) {
new StatisticsChart(chart, JSON.parse(chart.dataset.chartData));
const chartData = chart.dataset.chartData;
if (chartData) {
new StatisticsChart(chart, JSON.parse(chartData));
}
}
}
}

View File

@ -9,14 +9,15 @@ export class Updates {
const updaterComponent = document.getElementById("updater-component");
if (updaterComponent) {
const updateStatus = $(".update-status");
const spinner = $(".spinner");
const currentVersion = $(".current-version");
const currentVersionName = $(".current-version-name");
const newVersion = $(".new-version");
const newVersionName = $(".new-version-name");
const updateStatus = $(".update-status") as HTMLElement;
const spinner = $(".spinner") as HTMLElement;
const currentVersion = $(".current-version") as HTMLElement;
const currentVersionName = $(".current-version-name") as HTMLElement;
const newVersion = $(".new-version") as HTMLElement;
const newVersionName = $(".new-version-name") as HTMLElement;
const installCommand = $("[data-command=install-updates]") as HTMLElement;
const showNewVersion = (name) => {
const showNewVersion = (name: string) => {
spinner.classList.add("spinner-info");
insertIcon("info", spinner);
newVersionName.innerHTML = name;
@ -37,7 +38,7 @@ export class Updates {
};
setTimeout(() => {
const data = { "csrf-token": $("meta[name=csrf-token]").content };
const data = { "csrf-token": ($("meta[name=csrf-token]") as HTMLMetaElement).content };
new Request(
{
@ -62,16 +63,16 @@ export class Updates {
);
}, 1000);
$("[data-command=install-updates]").addEventListener("click", () => {
installCommand.addEventListener("click", () => {
newVersion.style.display = "none";
spinner.classList.remove("spinner-info");
updateStatus.innerHTML = updateStatus.dataset.installingText;
updateStatus.innerHTML = updateStatus.dataset.installingText as string;
new Request(
{
method: "POST",
url: `${app.config.baseUri}updates/update/`,
data: { "csrf-token": $("meta[name=csrf-token]").content },
data: { "csrf-token": ($("meta[name=csrf-token]") as HTMLMetaElement).content },
},
(response) => {
const notification = new Notification(response.message, response.status, { icon: "check-circle" });

View File

@ -1,16 +1,16 @@
// HTMLFormElement.prototype.requestSubmit polyfill
// see https://github.com/javan/form-request-submit-polyfill
if (!("requestSubmit" in window.HTMLFormElement.prototype)) {
window.HTMLFormElement.prototype.requestSubmit = function (submitter) {
if (typeof window.HTMLFormElement.prototype.requestSubmit === "undefined") {
window.HTMLFormElement.prototype.requestSubmit = function (submitter: HTMLInputElement) {
if (submitter) {
if (!(submitter instanceof HTMLElement)) {
raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
throw new TypeError("Failed to execute 'requestSubmit' on 'HTMLFormElement': parameter 1 is not of type 'HTMLElement'.");
}
if (submitter.type !== "submit") {
raise(TypeError, "The specified element is not a submit button");
throw new TypeError("Failed to execute 'requestSubmit' on 'HTMLFormElement': the specified element is not a submit button.");
}
if (submitter.form !== this) {
raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
throw new DOMException("Failed to execute 'requestSubmit' on 'HTMLFormElement': the specified element is not owned by this form element.", "NotFoundError");
}
submitter.click();
} else {
@ -21,9 +21,5 @@ if (!("requestSubmit" in window.HTMLFormElement.prototype)) {
submitter.click();
this.removeChild(submitter);
}
function raise(error, message, name) {
throw new error(`Failed to execute 'requestSubmit' on 'HTMLFormElement': ${message}.`, name);
}
};
}

View File

@ -1,4 +1,4 @@
export function arrayEquals(array1, array2) {
export function arrayEquals(array1: Array<any>, array2: Array<any>) {
if (array1.length !== array2.length) {
return false;
}

View File

@ -1,5 +1,5 @@
export function getCookies() {
const result = [];
const result: Record<string, string> = {};
const cookies = document.cookie.split(";");
for (const cookie of cookies) {
const nameAndValue = cookie.split("=", 2);
@ -10,7 +10,7 @@ export function getCookies() {
return result;
}
export function setCookie(name, value, options) {
export function setCookie(name: string, value: string, options: Record<string, string | number>) {
let cookie = `${name}=${value}`;
for (const option in options) {
cookie += `;${option}=${options[option]}`;

View File

@ -1,9 +1,9 @@
export function getOuterWidth(element) {
export function getOuterWidth(element: HTMLElement) {
const style = getComputedStyle(element);
return element.offsetWidth + parseInt(style.marginLeft) + parseInt(style.marginRight);
}
export function getOuterHeight(element) {
export function getOuterHeight(element: HTMLElement) {
const style = getComputedStyle(element);
return element.offsetHeight + parseInt(style.marginTop) + parseInt(style.marginBottom);
}

View File

@ -1,10 +1,10 @@
export function debounce(callback, delay, leading) {
let result;
let timer = null;
export function debounce(callback: (...args: any[]) => any, delay: number, leading: boolean = false) {
let result: any;
let timer: number | null = null;
function wrapper() {
function wrapper(this: any, ...args: any[]) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this;
const args = arguments;
if (timer) {
clearTimeout(timer);
}
@ -23,19 +23,19 @@ export function debounce(callback, delay, leading) {
return wrapper;
}
export function throttle(callback, delay) {
let result;
export function throttle(callback: (...args: any[]) => any, delay: number) {
let result: any;
let previous = 0;
let timer = null;
let timer: number | null = null;
function wrapper() {
function wrapper(this: any, ...args: any[]) {
const now = Date.now();
if (previous === 0) {
previous = now;
}
const remaining = previous + delay - now;
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this;
const args = arguments;
if (remaining <= 0 || remaining > delay) {
if (timer) {
clearTimeout(timer);

View File

@ -1,14 +1,14 @@
export function serializeObject(object) {
const serialized = [];
export function serializeObject(object: Record<string, string | number | boolean>) {
const serialized: string[] = [];
for (const property in object) {
serialized.push(`${encodeURIComponent(property)}=${encodeURIComponent(object[property])}`);
}
return serialized.join("&");
}
export function serializeForm(form) {
const serialized = [];
for (const field of form.elements) {
export function serializeForm(form: HTMLFormElement) {
const serialized: string[] = [];
for (const field of Array.from(form.elements) as HTMLFormElement[]) {
if (field.name && !field.disabled && field.dataset.formIgnore !== "true" && field.type !== "file" && field.type !== "reset" && field.type !== "submit" && field.type !== "button") {
if (field.type === "select-multiple") {
for (const option of field.options) {
@ -24,7 +24,7 @@ export function serializeForm(form) {
return serialized.join("&");
}
export function triggerDownload(uri, csrfToken) {
export function triggerDownload(uri: string, csrfToken: string) {
const form = document.createElement("form");
form.action = uri;
form.method = "post";

View File

@ -1,7 +1,13 @@
import { serializeObject } from "./forms";
type RequestOptions = {
method: string;
url: string;
data: Record<string, any>;
};
export class Request {
constructor(options, callback) {
constructor(options: RequestOptions, callback: (response: Record<string, any>, request: XMLHttpRequest) => void) {
const request = new XMLHttpRequest();
request.open(options.method, options.url, true);

View File

@ -0,0 +1,7 @@
export function $(selector: string, parent: ParentNode = document): HTMLElement | null {
return parent.querySelector(selector);
}
export function $$(selector: string, parent: ParentNode = document): NodeListOf<HTMLElement> {
return parent.querySelectorAll(selector);
}

View File

@ -1,9 +1,9 @@
export function escapeRegExp(string) {
export function escapeRegExp(string: string) {
return string.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
}
export function makeDiacriticsRegExp(string) {
const diacritics = {
export function makeDiacriticsRegExp(string: string) {
const diacritics: Record<string, string> = {
a: "[aáàăâǎåäãȧąāảȁạ]",
b: "[bḃḅ]",
c: "[cćĉčċç]",
@ -36,8 +36,8 @@ export function makeDiacriticsRegExp(string) {
return string;
}
export function makeSlug(string) {
const translate = {
export function makeSlug(string: string) {
const translate: Record<string, string> = {
"\t": "",
"\r": "",
"!": "",
@ -167,7 +167,7 @@ export function makeSlug(string) {
.replace(/-+/g, "-");
}
export function validateSlug(slug) {
export function validateSlug(slug: string) {
return slug
.toLowerCase()
.replace(" ", "-")

15
panel/tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"include": [
"./src/ts/**/*.ts"
],
"compilerOptions": {
"esModuleInterop": true,
"isolatedModules": true,
"lib": ["ES2017", "DOM"],
"noEmit": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"useDefineForClassFields": true,
}
}

File diff suppressed because it is too large Load Diff