589 lines
18 KiB
JavaScript
Executable File
589 lines
18 KiB
JavaScript
Executable File
(function (root, factory) {
|
|
// this is the stupid UMD pattern, but eh we lost that battle a long time ago
|
|
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
|
var exports_obj = factory();
|
|
module.exports = exports_obj.$;
|
|
// Also export individual components
|
|
for (var key in exports_obj) {
|
|
if (key !== '$' && exports_obj.hasOwnProperty(key)) {
|
|
exports[key] = exports_obj[key];
|
|
}
|
|
}
|
|
} else if (typeof define === 'function' && define.amd) {
|
|
define([], factory);
|
|
} else {
|
|
var exports_obj = factory();
|
|
root.$ = exports_obj.$;
|
|
for (var key in exports_obj) {
|
|
if (key !== '$' && exports_obj.hasOwnProperty(key)) {
|
|
root[key] = exports_obj[key];
|
|
}
|
|
}
|
|
}
|
|
}(typeof self !== 'undefined' ? self : this, function () {
|
|
|
|
// a collection of DOM utility functions that I thought were cool in jQuery
|
|
|
|
class EventEmitter {
|
|
constructor() {
|
|
/** Map<eventName, Map<slotKey|Symbol, Function>> */
|
|
this._topics = new Map();
|
|
}
|
|
|
|
/**
|
|
* Subscribe to `topic`. If `slot_key` is provided (truthy),
|
|
* it dedupes by that key; otherwise a unique Symbol is used.
|
|
* Returns an unsubscribe fn.
|
|
*/
|
|
on(topic, handler, slot_key = null) {
|
|
let map = this._topics.get(topic);
|
|
if (!map) {
|
|
map = new Map();
|
|
this._topics.set(topic, map);
|
|
}
|
|
// use the provided slot_key or a fresh Symbol()
|
|
const key = slot_key != null ? slot_key : Symbol();
|
|
map.set(key, handler);
|
|
return () => this.off(topic, key);
|
|
}
|
|
|
|
/**
|
|
* Unsubscribe by handler function or by slot_key.
|
|
*/
|
|
off(topic, handlerOrSlotKey) {
|
|
const map = this._topics.get(topic);
|
|
if (!map) return;
|
|
|
|
// if it matches a slotKey directly, remove it
|
|
if (map.has(handlerOrSlotKey)) {
|
|
map.delete(handlerOrSlotKey);
|
|
} else {
|
|
// otherwise assume it's a function: remove all matching fns
|
|
for (const [key, fn] of map.entries()) {
|
|
if (fn === handlerOrSlotKey) {
|
|
map.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (map.size === 0) {
|
|
this._topics.delete(topic);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emit to all handlers on `topic`. Handlers returning
|
|
* 'remove_handler' are auto-removed.
|
|
* Returns the number of handlers invoked.
|
|
*/
|
|
emit(topic, ...args) {
|
|
let count = 0;
|
|
const map = this._topics.get(topic);
|
|
if (!map) return count;
|
|
|
|
for (const [key, fn] of Array.from(map.entries())) {
|
|
const res = fn(...args);
|
|
count++;
|
|
if (res === 'remove_handler') {
|
|
map.delete(key);
|
|
}
|
|
}
|
|
|
|
if (map.size === 0) {
|
|
this._topics.delete(topic);
|
|
}
|
|
return count;
|
|
}
|
|
}
|
|
|
|
class QueryWrapper extends Array {
|
|
constructor(elements) {
|
|
super();
|
|
if (elements) {
|
|
this.push(...(Array.isArray(elements) ? elements : [elements]));
|
|
}
|
|
}
|
|
}
|
|
|
|
$ = function(selector_or_element) {
|
|
let elements;
|
|
if (selector_or_element instanceof Element || selector_or_element === document || selector_or_element === window) {
|
|
elements = [selector_or_element];
|
|
} else if (typeof selector_or_element === 'string') {
|
|
// Check if string looks like HTML (starts with < and ends with >)
|
|
if (selector_or_element.trim().charAt(0) === '<' && selector_or_element.trim().charAt(selector_or_element.trim().length - 1) === '>') {
|
|
// Create element from HTML string
|
|
let temp = document.createElement('div');
|
|
temp.innerHTML = selector_or_element.trim();
|
|
elements = Array.from(temp.children);
|
|
} else {
|
|
// Treat as CSS selector
|
|
elements = Array.from(document.querySelectorAll(selector_or_element));
|
|
}
|
|
} else if (selector_or_element instanceof NodeList || Array.isArray(selector_or_element)) {
|
|
elements = Array.from(selector_or_element);
|
|
} else {
|
|
elements = [selector_or_element];
|
|
}
|
|
return new QueryWrapper(elements);
|
|
};
|
|
|
|
$.options = {
|
|
alwaysDoDifferentialUpdate: true,
|
|
};
|
|
|
|
$.events = new EventEmitter();
|
|
$.on = $.events.on.bind($.events);
|
|
$.off = $.events.off.bind($.events);
|
|
$.emit = $.events.emit.bind($.events);
|
|
|
|
$.each = function(selector, callback) {
|
|
let elements;
|
|
if (typeof selector === 'string') {
|
|
elements = document.querySelectorAll(selector);
|
|
} else if (selector instanceof QueryWrapper) {
|
|
elements = selector;
|
|
} else {
|
|
elements = selector;
|
|
}
|
|
for (let i = 0; i < elements.length; i++) {
|
|
callback(elements[i], i);
|
|
}
|
|
}
|
|
|
|
$.post = function(url, data, callback) {
|
|
return $.ajax({
|
|
method: 'POST',
|
|
url: url,
|
|
data: data,
|
|
success: callback,
|
|
error: function(xhr) {
|
|
console.error('POST request failed:', xhr);
|
|
}
|
|
});
|
|
}
|
|
|
|
$.get = function(url, callback) {
|
|
return $.ajax({
|
|
method: 'GET',
|
|
url: url,
|
|
success: callback,
|
|
error: function(xhr) {
|
|
console.error('GET request failed:', xhr);
|
|
}
|
|
});
|
|
}
|
|
|
|
$.ajax = function(options) {
|
|
return new Promise((resolve, reject) => {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open(options.method || 'GET', options.url, true);
|
|
|
|
// Set default headers
|
|
if (options.method === 'POST' || options.data) {
|
|
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
|
|
}
|
|
|
|
// Set custom headers
|
|
if (options.headers) {
|
|
for (var key in options.headers) {
|
|
xhr.setRequestHeader(key, options.headers[key]);
|
|
}
|
|
}
|
|
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.readyState === 4) {
|
|
var response;
|
|
try {
|
|
response = JSON.parse(xhr.responseText);
|
|
} catch (e) {
|
|
// If JSON parsing fails, use raw response
|
|
response = xhr.responseText;
|
|
}
|
|
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
if (options.success) options.success(response);
|
|
resolve(response);
|
|
} else {
|
|
if (options.error) options.error(xhr);
|
|
reject(xhr);
|
|
}
|
|
}
|
|
};
|
|
|
|
var data = null;
|
|
if (options.data) {
|
|
data = typeof options.data === 'string' ? options.data : JSON.stringify(options.data);
|
|
}
|
|
|
|
xhr.send(data);
|
|
});
|
|
}
|
|
|
|
$.ready = function(callback) {
|
|
if (document.readyState !== 'loading') {
|
|
callback();
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', callback);
|
|
}
|
|
}
|
|
|
|
// Helper function to execute scripts in HTML content
|
|
function executeScripts(htmlContent) {
|
|
// Create a temporary container to parse the HTML
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = htmlContent;
|
|
|
|
// Find all script tags and disable them temporarily
|
|
const scripts = tempDiv.querySelectorAll('script');
|
|
const scriptData = [];
|
|
|
|
scripts.forEach((script) => {
|
|
// Store script data for later execution
|
|
scriptData.push({
|
|
content: script.textContent || script.innerText || '',
|
|
src: script.src,
|
|
type: script.type,
|
|
nonce: script.nonce,
|
|
async: script.async,
|
|
defer: script.defer
|
|
});
|
|
|
|
// Disable the script by changing its type
|
|
script.type = 'text/disabled-script';
|
|
});
|
|
|
|
// Return the modified HTML and script data
|
|
return {
|
|
html: tempDiv.innerHTML,
|
|
scripts: scriptData
|
|
};
|
|
}
|
|
|
|
// Helper function to execute a single script
|
|
function executeScript(scriptInfo) {
|
|
if (scriptInfo.src) {
|
|
// External script
|
|
const newScript = document.createElement('script');
|
|
if (scriptInfo.type && scriptInfo.type !== 'text/disabled-script') {
|
|
newScript.type = scriptInfo.type;
|
|
}
|
|
if (scriptInfo.nonce) newScript.nonce = scriptInfo.nonce;
|
|
if (scriptInfo.async) newScript.async = scriptInfo.async;
|
|
if (scriptInfo.defer) newScript.defer = scriptInfo.defer;
|
|
newScript.src = scriptInfo.src;
|
|
|
|
document.head.appendChild(newScript);
|
|
} else if (scriptInfo.content.trim()) {
|
|
// Inline script
|
|
const newScript = document.createElement('script');
|
|
if (scriptInfo.type && scriptInfo.type !== 'text/disabled-script') {
|
|
newScript.type = scriptInfo.type;
|
|
}
|
|
if (scriptInfo.nonce) newScript.nonce = scriptInfo.nonce;
|
|
newScript.text = scriptInfo.content;
|
|
|
|
// Execute by inserting and immediately removing
|
|
document.head.appendChild(newScript);
|
|
document.head.removeChild(newScript);
|
|
}
|
|
}
|
|
|
|
// Add all the jQuery-like methods to QueryWrapper
|
|
QueryWrapper.prototype.parent = function() {
|
|
const parents = Array.from(this).map(el => el.parentNode).filter(el => el);
|
|
return new QueryWrapper(parents);
|
|
}
|
|
|
|
QueryWrapper.prototype.load = function(url, opt = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const ajaxOptions = {
|
|
method: opt.method || (opt.postData ? 'POST' : 'GET'),
|
|
url: url,
|
|
success: (response) => {
|
|
if(opt.replace)
|
|
this.replaceWith(response, opt);
|
|
else
|
|
this.html(response, opt);
|
|
if (opt.onLoad) opt.onLoad(response, null);
|
|
resolve(this);
|
|
},
|
|
error: (xhr) => {
|
|
if (opt.onError) opt.onError(xhr);
|
|
reject(new Error('Failed to load: ' + url));
|
|
}
|
|
};
|
|
|
|
if (opt.postData) {
|
|
ajaxOptions.data = opt.postData;
|
|
}
|
|
|
|
$.ajax(ajaxOptions);
|
|
});
|
|
}
|
|
|
|
QueryWrapper.prototype.html = function(opt_content = false, prop = {}) {
|
|
if (opt_content === false) {
|
|
return Array.from(this).map(el => el.innerHTML).join('');
|
|
}
|
|
if(typeof prop.diff == 'undefined')
|
|
prop.diff = $.options.alwaysDoDifferentialUpdate;
|
|
if(prop.diff === true) {
|
|
const temp = document.createElement('div');
|
|
temp.innerHTML = opt_content;
|
|
this.forEach(el => {
|
|
morphdom(el, temp, { childrenOnly: true });
|
|
});
|
|
} else {
|
|
this.forEach(el => {
|
|
const result = executeScripts(opt_content);
|
|
el.innerHTML = result.html;
|
|
result.scripts.forEach(executeScript);
|
|
});
|
|
}
|
|
return this;
|
|
}
|
|
|
|
QueryWrapper.prototype.text = function(opt_content = false) {
|
|
if (opt_content === false) {
|
|
return Array.from(this).map(el => el.textContent).join('');
|
|
}
|
|
this.forEach(el => {
|
|
el.textContent = opt_content;
|
|
});
|
|
return this;
|
|
}
|
|
|
|
QueryWrapper.prototype.replaceWith = function(content, prop = {}) {
|
|
if(typeof prop.diff == 'undefined')
|
|
prop.diff = $.options.alwaysDoDifferentialUpdate;
|
|
if(prop.diff === true) {
|
|
this.forEach(el => {
|
|
morphdom(el, content);
|
|
});
|
|
} else {
|
|
if(typeof content === 'string') {
|
|
this.forEach(el => {
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = content;
|
|
el.parentNode.replaceChild(tempDiv.firstChild, el);
|
|
});
|
|
} else if(content instanceof Element) {
|
|
this.forEach(el => {
|
|
el.parentNode.replaceChild(content.cloneNode(true), el);
|
|
});
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
|
|
QueryWrapper.prototype.query = function(selector) {
|
|
const found = Array.from(this).reduce((acc, el) => {
|
|
const results = el.querySelectorAll(selector);
|
|
return acc.concat(Array.from(results));
|
|
}, []);
|
|
return new QueryWrapper(found);
|
|
}
|
|
|
|
QueryWrapper.prototype.find = function(selector) {
|
|
const found = Array.from(this).reduce((acc, el) => {
|
|
const results = el.querySelectorAll(selector);
|
|
return acc.concat(Array.from(results));
|
|
}, []);
|
|
return new QueryWrapper(found);
|
|
}
|
|
|
|
QueryWrapper.prototype.on = function(event, handler) {
|
|
this.forEach(function(el) {
|
|
el.addEventListener(event, handler);
|
|
});
|
|
return this;
|
|
}
|
|
|
|
QueryWrapper.prototype.off = function(event, handler) {
|
|
this.forEach(function(el) {
|
|
el.removeEventListener(event, handler);
|
|
});
|
|
return this;
|
|
}
|
|
|
|
QueryWrapper.prototype.hide = function() {
|
|
return this.css({ display: 'none' });
|
|
}
|
|
|
|
QueryWrapper.prototype.show = function() {
|
|
return this.css({ display: '' });
|
|
}
|
|
|
|
QueryWrapper.prototype.toggle = function() {
|
|
this.forEach(function(el) {
|
|
el.style.display = (el.style.display === 'none' || el.style.display === '') ? '' : 'none';
|
|
});
|
|
return this;
|
|
}
|
|
|
|
QueryWrapper.prototype.attr = function(name, value) {
|
|
if (value === undefined) {
|
|
return this.length > 0 ? this[0].getAttribute(name) : null;
|
|
}
|
|
this.forEach(function(el) {
|
|
el.setAttribute(name, value);
|
|
});
|
|
return this;
|
|
}
|
|
|
|
QueryWrapper.prototype.addClass = function(classNameOrList) {
|
|
var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList];
|
|
this.forEach(function(el) {
|
|
classList.forEach(function(classNames) {
|
|
// Split space-separated class names
|
|
var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; });
|
|
classes.forEach(function(className) {
|
|
el.classList.add(className);
|
|
});
|
|
});
|
|
});
|
|
return this;
|
|
}
|
|
|
|
QueryWrapper.prototype.removeClass = function(classNameOrList) {
|
|
var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList];
|
|
this.forEach(function(el) {
|
|
classList.forEach(function(classNames) {
|
|
// Split space-separated class names
|
|
var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; });
|
|
classes.forEach(function(className) {
|
|
el.classList.remove(className);
|
|
});
|
|
});
|
|
});
|
|
return this;
|
|
}
|
|
|
|
QueryWrapper.prototype.toggleClass = function(classNameOrList, force) {
|
|
var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList];
|
|
this.forEach(function(el) {
|
|
classList.forEach(function(classNames) {
|
|
// Split space-separated class names
|
|
var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; });
|
|
classes.forEach(function(className) {
|
|
if (typeof force !== 'undefined') {
|
|
el.classList.toggle(className, force);
|
|
} else {
|
|
el.classList.toggle(className);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
return this;
|
|
}
|
|
|
|
QueryWrapper.prototype.empty = function() {
|
|
this.forEach(function(el) {
|
|
el.innerHTML = '';
|
|
});
|
|
return this;
|
|
}
|
|
|
|
QueryWrapper.prototype.css = function(styles, optOrValue) {
|
|
// If no arguments or first argument is a string (getter mode)
|
|
if (arguments.length === 0 || (typeof styles === 'string' && arguments.length === 1)) {
|
|
if (this.length === 0) return undefined;
|
|
var el = this[0];
|
|
if (typeof styles === 'string') {
|
|
// Get computed style for a specific property
|
|
return window.getComputedStyle(el)[styles];
|
|
} else {
|
|
// Return computed styles object (not commonly used)
|
|
return window.getComputedStyle(el);
|
|
}
|
|
}
|
|
|
|
// Setter mode
|
|
this.forEach(function(el) {
|
|
if (typeof styles === 'string' && arguments.length >= 2) {
|
|
// Single property setter: .css('prop', 'value')
|
|
el.style[styles] = optOrValue;
|
|
} else if (typeof styles === 'object') {
|
|
// Multiple properties setter: .css({prop1: 'value1', prop2: 'value2'})
|
|
for (var key in styles) {
|
|
el.style[key] = styles[key];
|
|
}
|
|
}
|
|
});
|
|
return this;
|
|
}
|
|
|
|
QueryWrapper.prototype.each = function(callback) {
|
|
this.forEach(function(el, index) {
|
|
callback(el, index);
|
|
});
|
|
return this;
|
|
}
|
|
|
|
QueryWrapper.prototype.append = function(child_or_html) {
|
|
this.forEach(function(el) {
|
|
if (typeof child_or_html === 'string' && el.insertAdjacentHTML) {
|
|
if (child_or_html.includes('<script')) {
|
|
const result = executeScripts(child_or_html);
|
|
el.insertAdjacentHTML('beforeend', result.html);
|
|
result.scripts.forEach(executeScript);
|
|
} else {
|
|
el.insertAdjacentHTML('beforeend', child_or_html);
|
|
}
|
|
} else if (el.appendChild) {
|
|
if (typeof child_or_html === 'string') {
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = child_or_html;
|
|
el.appendChild(tempDiv.firstChild);
|
|
} else {
|
|
el.appendChild(child_or_html);
|
|
}
|
|
} else if(el.hasOwnProperty('innerHTML')) {
|
|
el.innerHTML += child_or_html;
|
|
}
|
|
});
|
|
return this;
|
|
}
|
|
|
|
QueryWrapper.prototype.remove = function() {
|
|
this.forEach(function(el) {
|
|
el.parentNode.removeChild(el);
|
|
});
|
|
return this;
|
|
}
|
|
|
|
function first(...args) {
|
|
for (const arg of args) {
|
|
if (arg !== undefined && arg !== null && arg !== '') {
|
|
return arg;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function clamp(v, min, max) {
|
|
if (v < min) return min;
|
|
if (v > max) return max;
|
|
return v;
|
|
}
|
|
|
|
function pick_entry_from_range(array, value) {
|
|
if (!array) return {};
|
|
let result = {};
|
|
for (const [pv] of Object.entries(array)) {
|
|
if (value >= pv.from && value <= pv.to) result = pv;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
return {
|
|
$: $,
|
|
EventEmitter: EventEmitter,
|
|
QueryWrapper: QueryWrapper,
|
|
first: first,
|
|
clamp: clamp,
|
|
pick_entry_from_range: pick_entry_from_range
|
|
};
|
|
|
|
})); |