mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-10-14 15:01:59 +00:00
4.0.0 Beta 1
This commit is contained in:
parent
c2688646af
commit
262757a20d
187 changed files with 22878 additions and 38882 deletions
635
src/utilities/color.js
Normal file
635
src/utilities/color.js
Normal file
|
@ -0,0 +1,635 @@
|
|||
'use strict';
|
||||
/* eslint-disable */
|
||||
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
export function hue2rgb(p, q, t) {
|
||||
if ( t < 0 ) t += 1;
|
||||
if ( t > 1 ) t -= 1;
|
||||
if ( t < 1/6 )
|
||||
return p + (q-p) * 6 * t;
|
||||
if ( t < 1/2 )
|
||||
return q;
|
||||
if ( t < 2/3 )
|
||||
return p + (q-p) * (2/3 - t) * 6;
|
||||
return p;
|
||||
}
|
||||
|
||||
export function bit2linear(channel) {
|
||||
// http://www.brucelindbloom.com/Eqn_RGB_to_XYZ.html
|
||||
// This converts rgb 8bit to rgb linear, lazy because the other algorithm is really really dumb
|
||||
//return Math.pow(channel, 2.2);
|
||||
|
||||
// CSS Colors Level 4 says 0.03928, Bruce Lindbloom who cared to write all algos says 0.04045, used bruce because whynawt
|
||||
return (channel <= 0.04045) ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
export function linear2bit(channel) {
|
||||
// Using lazy conversion in the other direction as well
|
||||
//return Math.pow(channel, 1/2.2);
|
||||
|
||||
// I'm honestly not sure about 0.0031308, I've only seen it referenced on Bruce Lindbloom's site
|
||||
return (channel <= 0.0031308) ? channel * 12.92 : Math.pow(1.055 * channel, 1/2.4) - 0.055;
|
||||
}
|
||||
|
||||
|
||||
export const Color = {};
|
||||
|
||||
Color._canvas = null;
|
||||
Color._context = null;
|
||||
|
||||
Color.CVDMatrix = {
|
||||
protanope: [ // reds are greatly reduced (1% men)
|
||||
0.0, 2.02344, -2.52581,
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 1.0
|
||||
],
|
||||
deuteranope: [ // greens are greatly reduced (1% men)
|
||||
1.0, 0.0, 0.0,
|
||||
0.494207, 0.0, 1.24827,
|
||||
0.0, 0.0, 1.0
|
||||
],
|
||||
tritanope: [ // blues are greatly reduced (0.003% population)
|
||||
1.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
-0.395913, 0.801109, 0.0
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
const RGBAColor = Color.RGBA = function(r, g, b, a) {
|
||||
this.r = r||0; this.g = g||0; this.b = b||0; this.a = a||0;
|
||||
};
|
||||
|
||||
const HSVAColor = Color.HSVA = function(h, s, v, a) {
|
||||
this.h = h||0; this.s = s||0; this.v = v||0; this.a = a||0;
|
||||
};
|
||||
|
||||
const HSLAColor = Color.HSLA = function(h, s, l, a) {
|
||||
this.h = h||0; this.s = s||0; this.l = l||0; this.a = a||0;
|
||||
};
|
||||
|
||||
const XYZAColor = Color.XYZA = function(x, y, z, a) {
|
||||
this.x = x||0; this.y = y||0; this.z = z||0; this.a = a||0;
|
||||
};
|
||||
|
||||
const LUVAColor = Color.LUVA = function(l, u, v, a) {
|
||||
this.l = l||0; this.u = u||0; this.v = v||0; this.a = a||0;
|
||||
};
|
||||
|
||||
|
||||
// RGBA Colors
|
||||
|
||||
RGBAColor.prototype.eq = function(rgb) {
|
||||
return rgb.r === this.r && rgb.g === this.g && rgb.b === this.b && rgb.a === this.a;
|
||||
}
|
||||
|
||||
RGBAColor.fromName = function(name) {
|
||||
let context = Color._context;
|
||||
if ( ! context ) {
|
||||
const canvas = Color._canvas = document.createElement('canvas');
|
||||
context = Color._context = canvas.getContext('2d');
|
||||
}
|
||||
|
||||
context.clearRect(0,0,1,1);
|
||||
context.fillStyle = name;
|
||||
context.fillRect(0,0,1,1);
|
||||
const data = context.getImageData(0,0,1,1);
|
||||
|
||||
if ( ! data || ! data.data || data.data.length !== 4 )
|
||||
return null;
|
||||
|
||||
return new RGBAColor(data.data[0], data.data[1], data.data[2], data.data[3] / 255);
|
||||
}
|
||||
|
||||
RGBAColor.fromCSS = function(rgb) {
|
||||
if ( ! rgb )
|
||||
return null;
|
||||
|
||||
rgb = rgb.trim();
|
||||
|
||||
if ( rgb.charAt(0) === '#' )
|
||||
return RGBAColor.fromHex(rgb);
|
||||
|
||||
const match = /rgba?\( *(\d+%?) *, *(\d+%?) *, *(\d+%?) *(?:, *([\d.]+))?\)/i.exec(rgb);
|
||||
if ( match ) {
|
||||
let r = match[1],
|
||||
g = match[2],
|
||||
b = match[3],
|
||||
a = match[4];
|
||||
|
||||
if ( r.charAt(r.length-1) === '%' )
|
||||
r = 255 * (parseInt(r,10) / 100);
|
||||
else
|
||||
r = parseInt(r,10);
|
||||
|
||||
if ( g.charAt(g.length-1) === '%' )
|
||||
g = 255 * (parseInt(g,10) / 100);
|
||||
else
|
||||
g = parseInt(g,10);
|
||||
|
||||
if ( b.charAt(b.length-1) === '%' )
|
||||
b = 255 * (parseInt(b,10) / 100);
|
||||
else
|
||||
b = parseInt(b,10);
|
||||
|
||||
if ( a )
|
||||
if ( a.charAt(a.length-1) === '%' )
|
||||
a = parseInt(a,10) / 100;
|
||||
else
|
||||
a = parseFloat(a);
|
||||
else
|
||||
a = 1;
|
||||
|
||||
return new RGBAColor(
|
||||
Math.min(Math.max(0, r), 255),
|
||||
Math.min(Math.max(0, g), 255),
|
||||
Math.min(Math.max(0, b), 255),
|
||||
Math.min(Math.max(0, a), 1)
|
||||
);
|
||||
}
|
||||
|
||||
return RGBAColor.fromName(rgb);
|
||||
}
|
||||
|
||||
RGBAColor.fromHex = function(code, alpha = 1) {
|
||||
if ( code.charAt(0) === '#' )
|
||||
code = code.slice(1);
|
||||
|
||||
if ( code.length === 3 )
|
||||
code = `${code[0]}${code[0]}${code[1]}${code[1]}${code[2]}${code[2]}`;
|
||||
|
||||
else if ( code.length === 4 )
|
||||
code = `${code[0]}${code[0]}${code[1]}${code[1]}${code[2]}${code[2]}${code[3]}${code[3]}`;
|
||||
|
||||
if ( code.length === 8 ) {
|
||||
alpha = parseInt(code.slice(6), 16) / 255;
|
||||
code = code.slice(0, 6);
|
||||
|
||||
} else if ( code.length !== 6 )
|
||||
throw new Error('invalid hex code');
|
||||
|
||||
const raw = parseInt(code, 16);
|
||||
return new RGBAColor(
|
||||
(raw >> 16), // Red
|
||||
(raw >> 8 & 0x00FF), // Green
|
||||
(raw & 0x0000FF), // Blue,
|
||||
alpha // Alpha
|
||||
);
|
||||
}
|
||||
|
||||
RGBAColor.fromHSVA = function(h, s, v, a) {
|
||||
let r, g, b,
|
||||
|
||||
i = Math.floor(h * 6),
|
||||
f = h * 6 - i,
|
||||
p = v * (1 - s),
|
||||
q = v * (1 - f * s),
|
||||
t = v * (1 - (1 - f) * s);
|
||||
|
||||
switch(i % 6) {
|
||||
case 0: r = v, g = t, b = p; break;
|
||||
case 1: r = q, g = v, b = p; break;
|
||||
case 2: r = p, g = v, b = t; break;
|
||||
case 3: r = p, g = q, b = v; break;
|
||||
case 4: r = t, g = p, b = v; break;
|
||||
case 5: r = v, g = p, b = q;
|
||||
}
|
||||
|
||||
return new RGBAColor(
|
||||
Math.round(Math.min(Math.max(0, r*255), 255)),
|
||||
Math.round(Math.min(Math.max(0, g*255), 255)),
|
||||
Math.round(Math.min(Math.max(0, b*255), 255)),
|
||||
a === undefined ? 1 : a
|
||||
);
|
||||
}
|
||||
|
||||
RGBAColor.fromXYZA = function(x, y, z, a) {
|
||||
const R = 3.240479 * x - 1.537150 * y - 0.498535 * z,
|
||||
G = -0.969256 * x + 1.875992 * y + 0.041556 * z,
|
||||
B = 0.055648 * x - 0.204043 * y + 1.057311 * z;
|
||||
|
||||
// Make sure we end up in a real color space
|
||||
return new RGBAColor(
|
||||
Math.max(0, Math.min(255, 255 * linear2bit(R))),
|
||||
Math.max(0, Math.min(255, 255 * linear2bit(G))),
|
||||
Math.max(0, Math.min(255, 255 * linear2bit(B))),
|
||||
a === undefined ? 1 : a
|
||||
);
|
||||
}
|
||||
|
||||
RGBAColor.fromHSLA = function(h, s, l, a) {
|
||||
if ( s === 0 ) {
|
||||
const v = Math.round(Math.min(Math.max(0, 255*l), 255));
|
||||
return new RGBAColor(v, v, v, a === undefined ? 1 : a);
|
||||
}
|
||||
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s,
|
||||
p = 2 * l - q;
|
||||
|
||||
return new RGBAColor(
|
||||
Math.round(Math.min(Math.max(0, 255 * hue2rgb(p, q, h + 1/3)), 255)),
|
||||
Math.round(Math.min(Math.max(0, 255 * hue2rgb(p, q, h)), 255)),
|
||||
Math.round(Math.min(Math.max(0, 255 * hue2rgb(p, q, h - 1/3)), 255)),
|
||||
a === undefined ? 1 : a
|
||||
);
|
||||
}
|
||||
|
||||
RGBAColor.prototype.toHSVA = function() { return HSVAColor.fromRGBA(this.r, this.g, this.b, this.a); }
|
||||
RGBAColor.prototype.toHSLA = function() { return HSLAColor.fromRGBA(this.r, this.g, this.b, this.a); }
|
||||
RGBAColor.prototype.toCSS = function() { return "rgb" + (this.a !== 1 ? "a" : "") + "(" + Math.round(this.r) + "," + Math.round(this.g) + "," + Math.round(this.b) + (this.a !== 1 ? "," + this.a : "") + ")"; }
|
||||
RGBAColor.prototype.toXYZA = function() { return XYZAColor.fromRGBA(this.r, this.g, this.b, this.a); }
|
||||
RGBAColor.prototype.toLUVA = function() { return this.toXYZA().toLUVA(); }
|
||||
|
||||
RGBAColor.prototype.toHex = function() {
|
||||
var rgb = this.b | (this.g << 8) | (this.r << 16);
|
||||
return '#' + (0x1000000 + rgb).toString(16).slice(1);
|
||||
}
|
||||
|
||||
|
||||
RGBAColor.prototype.get_Y = function() {
|
||||
return ((0.299 * this.r) + ( 0.587 * this.g) + ( 0.114 * this.b)) / 255;
|
||||
}
|
||||
|
||||
|
||||
RGBAColor.prototype.luminance = function() {
|
||||
var r = bit2linear(this.r / 255),
|
||||
g = bit2linear(this.g / 255),
|
||||
b = bit2linear(this.b / 255);
|
||||
|
||||
return (0.2126 * r) + (0.7152 * g) + (0.0722 * b);
|
||||
}
|
||||
|
||||
|
||||
RGBAColor.prototype.brighten = function(amount) {
|
||||
amount = typeof amount === "number" ? amount : 1;
|
||||
amount = Math.round(255 * (amount / 100));
|
||||
|
||||
return new RGBAColor(
|
||||
Math.max(0, Math.min(255, this.r + amount)),
|
||||
Math.max(0, Math.min(255, this.g + amount)),
|
||||
Math.max(0, Math.min(255, this.b + amount)),
|
||||
this.a
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
RGBAColor.prototype.daltonize = function(type, amount) {
|
||||
amount = typeof amount === "number" ? amount : 1.0;
|
||||
var cvd;
|
||||
if ( typeof type === "string" ) {
|
||||
if ( Color.CVDMatrix.hasOwnProperty(type) )
|
||||
cvd = Color.CVDMatrix[type];
|
||||
else
|
||||
throw "Invalid CVD matrix.";
|
||||
} else
|
||||
cvd = type;
|
||||
|
||||
var cvd_a = cvd[0], cvd_b = cvd[1], cvd_c = cvd[2],
|
||||
cvd_d = cvd[3], cvd_e = cvd[4], cvd_f = cvd[5],
|
||||
cvd_g = cvd[6], cvd_h = cvd[7], cvd_i = cvd[8],
|
||||
|
||||
L, M, S, l, m, s, R, G, B, RR, GG, BB;
|
||||
|
||||
// RGB to LMS matrix conversion
|
||||
L = (17.8824 * this.r) + (43.5161 * this.g) + (4.11935 * this.b);
|
||||
M = (3.45565 * this.r) + (27.1554 * this.g) + (3.86714 * this.b);
|
||||
S = (0.0299566 * this.r) + (0.184309 * this.g) + (1.46709 * this.b);
|
||||
// Simulate color blindness
|
||||
l = (cvd_a * L) + (cvd_b * M) + (cvd_c * S);
|
||||
m = (cvd_d * L) + (cvd_e * M) + (cvd_f * S);
|
||||
s = (cvd_g * L) + (cvd_h * M) + (cvd_i * S);
|
||||
// LMS to RGB matrix conversion
|
||||
R = (0.0809444479 * l) + (-0.130504409 * m) + (0.116721066 * s);
|
||||
G = (-0.0102485335 * l) + (0.0540193266 * m) + (-0.113614708 * s);
|
||||
B = (-0.000365296938 * l) + (-0.00412161469 * m) + (0.693511405 * s);
|
||||
// Isolate invisible colors to color vision deficiency (calculate error matrix)
|
||||
R = this.r - R;
|
||||
G = this.g - G;
|
||||
B = this.b - B;
|
||||
// Shift colors towards visible spectrum (apply error modifications)
|
||||
RR = (0.0 * R) + (0.0 * G) + (0.0 * B);
|
||||
GG = (0.7 * R) + (1.0 * G) + (0.0 * B);
|
||||
BB = (0.7 * R) + (0.0 * G) + (1.0 * B);
|
||||
// Add compensation to original values
|
||||
R = Math.min(Math.max(0, RR + this.r), 255);
|
||||
G = Math.min(Math.max(0, GG + this.g), 255);
|
||||
B = Math.min(Math.max(0, BB + this.b), 255);
|
||||
|
||||
return new RGBAColor(R, G, B, this.a);
|
||||
}
|
||||
|
||||
RGBAColor.prototype._r = function(r) { return new RGBAColor(r, this.g, this.b, this.a); }
|
||||
RGBAColor.prototype._g = function(g) { return new RGBAColor(this.r, g, this.b, this.a); }
|
||||
RGBAColor.prototype._b = function(b) { return new RGBAColor(this.r, this.g, b, this.a); }
|
||||
RGBAColor.prototype._a = function(a) { return new RGBAColor(this.r, this.g, this.b, a); }
|
||||
|
||||
|
||||
// HSL Colors
|
||||
|
||||
HSLAColor.prototype.eq = function(hsl) {
|
||||
return hsl.h === this.h && hsl.s === this.s && hsl.l === this.l && hsl.a === this.a;
|
||||
}
|
||||
|
||||
HSLAColor.fromRGBA = function(r, g, b, a) {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
|
||||
var max = Math.max(r,g,b),
|
||||
min = Math.min(r,g,b),
|
||||
|
||||
h, s, l = Math.min(Math.max(0, (max+min) / 2), 1),
|
||||
d = Math.min(Math.max(0, max - min), 1);
|
||||
|
||||
if ( d === 0 )
|
||||
h = s = 0;
|
||||
else {
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch(max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return new HSLAColor(h, s, l, a === undefined ? 1 : a);
|
||||
}
|
||||
|
||||
HSLAColor.prototype.targetLuminance = function (target) {
|
||||
var s = this.s;
|
||||
s *= Math.pow(this.l > 0.5 ? -this.l : this.l - 1, 7) + 1;
|
||||
|
||||
var min = 0, max = 1, d = (max - min) / 2, mid = min + d;
|
||||
for (; d > 1/65536; d /= 2, mid = min + d) {
|
||||
var luminance = RGBAColor.fromHSLA(this.h, s, mid, 1).luminance()
|
||||
if (luminance > target) {
|
||||
max = mid;
|
||||
} else {
|
||||
min = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return new HSLAColor(this.h, s, mid, this.a);
|
||||
}
|
||||
|
||||
HSLAColor.prototype.toRGBA = function() { return RGBAColor.fromHSLA(this.h, this.s, this.l, this.a); }
|
||||
HSLAColor.prototype.toCSS = function() { return "hsl" + (this.a !== 1 ? "a" : "") + "(" + Math.round(this.h*360) + "," + Math.round(this.s*100) + "%," + Math.round(this.l*100) + "%" + (this.a !== 1 ? "," + this.a : "") + ")"; }
|
||||
HSLAColor.prototype.toHSVA = function() { return this.toRGBA().toHSVA(); }
|
||||
HSLAColor.prototype.toXYZA = function() { return this.toRGBA().toXYZA(); }
|
||||
HSLAColor.prototype.toLUVA = function() { return this.toRGBA().toLUVA(); }
|
||||
|
||||
|
||||
HSLAColor.prototype._h = function(h) { return new HSLAColor(h, this.s, this.l, this.a); }
|
||||
HSLAColor.prototype._s = function(s) { return new HSLAColor(this.h, s, this.l, this.a); }
|
||||
HSLAColor.prototype._l = function(l) { return new HSLAColor(this.h, this.s, l, this.a); }
|
||||
HSLAColor.prototype._a = function(a) { return new HSLAColor(this.h, this.s, this.l, a); }
|
||||
|
||||
|
||||
// HSV Colors
|
||||
|
||||
HSVAColor.prototype.eq = function(hsv) { return hsv.h === this.h && hsv.s === this.s && hsv.v === this.v && hsv.a === this.a; }
|
||||
|
||||
HSVAColor.fromRGBA = function(r, g, b, a) {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
|
||||
var max = Math.max(r, g, b),
|
||||
min = Math.min(r, g, b),
|
||||
d = Math.min(Math.max(0, max - min), 1),
|
||||
|
||||
h,
|
||||
s = max === 0 ? 0 : d / max,
|
||||
v = max;
|
||||
|
||||
if ( d === 0 )
|
||||
h = 0;
|
||||
else {
|
||||
switch(max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return new HSVAColor(h, s, v, a === undefined ? 1 : a);
|
||||
}
|
||||
|
||||
|
||||
HSVAColor.prototype.toRGBA = function() { return RGBAColor.fromHSVA(this.h, this.s, this.v, this.a); }
|
||||
HSVAColor.prototype.toHSLA = function() { return this.toRGBA().toHSLA(); }
|
||||
HSVAColor.prototype.toXYZA = function() { return this.toRGBA().toXYZA(); }
|
||||
HSVAColor.prototype.toLUVA = function() { return this.toRGBA().toLUVA(); }
|
||||
|
||||
|
||||
HSVAColor.prototype._h = function(h) { return new HSVAColor(h, this.s, this.v, this.a); }
|
||||
HSVAColor.prototype._s = function(s) { return new HSVAColor(this.h, s, this.v, this.a); }
|
||||
HSVAColor.prototype._v = function(v) { return new HSVAColor(this.h, this.s, v, this.a); }
|
||||
HSVAColor.prototype._a = function(a) { return new HSVAColor(this.h, this.s, this.v, a); }
|
||||
|
||||
|
||||
// XYZ Colors
|
||||
|
||||
XYZAColor.prototype.eq = function(xyz) { return xyz.x === this.x && xyz.y === this.y && xyz.z === this.z; }
|
||||
|
||||
XYZAColor.fromRGBA = function(r, g, b, a) {
|
||||
var R = bit2linear(r / 255),
|
||||
G = bit2linear(g / 255),
|
||||
B = bit2linear(b / 255);
|
||||
|
||||
return new XYZAColor(
|
||||
0.412453 * R + 0.357580 * G + 0.180423 * B,
|
||||
0.212671 * R + 0.715160 * G + 0.072169 * B,
|
||||
0.019334 * R + 0.119193 * G + 0.950227 * B,
|
||||
a === undefined ? 1 : a
|
||||
);
|
||||
}
|
||||
|
||||
XYZAColor.fromLUVA = function(l, u, v, alpha) {
|
||||
var deltaGammaFactor = 1 / (XYZAColor.WHITE.x + 15 * XYZAColor.WHITE.y + 3 * XYZAColor.WHITE.z);
|
||||
var uDeltaGamma = 4 * XYZAColor.WHITE.x * deltaGammaFactor;
|
||||
var vDeltagamma = 9 * XYZAColor.WHITE.y * deltaGammaFactor;
|
||||
|
||||
// XYZAColor.EPSILON * XYZAColor.KAPPA = 8
|
||||
var Y = (l > 8) ? Math.pow((l + 16) / 116, 3) : l / XYZAColor.KAPPA;
|
||||
|
||||
var a = 1/3 * (((52 * l) / (u + 13 * l * uDeltaGamma)) - 1);
|
||||
var b = -5 * Y;
|
||||
var c = -1/3;
|
||||
var d = Y * (((39 * l) / (v + 13 * l * vDeltagamma)) - 5);
|
||||
|
||||
var X = (d - b) / (a - c);
|
||||
var Z = X * a + b;
|
||||
|
||||
return new XYZAColor(X, Y, Z, alpha === undefined ? 1 : alpha);
|
||||
}
|
||||
|
||||
|
||||
XYZAColor.prototype.toRGBA = function() { return RGBAColor.fromXYZA(this.x, this.y, this.z, this.a); }
|
||||
XYZAColor.prototype.toLUVA = function() { return LUVAColor.fromXYZA(this.x, this.y, this.z, this.a); }
|
||||
XYZAColor.prototype.toHSLA = function() { return this.toRGBA().toHSLA(); }
|
||||
XYZAColor.prototype.toHSVA = function() { return this.toRGBA().toHSVA(); }
|
||||
|
||||
|
||||
XYZAColor.prototype._x = function(x) { return new XYZAColor(x, this.y, this.z, this.a); }
|
||||
XYZAColor.prototype._y = function(y) { return new XYZAColor(this.x, y, this.z, this.a); }
|
||||
XYZAColor.prototype._z = function(z) { return new XYZAColor(this.x, this.y, z, this.a); }
|
||||
XYZAColor.prototype._a = function(a) { return new XYZAColor(this.x, this.y, this.z, a); }
|
||||
|
||||
|
||||
// LUV Colors
|
||||
|
||||
XYZAColor.EPSILON = Math.pow(6 / 29, 3);
|
||||
XYZAColor.KAPPA = Math.pow(29 / 3, 3);
|
||||
XYZAColor.WHITE = (new RGBAColor(255, 255, 255, 1)).toXYZA();
|
||||
|
||||
|
||||
LUVAColor.prototype.eq = function(luv) { return luv.l === this.l && luv.u === this.u && luv.v === this.v; }
|
||||
|
||||
LUVAColor.fromXYZA = function(X, Y, Z, a) {
|
||||
var deltaGammaFactor = 1 / (XYZAColor.WHITE.x + 15 * XYZAColor.WHITE.y + 3 * XYZAColor.WHITE.z);
|
||||
var uDeltaGamma = 4 * XYZAColor.WHITE.x * deltaGammaFactor;
|
||||
var vDeltagamma = 9 * XYZAColor.WHITE.y * deltaGammaFactor;
|
||||
|
||||
var yGamma = Y / XYZAColor.WHITE.y;
|
||||
var deltaDivider = (X + 15 * Y + 3 * Z);
|
||||
|
||||
if (deltaDivider === 0) {
|
||||
deltaDivider = 1;
|
||||
}
|
||||
|
||||
var deltaFactor = 1 / deltaDivider;
|
||||
|
||||
var uDelta = 4 * X * deltaFactor;
|
||||
var vDelta = 9 * Y * deltaFactor;
|
||||
|
||||
var L = (yGamma > XYZAColor.EPSILON) ? 116 * Math.pow(yGamma, 1/3) - 16 : XYZAColor.KAPPA * yGamma;
|
||||
var u = 13 * L * (uDelta - uDeltaGamma);
|
||||
var v = 13 * L * (vDelta - vDeltagamma);
|
||||
|
||||
return new LUVAColor(L, u, v, a === undefined ? 1 : a);
|
||||
}
|
||||
|
||||
|
||||
LUVAColor.prototype.toXYZA = function() { return XYZAColor.fromLUVA(this.l, this.u, this.v, this.a); }
|
||||
LUVAColor.prototype.toRGBA = function() { return this.toXYZA().toRGBA(); }
|
||||
LUVAColor.prototype.toHSLA = function() { return this.toXYZA().toHSLA(); }
|
||||
LUVAColor.prototype.toHSVA = function() { return this.toXYZA().toHSVA(); }
|
||||
|
||||
|
||||
LUVAColor.prototype._l = function(l) { return new LUVAColor(l, this.u, this.v, this.a); }
|
||||
LUVAColor.prototype._u = function(u) { return new LUVAColor(this.l, u, this.v, this.a); }
|
||||
LUVAColor.prototype._v = function(v) { return new LUVAColor(this.l, this.u, v, this.a); }
|
||||
LUVAColor.prototype._a = function(a) { return new LUVAColor(this.l, this.u, this.v, a); }
|
||||
|
||||
|
||||
|
||||
export class ColorAdjuster {
|
||||
constructor(base = '#232323', mode = 0, contrast = 4.5) {
|
||||
this._contrast = contrast;
|
||||
this._base = base;
|
||||
this._mode = mode;
|
||||
|
||||
this.rebuildContrast();
|
||||
}
|
||||
|
||||
get contrast() { return this._contrast }
|
||||
set contrast(val) { this._contrast = val; this.rebuildContrast() }
|
||||
|
||||
get base() { return this._base }
|
||||
set base(val) { this._base = val; this.rebuildContrast() }
|
||||
|
||||
get dark() { return this._dark }
|
||||
|
||||
get mode() { return this._mode }
|
||||
set mode(val) { this._mode = val; this.rebuildContrast() }
|
||||
|
||||
|
||||
rebuildContrast() {
|
||||
this._cache = {};
|
||||
|
||||
const base = RGBAColor.fromCSS(this._base),
|
||||
lum = base.luminance();
|
||||
|
||||
const dark = this._dark = lum < 0.5;
|
||||
|
||||
if ( dark ) {
|
||||
this._luv = new XYZAColor(
|
||||
0,
|
||||
(this._contrast * (base.toXYZA().y + 0.05) - 0.05),
|
||||
0,
|
||||
1
|
||||
).toLUVA().l;
|
||||
|
||||
this._luma = this._contrast * (base.luminance() + 0.05) - 0.05;
|
||||
|
||||
} else {
|
||||
this._luv = new XYZAColor(
|
||||
0,
|
||||
(base.toXYZA().y + 0.05) / this._contrast - 0.05,
|
||||
0,
|
||||
1
|
||||
).toLUVA().l;
|
||||
|
||||
this._luma = (base.luminance() + 0.05) / this._contrast - 0.05;
|
||||
}
|
||||
}
|
||||
|
||||
process(color) {
|
||||
if ( this._mode === -1 )
|
||||
return '';
|
||||
else if ( this._mode === 0 )
|
||||
return color;
|
||||
|
||||
if ( color instanceof RGBAColor )
|
||||
color = color.toCSS();
|
||||
|
||||
if ( ! color )
|
||||
return null;
|
||||
|
||||
if ( has(this._cache, color) )
|
||||
return this._cache[color];
|
||||
|
||||
let rgb = RGBAColor.fromCSS(color);
|
||||
|
||||
if ( this._mode === 1 ) {
|
||||
// HSL Luma
|
||||
const luma = rgb.luminance();
|
||||
|
||||
if ( this._dark ? luma < this._luma : luma > this._luma )
|
||||
rgb = rgb.toHSLA().targetLuminance(this._luma).toRGBA();
|
||||
|
||||
} else if ( this._mode === 2 ) {
|
||||
// LUV
|
||||
const luv = rgb.toLUVA();
|
||||
if ( this._dark ? luv.l < this._luv : luv.l > this._luv )
|
||||
rgb = luv._l(this._luv).toRGBA();
|
||||
|
||||
} else if ( this._mode === 3 ) {
|
||||
// HSL Loop (aka BTTV Style)
|
||||
if ( this._dark )
|
||||
while ( rgb.get_Y() < 0.5 ) {
|
||||
const hsl = rgb.toHSLA();
|
||||
rgb = hsl._l(Math.min(Math.max(0, 0.1 + 0.9 * hsl.l), 1)).toRGBA();
|
||||
}
|
||||
|
||||
else
|
||||
while ( rgb.get_Y() >= 0.5 ) {
|
||||
const hsl = rgb.toHSLA();
|
||||
rgb = hsl._l(Math.min(Math.max(0, 0.9 * hsl.l), 1)).toRGBA();
|
||||
}
|
||||
}
|
||||
|
||||
const out = this._cache[color] = rgb.toHex();
|
||||
return out;
|
||||
}
|
||||
}
|
308
src/utilities/compat/apollo.js
Normal file
308
src/utilities/compat/apollo.js
Normal file
|
@ -0,0 +1,308 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Apollo
|
||||
// Legendary Data Access Layer
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
export default class Apollo extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.modifiers = {};
|
||||
this.post_modifiers = {};
|
||||
|
||||
this.inject('..web_munch');
|
||||
this.inject('..fine');
|
||||
|
||||
this.registerModifier('ChannelPage_ChannelInfoBar_User', `query {
|
||||
user {
|
||||
stream {
|
||||
createdAt
|
||||
type
|
||||
}
|
||||
}
|
||||
}`);
|
||||
|
||||
this.registerModifier('FollowedIndex_CurrentUser', `query {
|
||||
currentUser {
|
||||
followedLiveUsers {
|
||||
nodes {
|
||||
profileImageURL(width: 70)
|
||||
stream {
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
followedHosts {
|
||||
nodes {
|
||||
hosting {
|
||||
profileImageURL(width: 70)
|
||||
stream {
|
||||
createdAt
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`);
|
||||
|
||||
this.registerModifier('FollowingLive_CurrentUser', `query {
|
||||
currentUser {
|
||||
followedLiveUsers {
|
||||
nodes {
|
||||
profileImageURL(width: 70)
|
||||
stream {
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`);
|
||||
|
||||
this.registerModifier('ViewerCard', `query {
|
||||
targetUser: user {
|
||||
createdAt
|
||||
profileViewCount
|
||||
}
|
||||
}`);
|
||||
|
||||
this.registerModifier('GamePage_Game', `query {
|
||||
game {
|
||||
streams {
|
||||
edges {
|
||||
node {
|
||||
createdAt
|
||||
type
|
||||
broadcaster {
|
||||
profileImageURL(width: 70)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`);
|
||||
|
||||
}
|
||||
|
||||
async onEnable() {
|
||||
// TODO: Come up with a better way to await something existing.
|
||||
let client = this.client,
|
||||
graphql = this.graphql;
|
||||
|
||||
if ( ! client ) {
|
||||
const root = this.fine.getParent(this.fine.react),
|
||||
ctx = root && root._context;
|
||||
|
||||
client = this.client = ctx && ctx.client;
|
||||
}
|
||||
|
||||
if ( ! graphql )
|
||||
graphql = this.graphql = await this.web_munch.findModule('graphql', m => m.parse && m.parseValue);
|
||||
|
||||
if ( ! client || ! graphql )
|
||||
return new Promise(s => setTimeout(s,50)).then(() => this.onEnable());
|
||||
|
||||
// Parse the queries for modifiers that were already registered.
|
||||
for(const key in this.modifiers)
|
||||
if ( has(this.modifiers, key) ) {
|
||||
const modifiers = this.modifiers[key];
|
||||
if ( modifiers )
|
||||
for(const mod of modifiers) {
|
||||
if ( typeof mod === 'function' || mod[1] === false )
|
||||
continue;
|
||||
|
||||
try {
|
||||
mod[1] = graphql.parse(mod[0], {noLocation: true});
|
||||
} catch(err) {
|
||||
this.log.error(`Error parsing GraphQL statement for "${key}" modifier.`, err);
|
||||
mod[1] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register middleware so that we can intercept requests.
|
||||
this.client.networkInterface.use([{
|
||||
applyBatchMiddleware: (req, next) => {
|
||||
if ( this.enabled )
|
||||
this.apolloPreFlight(req);
|
||||
|
||||
next();
|
||||
}
|
||||
}]);
|
||||
|
||||
this.client.networkInterface.useAfter([{
|
||||
applyBatchAfterware: (resp, next) => {
|
||||
if ( this.enabled )
|
||||
this.apolloPostFlight(resp);
|
||||
|
||||
next();
|
||||
}
|
||||
}]);
|
||||
}
|
||||
|
||||
|
||||
onDisable() {
|
||||
// TODO: Remove Apollo middleware.
|
||||
|
||||
// Tear down the parsed queries.
|
||||
for(const key in this.modifiers)
|
||||
if ( has(this.modifiers, key) ) {
|
||||
const modifiers = this.modifiers[key];
|
||||
if ( modifiers )
|
||||
for(const mod of modifiers) {
|
||||
if ( typeof mod === 'function' )
|
||||
continue;
|
||||
|
||||
mod[1] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// And finally, remove our references.
|
||||
this.client = this.graphql = null;
|
||||
}
|
||||
|
||||
|
||||
apolloPreFlight(request) {
|
||||
for(const req of request.requests) {
|
||||
const operation = req.operationName,
|
||||
modifiers = this.modifiers[operation];
|
||||
|
||||
if ( modifiers )
|
||||
for(const mod of modifiers) {
|
||||
if ( typeof mod === 'function' )
|
||||
mod(req);
|
||||
else if ( mod[1] )
|
||||
this.applyModifier(req, mod[1]);
|
||||
}
|
||||
|
||||
this.emit(`:request.${operation}`, req.query, req.variables);
|
||||
}
|
||||
}
|
||||
|
||||
apolloPostFlight(response) {
|
||||
for(const resp of response.responses) {
|
||||
const operation = resp.extensions.operationName,
|
||||
modifiers = this.post_modifiers[operation];
|
||||
|
||||
if ( modifiers )
|
||||
for(const mod of modifiers)
|
||||
mod(resp);
|
||||
|
||||
this.emit(`:response.${operation}`, resp.data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
applyModifier(request, modifier) { // eslint-disable-line class-methods-use-this
|
||||
request.query = merge(request.query, modifier);
|
||||
}
|
||||
|
||||
|
||||
registerModifier(operation, modifier) {
|
||||
if ( typeof modifier !== 'function' ) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = this.graphql ? this.graphql.parse(modifier, {noLocation: true}) : null;
|
||||
} catch(err) {
|
||||
this.log.error(`Error parsing GraphQL statement for "${operation}" modifier.`, err);
|
||||
parsed = false;
|
||||
}
|
||||
|
||||
modifier = [modifier, parsed];
|
||||
}
|
||||
|
||||
const mods = this.modifiers[operation] = this.modifiers[operation] || [];
|
||||
mods.push(modifier);
|
||||
}
|
||||
|
||||
unregisterModifier(operation, modifier) {
|
||||
const mods = this.modifiers[operation];
|
||||
if ( ! mods )
|
||||
return;
|
||||
|
||||
for(let i=0; i < mods.length; i++) {
|
||||
const mod = mods[i];
|
||||
if ( typeof mod === 'function' ? mod === modifier : mod[0] === modifier ) {
|
||||
mods.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Querying
|
||||
// ========================================================================
|
||||
|
||||
getQuery(operation) {
|
||||
const qm = this.client.queryManager,
|
||||
name_map = qm && qm.queryIdsByName,
|
||||
query_map = qm && qm.observableQueries,
|
||||
query_id = name_map && name_map[operation],
|
||||
query = query_map && query_map[query_id];
|
||||
|
||||
return query && query.observableQuery;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Query Merging
|
||||
// ============================================================================
|
||||
|
||||
function canMerge(a, b) {
|
||||
return a.kind === b.kind &&
|
||||
a.kind !== 'FragmentDefinition' &&
|
||||
(a.selectionSet == null) === (b.selectionSet == null);
|
||||
}
|
||||
|
||||
|
||||
function merge(a, b) {
|
||||
if ( ! canMerge(a, b) )
|
||||
return a;
|
||||
|
||||
if ( a.definitions ) {
|
||||
const a_def = a.definitions,
|
||||
b_def = b.definitions;
|
||||
|
||||
for(let i=0; i < a_def.length && i < b_def.length; i++)
|
||||
a_def[i] = merge(a_def[i], b_def[i]);
|
||||
}
|
||||
|
||||
if ( a.selectionSet ) {
|
||||
const s = a.selectionSet.selections,
|
||||
selects = {};
|
||||
for(const sel of b.selectionSet.selections)
|
||||
selects[`${sel.name.value}:${sel.alias?sel.alias.value:null}`] = sel;
|
||||
|
||||
for(let i=0, l = s.length; i < l; i++) {
|
||||
const sel = s[i],
|
||||
name = sel.name.value,
|
||||
alias = sel.alias ? sel.alias.value : null,
|
||||
key = `${name}:${alias}`,
|
||||
other = selects[key];
|
||||
|
||||
if ( other ) {
|
||||
s[i] = merge(sel, other);
|
||||
selects[key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
for(const key in selects)
|
||||
if ( has(selects, key) ) {
|
||||
const val = selects[key];
|
||||
if ( val )
|
||||
s.push(val);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Variables?
|
||||
|
||||
return a;
|
||||
}
|
87
src/utilities/compat/fine-router.js
Normal file
87
src/utilities/compat/fine-router.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Fine Router
|
||||
// ============================================================================
|
||||
|
||||
import {parse, tokensToRegExp, tokensToFunction} from 'path-to-regexp';
|
||||
import Module from 'utilities/module';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
|
||||
export default class FineRouter extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.inject('..fine');
|
||||
|
||||
this.__routes = [];
|
||||
this.routes = {};
|
||||
this.current = null;
|
||||
this.match = null;
|
||||
this.location = null;
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
const root = this.fine.getParent(this.fine.react),
|
||||
ctx = this.context = root && root._context,
|
||||
router = ctx && ctx.router,
|
||||
history = router && router.history;
|
||||
|
||||
if ( ! history )
|
||||
return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable());
|
||||
|
||||
history.listen(location => {
|
||||
if ( this.enabled )
|
||||
this._navigateTo(location);
|
||||
});
|
||||
|
||||
this._navigateTo(history.location);
|
||||
}
|
||||
|
||||
_navigateTo(location) {
|
||||
this.log.debug('New Location', location);
|
||||
const path = location.pathname;
|
||||
if ( path === this.location )
|
||||
return;
|
||||
|
||||
this.location = path;
|
||||
|
||||
for(const route of this.__routes) {
|
||||
const match = route.regex.exec(path);
|
||||
if ( match ) {
|
||||
this.log.debug('Matching Route', route, match);
|
||||
this.current = route;
|
||||
this.match = match;
|
||||
this.emit(':route', route, match);
|
||||
this.emit(`:route:${route.name}`, ...match);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.current = this.match = null;
|
||||
this.emit(':route', null, null);
|
||||
}
|
||||
|
||||
route(name, path) {
|
||||
if ( typeof name === 'object' ) {
|
||||
for(const key in name)
|
||||
if ( has(name, key) )
|
||||
this.route(key, name[key]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = parse(path),
|
||||
score = parts.length,
|
||||
route = this.routes[name] = {
|
||||
name,
|
||||
parts,
|
||||
score,
|
||||
regex: tokensToRegExp(parts),
|
||||
url: tokensToFunction(parts)
|
||||
}
|
||||
|
||||
this.__routes.push(route);
|
||||
this.__routes.sort(r => r.score);
|
||||
}
|
||||
}
|
486
src/utilities/compat/fine.js
Normal file
486
src/utilities/compat/fine.js
Normal file
|
@ -0,0 +1,486 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Fine Lib
|
||||
// It controls React.
|
||||
// ============================================================================
|
||||
|
||||
import {EventEmitter} from 'utilities/events';
|
||||
import Module from 'utilities/module';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
|
||||
export default class Fine extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this._wrappers = new Map;
|
||||
this._known_classes = new Map;
|
||||
this._observer = null;
|
||||
this._waiting = null;
|
||||
}
|
||||
|
||||
|
||||
async onEnable() {
|
||||
// TODO: Move awaitElement to utilities/dom
|
||||
if ( ! this.root_element )
|
||||
this.root_element = await this.parent.awaitElement(this.selector || '[data-reactroot]');
|
||||
|
||||
const accessor = this.accessor = Fine.findAccessor(this.root_element);
|
||||
if ( ! accessor )
|
||||
return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable());
|
||||
|
||||
this.react = this.getReactInstance(this.root_element);
|
||||
}
|
||||
|
||||
onDisable() {
|
||||
this.root_element = this.react = this.accessor = null;
|
||||
}
|
||||
|
||||
|
||||
static findAccessor(element) {
|
||||
for(const key in element)
|
||||
if ( key.startsWith('__reactInternalInstance$') )
|
||||
return key;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Low Level Accessors
|
||||
// ========================================================================
|
||||
|
||||
getReactInstance(element) {
|
||||
return element[this.accessor];
|
||||
}
|
||||
|
||||
getOwner(instance) {
|
||||
if ( instance._reactInternalInstance )
|
||||
instance = instance._reactInternalInstance;
|
||||
else if ( instance instanceof Node )
|
||||
instance = this.getReactInstance(instance);
|
||||
|
||||
if ( ! instance )
|
||||
return null;
|
||||
|
||||
return instance._owner || (instance._currentElement && instance._currentElement._owner);
|
||||
}
|
||||
|
||||
getHostNode(instance) { //eslint-disable-line class-methods-use-this
|
||||
if ( instance._reactInternalInstance )
|
||||
instance = instance._reactInternalInstance;
|
||||
else if ( instance instanceof Node )
|
||||
instance = this.getReactInstance(instance);
|
||||
|
||||
while( instance )
|
||||
if ( instance._hostNode )
|
||||
return instance._hostNode;
|
||||
else
|
||||
instance = instance._renderedComponent;
|
||||
}
|
||||
|
||||
getParent(instance) {
|
||||
const owner = this.getOwner(instance);
|
||||
return owner && this.getOwner(owner);
|
||||
}
|
||||
|
||||
searchParent(node, criteria, max_depth=15, depth=0) {
|
||||
if ( node._reactInternalInstance )
|
||||
node = node._reactInternalInstance;
|
||||
else if ( node instanceof Node )
|
||||
node = this.getReactInstance(node);
|
||||
|
||||
if ( ! node || depth > max_depth )
|
||||
return null;
|
||||
|
||||
const inst = node._instance;
|
||||
if ( inst && criteria(inst) )
|
||||
return inst;
|
||||
|
||||
if ( node._currentElement && node._currentElement._owner ) {
|
||||
const result = this.searchParent(node._currentElement._owner, criteria, max_depth, depth+1);
|
||||
if ( result )
|
||||
return result;
|
||||
}
|
||||
|
||||
if ( node._hostParent )
|
||||
return this.searchParent(node._hostParent, criteria, max_depth, depth+1);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
searchTree(node, criteria, max_depth=15, depth=0) {
|
||||
if ( ! node )
|
||||
node = this.react;
|
||||
else if ( node._reactInternalInstance )
|
||||
node = node._reactInternalInstance;
|
||||
else if ( node instanceof Node )
|
||||
node = this.getReactInstance(node);
|
||||
|
||||
if ( ! node || depth > max_depth )
|
||||
return null;
|
||||
|
||||
const inst = node._instance;
|
||||
if ( inst && criteria(inst) )
|
||||
return inst;
|
||||
|
||||
const children = node._renderedChildren,
|
||||
component = node._renderedComponent;
|
||||
|
||||
if ( children )
|
||||
for(const key in children)
|
||||
if ( has(children, key) ) {
|
||||
const child = children[key];
|
||||
const result = child && this.searchTree(child, criteria, max_depth, depth+1);
|
||||
if ( result )
|
||||
return result;
|
||||
}
|
||||
|
||||
if ( component )
|
||||
return this.searchTree(component, criteria, max_depth, depth+1);
|
||||
}
|
||||
|
||||
|
||||
searchAll(node, criterias, max_depth=15, depth=0, data) {
|
||||
if ( ! node )
|
||||
node = this.react;
|
||||
else if ( node._reactInternalInstance )
|
||||
node = node._reactInternalInstance;
|
||||
else if ( node instanceof Node )
|
||||
node = this.getReactInstance(node);
|
||||
|
||||
if ( ! data )
|
||||
data = {
|
||||
seen: new Set,
|
||||
classes: criterias.map(() => null),
|
||||
out: criterias.map(() => ({
|
||||
cls: null, instances: new Set, depth: null
|
||||
})),
|
||||
max_depth: depth
|
||||
};
|
||||
|
||||
if ( ! node || depth > max_depth )
|
||||
return data.out;
|
||||
|
||||
if ( depth > data.max_depth )
|
||||
data.max_depth = depth;
|
||||
|
||||
const inst = node._instance;
|
||||
if ( inst ) {
|
||||
const cls = inst.constructor,
|
||||
idx = data.classes.indexOf(cls);
|
||||
|
||||
if ( idx !== -1 )
|
||||
data.out[idx].instances.add(inst);
|
||||
|
||||
else if ( ! data.seen.has(cls) ) {
|
||||
let i = criterias.length;
|
||||
while(i-- > 0)
|
||||
if ( criterias[i](inst) ) {
|
||||
data.classes[i] = data.out[i].cls = cls;
|
||||
data.out[i].instances.add(inst);
|
||||
data.out[i].depth = depth;
|
||||
break;
|
||||
}
|
||||
|
||||
data.seen.add(cls);
|
||||
}
|
||||
}
|
||||
|
||||
const children = node._renderedChildren,
|
||||
component = node._renderedComponent;
|
||||
|
||||
if ( children )
|
||||
for(const key in children)
|
||||
if ( has(children, key) ) {
|
||||
const child = children[key];
|
||||
child && this.searchAll(child, criterias, max_depth, depth+1, data);
|
||||
}
|
||||
|
||||
if ( component )
|
||||
this.searchAll(component, criterias, max_depth, depth+1, data);
|
||||
|
||||
return data.out;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Class Wrapping
|
||||
// ========================================================================
|
||||
|
||||
define(key, criteria) {
|
||||
if ( this._wrappers.has(key) )
|
||||
return this._wrappers.get(key);
|
||||
|
||||
if ( ! criteria )
|
||||
throw new Error('cannot find definition and no criteria provided');
|
||||
|
||||
const wrapper = new FineWrapper(key, criteria, this);
|
||||
this._wrappers.set(key, wrapper);
|
||||
|
||||
const data = this.searchAll(this.react, [criteria], 1000)[0];
|
||||
if ( data.cls ) {
|
||||
wrapper._set(data.cls, data.instances);
|
||||
this._known_classes.set(data.cls, wrapper);
|
||||
|
||||
} else {
|
||||
if ( ! this._waiting )
|
||||
this._startWaiting();
|
||||
|
||||
this._waiting.push(wrapper);
|
||||
this._waiting_crit.push(criteria);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
|
||||
_checkWaiters(nodes) {
|
||||
if ( ! this._waiting )
|
||||
return;
|
||||
|
||||
if ( ! Array.isArray(nodes) )
|
||||
nodes = [nodes];
|
||||
|
||||
for(let node of nodes) {
|
||||
if ( ! node )
|
||||
node = this.react;
|
||||
else if ( node._reactInternalInstance )
|
||||
node = node._reactInternalInstance;
|
||||
else if ( node instanceof Node )
|
||||
node = this.getReactInstance(node);
|
||||
|
||||
if ( ! node || ! this._waiting.length )
|
||||
continue;
|
||||
|
||||
const data = this.searchAll(node, this._waiting_crit, 1000);
|
||||
let i = data.length;
|
||||
while(i-- > 0) {
|
||||
if ( data[i].cls ) {
|
||||
const d = data[i],
|
||||
w = this._waiting.splice(i, 1)[0];
|
||||
|
||||
this._waiting_crit.splice(i, 1);
|
||||
this.log.info(`Found class for "${w.name}" at depth ${d.depth}`, d);
|
||||
|
||||
w._set(d.cls, d.instances);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! this._waiting.length )
|
||||
this._stopWaiting();
|
||||
}
|
||||
|
||||
|
||||
_startWaiting() {
|
||||
this.log.info('Installing MutationObserver.');
|
||||
|
||||
this._waiting = [];
|
||||
this._waiting_crit = [];
|
||||
this._waiting_timer = setInterval(() => this._checkWaiters(), 500);
|
||||
|
||||
if ( ! this._observer )
|
||||
this._observer = new MutationObserver(mutations =>
|
||||
this._checkWaiters(mutations.map(x => x.target))
|
||||
);
|
||||
|
||||
this._observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
_stopWaiting() {
|
||||
this.log.info('Stopping MutationObserver.');
|
||||
|
||||
if ( this._observer )
|
||||
this._observer.disconnect();
|
||||
|
||||
if ( this._waiting_timer )
|
||||
clearInterval(this._waiting_timer);
|
||||
|
||||
this._waiting = null;
|
||||
this._waiting_crit = null;
|
||||
this._waiting_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EVENTS = {
|
||||
'will-mount': 'componentWillMount',
|
||||
mount: 'componentDidMount',
|
||||
render: 'render',
|
||||
'receive-props': 'componentWillReceiveProps',
|
||||
'should-update': 'shouldComponentUpdate',
|
||||
'will-update': 'componentWillUpdate',
|
||||
update: 'componentDidUpdate',
|
||||
unmount: 'componentWillUnmount'
|
||||
}
|
||||
|
||||
|
||||
export class FineWrapper extends EventEmitter {
|
||||
constructor(name, criteria, fine) {
|
||||
super();
|
||||
|
||||
this.name = name;
|
||||
this.criteria = criteria;
|
||||
this.fine = fine;
|
||||
|
||||
this.instances = new Set;
|
||||
|
||||
this._wrapped = new Set;
|
||||
this._class = null;
|
||||
}
|
||||
|
||||
ready(fn) {
|
||||
if ( this._class )
|
||||
fn(this._class, this.instances);
|
||||
else
|
||||
this.once('set', fn);
|
||||
}
|
||||
|
||||
_set(cls, instances) {
|
||||
if ( this._class )
|
||||
throw new Error('already have a class');
|
||||
|
||||
this._class = cls;
|
||||
this._wrapped.add('componentWillMount');
|
||||
this._wrapped.add('componentWillUnmount');
|
||||
|
||||
const t = this,
|
||||
_instances = this.instances,
|
||||
proto = cls.prototype,
|
||||
o_mount = proto.componentWillMount,
|
||||
o_unmount = proto.componentWillUnmount,
|
||||
|
||||
mount = proto.componentWillMount = o_mount ?
|
||||
function(...args) {
|
||||
this._ffz_mounted = true;
|
||||
_instances.add(this);
|
||||
t.emit('will-mount', this, ...args);
|
||||
return o_mount.apply(this, args);
|
||||
} :
|
||||
function(...args) {
|
||||
this._ffz_mounted = true;
|
||||
_instances.add(this);
|
||||
t.emit('will-mount', this, ...args);
|
||||
},
|
||||
|
||||
unmount = proto.componentWillUnmount = o_unmount ?
|
||||
function(...args) {
|
||||
t.emit('unmount', this, ...args);
|
||||
_instances.delete(this);
|
||||
this._ffz_mounted = false;
|
||||
return o_unmount.apply(this, args);
|
||||
} :
|
||||
function(...args) {
|
||||
t.emit('unmount', this, ...args);
|
||||
_instances.delete(this);
|
||||
this._ffz_mounted = false;
|
||||
};
|
||||
|
||||
this.__componentWillMount = [mount, o_mount];
|
||||
this.__componentWillUnmount = [unmount, o_unmount];
|
||||
|
||||
for(const event of this.events())
|
||||
this._maybeWrap(event);
|
||||
|
||||
if ( instances )
|
||||
for(const inst of instances) {
|
||||
if ( inst._reactInternalInstance && inst._reactInternalInstance._renderedComponent )
|
||||
inst._ffz_mounted = true;
|
||||
_instances.add(inst);
|
||||
}
|
||||
|
||||
this.emit('set', cls, _instances);
|
||||
}
|
||||
|
||||
_add(instances) {
|
||||
for(const inst of instances)
|
||||
this.instances.add(inst);
|
||||
}
|
||||
|
||||
|
||||
_maybeWrap(event) {
|
||||
const key = EVENTS[event];
|
||||
if ( ! this._class || ! key || this._wrapped.has(key) )
|
||||
return;
|
||||
|
||||
this._wrap(event, key);
|
||||
}
|
||||
|
||||
_wrap(event, key) {
|
||||
if ( this._wrapped.has(key) )
|
||||
return;
|
||||
|
||||
const t = this,
|
||||
proto = this._class.prototype,
|
||||
original = proto[key],
|
||||
|
||||
fn = proto[key] = original ?
|
||||
function(...args) {
|
||||
t.emit(event, this, ...args);
|
||||
return original.apply(this, args);
|
||||
} :
|
||||
|
||||
function(...args) {
|
||||
t.emit(event, this, ...args);
|
||||
};
|
||||
|
||||
this[`__${key}`] = [fn, original];
|
||||
}
|
||||
|
||||
_unwrap(key) {
|
||||
if ( ! this._wrapped.has(key) )
|
||||
return;
|
||||
|
||||
const k = `__${key}`,
|
||||
proto = this._class.prototype,
|
||||
[fn, original] = this[k];
|
||||
|
||||
if ( proto[key] !== fn )
|
||||
throw new Error('unable to unwrap -- prototype modified');
|
||||
|
||||
proto[key] = original;
|
||||
this[k] = undefined;
|
||||
this._wrapped.delete(key);
|
||||
}
|
||||
|
||||
|
||||
on(event, fn, ctx) {
|
||||
this._maybeWrap(event);
|
||||
return super.on(event, fn, ctx);
|
||||
}
|
||||
|
||||
prependOn(event, fn, ctx) {
|
||||
this._maybeWrap(event);
|
||||
return super.prependOn(event, fn, ctx);
|
||||
}
|
||||
|
||||
once(event, fn, ctx) {
|
||||
this._maybeWrap(event);
|
||||
return super.once(event, fn, ctx);
|
||||
}
|
||||
|
||||
prependOnce(event, fn, ctx) {
|
||||
this._maybeWrap(event);
|
||||
return super.prependOnce(event, fn, ctx);
|
||||
}
|
||||
|
||||
many(event, ttl, fn, ctx) {
|
||||
this._maybeWrap(event);
|
||||
return super.many(event, ttl, fn, ctx);
|
||||
}
|
||||
|
||||
prependMany(event, ttl, fn, ctx) {
|
||||
this._maybeWrap(event);
|
||||
return super.prependMany(event, ttl, fn, ctx);
|
||||
}
|
||||
|
||||
waitFor(event) {
|
||||
this._maybeWrap(event);
|
||||
return super.waitFor(event);
|
||||
}
|
||||
}
|
168
src/utilities/compat/webmunch.js
Normal file
168
src/utilities/compat/webmunch.js
Normal file
|
@ -0,0 +1,168 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// WebMunch
|
||||
// It consumes webpack.
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
|
||||
let last_muncher = 0;
|
||||
|
||||
export default class WebMunch extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this._id = `_ffz$${last_muncher++}`;
|
||||
this._rid = 0;
|
||||
this._original_loader = null;
|
||||
this._known_rules = {};
|
||||
this._require = null;
|
||||
this._module_names = {};
|
||||
this._mod_cache = {};
|
||||
|
||||
this.hookLoader();
|
||||
this.hookRequire();
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Loaded Modules
|
||||
// ========================================================================
|
||||
|
||||
hookLoader(attempts) {
|
||||
if ( this._original_loader )
|
||||
return this.log.warn('Attempted to call hookLoader twice.');
|
||||
|
||||
this._original_loader = window.webpackJsonp;
|
||||
if ( ! this._original_loader ) {
|
||||
if ( attempts > 500 )
|
||||
return this.log.error("Unable to find webpack's loader after two minutes.");
|
||||
|
||||
return setTimeout(this.hookLoader.bind(this, (attempts||0) + 1), 250);
|
||||
}
|
||||
|
||||
this.log.info(`Found and wrapped webpack's loader after ${(attempts||0)*250}ms.`);
|
||||
window.webpackJsonp = this.webpackJsonp.bind(this);
|
||||
}
|
||||
|
||||
webpackJsonp(chunk_ids, modules) {
|
||||
const names = chunk_ids.map(x => this._module_names[x] || x).join(', ');
|
||||
this.log.info(`Twitch Chunk Loaded: ${chunk_ids} (${names})`);
|
||||
this.log.debug(`Modules: ${Object.keys(modules)}`);
|
||||
|
||||
const res = this._original_loader.apply(window, arguments); // eslint-disable-line prefer-rest-params
|
||||
|
||||
this.emit(':loaded', chunk_ids, names, modules);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Finding Modules
|
||||
// ========================================================================
|
||||
|
||||
known(key, predicate) {
|
||||
if ( typeof key === 'object' ) {
|
||||
for(const k in key)
|
||||
if ( has(key, k) )
|
||||
this.known(k, key[k]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._known_rules[key] = predicate;
|
||||
}
|
||||
|
||||
|
||||
async findModule(key, predicate) {
|
||||
if ( ! this._require )
|
||||
await this.getRequire();
|
||||
|
||||
return this.getModule(key, predicate);
|
||||
}
|
||||
|
||||
|
||||
getModule(key, predicate) {
|
||||
if ( typeof key === 'function' ) {
|
||||
predicate = key;
|
||||
key = null;
|
||||
}
|
||||
|
||||
if ( key && this._mod_cache[key] )
|
||||
return this._mod_cache[key];
|
||||
|
||||
const require = this._require;
|
||||
if ( ! require || ! require.c )
|
||||
return null;
|
||||
|
||||
if ( ! predicate )
|
||||
predicate = this._known_rules[key];
|
||||
|
||||
if ( ! predicate )
|
||||
throw new Error(`no known predicate for locating ${key}`);
|
||||
|
||||
for(const k in require.c)
|
||||
if ( has(require.c, k) ) {
|
||||
const module = require.c[k],
|
||||
mod = module && module.exports;
|
||||
|
||||
if ( mod && predicate(mod) ) {
|
||||
if ( key )
|
||||
this._mod_cache[key] = mod;
|
||||
return mod;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Grabbing Require
|
||||
// ========================================================================
|
||||
|
||||
getRequire() {
|
||||
if ( this._require )
|
||||
return Promise.resolve(this._require);
|
||||
|
||||
return new Promise(resolve => {
|
||||
const id = `${this._id}$${this._rid++}`;
|
||||
(this._original_loader || window.webpackJsonp)(
|
||||
[],
|
||||
{
|
||||
[id]: (module, exports, __webpack_require__) => {
|
||||
resolve(this._require = __webpack_require__);
|
||||
}
|
||||
},
|
||||
[id]
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
async hookRequire() {
|
||||
const start_time = performance.now(),
|
||||
require = await this.getRequire(),
|
||||
time = performance.now() - start_time;
|
||||
|
||||
this.log.info(`require() grabbed in ${time.toFixed(5)}ms.`);
|
||||
|
||||
const loader = require.e && require.e.toString();
|
||||
let modules;
|
||||
if ( loader && loader.indexOf('Loading chunk') !== -1 ) {
|
||||
const data = /({0:.*?})/.exec(loader);
|
||||
if ( data )
|
||||
try {
|
||||
modules = JSON.parse(data[1].replace(/(\d+):/g, '"$1":'))
|
||||
} catch(err) { } // eslint-disable-line no-empty
|
||||
}
|
||||
|
||||
if ( modules ) {
|
||||
this._module_names = modules;
|
||||
this.log.info(`Loaded names for ${Object.keys(modules).length} chunks from require().`)
|
||||
} else
|
||||
this.log.warn(`Unable to find chunk names in require().`);
|
||||
}
|
||||
|
||||
}
|
22
src/utilities/constants.js
Normal file
22
src/utilities/constants.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
'use strict';
|
||||
|
||||
export const DEBUG = localStorage.ffzDebugMode === 'true' && document.body.classList.contains('ffz-dev');
|
||||
export const SERVER = DEBUG ? '//localhost:8000' : 'https://cdn.frankerfacez.com';
|
||||
|
||||
export const API_SERVER = '//api.frankerfacez.com';
|
||||
|
||||
export const WS_CLUSTERS = {
|
||||
Production: [
|
||||
['wss://catbag.frankerfacez.com/', 0.25],
|
||||
['wss://andknuckles.frankerfacez.com/', 1],
|
||||
['wss://tuturu.frankerfacez.com/', 1]
|
||||
],
|
||||
|
||||
Development: [
|
||||
['wss://127.0.0.1:8003/', 1]
|
||||
]
|
||||
}
|
||||
|
||||
export const IS_OSX = navigator.platform ? navigator.platform.indexOf('Mac') !== -1 : /OS X/.test(navigator.userAgent);
|
||||
export const IS_WIN = navigator.platform ? navigator.platform.indexOf('Win') !== -1 : /Windows/.test(navigator.userAgent);
|
||||
export const IS_WEBKIT = navigator.userAgent.indexOf('AppleWebKit/') !== -1 && navigator.userAgent.indexOf('Edge/') === -1;
|
149
src/utilities/dom.js
Normal file
149
src/utilities/dom.js
Normal file
|
@ -0,0 +1,149 @@
|
|||
'use strict';
|
||||
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
const ATTRS = [
|
||||
'accept', 'accept-charset', 'accesskey', 'action', 'align', 'alt', 'async',
|
||||
'autocomplete', 'autofocus', 'autoplay', 'bgcolor', 'border', 'buffered',
|
||||
'challenge', 'charset', 'checked', 'cite', 'class', 'code', 'codebase',
|
||||
'color', 'cols', 'colspan', 'content', 'contenteditable', 'contextmenu',
|
||||
'controls', 'coords', 'crossorigin', 'data', 'data-*', 'datetime',
|
||||
'default', 'defer', 'dir', 'dirname', 'disabled', 'download', 'draggable',
|
||||
'dropzone', 'enctype', 'for', 'form', 'formaction', 'headers', 'height',
|
||||
'hidden', 'high', 'href', 'hreflang', 'http-equiv', 'icon', 'id',
|
||||
'integrity', 'ismap', 'itemprop', 'keytype', 'kind', 'label', 'lang',
|
||||
'language', 'list', 'loop', 'low', 'manifest', 'max', 'maxlength',
|
||||
'minlength', 'media', 'method', 'min', 'multiple', 'muted', 'name',
|
||||
'novalidate', 'open', 'optimum', 'pattern', 'ping', 'placeholder', 'poster',
|
||||
'preload', 'radiogroup', 'readonly', 'rel', 'required', 'reversed', 'rows',
|
||||
'rowspan', 'sandbox', 'scope', 'scoped', 'seamless', 'selected', 'shape',
|
||||
'size', 'sizes', 'slot', 'span', 'spellcheck', 'src', 'srcdoc', 'srclang',
|
||||
'srcset', 'start', 'step', 'style', 'summary', 'tabindex', 'target',
|
||||
'title', 'type', 'usemap', 'value', 'width', 'wrap'
|
||||
];
|
||||
|
||||
|
||||
const range = document.createRange();
|
||||
|
||||
function camelCase(name) {
|
||||
return name.replace(/[-_]\w/g, m => m[1].toUpperCase());
|
||||
}
|
||||
|
||||
|
||||
export function createElement(tag, props, children, no_sanitize) {
|
||||
const el = document.createElement(tag);
|
||||
|
||||
if ( typeof props === 'string' )
|
||||
el.className = props;
|
||||
else if ( props )
|
||||
for(const key in props)
|
||||
if ( has(props, key) ) {
|
||||
const lk = key.toLowerCase(),
|
||||
prop = props[key];
|
||||
|
||||
if ( lk === 'style' ) {
|
||||
if ( typeof prop === 'string' )
|
||||
el.style.cssText = prop;
|
||||
else
|
||||
for(const k in prop)
|
||||
if ( has(prop, k) )
|
||||
el.style[k] = prop[k];
|
||||
|
||||
} else if ( lk === 'dataset' ) {
|
||||
for(const k in prop)
|
||||
if ( has(prop, k) )
|
||||
el.dataset[camelCase(k)] = prop[k];
|
||||
|
||||
} else if ( key === 'dangerouslySetInnerHTML' ) {
|
||||
// React compatibility is cool. SeemsGood
|
||||
if ( prop && prop.__html )
|
||||
el.innerHTML = prop.__html;
|
||||
|
||||
} else if ( lk.startsWith('on') )
|
||||
el.addEventListener(lk.slice(2), prop);
|
||||
|
||||
else if ( lk.startsWith('data-') )
|
||||
el.dataset[camelCase(lk.slice(5))] = prop;
|
||||
|
||||
else if ( lk.startsWith('aria-') || ATTRS.includes(lk) )
|
||||
el.setAttribute(key, prop);
|
||||
|
||||
else
|
||||
el[key] = props[key];
|
||||
}
|
||||
|
||||
if ( children )
|
||||
setChildren(el, children, no_sanitize);
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
export function setChildren(el, children, no_sanitize) {
|
||||
if ( typeof children === 'string' ) {
|
||||
if ( no_sanitize )
|
||||
el.innerHTML = children;
|
||||
else
|
||||
el.textContent = children;
|
||||
|
||||
} else if ( Array.isArray(children) ) {
|
||||
for(const child of children)
|
||||
if ( typeof child === 'string' )
|
||||
el.appendChild(no_sanitize ?
|
||||
range.createContextualFragment(child) :
|
||||
document.createTextNode(child)
|
||||
);
|
||||
|
||||
else if ( child )
|
||||
el.appendChild(child);
|
||||
|
||||
} else if ( children )
|
||||
el.appendChild(children);
|
||||
}
|
||||
|
||||
|
||||
const el = createElement('span');
|
||||
|
||||
export function sanitize(text) {
|
||||
el.textContent = text;
|
||||
return el.innerHTML;
|
||||
}
|
||||
|
||||
|
||||
let last_id = 0;
|
||||
|
||||
export class ManagedStyle {
|
||||
constructor(id) {
|
||||
this.id = id || last_id++;
|
||||
|
||||
this._blocks = {};
|
||||
|
||||
this._style = createElement('style', {
|
||||
type: 'text/css',
|
||||
id: `ffz--managed-style--${this.id}`
|
||||
});
|
||||
|
||||
document.head.appendChild(this._style);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
document.head.removeChild(this._style);
|
||||
this._blocks = null;
|
||||
this._style = null;
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
let block = this._blocks[key];
|
||||
if ( block )
|
||||
block.textContent = value;
|
||||
else
|
||||
this._style.appendChild(this._blocks[key] = document.createTextNode(value));
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
const block = this._blocks[key];
|
||||
if ( block ) {
|
||||
this._style.removeChild(block);
|
||||
this._blocks[key] = null;
|
||||
}
|
||||
}
|
||||
}
|
313
src/utilities/events.js
Normal file
313
src/utilities/events.js
Normal file
|
@ -0,0 +1,313 @@
|
|||
// ============================================================================
|
||||
// EventEmitter
|
||||
// Homegrown for that lean feeling.
|
||||
// ============================================================================
|
||||
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
const Detach = {};
|
||||
|
||||
const SNAKE_CAPS = /([a-z])([A-Z])/g,
|
||||
SNAKE_SPACE = /[ \t\W]/g,
|
||||
SNAKE_TRIM = /^_+|_+$/g;
|
||||
|
||||
|
||||
String.prototype.toSnakeCase = function() {
|
||||
return this
|
||||
.replace(SNAKE_CAPS, '$1_$2')
|
||||
.replace(SNAKE_SPACE, '_')
|
||||
.replace(SNAKE_TRIM, '')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
export class EventEmitter {
|
||||
constructor() {
|
||||
this.__listeners = {};
|
||||
this.__dead_events = 0;
|
||||
}
|
||||
|
||||
__cleanListeners() {
|
||||
if ( ! this.__dead_events )
|
||||
return;
|
||||
|
||||
const nl = {}, ol = this.__listeners;
|
||||
for(const key in ol)
|
||||
if ( has(ol, key) ) {
|
||||
const val = ol[key];
|
||||
if ( val )
|
||||
nl[key] = val;
|
||||
}
|
||||
|
||||
this.__listeners = nl;
|
||||
this.__dead_events = 0;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Public Methods
|
||||
// ========================================================================
|
||||
|
||||
on(event, fn, ctx) {
|
||||
if ( typeof fn !== 'function' )
|
||||
throw new TypeError('fn must be a function');
|
||||
|
||||
(this.__listeners[event] = this.__listeners[event] || []).push([fn, ctx, false])
|
||||
}
|
||||
|
||||
prependOn(event, fn, ctx) {
|
||||
if ( typeof fn !== 'function' )
|
||||
throw new TypeError('fn must be a function');
|
||||
|
||||
(this.__listeners[event] = this.__listeners[event] || []).unshift([fn, ctx, false])
|
||||
}
|
||||
|
||||
once(event, fn, ctx) { return this.many(event, 1, fn, ctx) }
|
||||
prependOnce(event, fn, ctx) { return this.prependMany(event, 1, fn, ctx) }
|
||||
|
||||
many(event, ttl, fn, ctx) {
|
||||
if ( typeof fn !== 'function' )
|
||||
throw new TypeError('fn must be a function');
|
||||
|
||||
if ( typeof ttl !== 'number' || isNaN(ttl) || ! isFinite(ttl) || ttl < 1 )
|
||||
throw new TypeError('ttl must be a positive, finite number');
|
||||
|
||||
(this.__listeners[event] = this.__listeners[event] || []).push([fn, ctx, ttl]);
|
||||
}
|
||||
|
||||
prependMany(event, ttl, fn, ctx) {
|
||||
if ( typeof fn !== 'function' )
|
||||
throw new TypeError('fn must be a function');
|
||||
|
||||
if ( typeof ttl !== 'number' || isNaN(ttl) || ! isFinite(ttl) || ttl < 1 )
|
||||
throw new TypeError('ttl must be a positive, finite number');
|
||||
|
||||
(this.__listeners[event] = this.__listeners[event] || []).unshift([fn, ctx, ttl]);
|
||||
}
|
||||
|
||||
waitFor(event) {
|
||||
return new Promise(resolve => {
|
||||
(this.__listeners[event] = this.__listeners[event] || []).push([resolve, null, 1]);
|
||||
})
|
||||
}
|
||||
|
||||
off(event, fn, ctx) {
|
||||
let list = this.__listeners[event];
|
||||
if ( ! list )
|
||||
return;
|
||||
|
||||
if ( ! fn )
|
||||
list = null;
|
||||
else {
|
||||
list = list.filter(([f, c]) => !(f === fn && (!ctx || ctx === c)));
|
||||
if ( ! list.length )
|
||||
list = null;
|
||||
}
|
||||
|
||||
this.__listeners[event] = list;
|
||||
if ( ! list )
|
||||
this.__dead_events++;
|
||||
}
|
||||
|
||||
events() {
|
||||
this.__cleanListeners();
|
||||
return Object.keys(this.__listeners);
|
||||
}
|
||||
|
||||
listeners(event) {
|
||||
const list = this.__listeners[event];
|
||||
return list ? Array.from(list) : [];
|
||||
}
|
||||
|
||||
emit(event, ...args) {
|
||||
const list = this.__listeners[event];
|
||||
if ( ! list )
|
||||
return;
|
||||
|
||||
// Track removals separately to make iteration over the event list
|
||||
// much, much simpler.
|
||||
const removed = new Set;
|
||||
|
||||
for(const item of list) {
|
||||
const [fn, ctx, ttl] = item,
|
||||
ret = fn.apply(ctx, args);
|
||||
|
||||
if ( ret === Detach )
|
||||
removed.add(item);
|
||||
else if ( ttl !== false ) {
|
||||
if ( ttl <= 1 )
|
||||
removed.add(item);
|
||||
else
|
||||
item[2] = ttl - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if ( removed.size ) {
|
||||
// Re-grab the list to make sure it wasn't removed mid-iteration.
|
||||
const new_list = this.__listeners[event];
|
||||
if ( new_list ) {
|
||||
for(const item of removed) {
|
||||
const idx = new_list.indexOf(item);
|
||||
if ( idx !== -1 )
|
||||
new_list.splice(idx, 1);
|
||||
}
|
||||
|
||||
if ( ! list.length ) {
|
||||
this.__listeners[event] = null;
|
||||
this.__dead_events++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emitAsync(event, ...args) {
|
||||
const list = this.__listeners[event];
|
||||
if ( ! list )
|
||||
return Promise.resolve([]);
|
||||
|
||||
// Track removals separately to make iteration over the event list
|
||||
// much, much simpler.
|
||||
const removed = new Set,
|
||||
promises = [];
|
||||
|
||||
for(const item of list) {
|
||||
const [fn, ctx, ttl] = item;
|
||||
const ret = fn.apply(ctx, args);
|
||||
if ( ret === Detach )
|
||||
removed.add(item);
|
||||
else if ( ttl !== false ) {
|
||||
if ( ttl <= 1 )
|
||||
removed.add(item);
|
||||
else
|
||||
item[2] = ttl - 1;
|
||||
}
|
||||
|
||||
if ( ret !== Detach )
|
||||
promises.push(ret);
|
||||
}
|
||||
|
||||
if ( removed.size ) {
|
||||
// Re-grab the list to make sure it wasn't removed mid-iteration.
|
||||
const new_list = this.__listeners[event];
|
||||
if ( new_list ) {
|
||||
for(const item of removed) {
|
||||
const idx = new_list.indexOf(item);
|
||||
if ( idx !== -1 )
|
||||
new_list.splice(idx, 1);
|
||||
}
|
||||
|
||||
if ( ! list.length ) {
|
||||
this.__listeners[event] = null;
|
||||
this.__dead_events++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
||||
EventEmitter.Detach = Detach;
|
||||
|
||||
|
||||
export default class HierarchicalEventEmitter extends EventEmitter {
|
||||
constructor(name, parent) {
|
||||
super();
|
||||
|
||||
this.name = name || (this.constructor.name || '').toSnakeCase();
|
||||
this.parent = parent;
|
||||
|
||||
if ( parent ) {
|
||||
this.root = parent.root;
|
||||
this.__listeners = parent.__listeners;
|
||||
this.__path = name && parent.__path ? `${parent.__path}.${name}` : name;
|
||||
|
||||
} else {
|
||||
this.root = this;
|
||||
this.__path = undefined;
|
||||
}
|
||||
|
||||
this.__path_parts = this.__path ? this.__path.split('.') : [];
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Public Properties
|
||||
// ========================================================================
|
||||
|
||||
get path() {
|
||||
return this.__path;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Public Methods
|
||||
// ========================================================================
|
||||
|
||||
abs_path(path) {
|
||||
if ( typeof path !== 'string' || ! path.length )
|
||||
throw new TypeError('path must be a non-empty string');
|
||||
|
||||
let i = 0, chr;
|
||||
const parts = this.__path_parts,
|
||||
depth = parts.length;
|
||||
|
||||
do {
|
||||
chr = path.charAt(i);
|
||||
if ( path.charAt(i) === '.' ) {
|
||||
if ( i > depth )
|
||||
throw new Error('invalid path: reached top of stack');
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
} while ( ++i < path.length );
|
||||
|
||||
const event = chr === ':';
|
||||
if ( i === 0 )
|
||||
return event && this.__path ? `${this.__path}${path}` : path;
|
||||
|
||||
const prefix = parts.slice(0, depth - (i-1)).join('.'),
|
||||
remain = path.slice(i);
|
||||
|
||||
if ( ! prefix.length )
|
||||
return remain;
|
||||
|
||||
else if ( ! remain.length )
|
||||
return prefix;
|
||||
|
||||
else if ( event )
|
||||
return prefix + remain;
|
||||
|
||||
return `${prefix}.${remain}`;
|
||||
}
|
||||
|
||||
|
||||
on(event, fn, ctx) { return super.on(this.abs_path(event), fn, ctx) }
|
||||
prependOn(event, fn, ctx) { return super.prependOn(this.abs_path(event), fn, ctx) }
|
||||
|
||||
once(event, fn, ctx) { return super.once(this.abs_path(event), fn, ctx) }
|
||||
prependOnce(event, fn, ctx) { return super.prependOnce(this.abs_path(event), fn, ctx) }
|
||||
|
||||
many(event, ttl, fn, ctx) { return super.many(this.abs_path(event), ttl, fn, ctx) }
|
||||
prependMany(event, ttl, fn, ctx) { return super.prependMany(this.abs_path(event), ttl, fn, ctx) }
|
||||
|
||||
waitFor(event) { return super.waitFor(this.abs_path(event)) }
|
||||
off(event, fn, ctx) { return super.off(this.abs_path(event), fn, ctx) }
|
||||
listeners(event) { return super.listeners(this.abs_path(event)) }
|
||||
|
||||
emit(event, ...args) { return super.emit(this.abs_path(event), ...args) }
|
||||
emitAsync(event, ...args) { return super.emitAsync(this.abs_path(event), ...args) }
|
||||
|
||||
events(include_children) {
|
||||
this.__cleanListeners();
|
||||
const keys = Object.keys(this.__listeners),
|
||||
path = this.__path || '',
|
||||
len = path.length;
|
||||
|
||||
return keys.filter(x => {
|
||||
const y = x.charAt(len);
|
||||
return x.startsWith(path) && (y === '' || (include_children && y === '.') || y === ':');
|
||||
});
|
||||
}
|
||||
}
|
6
src/utilities/filtering.js
Normal file
6
src/utilities/filtering.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Advanced Filter System
|
||||
// ============================================================================
|
||||
|
73
src/utilities/logging.js
Normal file
73
src/utilities/logging.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
'use strict';
|
||||
|
||||
export default class Logger {
|
||||
constructor(parent, name, level) {
|
||||
this.parent = parent;
|
||||
this.name = name;
|
||||
|
||||
this.enabled = true;
|
||||
this.level = level || (parent && parent.level) || Logger.DEFAULT_LEVEL;
|
||||
|
||||
this.children = {};
|
||||
}
|
||||
|
||||
get(name, level) {
|
||||
if ( ! this.children[name] )
|
||||
this.children[name] = new Logger(this, (this.name ? `${this.name}.${name}` : name), level);
|
||||
|
||||
return this.children[name];
|
||||
}
|
||||
|
||||
debug(...args) {
|
||||
return this.invoke(Logger.DEBUG, args);
|
||||
}
|
||||
|
||||
info(...args) {
|
||||
return this.invoke(Logger.INFO, args);
|
||||
}
|
||||
|
||||
warn(...args) {
|
||||
return this.invoke(Logger.WARN, args);
|
||||
}
|
||||
|
||||
error(...args) {
|
||||
return this.invoke(Logger.ERROR, args);
|
||||
}
|
||||
|
||||
/* eslint no-console: "off" */
|
||||
invoke(level, args) {
|
||||
if ( ! this.enabled || level < this.level )
|
||||
return;
|
||||
|
||||
const message = Array.prototype.slice.call(args);
|
||||
|
||||
if ( this.name )
|
||||
message.unshift(`%cFFZ [%c${this.name}%c]:%c`, 'color:#755000; font-weight:bold', '', 'color:#755000; font-weight:bold', '');
|
||||
else
|
||||
message.unshift('%cFFZ:%c', 'color:#755000; font-weight:bold', '');
|
||||
|
||||
if ( level === Logger.DEBUG )
|
||||
console.info(...message);
|
||||
|
||||
else if ( level === Logger.INFO )
|
||||
console.info(...message);
|
||||
|
||||
else if ( level === Logger.WARN )
|
||||
console.warn(...message);
|
||||
|
||||
else if ( level === Logger.ERROR )
|
||||
console.error(...message);
|
||||
|
||||
else
|
||||
console.log(...message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Logger.DEFAULT_LEVEL = 2;
|
||||
|
||||
Logger.DEBUG = 1;
|
||||
Logger.INFO = 2;
|
||||
Logger.WARN = 4;
|
||||
Logger.ERROR = 8;
|
||||
Logger.OFF = 99;
|
563
src/utilities/module.js
Normal file
563
src/utilities/module.js
Normal file
|
@ -0,0 +1,563 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Module System
|
||||
// Modules are cool.
|
||||
// ============================================================================
|
||||
|
||||
import EventEmitter from 'utilities/events';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Module
|
||||
// ============================================================================
|
||||
|
||||
export const State = {
|
||||
UNLOADED: 0,
|
||||
LOADING: 1,
|
||||
LOADED: 2,
|
||||
UNLOADING: 3,
|
||||
|
||||
DISABLED: 0,
|
||||
ENABLING: 1,
|
||||
ENABLED: 2,
|
||||
DISABLING: 3
|
||||
}
|
||||
|
||||
|
||||
export default class Module extends EventEmitter {
|
||||
constructor(name, parent) {
|
||||
if ( ! parent && name instanceof Module ) {
|
||||
parent = name;
|
||||
name = null;
|
||||
}
|
||||
|
||||
super(name, parent);
|
||||
this.__modules = parent ? parent.__modules : {};
|
||||
this.children = {};
|
||||
|
||||
if ( parent && ! parent.children[this.name] )
|
||||
parent.children[this.name] = this;
|
||||
|
||||
if ( this.root === this )
|
||||
this.__modules[this.__path || ''] = this;
|
||||
|
||||
this.__load_state = this.onLoad ? State.UNLOADED : State.LOADED;
|
||||
this.__state = this.onLoad || this.onEnable ?
|
||||
State.DISABLED : State.ENABLED;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Public Properties
|
||||
// ========================================================================
|
||||
|
||||
get state() { return this.__state }
|
||||
get load_state() { return this.__load_state }
|
||||
|
||||
get loaded() { return this.__load_state === State.LOADED }
|
||||
get loading() { return this.__load_state === State.LOADING }
|
||||
|
||||
get enabled() { return this.__state === State.ENABLED }
|
||||
get enabling() { return this.__state === State.ENABLING }
|
||||
|
||||
|
||||
get log() {
|
||||
if ( ! this.__log )
|
||||
this.__log = this.parent && this.parent.log.get(this.name);
|
||||
return this.__log
|
||||
}
|
||||
|
||||
set log(log) {
|
||||
this.__log = log;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// State! Glorious State
|
||||
// ========================================================================
|
||||
|
||||
load(...args) {
|
||||
return this.__load(args, this.__path, []);
|
||||
}
|
||||
|
||||
unload(...args) {
|
||||
return this.__unload(args, this.__path, []);
|
||||
}
|
||||
|
||||
enable(...args) {
|
||||
return this.__enable(args, this.__path, []);
|
||||
}
|
||||
|
||||
disable(...args) {
|
||||
return this.__disable(args, this.__path, []);
|
||||
}
|
||||
|
||||
|
||||
__load(args, initial, chain) {
|
||||
const path = this.__path || this.name,
|
||||
state = this.__load_state;
|
||||
|
||||
if ( state === State.LOADING )
|
||||
return this.__load_promise;
|
||||
|
||||
else if ( state === State.LOADED )
|
||||
return Promise.resolve();
|
||||
|
||||
else if ( state === State.UNLOADING )
|
||||
return Promise.reject(new ModuleError(`attempted to load module ${path} while module is being unloaded`));
|
||||
|
||||
else if ( chain.includes(this) )
|
||||
return Promise.reject(new CyclicDependencyError(`cyclic load requirements when loading ${initial}`, chain));
|
||||
|
||||
chain.push(this);
|
||||
|
||||
this.__load_state = State.LOADING;
|
||||
return this.__load_promise = (async () => {
|
||||
if ( this.load_requires ) {
|
||||
const promises = [];
|
||||
for(const name of this.load_requires) {
|
||||
const module = this.resolve(name);
|
||||
if ( ! module || !(module instanceof Module) )
|
||||
throw new ModuleError(`cannot find required module ${name} when loading ${path}`);
|
||||
|
||||
promises.push(module.__enable([], initial, Array.from(chain)));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
if ( this.onLoad )
|
||||
return this.onLoad(...args);
|
||||
|
||||
})().then(ret => {
|
||||
this.__load_state = State.LOADED;
|
||||
this.__load_promise = null;
|
||||
this.emit(':loaded', this);
|
||||
return ret;
|
||||
}).catch(err => {
|
||||
this.__load_state = State.UNLOADED;
|
||||
this.__load_promise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
__unload(args, initial, chain) {
|
||||
const path = this.__path || this.name,
|
||||
state = this.__load_state;
|
||||
|
||||
if ( state === State.UNLOADING )
|
||||
return this.__load_promise;
|
||||
|
||||
else if ( state === State.UNLOADED )
|
||||
return Promise.resolve();
|
||||
|
||||
else if ( ! this.onUnload )
|
||||
return Promise.reject(new ModuleError(`attempted to unload module ${path} but module cannot be unloaded`));
|
||||
|
||||
else if ( state === State.LOADING )
|
||||
return Promise.reject(new ModuleError(`attempted to unload module ${path} while module is being loaded`));
|
||||
|
||||
else if ( chain.includes(this) )
|
||||
return Promise.reject(new CyclicDependencyError(`cyclic load requirements when unloading ${initial}`, chain));
|
||||
|
||||
chain.push(this);
|
||||
|
||||
this.__load_state = State.UNLOADING;
|
||||
return this.__load_promise = (async () => {
|
||||
if ( this.__state !== State.DISABLED )
|
||||
await this.disable();
|
||||
|
||||
if ( this.load_dependents ) {
|
||||
const promises = [];
|
||||
for(const name of this.load_dependents) {
|
||||
const module = this.resolve(name);
|
||||
if ( ! module )
|
||||
throw new ModuleError(`cannot find depending module ${name} when unloading ${path}`);
|
||||
|
||||
promises.push(module.__unload([], initial, Array.from(chain)));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
return this.onUnload(...args);
|
||||
|
||||
})().then(ret => {
|
||||
this.__load_state = State.UNLOADED;
|
||||
this.__load_promise = null;
|
||||
this.emit(':unloaded', this);
|
||||
return ret;
|
||||
}).catch(err => {
|
||||
this.__load_state = State.LOADED;
|
||||
this.__load_promise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
__enable(args, initial, chain) {
|
||||
const path = this.__path || this.name,
|
||||
state = this.__state;
|
||||
|
||||
if ( state === State.ENABLING )
|
||||
return this.__state_promise;
|
||||
|
||||
else if ( state === State.ENABLED )
|
||||
return Promise.resolve();
|
||||
|
||||
else if ( state === State.DISABLING )
|
||||
return Promise.reject(new ModuleError(`attempted to enable module ${path} while module is being disabled`));
|
||||
|
||||
else if ( chain.includes(this) )
|
||||
return Promise.reject(new CyclicDependencyError(`cyclic requirements when enabling ${initial}`, chain));
|
||||
|
||||
chain.push(this);
|
||||
|
||||
this.__state = State.ENABLING;
|
||||
return this.__state_promise = (async () => {
|
||||
const promises = [],
|
||||
requires = this.requires,
|
||||
load_state = this.__load_state;
|
||||
|
||||
if ( load_state === State.UNLOADING )
|
||||
// We'd abort for this later to, but kill it now before we start
|
||||
// any unnecessary work.
|
||||
throw new ModuleError(`attempted to load module ${path} while module is being unloaded`);
|
||||
|
||||
else if ( load_state === State.LOADING || load_state === State.UNLOADED )
|
||||
promises.push(this.load());
|
||||
|
||||
if ( requires )
|
||||
for(const name of requires) {
|
||||
const module = this.resolve(name);
|
||||
if ( ! module || !(module instanceof Module) )
|
||||
throw new ModuleError(`cannot find required module ${name} when enabling ${path}`);
|
||||
|
||||
promises.push(module.__enable([], initial, Array.from(chain)));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
if ( this.onEnable )
|
||||
return this.onEnable(...args);
|
||||
|
||||
})().then(ret => {
|
||||
this.__state = State.ENABLED;
|
||||
this.__state_promise = null;
|
||||
this.emit(':enabled', this);
|
||||
return ret;
|
||||
|
||||
}).catch(err => {
|
||||
this.__state = State.DISABLED;
|
||||
this.__state_promise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
__disable(args, initial, chain) {
|
||||
const path = this.__path || this.name,
|
||||
state = this.__state;
|
||||
|
||||
if ( state === State.DISABLING )
|
||||
return this.__state_promise;
|
||||
|
||||
else if ( state === State.DISABLED )
|
||||
return Promise.resolve();
|
||||
|
||||
else if ( ! this.onDisable )
|
||||
return Promise.reject(new ModuleError(`attempted to disable module ${path} but module cannot be disabled`));
|
||||
|
||||
else if ( state === State.ENABLING )
|
||||
return Promise.reject(new ModuleError(`attempted to disable module ${path} but module is being enabled`));
|
||||
|
||||
else if ( chain.includes(this) )
|
||||
return Promise.reject(new CyclicDependencyError(`cyclic requirements when disabling ${initial}`, chain));
|
||||
|
||||
chain.push(this);
|
||||
|
||||
this.__state = State.DISABLING;
|
||||
return this.__state_promise = (async () => {
|
||||
if ( this.__load_state !== State.LOADED )
|
||||
// We'd abort for this later to, but kill it now before we start
|
||||
// any unnecessary work.
|
||||
throw new ModuleError(`attempted to disable module ${path} but module is unloaded -- weird state`);
|
||||
|
||||
if ( this.dependents ) {
|
||||
const promises = [];
|
||||
for(const name of this.dependents) {
|
||||
const module = this.resolve(name);
|
||||
if ( ! module )
|
||||
throw new ModuleError(`cannot find depending module ${name} when disabling ${path}`);
|
||||
|
||||
promises.push(module.__disable([], initial, Array.from(chain)));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
return this.onDisable(...args);
|
||||
|
||||
})().then(ret => {
|
||||
this.__state = State.ENABLED;
|
||||
this.__state_promise = null;
|
||||
this.emit(':disabled', this);
|
||||
return ret;
|
||||
|
||||
}).catch(err => {
|
||||
this.__state = State.DISABLED;
|
||||
this.__state_promise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Slightly Easier Events
|
||||
// ========================================================================
|
||||
|
||||
on(event, fn, ctx) {
|
||||
return super.on(event, fn, ctx === undefined ? this : ctx)
|
||||
}
|
||||
|
||||
prependOn(event, fn, ctx) {
|
||||
return super.prependOn(event, fn, ctx === undefined ? this : ctx)
|
||||
}
|
||||
|
||||
many(event, ttl, fn, ctx) {
|
||||
return super.many(event, ttl, fn, ctx === undefined ? this : ctx)
|
||||
}
|
||||
|
||||
prependMany(event, ttl, fn, ctx) {
|
||||
return super.prependMany(event, ttl, fn, ctx === undefined ? this : ctx)
|
||||
}
|
||||
|
||||
once(event, fn, ctx) {
|
||||
return super.once(event, fn, ctx === undefined ? this : ctx)
|
||||
}
|
||||
|
||||
prependOnce(event, fn, ctx) {
|
||||
return super.prependOnce(event, fn, ctx === undefined ? this : ctx)
|
||||
}
|
||||
|
||||
off(event, fn, ctx) {
|
||||
return super.off(event, fn, ctx === undefined ? this : ctx)
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Child Control
|
||||
// ========================================================================
|
||||
|
||||
loadModules(...names) {
|
||||
return Promise.all(names.map(n => this.resolve(n).load()))
|
||||
}
|
||||
|
||||
unloadModules(...names) {
|
||||
return Promise.all(names.map(n => this.resolve(n).unload()))
|
||||
}
|
||||
|
||||
enableModules(...names) {
|
||||
return Promise.all(names.map(n => this.resolve(n).enable()))
|
||||
}
|
||||
|
||||
disableModules(...names) {
|
||||
return Promise.all(names.map(n => this.resolve(n).disable()))
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Module Management
|
||||
// ========================================================================
|
||||
|
||||
resolve(name) {
|
||||
if ( name instanceof Module )
|
||||
return name;
|
||||
|
||||
return this.__modules[this.abs_path(name)];
|
||||
}
|
||||
|
||||
|
||||
__get_requires() {
|
||||
if ( has(this, 'requires') )
|
||||
return this.requires;
|
||||
if ( has(this.constructor, 'requires') )
|
||||
return this.constructor.requires;
|
||||
}
|
||||
|
||||
|
||||
__get_load_requires() {
|
||||
if ( has(this, 'load_requires') )
|
||||
return this.load_requires;
|
||||
if ( has(this.constructor, 'load_requires') )
|
||||
return this.constructor.load_requires;
|
||||
}
|
||||
|
||||
|
||||
inject(name, module) {
|
||||
if ( name instanceof Module || name.prototype instanceof Module ) {
|
||||
module = name;
|
||||
name = null;
|
||||
}
|
||||
|
||||
const requires = this.requires = this.__get_requires() || [];
|
||||
|
||||
if ( module instanceof Module ) {
|
||||
// Existing Instance
|
||||
if ( ! name )
|
||||
name = module.constructor.name.toSnakeCase();
|
||||
|
||||
} else if ( module && module.prototype instanceof Module ) {
|
||||
// New Instance
|
||||
if ( ! name )
|
||||
name = module.name.toSnakeCase();
|
||||
|
||||
module = this.register(name, module);
|
||||
|
||||
} else if ( name ) {
|
||||
// Just a Name
|
||||
const full_name = name;
|
||||
name = name.replace(/^(?:[^.]*\.)+/, '');
|
||||
module = this.resolve(full_name);
|
||||
|
||||
// Allow injecting a module that doesn't exist yet?
|
||||
|
||||
if ( ! module || !(module instanceof Module) ) {
|
||||
if ( module )
|
||||
module[2].push([this.__path, name]);
|
||||
else
|
||||
this.__modules[this.abs_path(full_name)] = [[], [], [[this.__path, name]]]
|
||||
|
||||
return this[name] = null;
|
||||
}
|
||||
|
||||
} else
|
||||
throw new TypeError(`must provide a valid module name or class`);
|
||||
|
||||
if ( ! module )
|
||||
throw new Error(`cannot find module ${name} or no module provided`);
|
||||
|
||||
requires.push(module.abs_path('.'));
|
||||
|
||||
if ( this.enabled && ! module.enabled )
|
||||
module.enable();
|
||||
|
||||
return this[name] = module;
|
||||
}
|
||||
|
||||
|
||||
register(name, module, inject_reference) {
|
||||
if ( name.prototype instanceof Module ) {
|
||||
inject_reference = module;
|
||||
module = name;
|
||||
name = module.name.toSnakeCase();
|
||||
}
|
||||
|
||||
const path = this.abs_path(`.${name}`),
|
||||
proto = module.prototype,
|
||||
old_val = this.__modules[path];
|
||||
|
||||
if ( !(proto instanceof Module) )
|
||||
throw new TypeError(`Module ${name} is not subclass of Module.`);
|
||||
|
||||
if ( old_val instanceof Module )
|
||||
throw new ModuleError(`Name Collision for Module ${path}`);
|
||||
|
||||
const dependents = old_val || [[], [], []],
|
||||
inst = this.__modules[path] = new module(name, this),
|
||||
requires = inst.requires = inst.__get_requires() || [],
|
||||
load_requires = inst.load_requires = inst.__get_load_requires() || [];
|
||||
|
||||
inst.dependents = dependents[0];
|
||||
inst.load_dependents = dependents[1];
|
||||
|
||||
if ( inst instanceof SiteModule && ! requires.includes('site') )
|
||||
requires.push('site');
|
||||
|
||||
for(const req_name of requires) {
|
||||
const req_path = inst.abs_path(req_name),
|
||||
req_mod = this.__modules[req_path];
|
||||
|
||||
if ( ! req_mod )
|
||||
this.__modules[req_path] = [[path],[],[]];
|
||||
else if ( Array.isArray(req_mod) )
|
||||
req_mod[0].push(path);
|
||||
else
|
||||
req_mod.dependents.push(path);
|
||||
}
|
||||
|
||||
for(const req_name of load_requires) {
|
||||
const req_path = inst.abs_path(req_name),
|
||||
req_mod = this.__modules[req_path];
|
||||
|
||||
if ( ! req_mod )
|
||||
this.__modules[req_path] = [[], [path], []];
|
||||
else if ( Array.isArray(req_mod) )
|
||||
req_mod[1].push(path);
|
||||
else
|
||||
req_mod.load_dependents.push(path);
|
||||
}
|
||||
|
||||
for(const [in_path, in_name] of dependents[2]) {
|
||||
const in_mod = this.resolve(in_path);
|
||||
if ( in_mod )
|
||||
in_mod[in_name] = inst;
|
||||
else
|
||||
this.log.warn(`Unable to find module "${in_path}" that wanted "${in_name}".`);
|
||||
}
|
||||
|
||||
if ( inject_reference )
|
||||
this[name] = inst;
|
||||
|
||||
return inst;
|
||||
}
|
||||
|
||||
|
||||
populate(ctx, log) {
|
||||
log = log || this.log;
|
||||
const added = {};
|
||||
for(const raw_path of ctx.keys()) {
|
||||
const raw_module = ctx(raw_path),
|
||||
module = raw_module.module || raw_module.default,
|
||||
name = raw_path.slice(2, raw_path.length - (raw_path.endsWith('/index.js') ? 9 : 3));
|
||||
|
||||
try {
|
||||
added[name] = this.register(name, module);
|
||||
} catch(err) {
|
||||
log && log.warn(err, `Skipping ${raw_path}`);
|
||||
}
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Module.State = State;
|
||||
Module.prototype.State = State;
|
||||
|
||||
|
||||
export class SiteModule extends Module {
|
||||
constructor(name, parent) {
|
||||
super(name, parent);
|
||||
this.site = this.resolve('site');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Errors
|
||||
// ============================================================================
|
||||
|
||||
export class ModuleError extends Error { }
|
||||
|
||||
export class CyclicDependencyError extends ModuleError {
|
||||
constructor(message, modules) {
|
||||
super(message);
|
||||
this.modules = modules;
|
||||
}
|
||||
}
|
167
src/utilities/object.js
Normal file
167
src/utilities/object.js
Normal file
|
@ -0,0 +1,167 @@
|
|||
'use strict';
|
||||
|
||||
const HOP = Object.prototype.hasOwnProperty;
|
||||
|
||||
export function has(object, key) {
|
||||
return HOP.call(object, key);
|
||||
}
|
||||
|
||||
|
||||
export function timeout(promise, delay) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let resolved = false;
|
||||
const timer = setTimeout(() => {
|
||||
if ( ! resolved ) {
|
||||
resolved = true;
|
||||
reject(new Error('timeout'));
|
||||
}
|
||||
}, delay);
|
||||
|
||||
promise.then(result => {
|
||||
if ( ! resolved ) {
|
||||
resolved = true;
|
||||
clearTimeout(timer);
|
||||
resolve(result);
|
||||
}
|
||||
}).catch(err => {
|
||||
if ( ! resolved ) {
|
||||
resolved = true;
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check that two arrays are the same length and that each array has the same
|
||||
* items in the same indices.
|
||||
* @param {Array} a The first array
|
||||
* @param {Array} b The second array
|
||||
* @returns {boolean} Whether or not they match
|
||||
*/
|
||||
export function array_equals(a, b) {
|
||||
if ( ! Array.isArray(a) || ! Array.isArray(b) || a.length !== b.length )
|
||||
return false;
|
||||
|
||||
let i = a.length;
|
||||
while(i--)
|
||||
if ( a[i] !== b[i] )
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Special logic to ensure that a target object is matched by a filter.
|
||||
* @param {object} filter The filter object
|
||||
* @param {object} target The object to check it against
|
||||
* @returns {boolean} Whether or not it matches
|
||||
*/
|
||||
export function filter_match(filter, target) {
|
||||
for(const key in filter) {
|
||||
if ( HOP.call(filter, key) ) {
|
||||
const filter_value = filter[key],
|
||||
target_value = target[key],
|
||||
type = typeof filter_value;
|
||||
|
||||
if ( type === 'function' ) {
|
||||
if ( ! filter_value(target_value) )
|
||||
return false;
|
||||
|
||||
} else if ( Array.isArray(filter_value) ) {
|
||||
if ( Array.isArray(target_value) ) {
|
||||
for(const val of filter_value)
|
||||
if ( ! target_value.includes(val) )
|
||||
return false;
|
||||
|
||||
} else if ( ! filter_value.include(target_value) )
|
||||
return false;
|
||||
|
||||
} else if ( typeof target_value !== type )
|
||||
return false;
|
||||
|
||||
else if ( type === 'object' ) {
|
||||
if ( ! filter_match(filter_value, target_value) )
|
||||
return false;
|
||||
|
||||
} else if ( filter_value !== target_value )
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a value from an object at a path.
|
||||
* @param {string|Array} path The path to follow, using periods to go down a level.
|
||||
* @param {object|Array} object The starting object.
|
||||
* @returns {*} The value at that point in the path, or undefined if part of the path doesn't exist.
|
||||
*/
|
||||
export function get(path, object) {
|
||||
if ( typeof path === 'string' )
|
||||
path = path.split('.');
|
||||
|
||||
for(let i=0, l = path.length; i < l; i++) {
|
||||
const part = path[i];
|
||||
if ( part === '@each' ) {
|
||||
const p = path.slice(i + 1);
|
||||
if ( p.length ) {
|
||||
if ( Array.isArray )
|
||||
object = object.map(x => get(p, x));
|
||||
else {
|
||||
const new_object = {};
|
||||
for(const key in object)
|
||||
if ( HOP.call(object, key) )
|
||||
new_object[key] = get(p, object[key]);
|
||||
object = new_object;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
} else
|
||||
object = object[path[i]];
|
||||
|
||||
if ( ! object )
|
||||
break;
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
|
||||
export function deep_copy(object) {
|
||||
if ( typeof object !== 'object' )
|
||||
return object;
|
||||
|
||||
if ( Array.isArray(object) )
|
||||
return object.map(deep_copy);
|
||||
|
||||
const out = {};
|
||||
for(const key in object)
|
||||
if ( HOP.call(object, key) ) {
|
||||
const val = object[key];
|
||||
if ( typeof val === 'object' )
|
||||
out[key] = deep_copy(val);
|
||||
else
|
||||
out[key] = val;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
export function maybe_call(fn, ctx, ...args) {
|
||||
if ( typeof fn === 'function' ) {
|
||||
if ( ctx )
|
||||
return fn.call(ctx, ...args);
|
||||
return fn(...args);
|
||||
}
|
||||
|
||||
return fn;
|
||||
}
|
24
src/utilities/time.js
Normal file
24
src/utilities/time.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
'use strict';
|
||||
|
||||
export function duration_to_string(elapsed, separate_days, days_only, no_hours, no_seconds) {
|
||||
const seconds = elapsed % 60;
|
||||
let minutes = Math.floor(elapsed / 60),
|
||||
hours = Math.floor(minutes / 60),
|
||||
days = '';
|
||||
|
||||
minutes = minutes % 60;
|
||||
|
||||
if ( separate_days ) {
|
||||
days = Math.floor(hours / 24);
|
||||
hours = hours % 24;
|
||||
if ( days_only && days > 0 )
|
||||
return `${days} days`;
|
||||
|
||||
days = days > 0 ? `${days} days, ` : '';
|
||||
}
|
||||
|
||||
return `${days}${
|
||||
(!no_hours || days || hours) ? `${days && hours < 10 ? '0' : ''}${hours}:` : ''
|
||||
}${minutes < 10 ? '0' : ''}${minutes}${
|
||||
no_seconds ? '' : `:${seconds < 10 ? '0' : ''}${seconds}`}`;
|
||||
}
|
296
src/utilities/tooltip.js
Normal file
296
src/utilities/tooltip.js
Normal file
|
@ -0,0 +1,296 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Dynamic Tooltip Handling
|
||||
//
|
||||
// Better because you can assign arbitrary content.
|
||||
// Better because they are asynchronous with loading indication.
|
||||
// Better because they aren't hidden by parents with overflow: hidden;
|
||||
// ============================================================================
|
||||
|
||||
import {createElement as e, setChildren} from 'utilities/dom';
|
||||
import {maybe_call} from 'utilities/object';
|
||||
|
||||
import Popper from 'popper.js';
|
||||
|
||||
let last_id = 0;
|
||||
|
||||
export const DefaultOptions = {
|
||||
html: false,
|
||||
delayShow: 0,
|
||||
delayHide: 0,
|
||||
|
||||
live: true,
|
||||
|
||||
tooltipClass: 'ffz__tooltip',
|
||||
innerClass: 'ffz__tooltip--inner',
|
||||
arrowClass: 'ffz__tooltip--arrow'
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Tooltip Class
|
||||
// ============================================================================
|
||||
|
||||
export default class Tooltip {
|
||||
constructor(parent, cls, options) {
|
||||
if ( typeof parent === 'string' )
|
||||
parent = document.querySelector(parent);
|
||||
|
||||
if (!( parent instanceof Node ))
|
||||
throw new TypeError('invalid parent');
|
||||
|
||||
this.options = Object.assign({}, DefaultOptions, options);
|
||||
this.live = this.options.live;
|
||||
|
||||
this.parent = parent;
|
||||
this.cls = cls;
|
||||
|
||||
if ( ! this.live ) {
|
||||
if ( typeof cls === 'string' )
|
||||
this.elements = parent.querySelectorAll(cls);
|
||||
else if ( Array.isArray(cls) )
|
||||
this.elements = cls;
|
||||
else if ( cls instanceof Node )
|
||||
this.elements = [cls];
|
||||
else
|
||||
throw new TypeError('invalid elements');
|
||||
|
||||
this.elements = new Set(this.elements);
|
||||
|
||||
} else {
|
||||
this.cls = cls;
|
||||
this.elements = new Set;
|
||||
}
|
||||
|
||||
this._accessor = `_ffz_tooltip$${last_id++}`;
|
||||
|
||||
this._onMouseOut = e => this._exit(e.target);
|
||||
|
||||
if ( this.live ) {
|
||||
this._onMouseOver = e => {
|
||||
const target = e.target;
|
||||
if ( target.classList.contains(this.cls) )
|
||||
this._enter(target);
|
||||
};
|
||||
|
||||
parent.addEventListener('mouseover', this._onMouseOver);
|
||||
parent.addEventListener('mouseout', this._onMouseOut);
|
||||
|
||||
} else {
|
||||
this._onMouseOver = e => {
|
||||
const target = e.target;
|
||||
if ( this.elements.has(target) )
|
||||
this._enter(e.target);
|
||||
}
|
||||
|
||||
if ( this.elements.size <= 5 )
|
||||
for(const el of this.elements) {
|
||||
el.addEventListener('mouseenter', this._onMouseOver);
|
||||
el.addEventListener('mouseleave', this._onMouseOut);
|
||||
}
|
||||
|
||||
else {
|
||||
parent.addEventListener('mouseover', this._onMouseOver);
|
||||
parent.addEventListener('mouseout', this._onMouseOut);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if ( this.live || this.elements.size > 5 ) {
|
||||
parent.removeEventListener('mouseover', this._onMouseOver);
|
||||
parent.removeEventListener('mouseout', this._onMouseOut);
|
||||
} else
|
||||
for(const el of this.elements) {
|
||||
el.removeEventListener('mouseenter', this._onMouseOver);
|
||||
el.removeEventListener('mouseleave', this._onMouseOut);
|
||||
}
|
||||
|
||||
for(const el of this.elements) {
|
||||
const tip = el[this._accessor];
|
||||
if ( tip && tip.visible )
|
||||
this.hide(tip);
|
||||
|
||||
el[this._accessor] = null;
|
||||
}
|
||||
|
||||
this.elements = null;
|
||||
this._onMouseOut = this._onMouseOver = null;
|
||||
this.parent = null;
|
||||
}
|
||||
|
||||
|
||||
_enter(target) {
|
||||
let tip = target[this._accessor],
|
||||
delay = this.options.delayShow;
|
||||
|
||||
if ( ! tip )
|
||||
tip = target[this._accessor] = {target};
|
||||
|
||||
tip.state = true;
|
||||
|
||||
if ( tip.visible )
|
||||
return;
|
||||
|
||||
if ( delay === 0 )
|
||||
this.show(tip);
|
||||
|
||||
else {
|
||||
if ( tip._show_timer )
|
||||
clearTimeout(tip._show_timer);
|
||||
|
||||
tip._show_timer = setTimeout(() => {
|
||||
tip._show_timer = null;
|
||||
if ( tip.state )
|
||||
this.show(tip);
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
_exit(target) {
|
||||
const tip = target[this._accessor];
|
||||
if ( ! tip || ! tip.visible )
|
||||
return;
|
||||
|
||||
const delay = this.options.delayHide;
|
||||
|
||||
tip.state = false;
|
||||
|
||||
if ( delay === 0 )
|
||||
this.hide(tip);
|
||||
|
||||
else {
|
||||
if ( tip._show_timer )
|
||||
clearTimeout(tip._show_timer);
|
||||
|
||||
tip._show_timer = setTimeout(() => {
|
||||
tip._show_timer = null;
|
||||
if ( ! tip.state )
|
||||
this.hide(tip);
|
||||
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
show(tip) {
|
||||
const opts = this.options,
|
||||
target = tip.target;
|
||||
|
||||
this.elements.add(target);
|
||||
|
||||
// Set this early in case content uses it early.
|
||||
tip.update = () => tip._update(); // tip.popper && tip.popper.scheduleUpdate();
|
||||
tip.rerender = () => {
|
||||
if ( tip.visible ) {
|
||||
this.hide(tip);
|
||||
this.show(tip);
|
||||
}
|
||||
}
|
||||
|
||||
let content = maybe_call(opts.content, null, target, tip);
|
||||
if ( content === undefined )
|
||||
content = tip.target.title;
|
||||
|
||||
if ( tip.visible || (! content && ! opts.onShow) )
|
||||
return;
|
||||
|
||||
// Build the DOM.
|
||||
const arrow = e('div', opts.arrowClass),
|
||||
inner = tip.element = e('div', opts.innerClass),
|
||||
|
||||
el = tip.outer = e('div', {
|
||||
className: opts.tooltipClass
|
||||
}, [inner, arrow]);
|
||||
|
||||
arrow.setAttribute('x-arrow', true);
|
||||
|
||||
if ( maybe_call(opts.interactive, null, target, tip) ) {
|
||||
el.classList.add('interactive');
|
||||
el.addEventListener('mouseover', () => this._enter(target));
|
||||
el.addEventListener('mouseout', () => this._exit(target));
|
||||
}
|
||||
|
||||
// Assign our content. If there's a Promise, we'll need
|
||||
// to do this weirdly.
|
||||
const use_html = maybe_call(opts.html, null, target, tip),
|
||||
setter = use_html ? 'innerHTML' : 'textContent';
|
||||
|
||||
const pop_opts = Object.assign({
|
||||
arrowElement: arrow,
|
||||
}, opts.popper);
|
||||
|
||||
tip._update = () => {
|
||||
if ( tip.popper ) {
|
||||
tip.popper.destroy();
|
||||
tip.popper = new Popper(target, el, pop_opts);
|
||||
}
|
||||
}
|
||||
|
||||
if ( content instanceof Promise ) {
|
||||
inner.innerHTML = '<div class="ffz-i-zreknarf loader"></div>';
|
||||
content.then(content => {
|
||||
if ( ! content )
|
||||
return this.hide(tip);
|
||||
|
||||
if ( use_html && (content instanceof Node || Array.isArray(content)) ) {
|
||||
inner.innerHTML = '';
|
||||
setChildren(inner, content, opts.sanitizeChildren);
|
||||
} else
|
||||
inner[setter] = content;
|
||||
|
||||
tip._update();
|
||||
|
||||
}).catch(err => {
|
||||
inner.textContent = `There was an error showing this tooltip.\n${err}`;
|
||||
tip._update();
|
||||
});
|
||||
|
||||
} else if ( content ) {
|
||||
if ( use_html && (content instanceof Node || Array.isArray(content)) )
|
||||
setChildren(inner, content, opts.sanitizeChildren);
|
||||
else
|
||||
inner[setter] = content;
|
||||
}
|
||||
|
||||
|
||||
// Add everything to the DOM and create the Popper instance.
|
||||
this.parent.appendChild(el);
|
||||
tip.popper = new Popper(target, el, pop_opts);
|
||||
tip.visible = true;
|
||||
|
||||
if ( opts.onShow )
|
||||
opts.onShow(target, tip);
|
||||
}
|
||||
|
||||
|
||||
hide(tip) { // eslint-disable-line class-methods-use-this
|
||||
const opts = this.options;
|
||||
if ( opts.onHide )
|
||||
opts.onHide(tip.target, tip);
|
||||
|
||||
if ( tip.popper ) {
|
||||
tip.popper.destroy();
|
||||
tip.popper = null;
|
||||
}
|
||||
|
||||
if ( tip.outer ) {
|
||||
const o = tip.outer;
|
||||
if ( o.parentElement )
|
||||
o.parentElement.removeChild(o);
|
||||
|
||||
tip.outer = null;
|
||||
}
|
||||
|
||||
tip.update = null;
|
||||
tip._update = noop;
|
||||
tip.element = null;
|
||||
tip.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Function Intentionally Left Blank
|
||||
function noop() { }
|
99
src/utilities/vue.js
Normal file
99
src/utilities/vue.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Vue Library
|
||||
// Loads Vue + Translation Shim
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
|
||||
export default class Vue extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this._components = {};
|
||||
this.inject('i18n');
|
||||
}
|
||||
|
||||
async onLoad() {
|
||||
const Vue = this.Vue = (await import(/* webpackChunkName: "vue" */ 'vue')).default,
|
||||
components = this._components;
|
||||
|
||||
this.component((await import(/* webpackChunkName: "vue" */ 'src/std-components/index.js')).default);
|
||||
|
||||
for(const key in components)
|
||||
if ( has(components, key) )
|
||||
Vue.component(key, components[key]);
|
||||
|
||||
this._components = null;
|
||||
Vue.use(this);
|
||||
}
|
||||
|
||||
component(name, component) {
|
||||
if ( typeof name === 'function' ) {
|
||||
for(const key of name.keys())
|
||||
this.component(key.slice(2, key.length - 4), name(key).default);
|
||||
|
||||
} else if ( typeof name === 'object' ) {
|
||||
for(const key in name)
|
||||
if ( has(name, key) )
|
||||
this.component(key, name[key]);
|
||||
|
||||
} else if ( this.Vue )
|
||||
this.Vue.component(name, component);
|
||||
|
||||
else
|
||||
this._components[name] = component;
|
||||
}
|
||||
|
||||
install(vue) {
|
||||
// This is a mess. I'm sure there's an easier way to tie the systems
|
||||
// together. However, for now, this works.
|
||||
|
||||
const t = this;
|
||||
if ( ! this._vue_i18n ) {
|
||||
this._vue_i18n = new this.Vue({
|
||||
data() {
|
||||
return {
|
||||
locale: t.i18n.locale,
|
||||
phrases: {}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
t_(key, phrase, options) {
|
||||
this.locale && this.phrases[key];
|
||||
return t.i18n.t(key, phrase, options);
|
||||
},
|
||||
|
||||
setLocale(locale) {
|
||||
t.i18n.locale = locale;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.on('i18n:changed', () => {
|
||||
this._vue_i18n.locale = this.i18n.locale;
|
||||
this._vue_i18n.phrases = {};
|
||||
});
|
||||
|
||||
this.on('i18n:loaded', keys => {
|
||||
const i = this._vue_i18n,
|
||||
p = i.phrases;
|
||||
for(const key of keys)
|
||||
i.$set(p, key, (p[key]||0) + 1);
|
||||
});
|
||||
|
||||
vue.prototype.$i18n = this._vue_i18n;
|
||||
}
|
||||
|
||||
vue.mixin({
|
||||
methods: {
|
||||
t(key, phrase, options) {
|
||||
return this.$i18n.t_(key, phrase, options);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue