1
0
Fork 0
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:
SirStendec 2017-11-13 01:23:39 -05:00
parent c2688646af
commit 262757a20d
187 changed files with 22878 additions and 38882 deletions

635
src/utilities/color.js Normal file
View 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;
}
}

View 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;
}

View 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);
}
}

View 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);
}
}

View 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().`);
}
}

View 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
View 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
View 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 === ':');
});
}
}

View file

@ -0,0 +1,6 @@
'use strict';
// ============================================================================
// Advanced Filter System
// ============================================================================

73
src/utilities/logging.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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);
}
}
});
}
}