mirror of https://github.com/movim/movim
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
482 lines
17 KiB
482 lines
17 KiB
/**
|
|
* Movim Utils
|
|
*
|
|
* This file include some useful functions used quite everywhere in Movim
|
|
*/
|
|
|
|
/**
|
|
* @brief Set object in localStorage
|
|
* @param key string
|
|
* @param value the object
|
|
*/
|
|
Storage.prototype.setObject = function (key, value) {
|
|
this.setItem(key, JSON.stringify(value));
|
|
};
|
|
|
|
/**
|
|
* @brief Get object in localStorage
|
|
* @param key
|
|
*/
|
|
Storage.prototype.getObject = function (key) {
|
|
return JSON.parse(this.getItem(key));
|
|
};
|
|
|
|
var MovimUtils = {
|
|
cleanupId: function (string) {
|
|
return 'id-' + string.replace(/([^a-z0-9]+)/gi, '-').toLowerCase();
|
|
},
|
|
hash(string) {
|
|
var hash = 0;
|
|
|
|
if (string.length == 0) return hash;
|
|
|
|
for (i = 0; i < string.length; i++) {
|
|
char = string.charCodeAt(i);
|
|
hash = ((hash << 5) - hash) + char;
|
|
hash = hash & hash;
|
|
}
|
|
|
|
return hash;
|
|
},
|
|
isMobile: function () {
|
|
return window.matchMedia('(max-width: 1024px)').matches;
|
|
},
|
|
disconnect: function () {
|
|
window.location.replace(ERROR_URI);
|
|
},
|
|
formToJson: function (formname) {
|
|
var form = document.forms[formname];
|
|
|
|
if (!form) {
|
|
return false;
|
|
}
|
|
|
|
var json = {};
|
|
|
|
for (var i = 0; i < form.elements.length; i++) {
|
|
json_att = {};
|
|
|
|
for (var j = 0; j < form.elements[i].attributes.length; j++) {
|
|
if (form.elements[i].attributes[j].name != 'value') {
|
|
json_att[form.elements[i].attributes[j].name] = form.elements[i].attributes[j].value;
|
|
}
|
|
}
|
|
|
|
if (form.elements[i].name.length != 0) {
|
|
if (form.elements[i].type == 'checkbox') {
|
|
json[form.elements[i].name] = {
|
|
'value': form.elements[i].checked,
|
|
'attributes': json_att
|
|
};
|
|
} else if (form.elements[i].type == 'radio' && form.elements[i].checked) {
|
|
json[form.elements[i].name] = {
|
|
'value': form.elements[i].value,
|
|
'attributes': json_att
|
|
};
|
|
} else if (form.elements[i].type != 'radio') {
|
|
json[form.elements[i].name] = {
|
|
'value': form.elements[i].value,
|
|
'attributes': json_att
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return json;
|
|
},
|
|
cleanTime: function (seconds) {
|
|
var currentMinute = parseInt(seconds / 60) % 60,
|
|
currentSecondsLong = seconds % 60,
|
|
currentSeconds = currentSecondsLong.toFixed(),
|
|
currentTime = (currentMinute < 10 ? "0" + currentMinute : currentMinute)
|
|
+ ":" + (currentSeconds < 10 ? "0" + currentSeconds : currentSeconds);
|
|
|
|
return currentTime;
|
|
},
|
|
setTitle: function (title) {
|
|
document.title = title;
|
|
},
|
|
pushState: function (url) {
|
|
window.history.pushState(null, '', url);
|
|
},
|
|
pushSoftState: function (url) {
|
|
if (window.location != url) {
|
|
window.history.pushState({ soft: true }, '', url);
|
|
MovimTpl.currentPage = window.location.pathname;
|
|
}
|
|
},
|
|
redirect: function (url) {
|
|
window.location.href = url;
|
|
},
|
|
openInNew: function (url) {
|
|
window.open(url, '_blank');
|
|
},
|
|
reload: function (uri, noHistory) {
|
|
requestUri = new URL(uri.replace(/^\/\//, 'https://'));
|
|
requestUri.searchParams.append('soft', 'true');
|
|
|
|
MovimTpl.loadingPage();
|
|
|
|
MovimRPC.fetchWithTimeout(requestUri.toString(), {
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
}).then(reponse => {
|
|
onloaders = [];
|
|
onfocused = [];
|
|
|
|
reponse.text().then(value => {
|
|
MovimTpl.finishedPage();
|
|
|
|
let page = JSON.parse(value);
|
|
|
|
if (noHistory != true) {
|
|
MovimUtils.pushSoftState(uri);
|
|
}
|
|
|
|
if (typeof MovimWebsocket != 'undefined') {
|
|
MovimWebsocket.clear();
|
|
}
|
|
|
|
document.head.querySelectorAll('link[rel=stylesheet].widget').forEach(e => e.remove());
|
|
document.head.querySelectorAll('script[type=\'text/javascript\'].widget').forEach(e => e.remove());
|
|
document.head.querySelectorAll('script[type=\'text/javascript\'].inline').forEach(e => e.remove());
|
|
document.querySelectorAll('#endcommon ~ *').forEach(e => e.remove());
|
|
|
|
document.body.insertAdjacentHTML('beforeend', page.content);
|
|
document.title = page.title;
|
|
|
|
// CSS
|
|
|
|
page.widgetsCSS.forEach(url => {
|
|
var css = document.createElement("link");
|
|
css.setAttribute('rel', 'stylesheet');
|
|
css.href = url;
|
|
css.classList.add('widget');
|
|
document.head.appendChild(css);
|
|
});
|
|
|
|
// Javascript
|
|
|
|
const promises = [];
|
|
page.widgetsScripts.forEach(script => {
|
|
promises.push(new Promise(function (resolve, reject) {
|
|
var js = document.createElement("script");
|
|
js.src = script;
|
|
js.setAttribute('type', 'text/javascript');
|
|
js.onload = resolve;
|
|
js.onerror = resolve;
|
|
js.classList.add('widget');
|
|
document.head.appendChild(js);
|
|
}));
|
|
});
|
|
|
|
var js = document.createElement("script");
|
|
js.classList.add('inline');
|
|
js.innerHTML = page.inlineScripts;
|
|
document.head.appendChild(js);
|
|
|
|
// Events
|
|
Promise.all(promises).then(() => {
|
|
MovimEvents.triggerWindow('loaded', null);
|
|
});
|
|
});
|
|
}).catch(error => {
|
|
document.body.classList.add('finished');
|
|
});
|
|
},
|
|
reloadThis: function () {
|
|
window.location.reload();
|
|
},
|
|
addClass: function (element, classname) {
|
|
let el = document.querySelector(element);
|
|
if (el) el.classList.add(classname);
|
|
},
|
|
removeClass: function (element, classname) {
|
|
let el = document.querySelector(element);
|
|
if (el) el.classList.remove(classname);
|
|
},
|
|
setDataItem: function (element, key, value) {
|
|
let el = document.querySelector(element);
|
|
if (el) {
|
|
if (value) {
|
|
el.dataset[key] = value;
|
|
} else {
|
|
delete el.dataset[key];
|
|
}
|
|
}
|
|
},
|
|
decodeHTMLEntities(text) {
|
|
var textarea = document.createElement('textarea');
|
|
textarea.innerHTML = text;
|
|
return textarea.value;
|
|
},
|
|
textareaAutoheight: function (textbox) {
|
|
if (textbox != null) {
|
|
var val = MovimUtils.htmlEscape(textbox.value).replace(/\n/g, '<br>');
|
|
var hidden = document.querySelector('#hiddendiv');
|
|
hidden.innerHTML = val + '<br/>';
|
|
|
|
textboxStyle = window.getComputedStyle(textbox);
|
|
|
|
hidden.style.paddingTop = textboxStyle.paddingTop;
|
|
hidden.style.paddingBottom = textboxStyle.paddingBottom;
|
|
hidden.style.width = textboxStyle.width;
|
|
hidden.style.fontSize = textboxStyle.fontSize;
|
|
|
|
textbox.style.height = hidden.scrollHeight + 'px';
|
|
}
|
|
},
|
|
copyToClipboard: function (text) {
|
|
var input = document.body.appendChild(document.createElement('input'));
|
|
input.value = text;
|
|
input.focus();
|
|
input.select();
|
|
document.execCommand('copy');
|
|
input.parentNode.removeChild(input);
|
|
},
|
|
applyAutoheight: function () {
|
|
var textareas = document.querySelectorAll('textarea[data-autoheight=true]')
|
|
|
|
for (var i = 0; i < textareas.length; i++) {
|
|
MovimUtils.textareaAutoheight(textareas[i]);
|
|
textareas[i].addEventListener('keyup', e => MovimUtils.textareaAutoheight(e.target));
|
|
};
|
|
},
|
|
htmlEscape: function (string) {
|
|
return String(string)
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
},
|
|
enhanceArticlesContent: function () {
|
|
document.querySelectorAll('article section > div video')
|
|
.forEach(item => item.setAttribute('controls', 'controls'));
|
|
|
|
document.querySelectorAll('article section > div a:not(.innertag)')
|
|
.forEach(link => link.setAttribute('target', '_blank'));
|
|
|
|
document.querySelectorAll('article section > div img')
|
|
.forEach(img => {
|
|
if (img.parentNode.localName != 'a') {
|
|
var div = document.createElement('div');
|
|
if (!img.parentNode.classList.contains('previewable')) {
|
|
img.parentNode.insertBefore(div, img);
|
|
div.classList.add('previewable');
|
|
img.classList.add('active');
|
|
img.addEventListener('click', () => Preview_ajaxHttpShow(img.src))
|
|
|
|
div.appendChild(img);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
urlParts: function () {
|
|
var str = window.location.pathname.split('/');
|
|
var page = str[1];
|
|
|
|
str.shift();
|
|
str.shift();
|
|
|
|
str = str.map(param => decodeURIComponent(param));
|
|
|
|
return { 'page': page, 'params': str, 'hash': window.location.hash.substring(1) };
|
|
},
|
|
humanFileSize: function (bytes, si) {
|
|
var thresh = si ? 1000 : 1024;
|
|
if (Math.abs(bytes) < thresh) {
|
|
return bytes + ' B';
|
|
}
|
|
var units = si
|
|
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
|
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
|
var u = -1;
|
|
do {
|
|
bytes /= thresh;
|
|
++u;
|
|
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
|
|
return bytes.toFixed(1) + ' ' + units[u];
|
|
},
|
|
getOrientation: function (file, callback) {
|
|
var testImageURL =
|
|
'data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAA' +
|
|
'AAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' +
|
|
'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' +
|
|
'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAIAAwMBEQACEQEDEQH/x' +
|
|
'ABRAAEAAAAAAAAAAAAAAAAAAAAKEAEBAQADAQEAAAAAAAAAAAAGBQQDCAkCBwEBAAAAAAA' +
|
|
'AAAAAAAAAAAAAABEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AG8T9NfSMEVMhQ' +
|
|
'voP3fFiRZ+MTHDifa/95OFSZU5OzRzxkyejv8ciEfhSceSXGjS8eSdLnZc2HDm4M3BxcXw' +
|
|
'H/9k='
|
|
|
|
var img = document.createElement('img');
|
|
img.onload = function () {
|
|
// Check if the browser supports automatic image orientation:
|
|
let orientation = img.width === 2 && img.height === 3;
|
|
|
|
if (orientation) {
|
|
return callback(-1);
|
|
} else {
|
|
var reader = new FileReader();
|
|
|
|
reader.onload = function (e) {
|
|
var view = new DataView(e.target.result);
|
|
if (view.getUint16(0, false) != 0xFFD8) return callback(-2);
|
|
var length = view.byteLength, offset = 2;
|
|
|
|
while (offset < length) {
|
|
var marker = view.getUint16(offset, false);
|
|
offset += 2;
|
|
if (marker == 0xFFE1) {
|
|
if (view.getUint32(offset += 2, false) != 0x45786966) return callback(-1);
|
|
|
|
var little = view.getUint16(offset += 6, false) == 0x4949;
|
|
offset += view.getUint32(offset + 4, little);
|
|
|
|
var tags = view.getUint16(offset, little);
|
|
offset += 2;
|
|
|
|
for (var i = 0; i < tags; i++)
|
|
if (view.getUint16(offset + (i * 12), little) == 0x0112)
|
|
|
|
return callback(view.getUint16(offset + (i * 12) + 8, little));
|
|
}
|
|
else if ((marker & 0xFF00) != 0xFF00) break;
|
|
else offset += view.getUint16(offset, false);
|
|
}
|
|
|
|
return callback(-1);
|
|
};
|
|
|
|
reader.readAsArrayBuffer(file);
|
|
}
|
|
}
|
|
img.src = testImageURL
|
|
},
|
|
applyOrientation: function (ctx, orientation, width, height) {
|
|
switch (orientation) {
|
|
case 2: ctx.transform(-1, 0, 0, 1, width, 0); break;
|
|
case 3: ctx.transform(-1, 0, 0, -1, width, height); break;
|
|
case 4: ctx.transform(1, 0, 0, -1, 0, height); break;
|
|
case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
|
|
case 6: ctx.transform(0, 1, -1, 0, height, 0); break;
|
|
case 7: ctx.transform(0, -1, -1, 0, height, width); break;
|
|
case 8: ctx.transform(0, -1, 1, 0, 0, width); break;
|
|
default: ctx.transform(1, 0, 0, 1, 0, 0);
|
|
}
|
|
},
|
|
drawImageProp: function (ctx, img, x, y, w, h, offsetX, offsetY) {
|
|
if (arguments.length === 2) {
|
|
x = y = 0;
|
|
w = ctx.canvas.width;
|
|
h = ctx.canvas.height;
|
|
}
|
|
|
|
// default offset is center
|
|
offsetX = typeof offsetX === "number" ? offsetX : 0.5;
|
|
offsetY = typeof offsetY === "number" ? offsetY : 0.5;
|
|
|
|
// keep bounds [0.0, 1.0]
|
|
if (offsetX < 0) offsetX = 0;
|
|
if (offsetY < 0) offsetY = 0;
|
|
if (offsetX > 1) offsetX = 1;
|
|
if (offsetY > 1) offsetY = 1;
|
|
|
|
var iw = img.width,
|
|
ih = img.height,
|
|
r = Math.min(w / iw, h / ih),
|
|
nw = iw * r, // new prop. width
|
|
nh = ih * r, // new prop. height
|
|
cx, cy, cw, ch, ar = 1;
|
|
|
|
// decide which gap to fill
|
|
if (nw < w) ar = w / nw;
|
|
if (Math.abs(ar - 1) < 1e-14 && nh < h) ar = h / nh; // updated
|
|
nw *= ar;
|
|
nh *= ar;
|
|
|
|
// calc source rectangle
|
|
cw = iw / (nw / w);
|
|
ch = ih / (nh / h);
|
|
|
|
cx = (iw - cw) * offsetX;
|
|
cy = (ih - ch) * offsetY;
|
|
|
|
// make sure source rectangle is valid
|
|
if (cx < 0) cx = 0;
|
|
if (cy < 0) cy = 0;
|
|
if (cw > iw) cw = iw;
|
|
if (ch > ih) ch = ih;
|
|
|
|
// fill image in dest. rectangle
|
|
ctx.drawImage(img, cx, cy, cw, ch, x, y, w, h);
|
|
},
|
|
imageToHex: function (img) {
|
|
const context = document.createElement("canvas").getContext("2d");
|
|
context.drawImage(img, 0, 0, 1, 1);
|
|
const i = context.getImageData(0, 0, 1, 1).data;
|
|
return "#" + ((1 << 24) + (i[0] << 16) + (i[1] << 8) + i[2]).toString(16).slice(1);
|
|
},
|
|
getEventLocation: function (e) {
|
|
if (e.touches && e.touches.length == 1) {
|
|
return { x: e.touches[0].clientX, y: e.touches[0].clientY }
|
|
}
|
|
else if (e.clientX && e.clientY) {
|
|
return { x: e.clientX, y: e.clientY }
|
|
}
|
|
},
|
|
base64ToBinary: function (base64) {
|
|
return new Uint8Array(atob(base64).split('').map(x => x.charCodeAt(0)));
|
|
},
|
|
arrayBufferToBase64: function (ab) {
|
|
return btoa((new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), ''));
|
|
},
|
|
base64ToArrayBuffer: function (base64) {
|
|
var binary_string = window.atob(base64);
|
|
var len = binary_string.length;
|
|
var bytes = new Uint8Array(len);
|
|
for (var i = 0; i < len; i++) {
|
|
bytes[i] = binary_string.charCodeAt(i);
|
|
}
|
|
return bytes.buffer;
|
|
},
|
|
stringToArrayBuffer: function (string) {
|
|
const bytes = new TextEncoder("utf-8").encode(string);
|
|
return bytes.buffer;
|
|
},
|
|
arrayBufferToString: function (ab) {
|
|
return new TextDecoder().decode(ab);
|
|
},
|
|
hexToArrayBuffer: function (hex) {
|
|
const typedArray = new Uint8Array(hex.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16)));
|
|
return typedArray.buffer;
|
|
},
|
|
appendArrayBuffer: function (buffer1, buffer2) {
|
|
const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
|
|
tmp.set(new Uint8Array(buffer1), 0);
|
|
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
|
|
return tmp.buffer;
|
|
},
|
|
range: function (start, end) {
|
|
return Array.from({ length: end - start + 1 }, (_, i) => i)
|
|
},
|
|
linkify: function (inputText) {
|
|
var replacedText, replacePattern1, replacePattern2;
|
|
|
|
//URLs starting with http://, https://, or ftp://
|
|
replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
|
|
replacedText = inputText.replace(replacePattern1, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
|
|
//Change email addresses to mailto:: links.
|
|
replacePattern2 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
|
|
replacedText = replacedText.replace(replacePattern2, '<a href="mailto:$1">$1</a>');
|
|
|
|
return replacedText;
|
|
},
|
|
logError: function(error) {
|
|
console.log(error.name + ': ' + error.message);
|
|
console.error(error);
|
|
}
|
|
};
|