mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-04-23 17:18:44 +02:00
feat: more changes and fixes
This commit is contained in:
66
frontend/src/js/_htmx.js
Normal file
66
frontend/src/js/_htmx.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import htmx from "htmx.org";
|
||||
|
||||
window.htmx = htmx;
|
||||
|
||||
htmx.defineExtension('htmx-download', {
|
||||
onEvent: function (name, evt) {
|
||||
|
||||
if (name === 'htmx:beforeRequest') {
|
||||
// Set the responseType to 'arraybuffer' to handle binary data
|
||||
evt.detail.xhr.responseType = 'arraybuffer';
|
||||
}
|
||||
|
||||
if (name === 'htmx:beforeSwap') {
|
||||
const xhr = evt.detail.xhr;
|
||||
|
||||
if (xhr.status === 200) {
|
||||
// Parse headers
|
||||
const headers = {};
|
||||
const headerStr = xhr.getAllResponseHeaders();
|
||||
const headerArr = headerStr.trim().split(/[\r\n]+/);
|
||||
headerArr.forEach((line) => {
|
||||
const parts = line.split(": ");
|
||||
const header = parts.shift().toLowerCase();
|
||||
const value = parts.join(": ");
|
||||
headers[header] = value;
|
||||
});
|
||||
|
||||
// Extract filename
|
||||
let filename = 'downloaded_file.xlsx';
|
||||
if (headers['content-disposition']) {
|
||||
const filenameMatch = headers['content-disposition'].match(/filename\*?=(?:UTF-8'')?"?([^;\n"]+)/i);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
filename = decodeURIComponent(filenameMatch[1].replace(/['"]/g, ''));
|
||||
}
|
||||
}
|
||||
|
||||
// Determine MIME type
|
||||
const mimetype = headers['content-type'] || 'application/octet-stream';
|
||||
|
||||
// Create Blob
|
||||
const blob = new Blob([xhr.response], {type: mimetype});
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Trigger download
|
||||
const link = document.createElement("a");
|
||||
link.style.display = "none";
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// Cleanup
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
link.remove();
|
||||
}, 100);
|
||||
|
||||
} else {
|
||||
console.warn(`[htmx-download] Unexpected response status: ${xhr.status}`);
|
||||
}
|
||||
|
||||
// Prevent htmx from swapping content
|
||||
evt.detail.shouldSwap = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
37
frontend/src/js/_utils.js
Normal file
37
frontend/src/js/_utils.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Converts ANY valid CSS color string (oklch, hex, hsl, etc.)
|
||||
* into a standard RGBA string that Chart.js can understand.
|
||||
* This method uses a canvas to force the browser to compute the color.
|
||||
* @param {string} colorString The color string to convert.
|
||||
* @returns {string} The computed 'rgba(r, g, b, a)' string.
|
||||
*/
|
||||
window.convertColorToRgba = function convertColorToRgba(colorString) {
|
||||
if (!colorString) return 'rgba(0,0,0,0.1)'; // Fallback
|
||||
|
||||
console.log(colorString)
|
||||
|
||||
// Create a 1x1 pixel canvas
|
||||
let canvas = document.createElement('canvas');
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
let ctx = canvas.getContext('2d');
|
||||
|
||||
// Set the fillStyle to the color string
|
||||
// The browser MUST parse the oklch string here
|
||||
ctx.fillStyle = colorString.trim();
|
||||
|
||||
// Draw the pixel
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
|
||||
// Get the pixel data. This is ALWAYS returned as [R, G, B, A]
|
||||
// with values from 0-255.
|
||||
const data = ctx.getImageData(0, 0, 1, 1).data;
|
||||
|
||||
// Convert the 0-255 alpha to a 0-1 float
|
||||
const a = data[3] / 255;
|
||||
|
||||
console.log(data)
|
||||
|
||||
// Return the standard rgba string
|
||||
return `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${a})`;
|
||||
}
|
||||
11
frontend/src/js/autosize.js
Normal file
11
frontend/src/js/autosize.js
Normal file
@@ -0,0 +1,11 @@
|
||||
document.addEventListener("input", function (e) {
|
||||
// Check if the element that triggered the input event is a <textarea>
|
||||
if (e.target.tagName.toLowerCase() === "textarea") {
|
||||
|
||||
// Reset height to 'auto' to allow the textarea to shrink
|
||||
e.target.style.height = "auto";
|
||||
|
||||
// Set the height to its scrollHeight (the full height of the content)
|
||||
e.target.style.height = (e.target.scrollHeight + 5) + "px";
|
||||
}
|
||||
}, false);
|
||||
27
frontend/src/js/bootstrap.js
vendored
Normal file
27
frontend/src/js/bootstrap.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import './_tooltip.js';
|
||||
import 'bootstrap/js/dist/dropdown';
|
||||
import Toast from 'bootstrap/js/dist/toast';
|
||||
import 'bootstrap/js/dist/dropdown';
|
||||
import 'bootstrap/js/dist/collapse';
|
||||
import Offcanvas from 'bootstrap/js/dist/offcanvas';
|
||||
|
||||
window.Offcanvas = Offcanvas;
|
||||
|
||||
|
||||
function initiateToasts() {
|
||||
const toastElList = document.querySelectorAll('.toasty');
|
||||
const toastList = [...toastElList].map(toastEl => new Toast(toastEl)); // eslint-disable-line no-undef
|
||||
|
||||
for (let i = 0; i < toastList.length; i++) {
|
||||
if (toastList[i].isShown() === false) {
|
||||
toastList[i].show();
|
||||
toastList[i]._element.addEventListener('hidden.bs.toast', (event) => {
|
||||
event.target.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initiateToasts, false);
|
||||
document.addEventListener('htmx:afterSwap', initiateToasts, false);
|
||||
initiateToasts();
|
||||
5
frontend/src/js/charts.js
Normal file
5
frontend/src/js/charts.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import Chart from 'chart.js/auto';
|
||||
import {SankeyController, Flow} from 'chartjs-chart-sankey';
|
||||
|
||||
Chart.register(SankeyController, Flow);
|
||||
window.Chart = Chart;
|
||||
308
frontend/src/js/datepicker.js
Normal file
308
frontend/src/js/datepicker.js
Normal file
@@ -0,0 +1,308 @@
|
||||
import AirDatepicker from 'air-datepicker';
|
||||
import {createPopper} from '@popperjs/core';
|
||||
|
||||
// --- Static Locale Imports ---
|
||||
// We import all locales statically to ensure Vite transforms them correctly.
|
||||
import localeAr from 'air-datepicker/locale/ar.js';
|
||||
import localeBg from 'air-datepicker/locale/bg.js';
|
||||
import localeCa from 'air-datepicker/locale/ca.js';
|
||||
import localeCs from 'air-datepicker/locale/cs.js';
|
||||
import localeDa from 'air-datepicker/locale/da.js';
|
||||
import localeDe from 'air-datepicker/locale/de.js';
|
||||
import localeEl from 'air-datepicker/locale/el.js';
|
||||
import localeEn from 'air-datepicker/locale/en.js';
|
||||
import localeEs from 'air-datepicker/locale/es.js';
|
||||
import localeEu from 'air-datepicker/locale/eu.js';
|
||||
import localeFi from 'air-datepicker/locale/fi.js';
|
||||
import localeFr from 'air-datepicker/locale/fr.js';
|
||||
import localeHr from 'air-datepicker/locale/hr.js';
|
||||
import localeHu from 'air-datepicker/locale/hu.js';
|
||||
import localeId from 'air-datepicker/locale/id.js';
|
||||
import localeIt from 'air-datepicker/locale/it.js';
|
||||
import localeJa from 'air-datepicker/locale/ja.js';
|
||||
import localeKo from 'air-datepicker/locale/ko.js';
|
||||
import localeNb from 'air-datepicker/locale/nb.js';
|
||||
import localeNl from 'air-datepicker/locale/nl.js';
|
||||
import localePl from 'air-datepicker/locale/pl.js';
|
||||
import localePtBr from 'air-datepicker/locale/pt-BR.js';
|
||||
import localePt from 'air-datepicker/locale/pt.js';
|
||||
import localeRo from 'air-datepicker/locale/ro.js';
|
||||
import localeRu from 'air-datepicker/locale/ru.js';
|
||||
import localeSi from 'air-datepicker/locale/si.js';
|
||||
import localeSk from 'air-datepicker/locale/sk.js';
|
||||
import localeSl from 'air-datepicker/locale/sl.js';
|
||||
import localeSv from 'air-datepicker/locale/sv.js';
|
||||
import localeTh from 'air-datepicker/locale/th.js';
|
||||
import localeTr from 'air-datepicker/locale/tr.js';
|
||||
import localeUk from 'air-datepicker/locale/uk.js';
|
||||
import localeZh from 'air-datepicker/locale/zh.js';
|
||||
|
||||
// Map language codes to their imported locale objects
|
||||
const allLocales = {
|
||||
'ar': localeAr,
|
||||
'bg': localeBg,
|
||||
'ca': localeCa,
|
||||
'cs': localeCs,
|
||||
'da': localeDa,
|
||||
'de': localeDe,
|
||||
'el': localeEl,
|
||||
'en': localeEn,
|
||||
'es': localeEs,
|
||||
'eu': localeEu,
|
||||
'fi': localeFi,
|
||||
'fr': localeFr,
|
||||
'hr': localeHr,
|
||||
'hu': localeHu,
|
||||
'id': localeId,
|
||||
'it': localeIt,
|
||||
'ja': localeJa,
|
||||
'ko': localeKo,
|
||||
'nb': localeNb,
|
||||
'nl': localeNl,
|
||||
'pl': localePl,
|
||||
'pt-BR': localePtBr,
|
||||
'pt': localePt,
|
||||
'ro': localeRo,
|
||||
'ru': localeRu,
|
||||
'si': localeSi,
|
||||
'sk': localeSk,
|
||||
'sl': localeSl,
|
||||
'sv': localeSv,
|
||||
'th': localeTh,
|
||||
'tr': localeTr,
|
||||
'uk': localeUk,
|
||||
'zh': localeZh
|
||||
};
|
||||
// --- End of Locale Imports ---
|
||||
|
||||
|
||||
/**
|
||||
* Selects a pre-imported language file from the locale map.
|
||||
*
|
||||
* @param {string} langCode - The two-letter language code (e.g., 'en', 'es').
|
||||
* @returns {Promise<object>} A promise that resolves with the locale object.
|
||||
*/
|
||||
export const getLocale = async (langCode) => {
|
||||
const locale = allLocales[langCode];
|
||||
|
||||
if (locale) {
|
||||
return locale;
|
||||
}
|
||||
|
||||
console.warn(`Could not find locale for '${langCode}'. Defaulting to English.`);
|
||||
return allLocales['en']; // Default to English
|
||||
};
|
||||
|
||||
function isMobileDevice() {
|
||||
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
|
||||
return mobileRegex.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
function isTouchDevice() {
|
||||
return ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0);
|
||||
}
|
||||
|
||||
function isMobile() {
|
||||
return isMobileDevice() || isTouchDevice();
|
||||
}
|
||||
|
||||
window.DatePicker = async function createDynamicDatePicker(element) {
|
||||
let todayButton = {
|
||||
content: element.dataset.nowButtonTxt,
|
||||
onClick: (dp) => {
|
||||
let date = new Date();
|
||||
dp.selectDate(date, {updateTime: true});
|
||||
dp.setViewDate(date);
|
||||
}
|
||||
};
|
||||
let isOnMobile = isMobile();
|
||||
let baseOpts = {
|
||||
isMobile: isOnMobile,
|
||||
dateFormat: element.dataset.dateFormat,
|
||||
timeFormat: element.dataset.timeFormat,
|
||||
timepicker: element.dataset.timepicker === 'true',
|
||||
toggleSelected: element.dataset.toggleSelected === 'true',
|
||||
autoClose: element.dataset.autoClose === 'true',
|
||||
buttons: element.dataset.clearButton === 'true' ? ['clear', todayButton] : [todayButton],
|
||||
locale: await getLocale(element.dataset.language),
|
||||
onSelect: ({date, formattedDate, datepicker}) => {
|
||||
const _event = new CustomEvent("change", {
|
||||
bubbles: true,
|
||||
});
|
||||
datepicker.$el.dispatchEvent(_event);
|
||||
}
|
||||
};
|
||||
const positionConfig = !isOnMobile ? {
|
||||
position({$datepicker, $target, $pointer, done}) {
|
||||
let popper = createPopper($target, $datepicker, {
|
||||
placement: 'bottom',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
padding: {
|
||||
top: 64
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 20]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'arrow',
|
||||
options: {
|
||||
element: $pointer
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
return function completeHide() {
|
||||
popper.destroy();
|
||||
done();
|
||||
};
|
||||
}
|
||||
} : {};
|
||||
let opts = {...baseOpts, ...positionConfig};
|
||||
if (element.dataset.value) {
|
||||
opts["selectedDates"] = [element.dataset.value];
|
||||
opts["startDate"] = [element.dataset.value];
|
||||
}
|
||||
return new AirDatepicker(element, opts);
|
||||
};
|
||||
|
||||
window.MonthYearPicker = async function createDynamicDatePicker(element) {
|
||||
let todayButton = {
|
||||
content: element.dataset.nowButtonTxt,
|
||||
onClick: (dp) => {
|
||||
let date = new Date();
|
||||
dp.selectDate(date, {updateTime: true});
|
||||
dp.setViewDate(date);
|
||||
}
|
||||
};
|
||||
let isOnMobile = isMobile();
|
||||
let baseOpts = {
|
||||
isMobile: isOnMobile,
|
||||
view: 'months',
|
||||
minView: 'months',
|
||||
dateFormat: 'MMMM yyyy',
|
||||
toggleSelected: element.dataset.toggleSelected === 'true',
|
||||
autoClose: element.dataset.autoClose === 'true',
|
||||
buttons: element.dataset.clearButton === 'true' ? ['clear', todayButton] : [todayButton],
|
||||
locale: await getLocale(element.dataset.language),
|
||||
onSelect: ({date, formattedDate, datepicker}) => {
|
||||
const _event = new CustomEvent("change", {
|
||||
bubbles: true,
|
||||
});
|
||||
datepicker.$el.dispatchEvent(_event);
|
||||
}
|
||||
};
|
||||
const positionConfig = !isOnMobile ? {
|
||||
position({$datepicker, $target, $pointer, done}) {
|
||||
let popper = createPopper($target, $datepicker, {
|
||||
placement: 'bottom',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
padding: {
|
||||
top: 64
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 20]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'arrow',
|
||||
options: {
|
||||
element: $pointer
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
return function completeHide() {
|
||||
popper.destroy();
|
||||
done();
|
||||
};
|
||||
}
|
||||
} : {};
|
||||
let opts = {...baseOpts, ...positionConfig};
|
||||
if (element.dataset.value) {
|
||||
opts["selectedDates"] = [new Date(element.dataset.value + "T00:00:00")];
|
||||
opts["startDate"] = [new Date(element.dataset.value + "T00:00:00")];
|
||||
}
|
||||
return new AirDatepicker(element, opts);
|
||||
};
|
||||
|
||||
window.YearPicker = async function createDynamicDatePicker(element) {
|
||||
let todayButton = {
|
||||
content: element.dataset.nowButtonTxt,
|
||||
onClick: (dp) => {
|
||||
let date = new Date();
|
||||
dp.selectDate(date, {updateTime: true});
|
||||
dp.setViewDate(date);
|
||||
}
|
||||
};
|
||||
let isOnMobile = isMobile();
|
||||
let baseOpts = {
|
||||
isMobile: isOnMobile,
|
||||
view: 'years',
|
||||
minView: 'years',
|
||||
dateFormat: 'yyyy',
|
||||
toggleSelected: element.dataset.toggleSelected === 'true',
|
||||
autoClose: element.dataset.autoClose === 'true',
|
||||
buttons: element.dataset.clearButton === 'true' ? ['clear', todayButton] : [todayButton],
|
||||
locale: await getLocale(element.dataset.language),
|
||||
onSelect: ({date, formattedDate, datepicker}) => {
|
||||
const _event = new CustomEvent("change", {
|
||||
bubbles: true,
|
||||
});
|
||||
datepicker.$el.dispatchEvent(_event);
|
||||
}
|
||||
};
|
||||
const positionConfig = !isOnMobile ? {
|
||||
position({$datepicker, $target, $pointer, done}) {
|
||||
let popper = createPopper($target, $datepicker, {
|
||||
placement: 'bottom',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
padding: {
|
||||
top: 64
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 20]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'arrow',
|
||||
options: {
|
||||
element: $pointer
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
return function completeHide() {
|
||||
popper.destroy();
|
||||
done();
|
||||
};
|
||||
}
|
||||
} : {};
|
||||
let opts = {...baseOpts, ...positionConfig};
|
||||
if (element.dataset.value) {
|
||||
opts["selectedDates"] = [new Date(element.dataset.value + "T00:00:00")];
|
||||
opts["startDate"] = [new Date(element.dataset.value + "T00:00:00")];
|
||||
}
|
||||
return new AirDatepicker(element, opts);
|
||||
};
|
||||
40
frontend/src/js/htmx.js
Normal file
40
frontend/src/js/htmx.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import _hyperscript from 'hyperscript.org/dist/_hyperscript.min';
|
||||
import './_htmx.js';
|
||||
import Alpine from "alpinejs";
|
||||
import mask from '@alpinejs/mask';
|
||||
import {create, all} from 'mathjs';
|
||||
|
||||
window.Alpine = Alpine;
|
||||
window._hyperscript = _hyperscript;
|
||||
window.math = create(all, {
|
||||
number: 'BigNumber', // Default type of number:
|
||||
// 'number' (default), 'BigNumber', or 'Fraction'
|
||||
precision: 64, // Number of significant digits for BigNumbers
|
||||
relTol: 1e-60,
|
||||
absTol: 1e-63
|
||||
});
|
||||
|
||||
Alpine.plugin(mask);
|
||||
Alpine.start();
|
||||
_hyperscript.browserInit();
|
||||
|
||||
|
||||
const successAudio = new Audio("/static/sounds/success.mp3");
|
||||
const popAudio = new Audio("/static/sounds/pop.mp3");
|
||||
window.paidSound = successAudio;
|
||||
window.unpaidSound = popAudio;
|
||||
|
||||
/**
|
||||
* Parse a localized number to a float.
|
||||
* @param {string} stringNumber - the localized number
|
||||
* @param {string} locale - [optional] the locale that the number is represented in. Omit this parameter to use the current locale.
|
||||
*/
|
||||
window.parseLocaleNumber = function parseLocaleNumber(stringNumber, locale) {
|
||||
let thousandSeparator = Intl.NumberFormat(locale).format(11111).replace(/\d/g, '');
|
||||
let decimalSeparator = Intl.NumberFormat(locale).format(1.1).replace(/\d/g, '');
|
||||
|
||||
return parseFloat(stringNumber
|
||||
.replace(new RegExp('\\' + thousandSeparator, 'g'), '')
|
||||
.replace(new RegExp('\\' + decimalSeparator), '.')
|
||||
);
|
||||
};
|
||||
3
frontend/src/js/jquery.js
vendored
Normal file
3
frontend/src/js/jquery.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
const $ = require('jquery');
|
||||
window.jQuery = $;
|
||||
window.$ = $;
|
||||
103
frontend/src/js/select.js
Normal file
103
frontend/src/js/select.js
Normal file
@@ -0,0 +1,103 @@
|
||||
// import 'tom-select/dist/css/tom-select.default.min.css';
|
||||
|
||||
import TomSelect from "tom-select";
|
||||
import * as Popper from "@popperjs/core";
|
||||
|
||||
|
||||
window.TomSelect = function createDynamicTomSelect(element) {
|
||||
// Basic configuration
|
||||
const config = {
|
||||
plugins: {},
|
||||
maxOptions: null,
|
||||
|
||||
// Extract 'create' option from data attribute
|
||||
create: element.dataset.create === 'true',
|
||||
copyClassesToDropdown: true,
|
||||
allowEmptyOption: element.dataset.allowEmptyOption === 'true',
|
||||
render: {
|
||||
no_results: function () {
|
||||
return `<div class="no-results">${element.dataset.txtNoResults || 'No results...'}</div>`;
|
||||
},
|
||||
option_create: function (data, escape) {
|
||||
return `<div class="create">${element.dataset.txtCreate || 'Add'} <strong>${escape(data.input)}</strong>…</div>`;
|
||||
},
|
||||
},
|
||||
|
||||
onInitialize: function () {
|
||||
// Move dropdown to body to escape stacking context issues
|
||||
document.body.appendChild(this.dropdown);
|
||||
|
||||
this.popper = Popper.createPopper(this.control, this.dropdown, {
|
||||
placement: "bottom-start",
|
||||
strategy: "fixed",
|
||||
modifiers: [
|
||||
{
|
||||
name: "sameWidth",
|
||||
enabled: true,
|
||||
fn: ({ state }) => {
|
||||
state.styles.popper.width = `${state.rects.reference.width}px`;
|
||||
},
|
||||
phase: "beforeWrite",
|
||||
requires: ["computeStyles"],
|
||||
},
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
fallbackPlacements: ['top-start'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundary: 'viewport',
|
||||
padding: 8,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
onDropdownOpen: function () {
|
||||
this.popper.update();
|
||||
},
|
||||
onDropdownClose: function () {
|
||||
// Optional: move back to wrapper to keep DOM clean, but not necessary
|
||||
}
|
||||
};
|
||||
|
||||
if (element.dataset.checkboxes === 'true') {
|
||||
config.plugins.checkbox_options = {
|
||||
'checkedClassNames': ['ts-checked'],
|
||||
'uncheckedClassNames': ['ts-unchecked'],
|
||||
};
|
||||
}
|
||||
|
||||
if (element.dataset.clearButton === 'true') {
|
||||
config.plugins.clear_button = {
|
||||
'title': element.dataset.txtClear || 'Clear',
|
||||
};
|
||||
}
|
||||
|
||||
if (element.dataset.removeButton === 'true') {
|
||||
config.plugins.remove_button = {
|
||||
'title': element.dataset.txtRemove || 'Remove',
|
||||
};
|
||||
}
|
||||
|
||||
if (element.dataset.load) {
|
||||
config.load = function (query, callback) {
|
||||
let url = element.dataset.load + '?q=' + encodeURIComponent(query);
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
callback(json);
|
||||
}).catch(() => {
|
||||
callback();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Create and return the TomSelect instance
|
||||
return new TomSelect(element, config);
|
||||
};
|
||||
4
frontend/src/js/style.js
Normal file
4
frontend/src/js/style.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import '@fontsource-variable/jetbrains-mono/wght-italic.css';
|
||||
import '@fontsource-variable/jetbrains-mono';
|
||||
import '../styles/tailwind.css';
|
||||
import '../styles/style.scss';
|
||||
2
frontend/src/js/sweetalert2.js
Normal file
2
frontend/src/js/sweetalert2.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Swal from 'sweetalert2';
|
||||
window.Swal = Swal;
|
||||
Reference in New Issue
Block a user