679 lines
18 KiB
JavaScript
679 lines
18 KiB
JavaScript
if (!String.prototype.startsWith) {
|
|
String.prototype.startsWith = function(search, pos) {
|
|
pos = !pos || pos < 0 ? 0 : +pos;
|
|
return this.substring(pos, pos + search.length) === search;
|
|
};
|
|
}
|
|
|
|
if (!String.prototype.endsWith) {
|
|
String.prototype.endsWith = function(search, this_len) {
|
|
if (this_len === undefined || this_len > this.length) {
|
|
this_len = this.length;
|
|
}
|
|
return this.substring(this_len - search.length, this_len) === search;
|
|
};
|
|
}
|
|
|
|
if (!Object.assign) {
|
|
Object.defineProperty(Object, 'assign', {
|
|
enumerable: false,
|
|
configurable: true,
|
|
writable: true,
|
|
value: function(target, firstSource) {
|
|
'use strict';
|
|
if (target === undefined || target === null) {
|
|
throw new TypeError('Cannot convert first argument to object');
|
|
}
|
|
|
|
var to = Object(target);
|
|
for (var i = 1; i < arguments.length; i++) {
|
|
var nextSource = arguments[i];
|
|
if (nextSource === undefined || nextSource === null) {
|
|
continue;
|
|
}
|
|
|
|
var keysArray = Object.keys(Object(nextSource));
|
|
for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
|
|
var nextKey = keysArray[nextIndex];
|
|
var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
|
|
if (desc !== undefined && desc.enumerable) {
|
|
to[nextKey] = nextSource[nextKey];
|
|
}
|
|
}
|
|
}
|
|
return to;
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
//
|
|
// AJAX
|
|
//
|
|
(function() {
|
|
|
|
var defaultOpts = {
|
|
json: true
|
|
};
|
|
|
|
function createXMLHttpRequest() {
|
|
if (window.XMLHttpRequest) {
|
|
return new XMLHttpRequest();
|
|
}
|
|
|
|
var xhr;
|
|
try {
|
|
xhr = new ActiveXObject('Msxml2.XMLHTTP');
|
|
} catch (e) {
|
|
try {
|
|
xhr = new ActiveXObject('Microsoft.XMLHTTP');
|
|
} catch (e) {}
|
|
}
|
|
if (!xhr) {
|
|
console.error('Your browser doesn\'t support XMLHttpRequest.');
|
|
}
|
|
return xhr;
|
|
}
|
|
|
|
function request(method, url, data, optarg1, optarg2) {
|
|
data = data || null;
|
|
|
|
var opts, callback;
|
|
if (optarg2 !== undefined) {
|
|
opts = optarg1;
|
|
callback = optarg2;
|
|
} else {
|
|
callback = optarg1;
|
|
}
|
|
|
|
opts = opts || {};
|
|
|
|
if (typeof callback != 'function') {
|
|
throw new Error('callback must be a function');
|
|
}
|
|
|
|
if (!url) {
|
|
throw new Error('no url specified');
|
|
}
|
|
|
|
switch (method) {
|
|
case 'GET':
|
|
if (isObject(data)) {
|
|
for (var k in data) {
|
|
if (data.hasOwnProperty(k)) {
|
|
url += (url.indexOf('?') == -1 ? '?' : '&')+encodeURIComponent(k)+'='+encodeURIComponent(data[k])
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'POST':
|
|
if (isObject(data)) {
|
|
var sdata = [];
|
|
for (var k in data) {
|
|
if (data.hasOwnProperty(k)) {
|
|
sdata.push(encodeURIComponent(k)+'='+encodeURIComponent(data[k]));
|
|
}
|
|
}
|
|
data = sdata.join('&');
|
|
}
|
|
break;
|
|
}
|
|
|
|
opts = Object.assign({}, defaultOpts, opts);
|
|
|
|
var xhr = createXMLHttpRequest();
|
|
xhr.open(method, url);
|
|
|
|
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
|
if (method == 'POST') {
|
|
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
|
}
|
|
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.readyState == 4) {
|
|
if ('status' in xhr && !/^2|1223/.test(xhr.status)) {
|
|
throw new Error('http code '+xhr.status)
|
|
}
|
|
if (opts.json) {
|
|
var resp = JSON.parse(xhr.responseText)
|
|
if (!isObject(resp)) {
|
|
throw new Error('ajax: object expected')
|
|
}
|
|
if (resp.error) {
|
|
throw new Error(resp.error)
|
|
}
|
|
callback(null, resp.response);
|
|
} else {
|
|
callback(null, xhr.responseText);
|
|
}
|
|
}
|
|
};
|
|
|
|
xhr.onerror = function(e) {
|
|
callback(e);
|
|
};
|
|
|
|
xhr.send(method == 'GET' ? null : data);
|
|
|
|
return xhr;
|
|
}
|
|
|
|
window.ajax = {
|
|
get: request.bind(request, 'GET'),
|
|
post: request.bind(request, 'POST')
|
|
}
|
|
|
|
})();
|
|
|
|
function bindEventHandlers(obj) {
|
|
for (var k in obj) {
|
|
if (obj.hasOwnProperty(k)
|
|
&& typeof obj[k] == 'function'
|
|
&& k.length > 2
|
|
&& k.startsWith('on')
|
|
&& k[2].charCodeAt(0) >= 65
|
|
&& k[2].charCodeAt(0) <= 90) {
|
|
obj[k] = obj[k].bind(obj)
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// DOM helpers
|
|
//
|
|
function ge(id) {
|
|
return document.getElementById(id)
|
|
}
|
|
|
|
function hasClass(el, name) {
|
|
return el && el.nodeType === 1 && (" " + el.className + " ").replace(/[\t\r\n\f]/g, " ").indexOf(" " + name + " ") >= 0
|
|
}
|
|
|
|
function addClass(el, name) {
|
|
if (!el) {
|
|
return console.warn('addClass: el is', el)
|
|
}
|
|
if (!hasClass(el, name)) {
|
|
el.className = (el.className ? el.className + ' ' : '') + name
|
|
}
|
|
}
|
|
|
|
function removeClass(el, name) {
|
|
if (!el) {
|
|
return console.warn('removeClass: el is', el)
|
|
}
|
|
if (isArray(name)) {
|
|
for (var i = 0; i < name.length; i++) {
|
|
removeClass(el, name[i]);
|
|
}
|
|
return;
|
|
}
|
|
el.className = ((el.className || '').replace((new RegExp('(\\s|^)' + name + '(\\s|$)')), ' ')).trim()
|
|
}
|
|
|
|
function addEvent(el, type, f, useCapture) {
|
|
if (!el) {
|
|
return console.warn('addEvent: el is', el, stackTrace())
|
|
}
|
|
|
|
if (isArray(type)) {
|
|
for (var i = 0; i < type.length; i++) {
|
|
addEvent(el, type[i], f, useCapture);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (el.addEventListener) {
|
|
el.addEventListener(type, f, useCapture || false);
|
|
return true;
|
|
} else if (el.attachEvent) {
|
|
return el.attachEvent('on' + type, f);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function removeEvent(el, type, f, useCapture) {
|
|
if (isArray(type)) {
|
|
for (var i = 0; i < type.length; i++) {
|
|
var t = type[i];
|
|
removeEvent(el, type[i], f, useCapture);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (el.removeEventListener) {
|
|
el.removeEventListener(type, f, useCapture || false);
|
|
} else if (el.detachEvent) {
|
|
return el.detachEvent('on' + type, f);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function cancelEvent(evt) {
|
|
if (!evt) {
|
|
return console.warn('cancelEvent: event is', evt)
|
|
}
|
|
|
|
if (evt.preventDefault) evt.preventDefault();
|
|
if (evt.stopPropagation) evt.stopPropagation();
|
|
|
|
evt.cancelBubble = true;
|
|
evt.returnValue = false;
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
//
|
|
// Cookies
|
|
//
|
|
function setCookie(name, value, days) {
|
|
var expires = "";
|
|
if (days) {
|
|
var date = new Date();
|
|
date.setTime(date.getTime() + (days*24*60*60*1000));
|
|
expires = "; expires=" + date.toUTCString();
|
|
}
|
|
document.cookie = name + "=" + (value || "") + expires + "; domain=" + window.appConfig.cookieHost + "; path=/";
|
|
}
|
|
|
|
function unsetCookie(name) {
|
|
document.cookie = name + '=; Max-Age=-99999999; domain=' + window.appConfig.cookieHost + "; path=/";
|
|
}
|
|
|
|
function getCookie(name) {
|
|
var nameEQ = name + "=";
|
|
var ca = document.cookie.split(';');
|
|
for (var i = 0; i < ca.length; i++) {
|
|
var c = ca[i];
|
|
while (c.charAt(0) === ' ')
|
|
c = c.substring(1, c.length);
|
|
if (c.indexOf(nameEQ) === 0)
|
|
return c.substring(nameEQ.length, c.length);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
//
|
|
// Misc
|
|
//
|
|
function isObject(o) {
|
|
return Object.prototype.toString.call(o) === '[object Object]';
|
|
}
|
|
|
|
function isArray(a) {
|
|
return Object.prototype.toString.call(a) === '[object Array]';
|
|
}
|
|
|
|
function extend(dst, src) {
|
|
if (!isObject(dst)) {
|
|
return console.error('extend: dst is not an object');
|
|
}
|
|
if (!isObject(src)) {
|
|
return console.error('extend: src is not an object');
|
|
}
|
|
for (var key in src) {
|
|
dst[key] = src[key];
|
|
}
|
|
return dst;
|
|
}
|
|
|
|
function stackTrace(split) {
|
|
if (split === undefined) {
|
|
split = true;
|
|
}
|
|
try {
|
|
o.lo.lo += 0;
|
|
} catch(e) {
|
|
if (e.stack) {
|
|
var stack = split ? e.stack.split('\n') : e.stack;
|
|
stack.shift();
|
|
stack.shift();
|
|
return stack.join('\n');
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function escape(str) {
|
|
var pre = document.createElement('pre');
|
|
var text = document.createTextNode(str);
|
|
pre.appendChild(text);
|
|
return pre.innerHTML;
|
|
}
|
|
|
|
function parseUrl(uri) {
|
|
var parser = document.createElement('a');
|
|
parser.href = uri;
|
|
|
|
return {
|
|
protocol: parser.protocol, // => "http:"
|
|
host: parser.host, // => "example.com:3000"
|
|
hostname: parser.hostname, // => "example.com"
|
|
port: parser.port, // => "3000"
|
|
pathname: parser.pathname, // => "/pathname/"
|
|
hash: parser.hash, // => "#hash"
|
|
search: parser.search, // => "?search=test"
|
|
origin: parser.origin, // => "http://example.com:3000"
|
|
path: (parser.pathname || '') + (parser.search || '')
|
|
}
|
|
}
|
|
|
|
function once(fn, context) {
|
|
var result;
|
|
return function() {
|
|
if (fn) {
|
|
result = fn.apply(context || this, arguments);
|
|
fn = null;
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
|
|
//
|
|
//
|
|
|
|
function lang(key) {
|
|
return __lang[key] !== undefined ? __lang[key] : '{'+key+'}';
|
|
}
|
|
|
|
var DynamicLogo = {
|
|
dynLink: null,
|
|
afr: null,
|
|
afrUrl: null,
|
|
|
|
init: function() {
|
|
this.dynLink = ge('head_dyn_link');
|
|
this.cdText = ge('head_cd_text');
|
|
|
|
if (!this.dynLink) {
|
|
return console.warn('DynamicLogo.init: !this.dynLink');
|
|
}
|
|
|
|
var spans = this.dynLink.querySelectorAll('span.head-logo-path');
|
|
for (var i = 0; i < spans.length; i++) {
|
|
var span = spans[i];
|
|
addEvent(span, 'mouseover', this.onSpanOver);
|
|
addEvent(span, 'mouseout', this.onSpanOut);
|
|
}
|
|
},
|
|
|
|
setUrl: function(url) {
|
|
if (this.afr !== null) {
|
|
cancelAnimationFrame(this.afr);
|
|
}
|
|
this.afrUrl = url;
|
|
this.afr = requestAnimationFrame(this.onAnimationFrame);
|
|
},
|
|
|
|
onAnimationFrame: function() {
|
|
var url = this.afrUrl;
|
|
|
|
// update link
|
|
this.dynLink.setAttribute('href', url);
|
|
|
|
// update console text
|
|
if (this.afrUrl === '/') {
|
|
url = '~';
|
|
} else {
|
|
url = '~'+url.replace(/\/$/, '');
|
|
}
|
|
this.cdText.innerHTML = escape(url);
|
|
|
|
this.afr = null;
|
|
},
|
|
|
|
onSpanOver: function() {
|
|
var span = event.target;
|
|
this.setUrl(span.getAttribute('data-url'));
|
|
cancelEvent(event);
|
|
},
|
|
|
|
onSpanOut: function() {
|
|
var span = event.target;
|
|
this.setUrl('/');
|
|
cancelEvent(event);
|
|
}
|
|
};
|
|
bindEventHandlers(DynamicLogo);
|
|
|
|
window.__lang = {};
|
|
|
|
// set/remove retina cookie
|
|
(function() {
|
|
var isRetina = window.devicePixelRatio >= 1.5;
|
|
if (isRetina) {
|
|
setCookie('is_retina', 1, 365);
|
|
} else {
|
|
unsetCookie('is_retina');
|
|
}
|
|
})();
|
|
|
|
var StaticManager = {
|
|
loadedStyles: [],
|
|
versions: {},
|
|
|
|
setStyles: function(list, versions) {
|
|
this.loadedStyles = list;
|
|
this.versions = versions;
|
|
},
|
|
|
|
loadStyle: function(name, theme, callback) {
|
|
var url;
|
|
if (!window.appConfig.devMode) {
|
|
var filename = name + (theme === 'dark' ? '_dark' : '') + '.css';
|
|
url = '/css/'+filename+'?'+this.versions[filename];
|
|
} else {
|
|
url = '/sass.php?name='+name+'&theme='+theme;
|
|
}
|
|
|
|
var el = document.createElement('link');
|
|
el.onerror = callback
|
|
el.onload = callback
|
|
el.setAttribute('rel', 'stylesheet');
|
|
el.setAttribute('type', 'text/css');
|
|
el.setAttribute('id', 'style_'+name+'_dark');
|
|
el.setAttribute('href', url);
|
|
|
|
document.getElementsByTagName('head')[0].appendChild(el);
|
|
}
|
|
};
|
|
|
|
var ThemeSwitcher = (function() {
|
|
/**
|
|
* @type {string[]}
|
|
*/
|
|
var modes = ['auto', 'dark', 'light'];
|
|
|
|
/**
|
|
* @type {number}
|
|
*/
|
|
var currentModeIndex = -1;
|
|
|
|
/**
|
|
* @type {boolean|null}
|
|
*/
|
|
var systemState = null;
|
|
|
|
/**
|
|
* @returns {boolean}
|
|
*/
|
|
function isSystemModeSupported() {
|
|
try {
|
|
// crashes on:
|
|
// Mozilla/5.0 (Windows NT 6.2; ARM; Trident/7.0; Touch; rv:11.0; WPDesktop; Lumia 630 Dual SIM) like Gecko
|
|
// Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1
|
|
// Mozilla/5.0 (iPad; CPU OS 12_5_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Mobile/15E148 Safari/604.1
|
|
//
|
|
// error examples:
|
|
// - window.matchMedia("(prefers-color-scheme: dark)").addEventListener is not a function. (In 'window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",this.onSystemSettingChange.bind(this))', 'window.matchMedia("(prefers-color-scheme: dark)").addEventListener' is undefined)
|
|
// - Object [object MediaQueryList] has no method 'addEventListener'
|
|
return !!window['matchMedia']
|
|
&& typeof window.matchMedia("(prefers-color-scheme: dark)").addEventListener === 'function';
|
|
} catch (e) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean}
|
|
*/
|
|
function isDarkModeApplied() {
|
|
var st = StaticManager.loadedStyles;
|
|
for (var i = 0; i < st.length; i++) {
|
|
var name = st[i];
|
|
if (ge('style_'+name+'_dark'))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @returns {string}
|
|
*/
|
|
function getSavedMode() {
|
|
var val = getCookie('theme');
|
|
if (!val)
|
|
return modes[0];
|
|
if (modes.indexOf(val) === -1) {
|
|
console.error('[ThemeSwitcher getSavedMode] invalid cookie value')
|
|
unsetCookie('theme')
|
|
return modes[0]
|
|
}
|
|
return val
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} dark
|
|
*/
|
|
function changeTheme(dark) {
|
|
addClass(document.body, 'theme-changing');
|
|
|
|
var onDone = function() {
|
|
window.requestAnimationFrame(function() {
|
|
removeClass(document.body, 'theme-changing');
|
|
})
|
|
};
|
|
|
|
window.requestAnimationFrame(function() {
|
|
if (dark)
|
|
enableDark(onDone);
|
|
else
|
|
disableDark(onDone);
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @param {function} callback
|
|
*/
|
|
function enableDark(callback) {
|
|
var names = [];
|
|
StaticManager.loadedStyles.forEach(function(name) {
|
|
var el = ge('style_'+name+'_dark');
|
|
if (el)
|
|
return;
|
|
names.push(name);
|
|
});
|
|
|
|
var left = names.length;
|
|
names.forEach(function(name) {
|
|
StaticManager.loadStyle(name, 'dark', once(function(e) {
|
|
left--;
|
|
if (left === 0)
|
|
callback();
|
|
}));
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @param {function} callback
|
|
*/
|
|
function disableDark(callback) {
|
|
StaticManager.loadedStyles.forEach(function(name) {
|
|
var el = ge('style_'+name+'_dark');
|
|
if (el)
|
|
el.remove();
|
|
})
|
|
callback();
|
|
}
|
|
|
|
/**
|
|
* @param {string} mode
|
|
*/
|
|
function setLabel(mode) {
|
|
var labelEl = ge('theme-switcher-label');
|
|
labelEl.innerHTML = escape(lang('theme_'+mode));
|
|
}
|
|
|
|
return {
|
|
init: function() {
|
|
var cur = getSavedMode();
|
|
currentModeIndex = modes.indexOf(cur);
|
|
|
|
var systemSupported = isSystemModeSupported();
|
|
if (!systemSupported) {
|
|
if (currentModeIndex === 0) {
|
|
modes.shift(); // remove 'auto' from the list
|
|
currentModeIndex = 1; // set to 'light'
|
|
if (isDarkModeApplied())
|
|
disableDark();
|
|
}
|
|
} else {
|
|
/**
|
|
* @param {boolean} dark
|
|
*/
|
|
var onSystemChange = function(dark) {
|
|
var prevSystemState = systemState;
|
|
systemState = dark;
|
|
|
|
if (modes[currentModeIndex] !== 'auto')
|
|
return;
|
|
|
|
if (systemState !== prevSystemState)
|
|
changeTheme(systemState);
|
|
};
|
|
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
|
|
onSystemChange(e.matches === true)
|
|
});
|
|
|
|
onSystemChange(window.matchMedia('(prefers-color-scheme: dark)').matches === true);
|
|
}
|
|
|
|
setLabel(modes[currentModeIndex]);
|
|
},
|
|
|
|
next: function(e) {
|
|
if (hasClass(document.body, 'theme-changing')) {
|
|
console.log('next: theme changing is in progress, ignoring...')
|
|
return;
|
|
}
|
|
|
|
currentModeIndex = (currentModeIndex + 1) % modes.length;
|
|
switch (modes[currentModeIndex]) {
|
|
case 'auto':
|
|
if (systemState !== null)
|
|
changeTheme(systemState);
|
|
break;
|
|
|
|
case 'light':
|
|
if (isDarkModeApplied())
|
|
changeTheme(false);
|
|
break;
|
|
|
|
case 'dark':
|
|
if (!isDarkModeApplied())
|
|
changeTheme(true);
|
|
break;
|
|
}
|
|
|
|
setLabel(modes[currentModeIndex]);
|
|
setCookie('theme', modes[currentModeIndex]);
|
|
|
|
return cancelEvent(e);
|
|
}
|
|
};
|
|
})(); |