Types and improvements to $

This commit is contained in:
Lalle 2023-04-21 22:28:39 +02:00
parent 6b81346b46
commit 22895eed98
No known key found for this signature in database
GPG Key ID: A6583D207A8F6B0D
5 changed files with 194 additions and 134 deletions

View File

@ -1 +0,0 @@
Chromium 73.0.3683.75 built on Debian buster/sid, running on Debian buster/sid

View File

@ -121,7 +121,13 @@ var ImageExpand = {
) {
return
}
return $.queueTask(func, post)
return $.queueTask(function () {
if (file.isExpanded) {
return ImageExpand.contract(post)
} else {
return ImageExpand.expand(post)
}
})
}
if (

View File

@ -1275,7 +1275,7 @@ var QR = {
QR.cooldown.changes = dict();
QR.cooldown.auto = false;
QR.cooldown.update();
return $.queueTask($.delete, 'cooldowns');
return $.queueTask($.delete('cooldowns', dict()));
},
update() {

View File

@ -1,30 +1,29 @@
/// <reference path="../types/globals.d.ts" />
import Notice from "../classes/Notice";
import { c, Conf, d, doc, g } from "../globals/globals";
import CrossOrigin from "./CrossOrigin";
import { debounce, dict, MINUTE, platform, SECOND } from "./helpers";
import { AjaxPageOptions, ElementProperties } from "../types/$";
import { AjaxPageOptions, Dict, ElementProperties, SyncObject, WhenModifiedOptions } from "../types/$";
import Callbacks from "../classes/Callbacks";
// not chainable
const $ = (selector, root = document.body) => root.querySelector(selector);
type AjaxPageRequest = XMLHttpRequest & {
abort: () => void;
}
$.id = id => d.getElementById(id);
$.cache = dict();
$.ajaxPage = function (url, options) {
$.ajaxPage = function (url: string, options: AjaxPageOptions = {}) {
if (options == null) { options = {}; }
const { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options;
const r = new XMLHttpRequest();
const id = ++Request;
const id = Date.now() + Math.random();
const e = new CustomEvent('4chanXAjax', { detail: { url, timeout, responseType, withCredentials, type, onprogress, form, headers, id } });
d.dispatchEvent(e);
r.onloadend = function () {
delete window.FCX.requests[id];
return onloadend.apply(this, arguments);
if (onloadend) { onloadend.call(r, r); }
return d.dispatchEvent(new CustomEvent('4chanXAjaxEnd', { detail: { id } }));
};
return r;
}
$.ready = function (fc) {
$.ready = function (fc: () => void) {
if (d.readyState !== 'loading') {
$.queueTask(fc);
return;
@ -36,7 +35,7 @@ $.ready = function (fc) {
return $.on(d, 'DOMContentLoaded', cb);
};
$.formData = function (form) {
$.formData = function (form: FormData | ElementProperties) {
if (form instanceof HTMLFormElement) {
return new FormData(form);
}
@ -54,28 +53,27 @@ $.formData = function (form) {
return fd;
};
$.extend = function (object, properties) {
$.extend = function (object: Object, properties: Object) {
for (var key in properties) {
var val = properties[key];
object[key] = val;
var value = properties[key];
object[key] = value;
}
return object;
};
$.hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
$.hasOwn = function (obj: Object, key: string) { return Object.prototype.hasOwnProperty.call(obj, key); };
$.getOwn = function (obj, key) {
if (Object.prototype.hasOwnProperty.call(obj, key)) { return obj[key]; } else { return undefined; }
};
$.getOwn = function (obj: Object, key: string) { if ($.hasOwn(obj, key)) { return obj[key]; } };
$.ajax = (function () {
let pageXHR;
let pageXHR = XMLHttpRequest;
if (window.wrappedJSObject && !XMLHttpRequest.wrappedJSObject) {
pageXHR = XPCNativeWrapper(window.wrappedJSObject.XMLHttpRequest);
} else {
pageXHR = XMLHttpRequest;
}
const r = (function (url, options = {}) {
const r = (function (url, options: AjaxPageOptions = {}) {
if (options.responseType == null) { options.responseType = 'json'; }
if (!options.type) { options.type = (options.form && 'post') || 'get'; }
// XXX https://forums.lanik.us/viewtopic.php?f=64&t=24173&p=78310
@ -86,8 +84,8 @@ $.ajax = (function () {
return $.ajaxPage(url, options);
}
}
const { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options;
const r = new pageXHR();
const { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options as AjaxPageOptions;
const r = new pageXHR() as XMLHttpRequest;
try {
r.open(type, url, true);
const object = headers || {};
@ -103,7 +101,8 @@ $.ajax = (function () {
// https://bugs.chromium.org/p/chromium/issues/detail?id=920638
$.on(r, 'load', () => {
if (!Conf['Work around CORB Bug'] && r.readyState === 4 && r.status === 200 && r.statusText === '' && r.response === null) {
$.set('Work around CORB Bug', (Conf['Work around CORB Bug'] = Date.now()));
$.set('Work around CORB Bug', (Conf['Work around CORB Bug'] = Date.now()), cb => cb());
return c.warn(`4chan X failed to load: ${url}`);
}
});
}
@ -112,8 +111,8 @@ $.ajax = (function () {
// XXX Some content blockers in Firefox (e.g. Adblock Plus and NoScript) throw an exception instead of simulating a connection error.
if (err.result !== 0x805e0006) { throw err; }
r.onloadend = onloadend;
$.queueTask($.event, 'error', null, r);
$.queueTask($.event, 'loadend', null, r);
$.queueTask($.event);
$.queueTask($.event);
}
return r;
});
@ -127,12 +126,13 @@ $.ajax = (function () {
$.ajaxPageInit = function () {
$.global(function () {
//@ts-ignore
window.FCX.requests = Object.create(null);
document.addEventListener('4chanXAjax', function (e) {
let fd, r;
const { url, timeout, responseType, withCredentials, type, onprogress, form, headers, id } = e.detail;
window.FCX.requests[id] = (r = new XMLHttpRequest());
//@ts-ignore
window.FCX.requests[id] = r = new pageXHR();
r.open(type, url, true);
const object = headers || {};
for (var key in object) {
@ -150,6 +150,7 @@ $.ajax = (function () {
};
}
r.onloadend = function () {
//@ts-ignore
delete window.FCX.requests[id];
const { status, statusText, response } = this;
const responseHeaderString = this.getAllResponseHeaders();
@ -174,20 +175,21 @@ $.ajax = (function () {
return document.addEventListener('4chanXAjaxAbort', function (e) {
let r;
//@ts-ignore
if (!(r = window.FCX.requests[e.detail.id])) { return; }
return r.abort();
}
, false);
});
}, 0);
$.on(d, '4chanXAjaxProgress', function (e) {
let req;
$.on(d, '4chanXAjaxProgress', function (e: CustomEvent) {
let req: any;
if (!(req = requests[e.detail.id])) { return; }
return req.upload.onprogress.call(req.upload, e.detail);
return req.onprogress(e);
});
return $.on(d, '4chanXAjaxLoadend', function (e) {
let req;
return $.on(d, '4chanXAjaxLoadend', function (e: CustomEvent) {
let req: any;
if (!(req = requests[e.detail.id])) { return; }
delete requests[e.detail.id];
if (e.detail.status) {
@ -203,14 +205,22 @@ $.ajax = (function () {
};
return $.ajaxPage = function (url, options = {}) {
let req;
let { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options;
let req: any;
let { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options || {};
const id = requestID++;
requests[id] = (req = new CrossOrigin.Request());
$.extend(req, { responseType, onloadend });
req.upload = { onprogress };
req.abort = () => $.event('4chanXAjaxAbort', { id });
if (form) { form = Array.from(form.entries()); }
if (form) {
form = new FormData(form);
for (var entry of form) {
if (entry[0] === 'json') {
form.delete(entry[0]);
form.append(entry[0], JSON.stringify(entry[1]));
}
}
}
$.event('4chanXAjax', { url, timeout, responseType, withCredentials, type, onprogress: !!onprogress, form, headers, id });
return req;
};
@ -221,26 +231,32 @@ $.ajax = (function () {
// With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses.
// This saves a lot of bandwidth and CPU time for both the users and the servers.
$.lastModified = dict();
$.whenModified = function (url, bucket, cb, options = {}) {
$.whenModified = function (
url: string,
bucket: string,
cb: (this: JQueryXHR) => void,
options: WhenModifiedOptions = {}
): JQueryXHR {
const { timeout, ajax = $.ajax } = options;
let params = [];
let lastModifiedTime;
const params: string[] = [];
const originalUrl = url;
if ($.engine === 'blink') {
if ($.engine === "blink") {
params.push(`s=${bucket}`);
}
if (url.split('/')[2] === 'a.4cdn.org') {
if (url.split("/")[2] === "a.4cdn.org") {
params.push(`t=${Date.now()}`);
}
const originalUrl = url;
if (params.length) {
url += '?' + params.join('&');
url += "?" + params.join("&");
}
const headers = {};
if ((lastModifiedTime = $.lastModified[bucket]?.[originalUrl]) != null) {
headers['If-Modified-Since'] = lastModifiedTime;
const headers: { [key: string]: string } = {};
const lastModifiedTime = $.lastModified[bucket]?.[originalUrl];
if (lastModifiedTime != null) {
headers["If-Modified-Since"] = lastModifiedTime;
}
return ajax(url, {
@ -251,15 +267,15 @@ $.whenModified = function (url, bucket, cb, options = {}) {
cb.call(this);
},
timeout,
headers
headers,
});
};
(function () {
const reqs = dict();
$.cache = function (url, cb, options = {}) {
let req;
$.cache = function (url, cb, options: { ajax?: typeof $.ajax } = {}) {
let req: any;
const { ajax } = options;
if (req = reqs[url]) {
if (req.callbacks) {
@ -294,49 +310,49 @@ $.whenModified = function (url, bucket, cb, options = {}) {
$.cb = {
checked() {
if ($.hasOwn(Conf, this.name)) {
$.set(this.name, this.checked);
$.set(this.name, this.checked, true);
return Conf[this.name] = this.checked;
}
},
value() {
if ($.hasOwn(Conf, this.name)) {
$.set(this.name, this.value.trim());
$.set(this.name, this.value.trim(), cb => {
if (cb) {
return this.value = cb;
}
});
}
return Conf[this.name] = this.value;
}
}
};
},
$.asap = function (test, cb) {
$.asap = function (test: () => boolean, cb: () => void) {
if (test()) {
return cb();
} else {
return setTimeout($.asap, 25, test, cb);
}
return setTimeout(() => $.asap(test, cb), 0);
};
$.onExists = function (root, selector, cb) {
let el;
if (el = $(selector, root)) {
return cb(el);
}
var observer = new MutationObserver(function () {
if (el = $(selector, root)) {
$.onExists = function (root: HTMLElement, selector: string, cb: (el: HTMLElement) => void): MutationObserver {
const observer = new MutationObserver(() => {
const el = root.querySelector(selector);
if (el) {
observer.disconnect();
return cb(el);
return cb(root.querySelector(selector));
}
});
return observer.observe(root, { childList: true, subtree: true });
observer.observe(root, { childList: true, subtree: true });
return observer;
};
$.addStyle = function (css, id, test = 'head') {
const style = $.el('style',
{ textContent: css });
if (id != null) { style.id = id; }
$.addStyle = function (css: string, id: string, test = 'head') {
if (id && d.getElementById(id)) { return; }
const style = $.el('style', { id, textContent: css });
$.onExists(doc, test, () => $.add(d.head, style));
return style;
};
$.addCSP = function (policy) {
$.addCSP = function (policy: string) {
const meta = $.el('meta', { httpEquiv: 'Content-Security-Policy', content: policy }, { display: 'none' });
$.onExists(doc, 'head', () => $.add(d.head, meta));
return meta;
@ -440,8 +456,24 @@ if (platform === 'userscript') {
return new CustomEvent('x', { detail: {} });
} catch (err) {
const unsafeConstructors = {
Object: unsafeWindow.Object,
Array: unsafeWindow.Array
'Object': Object,
'Array': Array,
'String': String,
'Number': Number,
'Boolean': Boolean,
'RegExp': RegExp,
'Date': Date,
'Error': Error,
'EvalError': EvalError,
'RangeError': RangeError,
'ReferenceError': ReferenceError,
'SyntaxError': SyntaxError,
'TypeError': TypeError,
'URIError': URIError,
'Map': Map,
'Set': Set,
'WeakMap': WeakMap,
'WeakSet': WeakSet,
};
var clone = function (obj) {
let constructor;
@ -494,32 +526,16 @@ $.debounce = function (wait, fn) {
return timeout = setTimeout(exec, wait);
};
};
$.queueTask = (function () {
// inspired by https://www.w3.org/Bugs/Public/show_bug.cgi?id=15007
const taskQueue = [];
const execTask = function () {
const task = taskQueue.shift();
const func = task[0];
const args = Array.prototype.slice.call(task, 1);
return func.apply(func, args);
};
if (window.MessageChannel) {
const taskChannel = new MessageChannel();
taskChannel.port1.onmessage = execTask;
return function () {
taskQueue.push(arguments);
return taskChannel.port2.postMessage(null);
};
} else { // XXX Firefox
return function () {
taskQueue.push(arguments);
return setTimeout(execTask, 0);
};
//ok
$.queueTask = function (fn) {
if (typeof requestIdleCallback === 'function') {
return requestIdleCallback(fn);
} else {
return setTimeout(fn, 0);
}
})();
};
$.global = function (fn, data) {
$.global = function (fn: Function, data: object) {
if (doc) {
const script = $.el('script',
{ textContent: `(${fn}).call(document.currentScript.dataset);` });
@ -536,7 +552,7 @@ $.global = function (fn, data) {
}
};
$.bytesToString = function (size) {
$.bytesToString = function (size: number) {
if (size < 1024) {
return `${size} B`;
} else if (size < 1048576) {
@ -548,7 +564,7 @@ $.bytesToString = function (size) {
}
};
$.minmax = (value, min, max) => Math.max(min, Math.min(max, value));
$.minmax = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value));
$.hasAudio = function (el: HTMLVideoElement | HTMLAudioElement) {
if (el.tagName === 'VIDEO') {
@ -565,13 +581,13 @@ $.luma = (rgb: number[]) => { // rgb: [r, g, b]
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
$.unescape = function (text) {
$.unescape = function (text: string): string {
if (text == null) { return text; }
return text.replace(/<[^>]*>/g, '').replace(/&(amp|#039|quot|lt|gt|#44);/g, c => ({ '&amp;': '&', '&#039;': "'", '&quot;': '"', '&lt;': '<', '&gt;': '>', '&#44;': ',' })[c]);
};
$.isImage = url => /\.(jpe?g|png|gif|bmp|webp|svg|ico|tiff?)$/i.test(url);
$.isVideo = url => /\.(webm|mp4|og[gv]|m4v|mov|avi|flv|wmv|mpg|mpeg|mkv|rm|rmvb|3gp|3g2|asf|swf|vob)$/i.test(url);
$.isImage = (url: string) => /\.(jpe?g|png|gif|bmp|webp|svg|ico|tiff?)$/i.test(url);
$.isVideo = (url: string) => /\.(webm|mp4|og[gv]|m4v|mov|avi|flv|wmv|mpg|mpeg|mkv|rm|rmvb|3gp|3g2|asf|swf|vob)$/i.test(url);
$.engine = (function () {
if (/Edge\//.test(navigator.userAgent)) { return 'edge'; }
@ -582,9 +598,9 @@ $.engine = (function () {
$.hasStorage = (function () {
try {
if (localStorage.getItem(g.NAMESPACE + 'hasStorage') === 'true') { return true; }
localStorage.setItem(g.NAMESPACE + 'hasStorage', 'true');
return localStorage.getItem(g.NAMESPACE + 'hasStorage') === 'true';
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
return true;
} catch (error) {
return false;
}
@ -630,8 +646,8 @@ if (platform === 'crx') {
}
}
});
$.sync = (key, cb) => $.syncing[key] = cb;
$.forceSync = function () { };
$.sync = (key: string, cb: () => void) => $.syncing[key] = cb;
$.forceSync = function (): void { };
$.crxWorking = function () {
try {
@ -649,6 +665,7 @@ if (platform === 'crx') {
return false;
};
$.get = $.oneItemSugar(function (data, cb) {
if (!$.crxWorking()) { return; }
const results = {};
@ -674,9 +691,8 @@ if (platform === 'crx') {
}
results[area] = result;
if (results.local && results.sync) {
$.extend(data, results.sync);
$.extend(data, results.local);
return cb(data);
for (key in results.local) { var val = results.local[key]; if (val != null) { results.sync[key] = val; } }
cb(results.sync);
}
});
};
@ -741,7 +757,7 @@ if (platform === 'crx') {
});
};
var setSync = debounce(SECOND, () => setArea('sync'));
var setSync = debounce(SECOND, () => setArea('sync', () => $.forceSync()));
$.set = $.oneItemSugar(function (data, cb) {
if (!$.crxWorking()) { return; }
@ -839,7 +855,11 @@ if (platform === 'crx') {
});
});
$.clear = cb => GM.listValues().then(keys => $.delete(keys.map(key => key.replace(g.NAMESPACE, '')), cb)).catch(() => $.delete(Object.keys(Conf).concat(['previousversion', 'QR Size', 'QR.persona']), cb));
$.clear = async function (cb: () => void) {
return GM.listValues().then(function (keys) {
return $.delete(keys.map(key => key.slice(g.NAMESPACE.length)), cb);
});
};
} else {
if (typeof GM_deleteValue === 'undefined' || GM_deleteValue === null) {
@ -902,12 +922,19 @@ if (platform === 'crx') {
}
if (typeof GM_addValueChangeListener !== 'undefined' && GM_addValueChangeListener !== null) {
$.sync = (key, cb) => $.syncing[key] = GM_addValueChangeListener(g.NAMESPACE + key, function (key2, oldValue, newValue, remote) {
if (remote) {
if (newValue !== undefined) { newValue = dict.json(newValue); }
return cb(newValue, key);
}
});
$.sync = function (key, cb) {
key = g.NAMESPACE + key;
$.syncing[key] = cb;
return GM_addValueChangeListener(key, function (name, oldVal, newVal) {
if (newVal != null) {
if (newVal === oldVal) { return; }
return cb(dict.json(newVal), name);
}
});
};
$.forceSync = function () { return GM.getValue(g.NAMESPACE + 'forceSync', 0).then(function (val) {
return GM.setValue(g.NAMESPACE + 'forceSync', val + 1);
}); };
$.forceSync = function () { };
} else if ((typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) || $.hasStorage) {
$.sync = function (key, cb) {
@ -945,16 +972,12 @@ if (platform === 'crx') {
$.forceSync = function () { };
}
$.delete = function (keys) {
if (!(keys instanceof Array)) {
keys = [keys];
}
for (var key of keys) {
$.deleteValue(g.NAMESPACE + key);
}
$.delete = function (keys, cb) {
if (keys.length === 0) { return cb?.(); }
return Promise.all(keys.map(key => $.deleteValue(g.NAMESPACE + key))).then(cb);
};
$.get = $.oneItemSugar((items, cb) => $.queueTask($.getSync, items, cb));
$.get = $.oneItemSugar((items, cb) => $.queueTask(() => $.getSync(items, cb)));
$.getSync = function (items, cb) {
for (var key in items) {
@ -963,7 +986,6 @@ if (platform === 'crx') {
try {
items[key] = dict.json(val2);
} catch (err) {
// XXX https://github.com/ccd0/4chan-x/issues/2218
if (!/^(?:undefined)*$/.test(val2)) {
throw err;
}
@ -987,14 +1009,15 @@ if (platform === 'crx') {
$.clear = function (cb) {
// XXX https://github.com/greasemonkey/greasemonkey/issues/2033
// Also support case where GM_listValues is not defined.
$.delete(Object.keys(Conf));
$.delete(['previousversion', 'QR Size', 'QR.persona']);
$.delete(Object.keys(Conf), cb);
$.delete(Object.keys(Conf), cb);
try {
$.delete($.listValues().map(key => key.replace(g.NAMESPACE, '')));
//delete(keys, cb)
$.delete($.listValues(), cb);
} catch (error) { }
return cb?.();
};
}
}
export default $;
export default $;

34
src/types/$.d.ts vendored
View File

@ -1,3 +1,5 @@
import SimpleDict from "../classes/SimpleDict";
export interface ElementProperties {
[key: string]: any;
}
@ -10,4 +12,34 @@ interface AjaxPageOptions {
onprogress?: (this: XMLHttpRequest, ev: ProgressEvent<EventTarget>) => void;
form?: FormData;
headers?: Record<string, string>;
}
}
export interface LastModified {
[bucket: string]: { [url: string]: string | undefined };
}
export interface WhenModifiedOptions {
timeout?: number;
ajax?: (url: string, settings?: AjaxPageOptions) => Promise<string>;
}
declare global {
interface JQueryStatic {
engine?: string;
lastModified: LastModified;
whenModified: (
url: string,
bucket: string,
cb: (this: JQueryXHR) => void,
options?: WhenModifiedOptions
) => JQueryXHR;
}
}
export type Dict = { [key: string]: any };
export interface SyncObject {
setValue: (key: string, val: any) => void;
deleteValue: (key: string) => void;
oldValue?: Dict;
syncing?: Dict;
hasStorage?: boolean;
cantSync?: boolean;
cantSet?: boolean;
}