mirror of
https://github.com/getformwork/formwork.git
synced 2025-01-17 13:38:22 +01:00
Migrate to TypeScript
This commit is contained in:
parent
a437035a53
commit
8355115c0b
58
panel/assets/js/app.min.js
vendored
58
panel/assets/js/app.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,9 +1,11 @@
|
|||||||
import eslintConfigPrettier from "eslint-config-prettier";
|
import eslintConfigPrettier from "eslint-config-prettier";
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import js from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
{
|
{
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 13,
|
ecmaVersion: 13,
|
||||||
@ -41,6 +43,13 @@ export default [
|
|||||||
allowSeparatedGroups: true,
|
allowSeparatedGroups: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/typedef": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
parameter: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
eslintConfigPrettier,
|
eslintConfigPrettier,
|
||||||
|
@ -14,12 +14,12 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yarn build:css && yarn build:js",
|
"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: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:css": "yarn build:css --watch",
|
||||||
"watch:js": "yarn build:js --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: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": {
|
"dependencies": {
|
||||||
"chartist": "^1.3.0",
|
"chartist": "^1.3.0",
|
||||||
@ -28,6 +28,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^8.56.0",
|
"@eslint/js": "^8.56.0",
|
||||||
|
"@types/codemirror": "^5.60.15",
|
||||||
|
"@types/sortablejs": "^1.15.8",
|
||||||
"esbuild": "^0.20.0",
|
"esbuild": "^0.20.0",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
@ -38,7 +40,9 @@
|
|||||||
"stylelint": "^15.11.0",
|
"stylelint": "^15.11.0",
|
||||||
"stylelint-config-standard-scss": "^11.1.0",
|
"stylelint-config-standard-scss": "^11.1.0",
|
||||||
"stylelint-order": "^6.0.4",
|
"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"
|
"packageManager": "yarn@4.0.2"
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)));
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
export function $(selector, parent = document) {
|
|
||||||
return parent.querySelector(selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function $$(selector, parent = document) {
|
|
||||||
return parent.querySelectorAll(selector);
|
|
||||||
}
|
|
@ -14,14 +14,34 @@ import { Pages } from "./components/views/pages";
|
|||||||
import { Statistics } from "./components/views/statistics";
|
import { Statistics } from "./components/views/statistics";
|
||||||
import { Updates } from "./components/views/updates";
|
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 {
|
class App {
|
||||||
config = {};
|
config: AppConfig = {
|
||||||
|
baseUri: "/",
|
||||||
|
};
|
||||||
|
|
||||||
modals = {};
|
modals: Modals = {};
|
||||||
|
|
||||||
forms = {};
|
forms: Forms = {};
|
||||||
|
|
||||||
load(config) {
|
[alias: string]: any;
|
||||||
|
|
||||||
|
load(config: AppConfig) {
|
||||||
this.loadConfig(config);
|
this.loadConfig(config);
|
||||||
|
|
||||||
this.loadComponent(Modals, {
|
this.loadComponent(Modals, {
|
||||||
@ -47,14 +67,14 @@ class App {
|
|||||||
this.loadComponent(Updates);
|
this.loadComponent(Updates);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadConfig(config) {
|
loadConfig(config: AppConfig) {
|
||||||
Object.assign(this.config, config);
|
Object.assign(this.config, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadComponent(
|
loadComponent(
|
||||||
component,
|
component: Component,
|
||||||
options = {
|
options: ComponentConfig = {
|
||||||
globalAlias: null,
|
globalAlias: undefined,
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const instance = new component(this);
|
const instance = new component(this);
|
@ -7,7 +7,7 @@ export class ColorScheme {
|
|||||||
const cookies = getCookies();
|
const cookies = getCookies();
|
||||||
const cookieName = "formwork_preferred_color_scheme";
|
const cookieName = "formwork_preferred_color_scheme";
|
||||||
const oldValue = cookieName in cookies ? cookies[cookieName] : null;
|
const oldValue = cookieName in cookies ? cookies[cookieName] : null;
|
||||||
let value = null;
|
let value: "light" | "dark" = "light";
|
||||||
|
|
||||||
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
||||||
value = "light";
|
value = "light";
|
||||||
@ -15,7 +15,7 @@ export class ColorScheme {
|
|||||||
value = "dark";
|
value = "dark";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value !== oldValue) {
|
if (value && value !== oldValue) {
|
||||||
setCookie(cookieName, value, {
|
setCookie(cookieName, value, {
|
||||||
"max-age": 2592000, // 1 month
|
"max-age": 2592000, // 1 month
|
||||||
path: app.config.baseUri,
|
path: app.config.baseUri,
|
@ -8,11 +8,11 @@ export class Dropdowns {
|
|||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
$$(".dropdown-menu").forEach((element) => (element.style.display = ""));
|
$$(".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) {
|
if (button) {
|
||||||
const dropdown = document.getElementById(button.dataset.dropdown);
|
const dropdown = document.getElementById(button.dataset.dropdown as string) as HTMLElement;
|
||||||
const isVisible = getComputedStyle(dropdown).display !== "none";
|
const isVisible = getComputedStyle(dropdown as HTMLElement).display !== "none";
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const resizeHandler = throttle(() => setDropdownPosition(dropdown), 100);
|
const resizeHandler = throttle(() => setDropdownPosition(dropdown), 100);
|
||||||
@ -30,8 +30,8 @@ export class Dropdowns {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDropdownPosition(dropdown) {
|
function setDropdownPosition(dropdown: HTMLElement) {
|
||||||
dropdown.style.left = 0;
|
dropdown.style.left = "0";
|
||||||
dropdown.style.right = "";
|
dropdown.style.right = "";
|
||||||
|
|
||||||
const dropdownRect = dropdown.getBoundingClientRect();
|
const dropdownRect = dropdown.getBoundingClientRect();
|
||||||
@ -45,7 +45,7 @@ function setDropdownPosition(dropdown) {
|
|||||||
|
|
||||||
if (dropdownLeft + dropdownWidth > windowWidth) {
|
if (dropdownLeft + dropdownWidth > windowWidth) {
|
||||||
dropdown.style.left = "auto";
|
dropdown.style.left = "auto";
|
||||||
dropdown.style.right = 0;
|
dropdown.style.right = "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dropdownTop < window.scrollY || window.scrollY < dropdownTop + dropdownHeight - windowHeight) {
|
if (dropdownTop < window.scrollY || window.scrollY < dropdownTop + dropdownHeight - windowHeight) {
|
26
panel/src/ts/components/files.ts
Normal file
26
panel/src/ts/components/files.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,10 @@ import { Inputs } from "./inputs";
|
|||||||
import { serializeForm } from "../utils/forms";
|
import { serializeForm } from "../utils/forms";
|
||||||
|
|
||||||
export class Form {
|
export class Form {
|
||||||
constructor(form) {
|
inputs: Inputs;
|
||||||
|
originalData: string;
|
||||||
|
|
||||||
|
constructor(form: HTMLFormElement) {
|
||||||
this.inputs = new Inputs(form);
|
this.inputs = new Inputs(form);
|
||||||
|
|
||||||
// Serialize after inputs are loaded
|
// Serialize after inputs are loaded
|
||||||
@ -16,11 +19,11 @@ export class Form {
|
|||||||
form.addEventListener("submit", removeBeforeUnload);
|
form.addEventListener("submit", removeBeforeUnload);
|
||||||
|
|
||||||
const hasChanged = (checkFileInputs = true) => {
|
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) {
|
if (checkFileInputs === true && fileInputs.length > 0) {
|
||||||
for (const fileInput of fileInputs) {
|
for (const fileInput of Array.from(fileInputs)) {
|
||||||
if (fileInput.files.length > 0) {
|
if (fileInput.files && fileInput.files.length > 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -29,12 +32,15 @@ export class Form {
|
|||||||
return serializeForm(form) !== this.originalData;
|
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) => {
|
element.addEventListener("click", (event) => {
|
||||||
if (hasChanged()) {
|
if (hasChanged()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
app.modals["changesModal"].show(null, (modal) => {
|
app.modals["changesModal"].show(undefined, (modal) => {
|
||||||
$("[data-command=continue]", modal.element).dataset.href = element.href;
|
const continueCommand = $("[data-command=continue]", modal.element);
|
||||||
|
if (continueCommand) {
|
||||||
|
continueCommand.dataset.href = element.href;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -50,10 +56,10 @@ export class Form {
|
|||||||
|
|
||||||
registerModalExceptions();
|
registerModalExceptions();
|
||||||
|
|
||||||
function handleBeforeunload(event) {
|
function handleBeforeunload(event: Event) {
|
||||||
if (hasChanged()) {
|
if (hasChanged()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.returnValue = "";
|
event.returnValue = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,18 +73,29 @@ export class Form {
|
|||||||
const deleteUserModal = document.getElementById("deleteUserModal");
|
const deleteUserModal = document.getElementById("deleteUserModal");
|
||||||
|
|
||||||
if (changesModal) {
|
if (changesModal) {
|
||||||
$("[data-command=continue]", changesModal).addEventListener("click", function () {
|
const continueCommand = $("[data-command=continue]", changesModal);
|
||||||
removeBeforeUnload();
|
if (continueCommand) {
|
||||||
window.location.href = this.dataset.href;
|
continueCommand.addEventListener("click", function () {
|
||||||
});
|
removeBeforeUnload();
|
||||||
|
if (this.dataset.href) {
|
||||||
|
window.location.href = this.dataset.href;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deletePageModal) {
|
if (deletePageModal) {
|
||||||
$("[data-command=delete]", deletePageModal).addEventListener("click", removeBeforeUnload);
|
const deleteCommand = $("[data-command=delete]", deletePageModal);
|
||||||
|
if (deleteCommand) {
|
||||||
|
deleteCommand.addEventListener("click", removeBeforeUnload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteUserModal) {
|
if (deleteUserModal) {
|
||||||
$("[data-command=delete]", deleteUserModal).addEventListener("click", removeBeforeUnload);
|
const deleteCommand = $("[data-command=delete]", deleteUserModal);
|
||||||
|
if (deleteCommand) {
|
||||||
|
deleteCommand.addEventListener("click", removeBeforeUnload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
14
panel/src/ts/components/forms.ts
Normal file
14
panel/src/ts/components/forms.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ import { app } from "../app";
|
|||||||
|
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
|
|
||||||
export function passIcon(icon, callback) {
|
export function passIcon(icon: string, callback: (iconData: string) => void) {
|
||||||
if (cache.has(icon)) {
|
if (cache.has(icon)) {
|
||||||
callback(cache.get(icon));
|
callback(cache.get(icon));
|
||||||
return;
|
return;
|
||||||
@ -22,6 +22,6 @@ export function passIcon(icon, callback) {
|
|||||||
request.send();
|
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));
|
passIcon(icon, (data) => element.insertAdjacentHTML(position, data));
|
||||||
}
|
}
|
66
panel/src/ts/components/inputs.ts
Normal file
66
panel/src/ts/components/inputs.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ import { $, $$ } from "../../utils/selectors";
|
|||||||
import Sortable from "sortablejs";
|
import Sortable from "sortablejs";
|
||||||
|
|
||||||
export class ArrayInput {
|
export class ArrayInput {
|
||||||
constructor(input) {
|
constructor(input: HTMLInputElement) {
|
||||||
const isAssociative = input.classList.contains("form-input-array-associative");
|
const isAssociative = input.classList.contains("form-input-array-associative");
|
||||||
const inputName = input.dataset.name;
|
const inputName = input.dataset.name;
|
||||||
|
|
||||||
@ -13,53 +13,55 @@ export class ArrayInput {
|
|||||||
forceFallback: true,
|
forceFallback: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
function addRow(row) {
|
function addRow(row: HTMLElement) {
|
||||||
const clone = row.cloneNode(true);
|
const clone = row.cloneNode(true) as HTMLElement;
|
||||||
|
const parent = row.parentNode as ParentNode;
|
||||||
clearRow(clone);
|
clearRow(clone);
|
||||||
bindRowEvents(clone);
|
bindRowEvents(clone);
|
||||||
if (row.nextSibling) {
|
if (row.nextSibling) {
|
||||||
row.parentNode.insertBefore(clone, row.nextSibling);
|
parent.insertBefore(clone, row.nextSibling);
|
||||||
} else {
|
} else {
|
||||||
row.parentNode.appendChild(clone);
|
parent.appendChild(clone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeRow(row) {
|
function removeRow(row: HTMLElement) {
|
||||||
if ($$(".form-input-array-row", row.parentNode).length > 1) {
|
const parent = row.parentNode as ParentNode;
|
||||||
row.parentNode.removeChild(row);
|
if ($$(".form-input-array-row", parent).length > 1) {
|
||||||
|
parent.removeChild(row);
|
||||||
} else {
|
} else {
|
||||||
clearRow(row);
|
clearRow(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearRow(row) {
|
function clearRow(row: HTMLElement) {
|
||||||
if (isAssociative) {
|
if (isAssociative) {
|
||||||
const inputKey = $(".form-input-array-key", row);
|
const inputKey = $(".form-input-array-key", row) as HTMLInputElement;
|
||||||
inputKey.value = "";
|
inputKey.value = "";
|
||||||
inputKey.removeAttribute("value");
|
inputKey.removeAttribute("value");
|
||||||
}
|
}
|
||||||
const inputValue = $(".form-input-array-value", row);
|
const inputValue = $(".form-input-array-value", row) as HTMLInputElement;
|
||||||
inputValue.value = "";
|
inputValue.value = "";
|
||||||
inputValue.removeAttribute("value");
|
inputValue.removeAttribute("value");
|
||||||
inputValue.name = `${inputName}[]`;
|
inputValue.name = `${inputName}[]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAssociativeRow(row) {
|
function updateAssociativeRow(row: HTMLElement) {
|
||||||
const inputKey = $(".form-input-array-key", row);
|
const inputKey = $(".form-input-array-key", row) as HTMLInputElement;
|
||||||
const inputValue = $(".form-input-array-value", row);
|
const inputValue = $(".form-input-array-value", row) as HTMLInputElement;
|
||||||
inputValue.name = `${inputName}[${inputKey.value.trim()}]`;
|
inputValue.name = `${inputName}[${inputKey.value.trim()}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindRowEvents(row) {
|
function bindRowEvents(row: HTMLElement) {
|
||||||
const inputAdd = $(".form-input-array-add", row);
|
const inputAdd = $(".form-input-array-add", row) as HTMLButtonElement;
|
||||||
const inputRemove = $(".form-input-array-remove", row);
|
const inputRemove = $(".form-input-array-remove", row) as HTMLButtonElement;
|
||||||
|
|
||||||
inputAdd.addEventListener("click", addRow.bind(inputAdd, row));
|
inputAdd.addEventListener("click", addRow.bind(inputAdd, row));
|
||||||
inputRemove.addEventListener("click", removeRow.bind(inputRemove, row));
|
inputRemove.addEventListener("click", removeRow.bind(inputRemove, row));
|
||||||
|
|
||||||
if (isAssociative) {
|
if (isAssociative) {
|
||||||
const inputKey = $(".form-input-array-key", row);
|
const inputKey = $(".form-input-array-key", row) as HTMLInputElement;
|
||||||
const inputValue = $(".form-input-array-value", row);
|
const inputValue = $(".form-input-array-value", row) as HTMLInputElement;
|
||||||
inputKey.addEventListener("keyup", updateAssociativeRow.bind(inputKey, row));
|
inputKey.addEventListener("keyup", updateAssociativeRow.bind(inputKey, row));
|
||||||
inputValue.addEventListener("keyup", updateAssociativeRow.bind(inputValue, row));
|
inputValue.addEventListener("keyup", updateAssociativeRow.bind(inputValue, row));
|
||||||
}
|
}
|
@ -3,14 +3,17 @@ import { getOuterHeight, getOuterWidth } from "../../utils/dimensions";
|
|||||||
import { insertIcon } from "../icons";
|
import { insertIcon } from "../icons";
|
||||||
import { throttle } from "../../utils/events";
|
import { throttle } from "../../utils/events";
|
||||||
|
|
||||||
const inputValues = {};
|
const inputValues: {
|
||||||
|
[id: string]: Date;
|
||||||
|
} = {};
|
||||||
|
|
||||||
function handleLongClick(element, callback, timeout, interval) {
|
function handleLongClick(element: HTMLElement, callback: (event: MouseEvent) => void, timeout: number, interval: number) {
|
||||||
let timer;
|
let timer: number;
|
||||||
function clear() {
|
function clear() {
|
||||||
clearTimeout(timer);
|
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;
|
const context = this;
|
||||||
if (event.button !== 0) {
|
if (event.button !== 0) {
|
||||||
clear();
|
clear();
|
||||||
@ -23,8 +26,26 @@ function handleLongClick(element, callback, timeout, interval) {
|
|||||||
window.addEventListener("mouseup", clear);
|
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 {
|
export class DateInput {
|
||||||
constructor(input, options) {
|
constructor(input: HTMLInputElement, userOptions: Partial<DateInputOptions> = {}) {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
weekStarts: 0,
|
weekStarts: 0,
|
||||||
format: "YYYY-MM-DD",
|
format: "YYYY-MM-DD",
|
||||||
@ -40,21 +61,20 @@ export class DateInput {
|
|||||||
short: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
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();
|
inputValues[input.id] = new Date();
|
||||||
|
|
||||||
const calendar = new Calendar($(".calendar"), inputValues[input.id]);
|
const calendar = Calendar($(".calendar") as HTMLElement, inputValues[input.id]);
|
||||||
|
|
||||||
options.onChange = (date) => {
|
|
||||||
const dateInput = getCurrentInput();
|
|
||||||
if (dateInput !== null) {
|
|
||||||
inputValues[dateInput.id] = date;
|
|
||||||
dateInput.value = formatDateTime(date);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
initInput();
|
initInput();
|
||||||
|
|
||||||
@ -78,7 +98,7 @@ export class DateInput {
|
|||||||
calendar.hide();
|
calendar.hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
input.addEventListener("keydown", (event) => {
|
input.addEventListener("keydown", (event: KeyboardEvent) => {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case "Backspace":
|
case "Backspace":
|
||||||
input.value = "";
|
input.value = "";
|
||||||
@ -96,18 +116,18 @@ export class DateInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentInput() {
|
function getCurrentInput() {
|
||||||
const currentElement = document.activeElement;
|
const currentElement = document.activeElement as HTMLInputElement;
|
||||||
return currentElement.matches(".form-input-date") ? currentElement : null;
|
return currentElement.matches(".form-input-date") ? currentElement : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Calendar(element, date) {
|
function Calendar(element: HTMLElement, date: Date) {
|
||||||
let year, month, day, hours, minutes, seconds;
|
let year: number, month: number, day: number, hours: number, minutes: number, seconds: number;
|
||||||
|
|
||||||
element = element || createElement();
|
element = element || createElement();
|
||||||
|
|
||||||
setDate(date);
|
setDate(date);
|
||||||
|
|
||||||
function setDate(date) {
|
function setDate(date: Date) {
|
||||||
year = date.getFullYear();
|
year = date.getFullYear();
|
||||||
month = date.getMonth();
|
month = date.getMonth();
|
||||||
day = date.getDate();
|
day = date.getDate();
|
||||||
@ -116,7 +136,7 @@ export class DateInput {
|
|||||||
seconds = date.getSeconds();
|
seconds = date.getSeconds();
|
||||||
}
|
}
|
||||||
|
|
||||||
function gotoDate(date) {
|
function gotoDate(date: Date) {
|
||||||
setDate(date);
|
setDate(date);
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
@ -333,7 +353,7 @@ export class DateInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
$(".calendar-table", element).innerHTML = getInnerHTML();
|
($(".calendar-table", element) as HTMLElement).innerHTML = getInnerHTML();
|
||||||
|
|
||||||
setEvents();
|
setEvents();
|
||||||
|
|
||||||
@ -394,7 +414,7 @@ export class DateInput {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
element.addEventListener("click", () => {
|
element.addEventListener("click", () => {
|
||||||
day = parseInt(element.textContent);
|
day = parseInt(`${element.textContent}`);
|
||||||
update();
|
update();
|
||||||
options.onChange(getDate());
|
options.onChange(getDate());
|
||||||
});
|
});
|
||||||
@ -402,9 +422,9 @@ export class DateInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateTime() {
|
function updateTime() {
|
||||||
$(".calendar-hours", element).innerHTML = pad(has12HourFormat(options.format) ? mod(hours, 12) || 12 : hours, 2);
|
($(".calendar-hours", element) as HTMLElement).innerHTML = pad(has12HourFormat(options.format) ? mod(hours, 12) || 12 : hours, 2);
|
||||||
$(".calendar-minutes", element).innerHTML = pad(minutes, 2);
|
($(".calendar-minutes", element) as HTMLElement).innerHTML = pad(minutes, 2);
|
||||||
$(".calendar-meridiem", element).innerHTML = has12HourFormat(options.format) ? (hours < 12 ? "AM" : "PM") : "";
|
($(".calendar-meridiem", element) as HTMLElement).innerHTML = has12HourFormat(options.format) ? (hours < 12 ? "AM" : "PM") : "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -416,26 +436,26 @@ export class DateInput {
|
|||||||
if (options.time) {
|
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>';
|
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-down", $(".prevHour", element) as HTMLElement);
|
||||||
insertIcon("chevron-up", $(".nextHour", element));
|
insertIcon("chevron-up", $(".nextHour", element) as HTMLElement);
|
||||||
|
|
||||||
insertIcon("chevron-down", $(".prevMinute", element));
|
insertIcon("chevron-down", $(".prevMinute", element) as HTMLElement);
|
||||||
insertIcon("chevron-up", $(".nextMinute", element));
|
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-left", $(".prevMonth", element) as HTMLElement);
|
||||||
insertIcon("chevron-right", $(".nextMonth", element));
|
insertIcon("chevron-right", $(".nextMonth", element) as HTMLElement);
|
||||||
|
|
||||||
$(".currentMonth", element).addEventListener("mousedown", (event) => {
|
($(".currentMonth", element) as HTMLElement).addEventListener("mousedown", (event) => {
|
||||||
now();
|
now();
|
||||||
options.onChange(getDate());
|
options.onChange(getDate());
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
handleLongClick(
|
handleLongClick(
|
||||||
$(".prevMonth", element),
|
$(".prevMonth", element) as HTMLElement,
|
||||||
(event) => {
|
(event) => {
|
||||||
prevMonth();
|
prevMonth();
|
||||||
options.onChange(getDate());
|
options.onChange(getDate());
|
||||||
@ -446,7 +466,7 @@ export class DateInput {
|
|||||||
);
|
);
|
||||||
|
|
||||||
handleLongClick(
|
handleLongClick(
|
||||||
$(".nextMonth", element),
|
$(".nextMonth", element) as HTMLElement,
|
||||||
(event) => {
|
(event) => {
|
||||||
nextMonth();
|
nextMonth();
|
||||||
options.onChange(getDate());
|
options.onChange(getDate());
|
||||||
@ -458,7 +478,7 @@ export class DateInput {
|
|||||||
|
|
||||||
if (options.time) {
|
if (options.time) {
|
||||||
handleLongClick(
|
handleLongClick(
|
||||||
$(".nextHour", element),
|
$(".nextHour", element) as HTMLElement,
|
||||||
(event) => {
|
(event) => {
|
||||||
nextHour();
|
nextHour();
|
||||||
options.onChange(getDate());
|
options.onChange(getDate());
|
||||||
@ -469,7 +489,7 @@ export class DateInput {
|
|||||||
);
|
);
|
||||||
|
|
||||||
handleLongClick(
|
handleLongClick(
|
||||||
$(".prevHour", element),
|
$(".prevHour", element) as HTMLElement,
|
||||||
(event) => {
|
(event) => {
|
||||||
prevHour();
|
prevHour();
|
||||||
options.onChange(getDate());
|
options.onChange(getDate());
|
||||||
@ -480,7 +500,7 @@ export class DateInput {
|
|||||||
);
|
);
|
||||||
|
|
||||||
handleLongClick(
|
handleLongClick(
|
||||||
$(".nextMinute", element),
|
$(".nextMinute", element) as HTMLElement,
|
||||||
(event) => {
|
(event) => {
|
||||||
nextMinute();
|
nextMinute();
|
||||||
options.onChange(getDate());
|
options.onChange(getDate());
|
||||||
@ -491,7 +511,7 @@ export class DateInput {
|
|||||||
);
|
);
|
||||||
|
|
||||||
handleLongClick(
|
handleLongClick(
|
||||||
$(".prevMinute", element),
|
$(".prevMinute", element) as HTMLElement,
|
||||||
(event) => {
|
(event) => {
|
||||||
prevMinute();
|
prevMinute();
|
||||||
options.onChange(getDate());
|
options.onChange(getDate());
|
||||||
@ -506,7 +526,7 @@ export class DateInput {
|
|||||||
|
|
||||||
window.addEventListener("mousedown", (event) => {
|
window.addEventListener("mousedown", (event) => {
|
||||||
if (element.style.display !== "none") {
|
if (element.style.display !== "none") {
|
||||||
if (event.target.closest(".calendar")) {
|
if ((event.target as HTMLElement).closest(".calendar")) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -518,7 +538,7 @@ export class DateInput {
|
|||||||
}
|
}
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case "Enter":
|
case "Enter":
|
||||||
$(".calendar-day.selected", element).click();
|
($(".calendar-day.selected", element) as HTMLElement).click();
|
||||||
hide();
|
hide();
|
||||||
break;
|
break;
|
||||||
case "Backspace":
|
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 mod y (always rounded downwards, differs from x % y which is the remainder)
|
||||||
return x - y * Math.floor(x / y);
|
return x - y * Math.floor(x / y);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pad(num, length) {
|
function pad(num: number, length: number) {
|
||||||
let result = num.toString();
|
let result = num.toString();
|
||||||
while (result.length < length) {
|
while (result.length < length) {
|
||||||
result = `0${result}`;
|
result = `0${result}`;
|
||||||
@ -649,26 +669,26 @@ export class DateInput {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidDate(date) {
|
function isValidDate(date: string) {
|
||||||
return date && !isNaN(Date.parse(date));
|
return date && !isNaN(Date.parse(date));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLeapYear(year) {
|
function isLeapYear(year: number) {
|
||||||
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
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];
|
const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||||
return month === 1 && isLeapYear(year) ? 29 : daysInMonth[month];
|
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();
|
let day = date.getDate();
|
||||||
day -= mod(date.getDay() - firstDay, 7);
|
day -= mod(date.getDay() - firstDay, 7);
|
||||||
return new Date(date.getFullYear(), date.getMonth(), day);
|
return new Date(date.getFullYear(), date.getMonth(), day);
|
||||||
}
|
}
|
||||||
|
|
||||||
function weekNumberingYear(date) {
|
function weekNumberingYear(date: Date) {
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const thisYearFirstWeekStart = weekStart(new Date(year, 0, 4), 1);
|
const thisYearFirstWeekStart = weekStart(new Date(year, 0, 4), 1);
|
||||||
const nextYearFirstWeekStart = weekStart(new Date(year + 1, 0, 4), 1);
|
const nextYearFirstWeekStart = weekStart(new Date(year + 1, 0, 4), 1);
|
||||||
@ -680,22 +700,22 @@ export class DateInput {
|
|||||||
return year - 1;
|
return year - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function weekOfYear(date) {
|
function weekOfYear(date: Date) {
|
||||||
const weekNumberingYear = weekNumberingYear(date);
|
const dateWeekNumberingYear = weekNumberingYear(date);
|
||||||
const firstWeekStart = weekStart(new Date(weekNumberingYear, 0, 4), 1);
|
const dateFirstWeekStart = weekStart(new Date(dateWeekNumberingYear, 0, 4), 1);
|
||||||
const weekStart = weekStart(date, 1);
|
const dateWeekStart = weekStart(date, 1);
|
||||||
return Math.round((weekStart.getTime() - firstWeekStart.getTime()) / 604800000) + 1;
|
return Math.round((dateWeekStart.getTime() - dateFirstWeekStart.getTime()) / 604800000) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function has12HourFormat(format) {
|
function has12HourFormat(format: string) {
|
||||||
const match = format.match(/\[([^\]]*)\]|H{1,2}/);
|
const match = format.match(/\[([^\]]*)\]|H{1,2}/);
|
||||||
return match !== null && match[0][0] === "H";
|
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;
|
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()
|
// Note that the offset returned by Date.getTimezoneOffset()
|
||||||
// is positive if behind UTC and negative if ahead UTC
|
// is positive if behind UTC and negative if ahead UTC
|
||||||
const sign = offset > 0 ? "-" : "+";
|
const sign = offset > 0 ? "-" : "+";
|
||||||
@ -704,7 +724,7 @@ export class DateInput {
|
|||||||
return [sign + pad(hours, 2), pad(minutes, 2)];
|
return [sign + pad(hours, 2), pad(minutes, 2)];
|
||||||
}
|
}
|
||||||
|
|
||||||
return format.replace(regex, (match, $1) => {
|
return format.replace(regex, (match: string, $1) => {
|
||||||
switch (match) {
|
switch (match) {
|
||||||
case "YY":
|
case "YY":
|
||||||
return date.getFullYear().toString().substr(-2);
|
return date.getFullYear().toString().substr(-2);
|
@ -1,6 +1,6 @@
|
|||||||
import { $ } from "../../utils/selectors";
|
import { $ } from "../../utils/selectors";
|
||||||
|
|
||||||
function getSafeInteger(value) {
|
function getSafeInteger(value: number) {
|
||||||
const max = Number.MAX_SAFE_INTEGER;
|
const max = Number.MAX_SAFE_INTEGER;
|
||||||
const min = -max;
|
const min = -max;
|
||||||
if (value > max) {
|
if (value > max) {
|
||||||
@ -9,12 +9,31 @@ function getSafeInteger(value) {
|
|||||||
if (value < min) {
|
if (value < min) {
|
||||||
return 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 {
|
export class DurationInput {
|
||||||
constructor(input, options) {
|
constructor(input: HTMLInputElement, userOptions: Partial<DurationInputOptions>) {
|
||||||
const defaults = {
|
const defaults: DurationInputOptions = {
|
||||||
unit: "seconds",
|
unit: "seconds",
|
||||||
intervals: ["years", "months", "weeks", "days", "hours", "minutes", "seconds"],
|
intervals: ["years", "months", "weeks", "days", "hours", "minutes", "seconds"],
|
||||||
labels: {
|
labels: {
|
||||||
@ -28,91 +47,81 @@ export class DurationInput {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const TIME_INTERVALS = {
|
let field: HTMLElement, hiddenInput: HTMLInputElement;
|
||||||
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, hiddenInput;
|
const innerInputs: Partial<Record<TimeInterval, HTMLInputElement>> = {};
|
||||||
|
|
||||||
const innerInputs = {};
|
const labels: Partial<Record<TimeInterval, HTMLLabelElement>> = {};
|
||||||
|
|
||||||
const labels = {};
|
const options = Object.assign({}, defaults, userOptions);
|
||||||
|
|
||||||
options = Object.assign({}, defaults, options);
|
|
||||||
|
|
||||||
createField();
|
createField();
|
||||||
|
|
||||||
function secondsToIntervals(seconds, intervalNames = options.intervals) {
|
function secondsToIntervals(seconds: number, intervalNames: TimeInterval[] = options.intervals) {
|
||||||
const intervals = {};
|
const intervals: Partial<Record<TimeInterval, number>> = {};
|
||||||
seconds = getSafeInteger(seconds);
|
seconds = getSafeInteger(seconds);
|
||||||
for (const t in TIME_INTERVALS) {
|
Object.keys(TIME_INTERVALS).forEach((t: TimeInterval) => {
|
||||||
if (intervalNames.includes(t)) {
|
if (intervalNames.includes(t)) {
|
||||||
intervals[t] = Math.floor(seconds / TIME_INTERVALS[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;
|
return intervals;
|
||||||
}
|
}
|
||||||
|
|
||||||
function intervalsToSeconds(intervals) {
|
function intervalsToSeconds(intervals: Partial<Record<TimeInterval, number>>) {
|
||||||
let seconds = 0;
|
let seconds = 0;
|
||||||
for (const interval in intervals) {
|
Object.entries(intervals).forEach(([interval, value]: [TimeInterval, number]) => {
|
||||||
seconds += intervals[interval] * TIME_INTERVALS[interval];
|
seconds += value * TIME_INTERVALS[interval];
|
||||||
}
|
});
|
||||||
return getSafeInteger(seconds);
|
return getSafeInteger(seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateHiddenInput() {
|
function updateHiddenInput() {
|
||||||
const intervals = {};
|
const intervals: Partial<Record<TimeInterval, number>> = {};
|
||||||
let seconds = 0;
|
let seconds = 0;
|
||||||
let step = 0;
|
let step = 0;
|
||||||
for (const i in innerInputs) {
|
Object.entries(innerInputs).forEach(([i, input]: [TimeInterval, HTMLInputElement]) => {
|
||||||
intervals[i] = innerInputs[i].value;
|
intervals[i] = parseInt(input.value);
|
||||||
}
|
});
|
||||||
seconds = intervalsToSeconds(intervals);
|
seconds = intervalsToSeconds(intervals);
|
||||||
if (hiddenInput.step) {
|
if (hiddenInput.step) {
|
||||||
step = hiddenInput.step * TIME_INTERVALS[options.unit];
|
step = parseInt(hiddenInput.step) * TIME_INTERVALS[options.unit];
|
||||||
seconds = Math.floor(seconds / step) * step;
|
seconds = Math.floor(seconds / step) * step;
|
||||||
}
|
}
|
||||||
if (hiddenInput.min) {
|
if (hiddenInput.min) {
|
||||||
seconds = Math.max(seconds, hiddenInput.min);
|
seconds = Math.max(seconds, parseInt(hiddenInput.min));
|
||||||
}
|
}
|
||||||
if (hiddenInput.max) {
|
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() {
|
function updateInnerInputs() {
|
||||||
const intervals = secondsToIntervals(hiddenInput.value * TIME_INTERVALS[options.unit]);
|
const intervals = secondsToIntervals(parseInt(hiddenInput.value) * TIME_INTERVALS[options.unit]);
|
||||||
for (const i in innerInputs) {
|
Object.entries(innerInputs).forEach(([i, input]: [TimeInterval, HTMLInputElement]) => {
|
||||||
innerInputs[i].value = intervals[i];
|
input.value = `${intervals[i] || 0}`;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateInnerInputsLength() {
|
function updateInnerInputsLength() {
|
||||||
for (const i in innerInputs) {
|
Object.values(innerInputs).forEach((input) => {
|
||||||
innerInputs[i].style.width = `${Math.max(3, innerInputs[i].value.length + 2)}ch`;
|
input.style.width = `${Math.max(3, input.value.length + 2)}ch`;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLabels() {
|
function updateLabels() {
|
||||||
for (const i in innerInputs) {
|
Object.entries(innerInputs).forEach(([i, input]: [TimeInterval, HTMLInputElement]) => {
|
||||||
labels[i].innerHTML = options.labels[i][parseInt(innerInputs[i].value) === 1 ? 0 : 1];
|
(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 = document.createElement("div");
|
||||||
field.className = "form-input-duration";
|
field.className = "form-input-duration";
|
||||||
|
|
||||||
let innerInput;
|
let innerInput: HTMLInputElement;
|
||||||
|
|
||||||
for (const name of options.intervals) {
|
for (const name of options.intervals) {
|
||||||
innerInput = document.createElement("input");
|
innerInput = document.createElement("input");
|
||||||
@ -120,10 +129,10 @@ export class DurationInput {
|
|||||||
const wrap = document.createElement("span");
|
const wrap = document.createElement("span");
|
||||||
wrap.className = `duration-${name}`;
|
wrap.className = `duration-${name}`;
|
||||||
innerInput.type = "number";
|
innerInput.type = "number";
|
||||||
innerInput.value = intervals[name] || 0;
|
innerInput.value = `${intervals[name] || 0}`;
|
||||||
innerInput.style.width = `${Math.max(3, innerInput.value.length + 2)}ch`;
|
innerInput.style.width = `${Math.max(3, innerInput.value.length + 2)}ch`;
|
||||||
if (steps[name] > 1) {
|
if ((steps[name] as number) > 1) {
|
||||||
innerInput.step = steps[name];
|
innerInput.step = `${steps[name]}`;
|
||||||
}
|
}
|
||||||
if (input.disabled) {
|
if (input.disabled) {
|
||||||
innerInput.disabled = true;
|
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(/[.,]/)) {
|
while (this.value.charAt(0) === "0" && this.value.length > 1 && !this.value.charAt(1).match(/[.,]/)) {
|
||||||
this.value = this.value.slice(1);
|
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);
|
this.value = this.value.slice(0, -1);
|
||||||
}
|
}
|
||||||
updateInnerInputsLength();
|
updateInnerInputsLength();
|
||||||
@ -151,7 +160,7 @@ export class DurationInput {
|
|||||||
|
|
||||||
innerInput.addEventListener("blur", () => field.classList.remove("focused"));
|
innerInput.addEventListener("blur", () => field.classList.remove("focused"));
|
||||||
|
|
||||||
wrap.addEventListener("mousedown", function (event) {
|
wrap.addEventListener("mousedown", function (event: MouseEvent) {
|
||||||
const input = $("input", this);
|
const input = $("input", this);
|
||||||
if (input && event.target !== input) {
|
if (input && event.target !== input) {
|
||||||
input.focus();
|
input.focus();
|
||||||
@ -167,7 +176,7 @@ export class DurationInput {
|
|||||||
field.appendChild(wrap);
|
field.appendChild(wrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
field.addEventListener("mousedown", function (event) {
|
field.addEventListener("mousedown", function (event: MouseEvent) {
|
||||||
if (event.target === this) {
|
if (event.target === this) {
|
||||||
innerInput.focus();
|
innerInput.focus();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -202,15 +211,15 @@ export class DurationInput {
|
|||||||
hiddenInput.disabled = true;
|
hiddenInput.disabled = true;
|
||||||
}
|
}
|
||||||
if ("intervals" in input.dataset) {
|
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) {
|
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 valueSeconds = parseInt(input.value) * TIME_INTERVALS[options.unit];
|
||||||
const stepSeconds = input.step * TIME_INTERVALS[options.unit];
|
const stepSeconds = parseInt(input.step) * TIME_INTERVALS[options.unit];
|
||||||
const field = createInnerInputs(secondsToIntervals(valueSeconds || 0), secondsToIntervals(stepSeconds || 1));
|
const field = createInnerInputs(secondsToIntervals(valueSeconds || 0), secondsToIntervals(stepSeconds || 1));
|
||||||
input.parentNode.replaceChild(field, input);
|
(input.parentNode as ParentNode).replaceChild(field, input);
|
||||||
field.appendChild(hiddenInput);
|
field.appendChild(hiddenInput);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,16 +1,16 @@
|
|||||||
import CodeMirror from "codemirror/lib/codemirror.js";
|
|
||||||
|
|
||||||
import { $ } from "../../utils/selectors";
|
import { $ } from "../../utils/selectors";
|
||||||
import { app } from "../../app";
|
import { app } from "../../app";
|
||||||
import { arrayEquals } from "../../utils/arrays";
|
import { arrayEquals } from "../../utils/arrays";
|
||||||
import { debounce } from "../../utils/events";
|
import { debounce } from "../../utils/events";
|
||||||
|
|
||||||
|
import CodeMirror from "codemirror";
|
||||||
|
|
||||||
import "codemirror/mode/markdown/markdown.js";
|
import "codemirror/mode/markdown/markdown.js";
|
||||||
import "codemirror/addon/display/placeholder.js";
|
import "codemirror/addon/display/placeholder.js";
|
||||||
import "codemirror/addon/edit/continuelist.js";
|
import "codemirror/addon/edit/continuelist.js";
|
||||||
|
|
||||||
export class EditorInput {
|
export class EditorInput {
|
||||||
constructor(textarea) {
|
constructor(textarea: HTMLTextAreaElement) {
|
||||||
const height = textarea.offsetHeight;
|
const height = textarea.offsetHeight;
|
||||||
|
|
||||||
const editor = CodeMirror.fromTextArea(textarea, {
|
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`;
|
editor.getWrapperElement().style.height = `${height}px`;
|
||||||
|
|
||||||
$("[data-command=bold]", toolbar).addEventListener("click", () => {
|
$("[data-command=bold]", toolbar)?.addEventListener("click", () => {
|
||||||
insertAtCursor("**");
|
insertAtCursor("**");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("[data-command=italic]", toolbar).addEventListener("click", () => {
|
$("[data-command=italic]", toolbar)?.addEventListener("click", () => {
|
||||||
insertAtCursor("_");
|
insertAtCursor("_");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("[data-command=ul]", toolbar).addEventListener("click", () => {
|
$("[data-command=ul]", toolbar)?.addEventListener("click", () => {
|
||||||
insertAtCursor(`${prependSequence()}- `, "");
|
insertAtCursor(`${prependSequence()}- `, "");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("[data-command=ol]", toolbar).addEventListener("click", () => {
|
$("[data-command=ol]", toolbar)?.addEventListener("click", () => {
|
||||||
const num = /^\d+\./.exec(lastLine(editor.getValue()));
|
const num = /^(\d+)\./.exec(lastLine(editor.getValue()));
|
||||||
if (num) {
|
if (num) {
|
||||||
insertAtCursor(`\n${parseInt(num) + 1}. `, "");
|
insertAtCursor(`\n${parseInt(num[1]) + 1}. `, "");
|
||||||
} else {
|
} else {
|
||||||
insertAtCursor(`${prependSequence()}1. `, "");
|
insertAtCursor(`${prependSequence()}1. `, "");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$("[data-command=quote]", toolbar).addEventListener("click", () => {
|
$("[data-command=quote]", toolbar)?.addEventListener("click", () => {
|
||||||
insertAtCursor(`${prependSequence()}> `, "");
|
insertAtCursor(`${prependSequence()}> `, "");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("[data-command=link]", toolbar).addEventListener("click", () => {
|
$("[data-command=link]", toolbar)?.addEventListener("click", () => {
|
||||||
const selection = editor.getSelection();
|
const selection = editor.getSelection();
|
||||||
if (/^(https?:\/\/|mailto:)/i.test(selection)) {
|
if (/^(https?:\/\/|mailto:)/i.test(selection)) {
|
||||||
insertAtCursor("[", `](${selection})`, true);
|
insertAtCursor("[", `](${selection})`, true);
|
||||||
@ -73,13 +73,13 @@ export class EditorInput {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$("[data-command=image]", toolbar).addEventListener("click", () => {
|
$("[data-command=image]", toolbar)?.addEventListener("click", () => {
|
||||||
app.modals["imagesModal"].show(null, (modal) => {
|
app.modals["imagesModal"].show(undefined, (modal) => {
|
||||||
const selected = $(".image-picker-thumbnail.selected", modal.element);
|
const selected = $(".image-picker-thumbnail.selected", modal.element);
|
||||||
if (selected) {
|
if (selected) {
|
||||||
selected.classList.remove("selected");
|
selected.classList.remove("selected");
|
||||||
}
|
}
|
||||||
function confirmImage() {
|
function confirmImage(this: HTMLElement) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
const filename = selected.dataset.filename;
|
const filename = selected.dataset.filename;
|
||||||
insertAtCursor(`${prependSequence()}![`, `](${filename})`);
|
insertAtCursor(`${prependSequence()}![`, `](${filename})`);
|
||||||
@ -87,16 +87,16 @@ export class EditorInput {
|
|||||||
modal.hide();
|
modal.hide();
|
||||||
this.removeEventListener("click", confirmImage);
|
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.undo();
|
||||||
editor.focus();
|
editor.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("[data-command=redo]", toolbar).addEventListener("click", () => {
|
$("[data-command=redo]", toolbar)?.addEventListener("click", () => {
|
||||||
editor.redo();
|
editor.redo();
|
||||||
editor.focus();
|
editor.focus();
|
||||||
});
|
});
|
||||||
@ -106,14 +106,14 @@ export class EditorInput {
|
|||||||
debounce(() => {
|
debounce(() => {
|
||||||
textarea.value = editor.getValue();
|
textarea.value = editor.getValue();
|
||||||
if (editor.historySize().undo < 1) {
|
if (editor.historySize().undo < 1) {
|
||||||
$("[data-command=undo]").disabled = true;
|
($("[data-command=undo]") as HTMLButtonElement).disabled = true;
|
||||||
} else {
|
} else {
|
||||||
$("[data-command=undo]").disabled = false;
|
($("[data-command=undo]") as HTMLButtonElement).disabled = false;
|
||||||
}
|
}
|
||||||
if (editor.historySize().redo < 1) {
|
if (editor.historySize().redo < 1) {
|
||||||
$("[data-command=redo]").disabled = true;
|
($("[data-command=redo]") as HTMLButtonElement).disabled = true;
|
||||||
} else {
|
} else {
|
||||||
$("[data-command=redo]").disabled = false;
|
($("[data-command=redo]") as HTMLButtonElement).disabled = false;
|
||||||
}
|
}
|
||||||
}, 500),
|
}, 500),
|
||||||
);
|
);
|
||||||
@ -148,22 +148,22 @@ export class EditorInput {
|
|||||||
if (!event.altKey && (event.ctrlKey || event.metaKey)) {
|
if (!event.altKey && (event.ctrlKey || event.metaKey)) {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case "b":
|
case "b":
|
||||||
$("[data-command=bold]", toolbar).click();
|
$("[data-command=bold]", toolbar)?.click();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
break;
|
break;
|
||||||
case "i":
|
case "i":
|
||||||
$("[data-command=italic]", toolbar).click();
|
$("[data-command=italic]", toolbar)?.click();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
break;
|
break;
|
||||||
case "k":
|
case "k":
|
||||||
$("[data-command=link]", toolbar).click();
|
$("[data-command=link]", toolbar)?.click();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function lastLine(text) {
|
function lastLine(text: string) {
|
||||||
const index = text.lastIndexOf("\n");
|
const index = text.lastIndexOf("\n");
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
return text;
|
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) {
|
if (rightValue === undefined) {
|
||||||
rightValue = leftValue;
|
rightValue = leftValue;
|
||||||
}
|
}
|
||||||
@ -199,21 +199,21 @@ export class EditorInput {
|
|||||||
editor.focus();
|
editor.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLinesFromRange(ranges) {
|
function getLinesFromRange(ranges: CodeMirror.Range[]) {
|
||||||
const lines = [];
|
const lines: number[] = [];
|
||||||
for (const range of ranges) {
|
for (const range of ranges) {
|
||||||
lines.push(range.head.line);
|
lines.push(range.head.line);
|
||||||
}
|
}
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeActiveLines(editor, lines) {
|
function removeActiveLines(editor: CodeMirror.Editor, lines: number[]) {
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
editor.removeLineClass(line, "wrap", "CodeMirror-activeline");
|
editor.removeLineClass(line, "wrap", "CodeMirror-activeline");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addActiveLines(editor, lines) {
|
function addActiveLines(editor: CodeMirror.Editor, lines: number[]) {
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
editor.addLineClass(line, "wrap", "CodeMirror-activeline");
|
editor.addLineClass(line, "wrap", "CodeMirror-activeline");
|
||||||
}
|
}
|
@ -1,18 +1,18 @@
|
|||||||
import { $ } from "../../utils/selectors";
|
import { $ } from "../../utils/selectors";
|
||||||
|
|
||||||
export class FileInput {
|
export class FileInput {
|
||||||
constructor(input) {
|
constructor(input: HTMLInputElement) {
|
||||||
const label = $(`label[for="${input.id}"]`);
|
const label = $(`label[for="${input.id}"]`) as HTMLElement;
|
||||||
const span = $("span", label);
|
const span = $("span", label) as HTMLElement;
|
||||||
|
|
||||||
let isSubmitted = false;
|
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("change", updateLabel);
|
||||||
input.addEventListener("input", updateLabel);
|
input.addEventListener("input", updateLabel);
|
||||||
|
|
||||||
input.form.addEventListener("submit", () => {
|
input.form?.addEventListener("submit", () => {
|
||||||
if (input.files.length > 0) {
|
if (input.files && input.files.length > 0) {
|
||||||
span.innerHTML += ' <span class="spinner"></span>';
|
span.innerHTML += ' <span class="spinner"></span>';
|
||||||
}
|
}
|
||||||
isSubmitted = true;
|
isSubmitted = true;
|
||||||
@ -30,9 +30,11 @@ export class FileInput {
|
|||||||
if (isSubmitted) {
|
if (isSubmitted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
input.files = event.dataTransfer.files;
|
if (event.dataTransfer) {
|
||||||
// Firefox won't trigger a change event, so we explicitly do that
|
input.files = event.dataTransfer.files;
|
||||||
input.dispatchEvent(new Event("change"));
|
// Firefox won't trigger a change event, so we explicitly do that
|
||||||
|
input.dispatchEvent(new Event("change"));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
label.addEventListener("click", (event) => {
|
label.addEventListener("click", (event) => {
|
||||||
@ -41,28 +43,28 @@ export class FileInput {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateLabel() {
|
function updateLabel(this: HTMLInputElement) {
|
||||||
if (this.files.length > 0) {
|
if (this.files && this.files.length > 0) {
|
||||||
const filenames = [];
|
const filenames: string[] = [];
|
||||||
for (const file of this.files) {
|
for (const file of Array.from(this.files)) {
|
||||||
filenames.push(file.name);
|
filenames.push(file.name);
|
||||||
}
|
}
|
||||||
span.innerHTML = filenames.join(", ");
|
span.innerHTML = filenames.join(", ");
|
||||||
} else {
|
} else {
|
||||||
span.innerHTML = this.dataset.label;
|
span.innerHTML = this.dataset.label as string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function preventDefault(event) {
|
function preventDefault(event: Event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDragenter(event) {
|
function handleDragenter(this: HTMLInputElement, event: DragEvent) {
|
||||||
this.classList.add("drag");
|
this.classList.add("drag");
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDragleave(event) {
|
function handleDragleave(this: HTMLInputElement, event: DragEvent) {
|
||||||
this.classList.remove("drag");
|
this.classList.remove("drag");
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
@ -2,9 +2,9 @@ import { $ } from "../../utils/selectors";
|
|||||||
import { app } from "../../app";
|
import { app } from "../../app";
|
||||||
|
|
||||||
export class ImageInput {
|
export class ImageInput {
|
||||||
constructor(element) {
|
constructor(element: HTMLInputElement) {
|
||||||
element.addEventListener("click", () => {
|
element.addEventListener("click", () => {
|
||||||
app.modals["imagesModal"].show(null, (modal) => {
|
app.modals["imagesModal"].show(undefined, (modal) => {
|
||||||
const selected = $(".image-picker-thumbnail.selected", modal.element);
|
const selected = $(".image-picker-thumbnail.selected", modal.element);
|
||||||
if (selected) {
|
if (selected) {
|
||||||
selected.classList.remove("selected");
|
selected.classList.remove("selected");
|
||||||
@ -15,8 +15,9 @@ export class ImageInput {
|
|||||||
thumbnail.classList.add("selected");
|
thumbnail.classList.add("selected");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$(".image-picker-confirm", modal.element).dataset.target = element.id;
|
const confirm = $(".image-picker-confirm", modal.element) as HTMLElement;
|
||||||
$(".image-picker-confirm", modal.element).addEventListener("click", () => modal.hide());
|
confirm.dataset.target = element.id;
|
||||||
|
confirm.addEventListener("click", () => modal.hide());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
69
panel/src/ts/components/inputs/image-picker.ts
Normal file
69
panel/src/ts/components/inputs/image-picker.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +1,22 @@
|
|||||||
import { $ } from "../../utils/selectors";
|
import { $ } from "../../utils/selectors";
|
||||||
|
|
||||||
export class RangeInput {
|
export class RangeInput {
|
||||||
constructor(input) {
|
constructor(input: HTMLInputElement) {
|
||||||
input.addEventListener("change", updateValueLabel);
|
input.addEventListener("change", updateValueLabel);
|
||||||
input.addEventListener("input", updateValueLabel);
|
input.addEventListener("input", updateValueLabel);
|
||||||
|
|
||||||
updateValueLabel.call(input);
|
updateValueLabel.call(input);
|
||||||
|
|
||||||
if ("ticks" in input.dataset) {
|
if ("ticks" in input.dataset) {
|
||||||
const count = input.dataset.ticks;
|
const count = input.dataset.ticks as string;
|
||||||
|
|
||||||
switch (count) {
|
switch (count) {
|
||||||
case 0:
|
case "0":
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "true":
|
case "true":
|
||||||
case "":
|
case "":
|
||||||
addTicks((input.max - input.min) / (input.step || 1) + 1);
|
addTicks((parseInt(input.max) - parseInt(input.min)) / (parseInt(input.step) || 1) + 1);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -25,16 +25,19 @@ export class RangeInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateValueLabel() {
|
function updateValueLabel(this: HTMLInputElement) {
|
||||||
this.style.setProperty("--progress", `${Math.round((this.value / (this.max - this.min)) * 100)}%`);
|
this.style.setProperty("--progress", `${Math.round((parseInt(this.value) / (parseInt(this.max) - parseInt(this.min))) * 100)}%`);
|
||||||
$(`output[for="${this.id}"]`).innerHTML = this.value;
|
const outputElement = $(`output[for="${this.id}"]`);
|
||||||
|
if (outputElement) {
|
||||||
|
outputElement.innerHTML = this.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTicks(count) {
|
function addTicks(count: number) {
|
||||||
const ticks = document.createElement("div");
|
const ticks = document.createElement("div");
|
||||||
ticks.className = "form-input-range-ticks";
|
ticks.className = "form-input-range-ticks";
|
||||||
ticks.dataset.for = input.id;
|
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++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const tick = document.createElement("div");
|
const tick = document.createElement("div");
|
@ -1,13 +1,27 @@
|
|||||||
import { $, $$ } from "../../utils/selectors";
|
import { $, $$ } from "../../utils/selectors";
|
||||||
import { escapeRegExp, makeDiacriticsRegExp } from "../../utils/validation";
|
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 {
|
export class SelectInput {
|
||||||
constructor(select, options) {
|
constructor(select: HTMLSelectElement, userOptions: Partial<SelectInputOptions>) {
|
||||||
const defaults = { labels: { empty: "No matching options" } };
|
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");
|
const labelInput = document.createElement("input");
|
||||||
|
|
||||||
@ -40,13 +54,13 @@ export class SelectInput {
|
|||||||
labelInput.dataset[key] = select.dataset[key];
|
labelInput.dataset[key] = select.dataset[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = [];
|
const list: SelectInputListItem[] = [];
|
||||||
|
|
||||||
$$("option", select).forEach((option) => {
|
$$("option", select).forEach((option: HTMLOptionElement) => {
|
||||||
const dataset = {};
|
const dataset: Record<string, string> = {};
|
||||||
|
|
||||||
for (const key in option.dataset) {
|
for (const key in option.dataset) {
|
||||||
dataset[key] = option.dataset[key];
|
dataset[key] = option.dataset[key] as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.push({
|
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);
|
wrap.appendChild(select);
|
||||||
|
|
||||||
@ -71,7 +85,7 @@ export class SelectInput {
|
|||||||
createDropdown(list, wrap);
|
createDropdown(list, wrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDropdown(list, wrap) {
|
function createDropdown(list: SelectInputListItem[], wrap: HTMLElement) {
|
||||||
dropdown = document.createElement("div");
|
dropdown = document.createElement("div");
|
||||||
dropdown.className = "dropdown-list";
|
dropdown.className = "dropdown-list";
|
||||||
|
|
||||||
@ -227,9 +241,9 @@ export class SelectInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterDropdown(value) {
|
function filterDropdown(value: string) {
|
||||||
const filter = (element) => {
|
const filter = (element: HTMLElement) => {
|
||||||
const text = element.textContent;
|
const text = `${element.textContent}`;
|
||||||
const regexp = new RegExp(makeDiacriticsRegExp(escapeRegExp(value)), "i");
|
const regexp = new RegExp(makeDiacriticsRegExp(escapeRegExp(value)), "i");
|
||||||
return regexp.test(text);
|
return regexp.test(text);
|
||||||
};
|
};
|
||||||
@ -251,7 +265,7 @@ export class SelectInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToDropdownItem(item) {
|
function scrollToDropdownItem(item: HTMLElement) {
|
||||||
const dropdownScrollTop = dropdown.scrollTop;
|
const dropdownScrollTop = dropdown.scrollTop;
|
||||||
const dropdownHeight = dropdown.clientHeight;
|
const dropdownHeight = dropdown.clientHeight;
|
||||||
const dropdownScrollBottom = dropdownScrollTop + dropdownHeight;
|
const dropdownScrollBottom = dropdownScrollTop + dropdownHeight;
|
||||||
@ -268,7 +282,7 @@ export class SelectInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectDropdownItem(item) {
|
function selectDropdownItem(item: HTMLElement) {
|
||||||
const selectedItem = $(".dropdown-item.selected", dropdown);
|
const selectedItem = $(".dropdown-item.selected", dropdown);
|
||||||
if (selectedItem) {
|
if (selectedItem) {
|
||||||
selectedItem.classList.remove("selected");
|
selectedItem.classList.remove("selected");
|
||||||
@ -303,26 +317,26 @@ export class SelectInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectPrevDropdownItem() {
|
function selectPrevDropdownItem() {
|
||||||
const selectedItem = $(".dropdown-item.selected", dropdown);
|
const selectedItem = $(".dropdown-item.selected", dropdown) as HTMLElement;
|
||||||
if (selectedItem) {
|
if (selectedItem) {
|
||||||
let previousItem = selectedItem.previousSibling;
|
let previousItem = selectedItem.previousSibling as HTMLElement;
|
||||||
while (previousItem && (previousItem.style.display === "none" || previousItem.classList.contains("disabled"))) {
|
while (previousItem && (previousItem.style.display === "none" || previousItem.classList.contains("disabled"))) {
|
||||||
previousItem = previousItem.previousSibling;
|
previousItem = previousItem.previousSibling as HTMLElement;
|
||||||
}
|
}
|
||||||
if (previousItem) {
|
if (previousItem) {
|
||||||
return selectDropdownItem(previousItem);
|
return selectDropdownItem(previousItem);
|
||||||
}
|
}
|
||||||
selectDropdownItem(selectedItem.previousSibling);
|
selectDropdownItem(selectedItem.previousSibling as HTMLElement);
|
||||||
}
|
}
|
||||||
selectLastDropdownItem();
|
selectLastDropdownItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectNextDropdownItem() {
|
function selectNextDropdownItem() {
|
||||||
const selectedItem = $(".dropdown-item.selected", dropdown);
|
const selectedItem = $(".dropdown-item.selected", dropdown) as HTMLElement;
|
||||||
if (selectedItem) {
|
if (selectedItem) {
|
||||||
let nextItem = selectedItem.nextSibling;
|
let nextItem = selectedItem.nextSibling as HTMLElement;
|
||||||
while (nextItem && (nextItem.style.display === "none" || nextItem.classList.contains("disabled"))) {
|
while (nextItem && (nextItem.style.display === "none" || nextItem.classList.contains("disabled"))) {
|
||||||
nextItem = nextItem.nextSibling;
|
nextItem = nextItem.nextSibling as HTMLElement;
|
||||||
}
|
}
|
||||||
if (nextItem) {
|
if (nextItem) {
|
||||||
return selectDropdownItem(nextItem);
|
return selectDropdownItem(nextItem);
|
||||||
@ -331,14 +345,14 @@ export class SelectInput {
|
|||||||
selectFirstDropdownItem();
|
selectFirstDropdownItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCurrent(item) {
|
function setCurrent(item: HTMLElement) {
|
||||||
select.value = item.dataset.value;
|
select.value = item.dataset.value as string;
|
||||||
labelInput.value = item.innerText;
|
labelInput.value = item.innerText;
|
||||||
select.dispatchEvent(new Event("change"));
|
select.dispatchEvent(new Event("change"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrent() {
|
function getCurrent() {
|
||||||
return $(`[data-value="${select.value}"]`, dropdown);
|
return $(`[data-value="${select.value}"]`, dropdown) as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentLabel() {
|
function getCurrentLabel() {
|
||||||
@ -347,7 +361,7 @@ export class SelectInput {
|
|||||||
|
|
||||||
function selectCurrent() {
|
function selectCurrent() {
|
||||||
if (getComputedStyle(dropdown).display === "none") {
|
if (getComputedStyle(dropdown).display === "none") {
|
||||||
filterDropdown(null);
|
filterDropdown("");
|
||||||
updateDropdown();
|
updateDropdown();
|
||||||
selectDropdownItem(getCurrent());
|
selectDropdownItem(getCurrent());
|
||||||
dropdown.style.display = "block";
|
dropdown.style.display = "block";
|
||||||
@ -355,7 +369,7 @@ export class SelectInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateDropdownItem(value) {
|
function validateDropdownItem(value: string) {
|
||||||
const items = $$(".dropdown-item", dropdown);
|
const items = $$(".dropdown-item", dropdown);
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
if (items[i].innerText === value) {
|
if (items[i].innerText === value) {
|
@ -3,10 +3,10 @@ import { escapeRegExp, makeDiacriticsRegExp } from "../../utils/validation";
|
|||||||
import { debounce } from "../../utils/events";
|
import { debounce } from "../../utils/events";
|
||||||
|
|
||||||
export class TagInput {
|
export class TagInput {
|
||||||
constructor(input) {
|
constructor(input: HTMLInputElement) {
|
||||||
const options = { addKeyCodes: ["Space"] };
|
const options = { addKeyCodes: ["Space"] };
|
||||||
let tags = [];
|
let tags: string[] = [];
|
||||||
let placeholder, dropdown;
|
let placeholder: string, dropdown: HTMLElement;
|
||||||
|
|
||||||
const field = document.createElement("div");
|
const field = document.createElement("div");
|
||||||
const innerInput = document.createElement("input");
|
const innerInput = document.createElement("input");
|
||||||
@ -40,12 +40,12 @@ export class TagInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isDisabled) {
|
if (isDisabled) {
|
||||||
field.disabled = true;
|
field.setAttribute("disabled", "disabled");
|
||||||
innerInput.disabled = true;
|
innerInput.disabled = true;
|
||||||
hiddenInput.disabled = true;
|
hiddenInput.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.parentNode.replaceChild(field, input);
|
(input.parentNode as ParentNode).replaceChild(field, input);
|
||||||
field.appendChild(innerInput);
|
field.appendChild(innerInput);
|
||||||
field.appendChild(hiddenInput);
|
field.appendChild(hiddenInput);
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ export class TagInput {
|
|||||||
|
|
||||||
function createDropdown() {
|
function createDropdown() {
|
||||||
if ("options" in input.dataset) {
|
if ("options" in input.dataset) {
|
||||||
const list = JSON.parse(input.dataset.options);
|
const list = JSON.parse(input.dataset.options ?? "{}");
|
||||||
|
|
||||||
dropdown = document.createElement("div");
|
dropdown = document.createElement("div");
|
||||||
dropdown.className = "dropdown-list";
|
dropdown.className = "dropdown-list";
|
||||||
@ -84,7 +84,7 @@ export class TagInput {
|
|||||||
item.innerHTML = list[key];
|
item.innerHTML = list[key];
|
||||||
item.dataset.value = key;
|
item.dataset.value = key;
|
||||||
item.addEventListener("click", function () {
|
item.addEventListener("click", function () {
|
||||||
addTag(this.dataset.value);
|
this.dataset.value && addTag(this.dataset.value);
|
||||||
});
|
});
|
||||||
dropdown.appendChild(item);
|
dropdown.appendChild(item);
|
||||||
}
|
}
|
||||||
@ -139,7 +139,7 @@ export class TagInput {
|
|||||||
|
|
||||||
innerInput.addEventListener(
|
innerInput.addEventListener(
|
||||||
"keyup",
|
"keyup",
|
||||||
debounce((event) => {
|
debounce((event: KeyboardEvent) => {
|
||||||
const value = innerInput.value.trim();
|
const value = innerInput.value.trim();
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case "Escape":
|
case "Escape":
|
||||||
@ -178,7 +178,7 @@ export class TagInput {
|
|||||||
if (value === "") {
|
if (value === "") {
|
||||||
removeTag(tags[tags.length - 1]);
|
removeTag(tags[tags.length - 1]);
|
||||||
if (innerInput.previousSibling) {
|
if (innerInput.previousSibling) {
|
||||||
innerInput.parentNode.removeChild(innerInput.previousSibling);
|
(innerInput.parentNode as ParentNode).removeChild(innerInput.previousSibling);
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else {
|
} else {
|
||||||
@ -228,7 +228,7 @@ export class TagInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateTag(value) {
|
function validateTag(value: string) {
|
||||||
if (!tags.includes(value)) {
|
if (!tags.includes(value)) {
|
||||||
if (dropdown) {
|
if (dropdown) {
|
||||||
return $(`[data-value="${value}"]`, dropdown) !== null;
|
return $(`[data-value="${value}"]`, dropdown) !== null;
|
||||||
@ -238,25 +238,25 @@ export class TagInput {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertTag(value) {
|
function insertTag(value: string) {
|
||||||
const tag = document.createElement("span");
|
const tag = document.createElement("span");
|
||||||
const tagRemove = document.createElement("i");
|
const tagRemove = document.createElement("i");
|
||||||
tag.className = "tag";
|
tag.className = "tag";
|
||||||
tag.innerHTML = value;
|
tag.innerHTML = value;
|
||||||
tag.style.marginRight = ".25rem";
|
tag.style.marginRight = ".25rem";
|
||||||
innerInput.parentNode.insertBefore(tag, innerInput);
|
(innerInput.parentNode as ParentNode).insertBefore(tag, innerInput);
|
||||||
|
|
||||||
tagRemove.className = "tag-remove";
|
tagRemove.className = "tag-remove";
|
||||||
tagRemove.setAttribute("role", "button");
|
tagRemove.setAttribute("role", "button");
|
||||||
tagRemove.addEventListener("mousedown", (event) => {
|
tagRemove.addEventListener("mousedown", (event) => {
|
||||||
removeTag(value);
|
removeTag(value);
|
||||||
tag.parentNode.removeChild(tag);
|
(tag.parentNode as ParentNode).removeChild(tag);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
tag.appendChild(tagRemove);
|
tag.appendChild(tagRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTag(value) {
|
function addTag(value: string) {
|
||||||
if (validateTag(value)) {
|
if (validateTag(value)) {
|
||||||
tags.push(value);
|
tags.push(value);
|
||||||
insertTag(value);
|
insertTag(value);
|
||||||
@ -270,7 +270,7 @@ export class TagInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeTag(value) {
|
function removeTag(value: string) {
|
||||||
const index = tags.indexOf(value);
|
const index = tags.indexOf(value);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
tags.splice(index, 1);
|
tags.splice(index, 1);
|
||||||
@ -292,7 +292,7 @@ export class TagInput {
|
|||||||
if (getComputedStyle(element).display !== "none") {
|
if (getComputedStyle(element).display !== "none") {
|
||||||
visibleItems++;
|
visibleItems++;
|
||||||
}
|
}
|
||||||
if (!tags.includes(element.dataset.value)) {
|
if (!tags.includes(element.dataset.value as string)) {
|
||||||
element.style.display = "block";
|
element.style.display = "block";
|
||||||
} else {
|
} else {
|
||||||
element.style.display = "none";
|
element.style.display = "none";
|
||||||
@ -306,11 +306,11 @@ export class TagInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterDropdown(value) {
|
function filterDropdown(value: string) {
|
||||||
let visibleItems = 0;
|
let visibleItems = 0;
|
||||||
dropdown.style.display = "block";
|
dropdown.style.display = "block";
|
||||||
$$(".dropdown-item", dropdown).forEach((element) => {
|
$$(".dropdown-item", dropdown).forEach((element) => {
|
||||||
const text = element.textContent;
|
const text = `${element.textContent}`;
|
||||||
const regexp = new RegExp(makeDiacriticsRegExp(escapeRegExp(value)), "i");
|
const regexp = new RegExp(makeDiacriticsRegExp(escapeRegExp(value)), "i");
|
||||||
if (text.match(regexp) !== null && element.style.display !== "none") {
|
if (text.match(regexp) !== null && element.style.display !== "none") {
|
||||||
element.style.display = "block";
|
element.style.display = "block";
|
||||||
@ -326,7 +326,7 @@ export class TagInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToDropdownItem(item) {
|
function scrollToDropdownItem(item: HTMLElement) {
|
||||||
const dropdownScrollTop = dropdown.scrollTop;
|
const dropdownScrollTop = dropdown.scrollTop;
|
||||||
const dropdownHeight = dropdown.clientHeight;
|
const dropdownHeight = dropdown.clientHeight;
|
||||||
const dropdownScrollBottom = dropdownScrollTop + dropdownHeight;
|
const dropdownScrollBottom = dropdownScrollTop + dropdownHeight;
|
||||||
@ -345,12 +345,12 @@ export class TagInput {
|
|||||||
|
|
||||||
function addTagFromSelectedDropdownItem() {
|
function addTagFromSelectedDropdownItem() {
|
||||||
const selectedItem = $(".dropdown-item.selected", dropdown);
|
const selectedItem = $(".dropdown-item.selected", dropdown);
|
||||||
if (getComputedStyle(selectedItem).display !== "none") {
|
if (selectedItem && getComputedStyle(selectedItem).display !== "none") {
|
||||||
innerInput.value = selectedItem.dataset.value;
|
innerInput.value = selectedItem.dataset.value as string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectDropdownItem(item) {
|
function selectDropdownItem(item: HTMLElement) {
|
||||||
const selectedItem = $(".dropdown-item.selected", dropdown);
|
const selectedItem = $(".dropdown-item.selected", dropdown);
|
||||||
if (selectedItem) {
|
if (selectedItem) {
|
||||||
selectedItem.classList.remove("selected");
|
selectedItem.classList.remove("selected");
|
||||||
@ -384,14 +384,14 @@ export class TagInput {
|
|||||||
function selectPrevDropdownItem() {
|
function selectPrevDropdownItem() {
|
||||||
const selectedItem = $(".dropdown-item.selected", dropdown);
|
const selectedItem = $(".dropdown-item.selected", dropdown);
|
||||||
if (selectedItem) {
|
if (selectedItem) {
|
||||||
let previousItem = selectedItem.previousSibling;
|
let previousItem = selectedItem.previousSibling as HTMLElement;
|
||||||
while (previousItem && previousItem.style.display === "none") {
|
while (previousItem && previousItem.style.display === "none") {
|
||||||
previousItem = previousItem.previousSibling;
|
previousItem = previousItem.previousSibling as HTMLElement;
|
||||||
}
|
}
|
||||||
if (previousItem) {
|
if (previousItem) {
|
||||||
return selectDropdownItem(previousItem);
|
return selectDropdownItem(previousItem);
|
||||||
}
|
}
|
||||||
selectDropdownItem(selectedItem.previousSibling);
|
selectDropdownItem(selectedItem.previousSibling as HTMLElement);
|
||||||
}
|
}
|
||||||
selectLastDropdownItem();
|
selectLastDropdownItem();
|
||||||
}
|
}
|
||||||
@ -399,12 +399,12 @@ export class TagInput {
|
|||||||
function selectNextDropdownItem() {
|
function selectNextDropdownItem() {
|
||||||
const selectedItem = $(".dropdown-item.selected", dropdown);
|
const selectedItem = $(".dropdown-item.selected", dropdown);
|
||||||
if (selectedItem) {
|
if (selectedItem) {
|
||||||
let nextItem = selectedItem.nextSibling;
|
let nextItem = selectedItem.nextSibling as HTMLElement;
|
||||||
while (nextItem && nextItem.style.display === "none") {
|
while (nextItem && nextItem.style.display === "none") {
|
||||||
nextItem = nextItem.nextSibling;
|
nextItem = nextItem.nextSibling as HTMLElement;
|
||||||
}
|
}
|
||||||
if (nextItem) {
|
if (nextItem) {
|
||||||
return selectDropdownItem(nextItem);
|
return selectDropdownItem(nextItem as HTMLElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selectFirstDropdownItem();
|
selectFirstDropdownItem();
|
@ -1,12 +1,15 @@
|
|||||||
import { $, $$ } from "../utils/selectors";
|
import { $, $$ } from "../utils/selectors";
|
||||||
import { Inputs } from "./inputs";
|
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;
|
return parent.querySelector("button, .button, input:not([type=hidden]), select, textarea") || parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Modal {
|
export class Modal {
|
||||||
constructor(element) {
|
element: HTMLElement;
|
||||||
|
inputs: Inputs;
|
||||||
|
|
||||||
|
constructor(element: HTMLElement) {
|
||||||
this.element = element;
|
this.element = element;
|
||||||
|
|
||||||
document.addEventListener("keyup", (event) => {
|
document.addEventListener("keyup", (event) => {
|
||||||
@ -19,7 +22,7 @@ export class Modal {
|
|||||||
|
|
||||||
this.inputs = new Inputs(this.element);
|
this.inputs = new Inputs(this.element);
|
||||||
|
|
||||||
$("[data-dismiss]", element).addEventListener("click", () => this.hide());
|
$("[data-dismiss]", element)?.addEventListener("click", () => this.hide());
|
||||||
|
|
||||||
let mousedownTriggered = false;
|
let mousedownTriggered = false;
|
||||||
element.addEventListener("mousedown", () => (mousedownTriggered = true));
|
element.addEventListener("mousedown", () => (mousedownTriggered = true));
|
||||||
@ -31,7 +34,7 @@ export class Modal {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("click", (event) => {
|
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) {
|
if (target && target.dataset.modal === element.id) {
|
||||||
const modalAction = target.dataset.modalAction;
|
const modalAction = target.dataset.modalAction;
|
||||||
if (modalAction) {
|
if (modalAction) {
|
||||||
@ -43,24 +46,24 @@ export class Modal {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
show(action, callback) {
|
show(action?: string, callback?: (modal: this) => void) {
|
||||||
const modal = this.element;
|
const modal = this.element;
|
||||||
modal.setAttribute("role", "dialog");
|
modal.setAttribute("role", "dialog");
|
||||||
modal.setAttribute("aria-modal", "true");
|
modal.setAttribute("aria-modal", "true");
|
||||||
modal.classList.add("show");
|
modal.classList.add("show");
|
||||||
if (action) {
|
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)) {
|
if ($("[autofocus]", modal)) {
|
||||||
$("[autofocus]", modal).focus(); // Firefox bug
|
($("[autofocus]", modal) as HTMLFormElement).focus(); // Firefox bug
|
||||||
} else {
|
} else {
|
||||||
getFirstFocusableElement(modal).focus();
|
getFirstFocusableElement(modal).focus();
|
||||||
}
|
}
|
||||||
if (typeof callback === "function") {
|
if (typeof callback === "function") {
|
||||||
callback(this);
|
callback(this);
|
||||||
}
|
}
|
||||||
$$(".tooltip").forEach((element) => element.parentNode.removeChild(element));
|
$$(".tooltip").forEach((element) => element.parentNode && element.parentNode.removeChild(element));
|
||||||
this.createBackdrop();
|
this.createBackdrop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +85,7 @@ export class Modal {
|
|||||||
|
|
||||||
removeBackdrop() {
|
removeBackdrop() {
|
||||||
const backdrop = $(".modal-backdrop");
|
const backdrop = $(".modal-backdrop");
|
||||||
if (backdrop) {
|
if (backdrop && backdrop.parentNode) {
|
||||||
backdrop.parentNode.removeChild(backdrop);
|
backdrop.parentNode.removeChild(backdrop);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,7 +2,8 @@ import { $$ } from "../utils/selectors";
|
|||||||
import { Modal } from "./modal";
|
import { Modal } from "./modal";
|
||||||
|
|
||||||
export class Modals {
|
export class Modals {
|
||||||
|
[id: string]: Modal;
|
||||||
constructor() {
|
constructor() {
|
||||||
$$(".modal").forEach((element) => (this[element.id] = new Modal(element)));
|
$$(".modal").forEach((element: HTMLElement) => (this[element.id] = new Modal(element)));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,8 +3,8 @@ import { $ } from "../utils/selectors";
|
|||||||
export class Navigation {
|
export class Navigation {
|
||||||
constructor() {
|
constructor() {
|
||||||
if ($(".sidebar-toggle")) {
|
if ($(".sidebar-toggle")) {
|
||||||
$(".sidebar-toggle").addEventListener("click", () => {
|
$(".sidebar-toggle")?.addEventListener("click", () => {
|
||||||
if ($(".sidebar").classList.toggle("show")) {
|
if (($(".sidebar") as HTMLElement).classList.toggle("show")) {
|
||||||
if (!$(".sidebar-backdrop")) {
|
if (!$(".sidebar-backdrop")) {
|
||||||
const backdrop = document.createElement("div");
|
const backdrop = document.createElement("div");
|
||||||
backdrop.className = "sidebar-backdrop hide-from-s";
|
backdrop.className = "sidebar-backdrop hide-from-s";
|
||||||
@ -13,7 +13,7 @@ export class Navigation {
|
|||||||
} else {
|
} else {
|
||||||
const backdrop = $(".sidebar-backdrop");
|
const backdrop = $(".sidebar-backdrop");
|
||||||
if (backdrop) {
|
if (backdrop) {
|
||||||
backdrop.parentNode.removeChild(backdrop);
|
(backdrop.parentNode as ParentNode).removeChild(backdrop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -23,7 +23,7 @@ export class Navigation {
|
|||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", (event) => {
|
||||||
if (!event.altKey && (event.ctrlKey || event.metaKey)) {
|
if (!event.altKey && (event.ctrlKey || event.metaKey)) {
|
||||||
if (event.key === "s") {
|
if (event.key === "s") {
|
||||||
$("[data-command=save]").click();
|
$("[data-command=save]")?.click();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,8 +1,22 @@
|
|||||||
import { $ } from "../utils/selectors";
|
import { $ } from "../utils/selectors";
|
||||||
import { passIcon } from "./icons";
|
import { passIcon } from "./icons";
|
||||||
|
|
||||||
|
type NotificationOptions = {
|
||||||
|
interval: number;
|
||||||
|
icon?: string;
|
||||||
|
newestOnTop: boolean;
|
||||||
|
fadeOutDelay: number;
|
||||||
|
mouseleaveDelay: number;
|
||||||
|
};
|
||||||
|
|
||||||
export class Notification {
|
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 = {
|
const defaults = {
|
||||||
interval: 5000,
|
interval: 5000,
|
||||||
icon: null,
|
icon: null,
|
||||||
@ -14,13 +28,13 @@ export class Notification {
|
|||||||
this.text = text;
|
this.text = text;
|
||||||
this.type = type;
|
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() {
|
show() {
|
||||||
const create = (text, type, interval) => {
|
const create = (text: string, type: string, interval: number) => {
|
||||||
if (!this.containerElement) {
|
if (!this.containerElement) {
|
||||||
this.containerElement = document.createElement("div");
|
this.containerElement = document.createElement("div");
|
||||||
this.containerElement.className = "notification-container";
|
this.containerElement.className = "notification-container";
|
||||||
@ -48,10 +62,10 @@ export class Notification {
|
|||||||
return notification;
|
return notification;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.options.icon !== null) {
|
if (this.options.icon) {
|
||||||
passIcon(this.options.icon, (icon) => {
|
passIcon(this.options.icon, (icon) => {
|
||||||
this.notificationElement = create(this.text, this.type, this.options.interval);
|
this.notificationElement = create(this.text, this.type, this.options.interval);
|
||||||
this.notificationElement.insertAdjacentHTML("afterBegin", icon);
|
this.notificationElement.insertAdjacentHTML("afterbegin", icon);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.notificationElement = create(this.text, this.type, this.options.interval);
|
this.notificationElement = create(this.text, this.type, this.options.interval);
|
||||||
@ -62,7 +76,7 @@ export class Notification {
|
|||||||
this.notificationElement.classList.add("fadeout");
|
this.notificationElement.classList.add("fadeout");
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.notificationElement && this.notificationElement.parentNode) {
|
if (this.containerElement && this.notificationElement && this.notificationElement.parentNode) {
|
||||||
this.containerElement.removeChild(this.notificationElement);
|
this.containerElement.removeChild(this.notificationElement);
|
||||||
}
|
}
|
||||||
if (this.containerElement && this.containerElement.childNodes.length < 1) {
|
if (this.containerElement && this.containerElement.childNodes.length < 1) {
|
@ -5,7 +5,7 @@ export class Notifications {
|
|||||||
constructor() {
|
constructor() {
|
||||||
let delay = 0;
|
let delay = 0;
|
||||||
|
|
||||||
$$("meta[name=notification]").forEach((element) => {
|
$$("meta[name=notification]").forEach((element: HTMLMetaElement) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const data = JSON.parse(element.content);
|
const data = JSON.parse(element.content);
|
||||||
const notification = new Notification(data.text, data.type, {
|
const notification = new Notification(data.text, data.type, {
|
||||||
@ -15,7 +15,7 @@ export class Notifications {
|
|||||||
notification.show();
|
notification.show();
|
||||||
}, delay);
|
}, delay);
|
||||||
delay += 500;
|
delay += 500;
|
||||||
element.parentNode.removeChild(element);
|
(element.parentNode as ParentNode).removeChild(element);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,7 +4,7 @@ export class Sections {
|
|||||||
constructor() {
|
constructor() {
|
||||||
$$(".collapsible .section-header").forEach((element) => {
|
$$(".collapsible .section-header").forEach((element) => {
|
||||||
element.addEventListener("click", () => {
|
element.addEventListener("click", () => {
|
||||||
const section = element.parentNode;
|
const section = element.parentNode as HTMLElement;
|
||||||
section.classList.toggle("collapsed");
|
section.classList.toggle("collapsed");
|
||||||
});
|
});
|
||||||
});
|
});
|
63
panel/src/ts/components/statistics-chart.ts
Normal file
63
panel/src/ts/components/statistics-chart.ts
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
export class Tooltip {
|
||||||
constructor(text, options) {
|
text: string;
|
||||||
|
options: TooltipOptions;
|
||||||
|
delayTimer: number;
|
||||||
|
timeoutTimer: number;
|
||||||
|
tooltipElement: HTMLElement;
|
||||||
|
|
||||||
|
constructor(text: string, options: Partial<TooltipOptions> = {}) {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
container: document.body,
|
container: document.body,
|
||||||
referenceElement: document.body,
|
referenceElement: document.body,
|
||||||
@ -29,7 +49,7 @@ export class Tooltip {
|
|||||||
tooltip.style.display = "block";
|
tooltip.style.display = "block";
|
||||||
tooltip.innerHTML = this.text;
|
tooltip.innerHTML = this.text;
|
||||||
|
|
||||||
const getTooltipPosition = (tooltip) => {
|
const getTooltipPosition = (tooltip: HTMLElement) => {
|
||||||
const referenceElement = options.referenceElement;
|
const referenceElement = options.referenceElement;
|
||||||
const offset = options.offset;
|
const offset = options.offset;
|
||||||
const rect = referenceElement.getBoundingClientRect();
|
const rect = referenceElement.getBoundingClientRect();
|
@ -11,7 +11,7 @@ export class Tooltips {
|
|||||||
|
|
||||||
$$("[data-tooltip]").forEach((element) => {
|
$$("[data-tooltip]").forEach((element) => {
|
||||||
element.addEventListener("mouseover", () => {
|
element.addEventListener("mouseover", () => {
|
||||||
const tooltip = new Tooltip(element.dataset.tooltip, {
|
const tooltip = new Tooltip(element.dataset.tooltip as string, {
|
||||||
referenceElement: element,
|
referenceElement: element,
|
||||||
position: "bottom",
|
position: "bottom",
|
||||||
offset: {
|
offset: {
|
||||||
@ -25,7 +25,7 @@ export class Tooltips {
|
|||||||
// Immediately show tooltip on focused buttons
|
// Immediately show tooltip on focused buttons
|
||||||
if (element.tagName.toLowerCase() === "button" || element.classList.contains("button")) {
|
if (element.tagName.toLowerCase() === "button" || element.classList.contains("button")) {
|
||||||
element.addEventListener("focus", () => {
|
element.addEventListener("focus", () => {
|
||||||
const tooltip = new Tooltip(element.dataset.tooltip, {
|
const tooltip = new Tooltip(element.dataset.tooltip as string, {
|
||||||
referenceElement: element,
|
referenceElement: element,
|
||||||
position: "bottom",
|
position: "bottom",
|
||||||
offset: {
|
offset: {
|
@ -11,7 +11,7 @@ export class Backups {
|
|||||||
|
|
||||||
if (makeBackupCommand) {
|
if (makeBackupCommand) {
|
||||||
makeBackupCommand.addEventListener("click", function () {
|
makeBackupCommand.addEventListener("click", function () {
|
||||||
const button = this;
|
const button = this as HTMLButtonElement;
|
||||||
|
|
||||||
const getSpinner = () => {
|
const getSpinner = () => {
|
||||||
let spinner = $(".spinner");
|
let spinner = $(".spinner");
|
||||||
@ -35,7 +35,7 @@ export class Backups {
|
|||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${app.config.baseUri}backup/make/`,
|
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) => {
|
(response) => {
|
||||||
if (response.status === "success") {
|
if (response.status === "success") {
|
||||||
@ -44,22 +44,22 @@ export class Backups {
|
|||||||
spinner.classList.add("spinner-success");
|
spinner.classList.add("spinner-success");
|
||||||
insertIcon("check", spinner);
|
insertIcon("check", spinner);
|
||||||
|
|
||||||
const template = $("#backups-row");
|
const template = $("#backups-row") as HTMLTemplateElement;
|
||||||
if (template) {
|
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) as HTMLAnchorElement).href = response.data.uri;
|
||||||
$(".backup-uri", node).innerHTML = response.data.filename;
|
($(".backup-uri", node) as HTMLElement).innerHTML = response.data.filename;
|
||||||
|
|
||||||
$(".backup-date", node).innerHTML = response.data.date;
|
($(".backup-date", node) as HTMLElement).innerHTML = response.data.date;
|
||||||
$(".backup-size", node).innerHTML = response.data.size;
|
($(".backup-size", node) as HTMLElement).innerHTML = response.data.size;
|
||||||
$(".backup-delete", node).dataset.modalAction = response.data.deleteUri;
|
($(".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;
|
const limit = response.data.maxFiles;
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ export class Backups {
|
|||||||
|
|
||||||
if (response.status === "success") {
|
if (response.status === "success") {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
triggerDownload(response.data.uri, $("meta[name=csrf-token]").content);
|
triggerDownload(response.data.uri, ($("meta[name=csrf-token]") as HTMLMetaElement).content);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
},
|
},
|
@ -17,7 +17,7 @@ export class Dashboard {
|
|||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${app.config.baseUri}cache/clear/`,
|
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) => {
|
(response) => {
|
||||||
const notification = new Notification(response.message, response.status, { icon: "check-circle" });
|
const notification = new Notification(response.message, response.status, { icon: "check-circle" });
|
||||||
@ -29,14 +29,14 @@ export class Dashboard {
|
|||||||
|
|
||||||
if (makeBackupCommand) {
|
if (makeBackupCommand) {
|
||||||
makeBackupCommand.addEventListener("click", function () {
|
makeBackupCommand.addEventListener("click", function () {
|
||||||
const button = this;
|
const button = this as HTMLButtonElement;
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
|
|
||||||
new Request(
|
new Request(
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${app.config.baseUri}backup/make/`,
|
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) => {
|
(response) => {
|
||||||
const notification = new Notification(response.message, response.status, { icon: "check-circle" });
|
const notification = new Notification(response.message, response.status, { icon: "check-circle" });
|
||||||
@ -45,7 +45,7 @@ export class Dashboard {
|
|||||||
if (response.status === "success") {
|
if (response.status === "success") {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
triggerDownload(response.data.uri, $("meta[name=csrf-token]").content);
|
triggerDownload(response.data.uri, ($("meta[name=csrf-token]") as HTMLMetaElement).content);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +58,10 @@ export class Dashboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (chart) {
|
if (chart) {
|
||||||
new StatisticsChart(chart, JSON.parse(chart.dataset.chartData));
|
const chartData = chart.dataset.chartData;
|
||||||
|
if (chartData) {
|
||||||
|
new StatisticsChart(chart, JSON.parse(chartData));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,7 +4,7 @@ import { app } from "../../app";
|
|||||||
import { debounce } from "../../utils/events";
|
import { debounce } from "../../utils/events";
|
||||||
import { Notification } from "../notification";
|
import { Notification } from "../notification";
|
||||||
import { Request } from "../../utils/request";
|
import { Request } from "../../utils/request";
|
||||||
import { Sortable } from "sortablejs";
|
import Sortable from "sortablejs";
|
||||||
|
|
||||||
export class Pages {
|
export class Pages {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -62,7 +62,7 @@ export class Pages {
|
|||||||
if (commandReorderPages) {
|
if (commandReorderPages) {
|
||||||
commandReorderPages.addEventListener("click", () => {
|
commandReorderPages.addEventListener("click", () => {
|
||||||
commandReorderPages.classList.toggle("active");
|
commandReorderPages.classList.toggle("active");
|
||||||
$(".pages-tree").classList.toggle("is-reordering");
|
($(".pages-tree") as HTMLElement).classList.toggle("is-reordering");
|
||||||
commandReorderPages.blur();
|
commandReorderPages.blur();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -74,26 +74,26 @@ export class Pages {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSearch = (event) => {
|
const handleSearch = (event: Event) => {
|
||||||
const value = event.target.value;
|
const value = (event.target as HTMLInputElement).value;
|
||||||
if (value.length === 0) {
|
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) => {
|
$$(".pages-tree-item").forEach((element) => {
|
||||||
const title = $(".page-title a", element);
|
const title = $(".page-title a", element) as HTMLElement;
|
||||||
title.innerHTML = title.textContent;
|
title.innerHTML = title.textContent as string;
|
||||||
$(".pages-tree-row", element).style.display = "";
|
($(".pages-tree-row", element) as HTMLElement).style.display = "";
|
||||||
element.classList.toggle("is-expanded", element.dataset.expanded === "true");
|
element.classList.toggle("is-expanded", element.dataset.expanded === "true");
|
||||||
});
|
});
|
||||||
} else {
|
} 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");
|
const regexp = new RegExp(makeDiacriticsRegExp(escapeRegExp(value)), "gi");
|
||||||
|
|
||||||
$$(".pages-tree-item").forEach((element) => {
|
$$(".pages-tree-item").forEach((element) => {
|
||||||
const title = $(".page-title a", element);
|
const title = $(".page-title a", element) as HTMLElement;
|
||||||
const text = title.textContent;
|
const text = title.textContent as string;
|
||||||
const pagesItem = $(".pages-tree-row", element);
|
const pagesItem = $(".pages-tree-row", element) as HTMLElement;
|
||||||
|
|
||||||
if (text.match(regexp) !== null) {
|
if (text.match(regexp) !== null) {
|
||||||
title.innerHTML = text.replace(regexp, "<mark>$&</mark>");
|
title.innerHTML = text.replace(regexp, "<mark>$&</mark>");
|
||||||
@ -121,45 +121,44 @@ export class Pages {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newPageModal) {
|
if (newPageModal) {
|
||||||
$("#page-title", newPageModal).addEventListener("keyup", (event) => {
|
($("#page-title", newPageModal) as HTMLElement).addEventListener("keyup", (event) => {
|
||||||
$("#page-slug", newPageModal).value = makeSlug(event.target.value);
|
($("#page-slug", newPageModal) as HTMLInputElement).value = makeSlug((event.target as HTMLInputElement).value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSlugChange = (event) => {
|
const handleSlugChange = (event: Event) => {
|
||||||
event.target.value = validateSlug(event.target.value);
|
const target = event.target as HTMLInputElement;
|
||||||
|
target.value = validateSlug(target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
$("#page-slug", newPageModal).addEventListener("keyup", handleSlugChange);
|
($("#page-slug", newPageModal) as HTMLElement).addEventListener("keyup", handleSlugChange);
|
||||||
$("#page-slug", newPageModal).addEventListener("blur", 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');
|
const option = $('.dropdown-list[data-for="page-parent"] .selected');
|
||||||
|
|
||||||
if (!option) {
|
if (!option) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let allowedTemplates = option.dataset.allowedTemplates;
|
const allowedTemplates = (option.dataset.allowedTemplates as string).split(", ");
|
||||||
|
|
||||||
const pageTemplate = $("#page-template", newPageModal);
|
const pageTemplate = $("#page-template", newPageModal) as HTMLInputElement;
|
||||||
|
|
||||||
if (allowedTemplates) {
|
|
||||||
allowedTemplates = allowedTemplates.split(", ");
|
|
||||||
|
|
||||||
|
if (allowedTemplates.length > 0) {
|
||||||
pageTemplate.dataset.previousValue = pageTemplate.value;
|
pageTemplate.dataset.previousValue = pageTemplate.value;
|
||||||
pageTemplate.value = allowedTemplates[0];
|
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) => {
|
$$('.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");
|
option.classList.add("disabled");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if ("previousValue" in pageTemplate.dataset) {
|
if ("previousValue" in pageTemplate.dataset) {
|
||||||
pageTemplate.value = pageTemplate.dataset.previousValue;
|
pageTemplate.value = pageTemplate.dataset.previousValue as string;
|
||||||
delete pageTemplate.dataset.previousValue;
|
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) => {
|
$$('.dropdown-list[data-for="page-template"] .dropdown-item').forEach((option) => {
|
||||||
@ -171,55 +170,56 @@ export class Pages {
|
|||||||
|
|
||||||
if (slugModal && commandChangeSlug) {
|
if (slugModal && commandChangeSlug) {
|
||||||
commandChangeSlug.addEventListener("click", () => {
|
commandChangeSlug.addEventListener("click", () => {
|
||||||
app.modals["slugModal"].show(null, (modal) => {
|
app.modals["slugModal"].show(undefined, (modal) => {
|
||||||
const slug = document.getElementById("slug").value;
|
const slug = (document.getElementById("slug") as HTMLInputElement).value;
|
||||||
const slugInput = $("#page-slug", modal.element);
|
const slugInput = $("#page-slug", modal.element) as HTMLInputElement;
|
||||||
slugInput.value = slug;
|
slugInput.value = slug;
|
||||||
slugInput.placeholder = slug;
|
slugInput.placeholder = slug;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#page-slug", slugModal).addEventListener("keydown", (event) => {
|
($("#page-slug", slugModal) as HTMLElement).addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
$("[data-command=continue]", slugModal).click();
|
($("[data-command=continue]", slugModal) as HTMLElement).click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSlugChange = (event) => {
|
const handleSlugChange = (event: Event) => {
|
||||||
event.target.value = validateSlug(event.target.value);
|
const target = event.target as HTMLInputElement;
|
||||||
|
target.value = validateSlug(target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
$("#page-slug", slugModal).addEventListener("keyup", handleSlugChange);
|
($("#page-slug", slugModal) as HTMLElement).addEventListener("keyup", handleSlugChange);
|
||||||
$("#page-slug", slugModal).addEventListener("blur", handleSlugChange);
|
($("#page-slug", slugModal) as HTMLElement).addEventListener("blur", handleSlugChange);
|
||||||
|
|
||||||
$("[data-command=generate-slug]", slugModal).addEventListener("click", () => {
|
($("[data-command=generate-slug]", slugModal) as HTMLElement).addEventListener("click", () => {
|
||||||
const slug = makeSlug(document.getElementById("title").value);
|
const slug = makeSlug((document.getElementById("title") as HTMLInputElement).value);
|
||||||
$("#page-slug", slugModal).value = slug;
|
($("#page-slug", slugModal) as HTMLInputElement).value = slug;
|
||||||
$("#page-slug", slugModal).focus();
|
($("#page-slug", slugModal) as HTMLElement).focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("[data-command=continue]", slugModal).addEventListener("click", () => {
|
($("[data-command=continue]", slugModal) as HTMLElement).addEventListener("click", () => {
|
||||||
const slug = $("#page-slug", slugModal).value.replace(/^-+|-+$/, "");
|
const slug = ($("#page-slug", slugModal) as HTMLInputElement).value.replace(/^-+|-+$/, "");
|
||||||
|
|
||||||
if (slug.length > 0) {
|
if (slug.length > 0) {
|
||||||
const route = $(".page-route-inner").innerHTML;
|
const route = ($(".page-route-inner") as HTMLElement).innerHTML;
|
||||||
$$("#page-slug, #slug").forEach((element) => {
|
$$("#page-slug, #slug").forEach((element: HTMLInputElement) => {
|
||||||
element.value = slug;
|
element.value = slug;
|
||||||
});
|
});
|
||||||
$("#page-slug", slugModal).value = slug;
|
($("#page-slug", slugModal) as HTMLInputElement).value = slug;
|
||||||
document.getElementById("slug").value = slug;
|
(document.getElementById("slug") as HTMLInputElement).value = slug;
|
||||||
$(".page-route-inner").innerHTML = route.replace(/\/[a-z0-9-]+\/$/, `/${slug}/`);
|
($(".page-route-inner") as HTMLElement).innerHTML = route.replace(/\/[a-z0-9-]+\/$/, `/${slug}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.modals["slugModal"].hide();
|
app.modals["slugModal"].hide();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$$(["[data-modal=renameFileModal]"]).forEach((element) => {
|
$$("[data-modal=renameFileModal]").forEach((element) => {
|
||||||
element.addEventListener("click", () => {
|
element.addEventListener("click", () => {
|
||||||
const modal = document.getElementById("renameFileModal");
|
const modal = document.getElementById("renameFileModal") as HTMLElement;
|
||||||
const input = $("#file-name", modal);
|
const input = $("#file-name", modal) as HTMLInputElement;
|
||||||
input.value = element.dataset.filename;
|
input.value = element.dataset.filename as string;
|
||||||
input.setSelectionRange(0, input.value.lastIndexOf("."));
|
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");
|
const element = list.closest(".pages-tree-item");
|
||||||
element.classList.toggle("is-expanded");
|
element?.classList.toggle("is-expanded");
|
||||||
}
|
}
|
||||||
|
|
||||||
function initSortable(element) {
|
function initSortable(element: HTMLElement) {
|
||||||
let originalOrder = [];
|
let originalOrder: string[] = [];
|
||||||
|
|
||||||
const sortable = Sortable.create(element, {
|
const sortable = Sortable.create(element, {
|
||||||
handle: ".sortable-handle",
|
handle: ".sortable-handle",
|
||||||
@ -256,23 +256,24 @@ export class Pages {
|
|||||||
const height = document.body.offsetHeight;
|
const height = document.body.offsetHeight;
|
||||||
document.body.style.height = `${height}px`;
|
document.body.style.height = `${height}px`;
|
||||||
|
|
||||||
const e = window.addEventListener("scroll", () => {
|
const e = () => {
|
||||||
window.document.body.style.height = "";
|
window.document.body.style.height = "";
|
||||||
window.removeEventListener("scroll", e);
|
window.removeEventListener("scroll", e);
|
||||||
});
|
};
|
||||||
|
window.addEventListener("scroll", e);
|
||||||
},
|
},
|
||||||
|
|
||||||
onStart() {
|
onStart() {
|
||||||
element.classList.add("is-dragging");
|
element.classList.add("is-dragging");
|
||||||
},
|
},
|
||||||
|
|
||||||
onMove(event) {
|
onMove(event: Sortable.MoveEvent) {
|
||||||
if (event.related.classList.contains("is-not-orderable")) {
|
if (event.related.classList.contains("is-not-orderable")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onEnd(event) {
|
onEnd(event: Sortable.SortableEvent) {
|
||||||
element.classList.remove("is-dragging");
|
element.classList.remove("is-dragging");
|
||||||
|
|
||||||
document.body.style.height = "";
|
document.body.style.height = "";
|
||||||
@ -284,9 +285,9 @@ export class Pages {
|
|||||||
sortable.option("disabled", true);
|
sortable.option("disabled", true);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
"csrf-token": $("meta[name=csrf-token]").content,
|
"csrf-token": ($("meta[name=csrf-token]") as HTMLMetaElement).content,
|
||||||
page: element.children[event.newIndex].dataset.route,
|
page: (element.children[event.newIndex as number] as HTMLElement).dataset.route,
|
||||||
before: element.children[event.oldIndex].dataset.route,
|
before: (element.children[event.oldIndex as number] as HTMLElement).dataset.route,
|
||||||
parent: element.dataset.parent,
|
parent: element.dataset.parent,
|
||||||
};
|
};
|
||||||
|
|
@ -4,9 +4,11 @@ import { StatisticsChart } from "../statistics-chart";
|
|||||||
export class Statistics {
|
export class Statistics {
|
||||||
constructor() {
|
constructor() {
|
||||||
const chart = $(".statistics-chart");
|
const chart = $(".statistics-chart");
|
||||||
|
|
||||||
if (chart) {
|
if (chart) {
|
||||||
new StatisticsChart(chart, JSON.parse(chart.dataset.chartData));
|
const chartData = chart.dataset.chartData;
|
||||||
|
if (chartData) {
|
||||||
|
new StatisticsChart(chart, JSON.parse(chartData));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,14 +9,15 @@ export class Updates {
|
|||||||
const updaterComponent = document.getElementById("updater-component");
|
const updaterComponent = document.getElementById("updater-component");
|
||||||
|
|
||||||
if (updaterComponent) {
|
if (updaterComponent) {
|
||||||
const updateStatus = $(".update-status");
|
const updateStatus = $(".update-status") as HTMLElement;
|
||||||
const spinner = $(".spinner");
|
const spinner = $(".spinner") as HTMLElement;
|
||||||
const currentVersion = $(".current-version");
|
const currentVersion = $(".current-version") as HTMLElement;
|
||||||
const currentVersionName = $(".current-version-name");
|
const currentVersionName = $(".current-version-name") as HTMLElement;
|
||||||
const newVersion = $(".new-version");
|
const newVersion = $(".new-version") as HTMLElement;
|
||||||
const newVersionName = $(".new-version-name");
|
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");
|
spinner.classList.add("spinner-info");
|
||||||
insertIcon("info", spinner);
|
insertIcon("info", spinner);
|
||||||
newVersionName.innerHTML = name;
|
newVersionName.innerHTML = name;
|
||||||
@ -37,7 +38,7 @@ export class Updates {
|
|||||||
};
|
};
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const data = { "csrf-token": $("meta[name=csrf-token]").content };
|
const data = { "csrf-token": ($("meta[name=csrf-token]") as HTMLMetaElement).content };
|
||||||
|
|
||||||
new Request(
|
new Request(
|
||||||
{
|
{
|
||||||
@ -62,16 +63,16 @@ export class Updates {
|
|||||||
);
|
);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
$("[data-command=install-updates]").addEventListener("click", () => {
|
installCommand.addEventListener("click", () => {
|
||||||
newVersion.style.display = "none";
|
newVersion.style.display = "none";
|
||||||
spinner.classList.remove("spinner-info");
|
spinner.classList.remove("spinner-info");
|
||||||
updateStatus.innerHTML = updateStatus.dataset.installingText;
|
updateStatus.innerHTML = updateStatus.dataset.installingText as string;
|
||||||
|
|
||||||
new Request(
|
new Request(
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${app.config.baseUri}updates/update/`,
|
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) => {
|
(response) => {
|
||||||
const notification = new Notification(response.message, response.status, { icon: "check-circle" });
|
const notification = new Notification(response.message, response.status, { icon: "check-circle" });
|
@ -1,16 +1,16 @@
|
|||||||
// HTMLFormElement.prototype.requestSubmit polyfill
|
// HTMLFormElement.prototype.requestSubmit polyfill
|
||||||
// see https://github.com/javan/form-request-submit-polyfill
|
// see https://github.com/javan/form-request-submit-polyfill
|
||||||
if (!("requestSubmit" in window.HTMLFormElement.prototype)) {
|
if (typeof window.HTMLFormElement.prototype.requestSubmit === "undefined") {
|
||||||
window.HTMLFormElement.prototype.requestSubmit = function (submitter) {
|
window.HTMLFormElement.prototype.requestSubmit = function (submitter: HTMLInputElement) {
|
||||||
if (submitter) {
|
if (submitter) {
|
||||||
if (!(submitter instanceof HTMLElement)) {
|
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") {
|
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) {
|
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();
|
submitter.click();
|
||||||
} else {
|
} else {
|
||||||
@ -21,9 +21,5 @@ if (!("requestSubmit" in window.HTMLFormElement.prototype)) {
|
|||||||
submitter.click();
|
submitter.click();
|
||||||
this.removeChild(submitter);
|
this.removeChild(submitter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function raise(error, message, name) {
|
|
||||||
throw new error(`Failed to execute 'requestSubmit' on 'HTMLFormElement': ${message}.`, name);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
export function arrayEquals(array1, array2) {
|
export function arrayEquals(array1: Array<any>, array2: Array<any>) {
|
||||||
if (array1.length !== array2.length) {
|
if (array1.length !== array2.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
export function getCookies() {
|
export function getCookies() {
|
||||||
const result = [];
|
const result: Record<string, string> = {};
|
||||||
const cookies = document.cookie.split(";");
|
const cookies = document.cookie.split(";");
|
||||||
for (const cookie of cookies) {
|
for (const cookie of cookies) {
|
||||||
const nameAndValue = cookie.split("=", 2);
|
const nameAndValue = cookie.split("=", 2);
|
||||||
@ -10,7 +10,7 @@ export function getCookies() {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setCookie(name, value, options) {
|
export function setCookie(name: string, value: string, options: Record<string, string | number>) {
|
||||||
let cookie = `${name}=${value}`;
|
let cookie = `${name}=${value}`;
|
||||||
for (const option in options) {
|
for (const option in options) {
|
||||||
cookie += `;${option}=${options[option]}`;
|
cookie += `;${option}=${options[option]}`;
|
@ -1,9 +1,9 @@
|
|||||||
export function getOuterWidth(element) {
|
export function getOuterWidth(element: HTMLElement) {
|
||||||
const style = getComputedStyle(element);
|
const style = getComputedStyle(element);
|
||||||
return element.offsetWidth + parseInt(style.marginLeft) + parseInt(style.marginRight);
|
return element.offsetWidth + parseInt(style.marginLeft) + parseInt(style.marginRight);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOuterHeight(element) {
|
export function getOuterHeight(element: HTMLElement) {
|
||||||
const style = getComputedStyle(element);
|
const style = getComputedStyle(element);
|
||||||
return element.offsetHeight + parseInt(style.marginTop) + parseInt(style.marginBottom);
|
return element.offsetHeight + parseInt(style.marginTop) + parseInt(style.marginBottom);
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
export function debounce(callback, delay, leading) {
|
export function debounce(callback: (...args: any[]) => any, delay: number, leading: boolean = false) {
|
||||||
let result;
|
let result: any;
|
||||||
let timer = null;
|
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 context = this;
|
||||||
const args = arguments;
|
|
||||||
if (timer) {
|
if (timer) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
@ -23,19 +23,19 @@ export function debounce(callback, delay, leading) {
|
|||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function throttle(callback, delay) {
|
export function throttle(callback: (...args: any[]) => any, delay: number) {
|
||||||
let result;
|
let result: any;
|
||||||
let previous = 0;
|
let previous = 0;
|
||||||
let timer = null;
|
let timer: number | null = null;
|
||||||
|
|
||||||
function wrapper() {
|
function wrapper(this: any, ...args: any[]) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (previous === 0) {
|
if (previous === 0) {
|
||||||
previous = now;
|
previous = now;
|
||||||
}
|
}
|
||||||
const remaining = previous + delay - now;
|
const remaining = previous + delay - now;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
const context = this;
|
const context = this;
|
||||||
const args = arguments;
|
|
||||||
if (remaining <= 0 || remaining > delay) {
|
if (remaining <= 0 || remaining > delay) {
|
||||||
if (timer) {
|
if (timer) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
@ -1,14 +1,14 @@
|
|||||||
export function serializeObject(object) {
|
export function serializeObject(object: Record<string, string | number | boolean>) {
|
||||||
const serialized = [];
|
const serialized: string[] = [];
|
||||||
for (const property in object) {
|
for (const property in object) {
|
||||||
serialized.push(`${encodeURIComponent(property)}=${encodeURIComponent(object[property])}`);
|
serialized.push(`${encodeURIComponent(property)}=${encodeURIComponent(object[property])}`);
|
||||||
}
|
}
|
||||||
return serialized.join("&");
|
return serialized.join("&");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeForm(form) {
|
export function serializeForm(form: HTMLFormElement) {
|
||||||
const serialized = [];
|
const serialized: string[] = [];
|
||||||
for (const field of form.elements) {
|
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.name && !field.disabled && field.dataset.formIgnore !== "true" && field.type !== "file" && field.type !== "reset" && field.type !== "submit" && field.type !== "button") {
|
||||||
if (field.type === "select-multiple") {
|
if (field.type === "select-multiple") {
|
||||||
for (const option of field.options) {
|
for (const option of field.options) {
|
||||||
@ -24,7 +24,7 @@ export function serializeForm(form) {
|
|||||||
return serialized.join("&");
|
return serialized.join("&");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function triggerDownload(uri, csrfToken) {
|
export function triggerDownload(uri: string, csrfToken: string) {
|
||||||
const form = document.createElement("form");
|
const form = document.createElement("form");
|
||||||
form.action = uri;
|
form.action = uri;
|
||||||
form.method = "post";
|
form.method = "post";
|
@ -1,7 +1,13 @@
|
|||||||
import { serializeObject } from "./forms";
|
import { serializeObject } from "./forms";
|
||||||
|
|
||||||
|
type RequestOptions = {
|
||||||
|
method: string;
|
||||||
|
url: string;
|
||||||
|
data: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
export class Request {
|
export class Request {
|
||||||
constructor(options, callback) {
|
constructor(options: RequestOptions, callback: (response: Record<string, any>, request: XMLHttpRequest) => void) {
|
||||||
const request = new XMLHttpRequest();
|
const request = new XMLHttpRequest();
|
||||||
|
|
||||||
request.open(options.method, options.url, true);
|
request.open(options.method, options.url, true);
|
7
panel/src/ts/utils/selectors.ts
Normal file
7
panel/src/ts/utils/selectors.ts
Normal 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);
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
export function escapeRegExp(string) {
|
export function escapeRegExp(string: string) {
|
||||||
return string.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
|
return string.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeDiacriticsRegExp(string) {
|
export function makeDiacriticsRegExp(string: string) {
|
||||||
const diacritics = {
|
const diacritics: Record<string, string> = {
|
||||||
a: "[aáàăâǎåäãȧąāảȁạ]",
|
a: "[aáàăâǎåäãȧąāảȁạ]",
|
||||||
b: "[bḃḅ]",
|
b: "[bḃḅ]",
|
||||||
c: "[cćĉčċç]",
|
c: "[cćĉčċç]",
|
||||||
@ -36,8 +36,8 @@ export function makeDiacriticsRegExp(string) {
|
|||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeSlug(string) {
|
export function makeSlug(string: string) {
|
||||||
const translate = {
|
const translate: Record<string, string> = {
|
||||||
"\t": "",
|
"\t": "",
|
||||||
"\r": "",
|
"\r": "",
|
||||||
"!": "",
|
"!": "",
|
||||||
@ -167,7 +167,7 @@ export function makeSlug(string) {
|
|||||||
.replace(/-+/g, "-");
|
.replace(/-+/g, "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateSlug(slug) {
|
export function validateSlug(slug: string) {
|
||||||
return slug
|
return slug
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(" ", "-")
|
.replace(" ", "-")
|
15
panel/tsconfig.json
Normal file
15
panel/tsconfig.json
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
639
panel/yarn.lock
639
panel/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user