mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-01-29 18:59:58 +01:00
* chore: up deps * chore: up composer * fix(deps): bump 2factor to v7 * chore: up harmony * chore: up harmony * fix: missing await * feat: allow middlewares to pass in template values via res.locals * feat: buildAccountData middleware automatically added ot all account routes * fix: properly allow values in res.locals.templateValues to be added to the template data * refactor: user/blocks * refactor(accounts): categories and consent * feat: automatically 404 if exposeUid or exposeGroupName come up empty * refactor: remove calls to getUserDataByUserSlug for most account routes, since it is populated via middleware now * fix: allow exposeUid and exposeGroupName to work with slugs with mixed capitalization * fix: move reputation removal check to accountHelpers method * test: skip i18n tests if ref branch when present is not develop * fix(deps): bump theme versions * fix(deps): bump ntfy and 2factor * chore: up harmony * fix: add missing return * fix: #11191, only focus on search input on md environments and up * feat: allow file uploads on mobile chat closes https://github.com/NodeBB/NodeBB/issues/11217 * chore: up themes * chore: add lang string * fix(deps): bump ntfy to 1.0.15 * refactor: use new if/each syntax * chore: up composer * fix: regression from user helper refactor * chore: up harmony * chore: up composer * chore: up harmony * chore: up harmony * chore: up harmony * chore: fix composer version * feat: add increment helper * chore: up harmony * fix: #11228 no timestamps in future ⌛ * chore: up harmony * check config.theme as well fire action:posts.loaded after processing dom * chore: up harmony * chore: up harmony * chore: up harmony * chore: up themes * chore: up harmony * remove extra class * refactor: move these to core from harmony * chore: up widgets * chore: up widgets * height auto * fix: closes #11238 * dont focus inputs, annoying on mobile * fix: dont focus twice, only focus on chat input on desktop dont wrap widget footer in row * chore: up harmony * chore: up harmony * update chat window * chore: up themes * fix cache buster for skins * chat fixes * chore: up harmony * chore: up composer * refactor: change hook logs to debug * fix: scroll to post right after adding to dom * fix: hash scrolling and highlighting correct post * test: re-enable read API schema tests * fix: add back schema changes for179faa2270andc3920ccb10* fix: schema changes from488f0978a4* fix: schema changes forf4cf482a87* fix: schema update forbe6bbabd0e* fix: schema changes for69c96078ea* fix: schema changes ford1364c3130* fix: schema changes for84ff1152f7* fix: schema changes forb860c2605c* fix: schema changes for23cb67a112* fix: schema changes forb916e42f40* fix: schema change fora9bbb586fc* fix: schema changes for4b738c8cd3* fix: schema changes for58b5781cea* fix: schema changes for794bf01b21* fix: schema changes for80ea12c1c1,e368feef51, and52ead114be* fix: composer-default object in config? * fix: schema changes for9acdc6808cand0930934200* fix: schema changes forc0a52924f1* fix: schema change foraba420a3f3, move loggedInUser to optional props * fix: schema changes for8c67031609* fix: schema changes for27e53b42f3* fix: schema changes for2835966518* fix: breaking test for email confirmation API call * fix: schema changes for refactored search page * fix: schema changes for user object * fix: schema changes for9f531f957e* fix: schema changes forc4042c70deand23175110a2* fix: schema changes for9b3616b103* fix: schema changes for5afd5de07d* fix: schema change for1d7baf1217* fix: schema changes for57bfb37c55andbe6bbabd0e* fix: schema changes for6e86b4afa2and3efad2e13band68f66223e7* fix: allowing optional qs prop in pagination keys (not sure why this didn't break before) * fix: re-login on email change * fix: schema changes forc926358d73* fix: schema changes for388a8270c9* fix: schema change for2658bcc821* fix: no need to call account middlewares for chats routes * fix: schema changes for71743affc3* fix: final schema changes * test: support for anyOf and oneOf * fix: check thumb * dont scroll to top on back press * remove group log * fix: add top margin to merged and deleted alerts * chore: up widgets * fix: improve fix-lists mixin * chore: up harmony/composer * feat: allow hiding quicksearch results during search * dont record searches made by composer * chore: up 54 * chore: up spam be gone * feat: add prev/next page and page count into mobile paginator * chore: up harmony * chore: up harmony * use old style for IS * fix: hide entire toolbar row if no posts or not singlePost * fix: updated messaging for post-queue template, #11206 * fix: btn-sm on post queue back button * fix: bump harmony, closes #11206 * fix: remove unused alert module import * fix: bump harmony * fix: bump harmony * chore: up harmony * refactor: IS scrolltop * fix: update users:search-user-for-chat source string * feat: support for mark-read toggle on chats dropdown and recent chats list * feat: api v3 calls to mark chat read/unread * feat: send event:chats.mark socket event on mark read or unread * refactor: allow frontend to mark chats as unread, use new API v3 routes instead of socket calls, better frontend event handling * docs: openapi schema updates for chat marking * fix: allow unread state toggling in chats dropdown too * fix: issue where repeated openings of the chats dropdown would continually add events for mark-read/unread * fix: debug log * refactor: move userSearch filter to a module * feat(routes): allow remounting /categories (#11230) * feat: send flags count to frontend on flags list page * refactor: filter form client-side js to extract out some logic * fix: applyFilters to not take any arguments, update selectedCids in updateButton instead of onHidden * fix: use userFilter module for assignee, reporterId, targetUid * fix(openapi): schema changes for updated flags page * fix: dont allow adding duplicates to userFilter * use same var * remove log * fix: closes #11282 * feat: lang key for x-topics * chore: up harmony * chore: up emoji * chore: up harmony * fix: update userFilter to allow new option `selectedBlock` * fix: wrong block name passed to userFilter * fix: https://github.com/NodeBB/NodeBB/issues/11283 * fix: chats, allow multiple dropdowns like in harmony * chore: up harmony * refactor: flag note adding/editing, closes #11285 * fix: remove old prepareEdit logic * chore: add caveat about hacky code block in userFilter module * fix: placeholders for userFilter module * refactor: navigator so it works with multiple thumbs/navigators * chore: up harmony * fix: closes #11287, destroy quick reply autocomplete on navigation * fix: filter disabled categories on user categories page count * chore: up harmony * docs: update openapi spec to include info about passing in timestamps for topic creation, removing timestamp as valid request param for topic replying * fix: send back null values on ACP search dashboard for startDate and endDate if not expicitly passed in, fix tests * fix: tweak table order in ACP dash searches * fix: only invoke navigator click drag on left mouse button * feat: add back unread indicator to navigator * clear bookmark on mark unread * fix: navigator crash on ajaxify * better thumb top calculation * fix: reset user bookmark when topic is marked unread * Revert "fix: reset user bookmark when topic is marked unread" This reverts commit9bcd85c2c6. * fix: update unread indicator on scroll, add unread count * chore: bump harmony * fix: crash on navigator unread update when backing out of a topic * fix: closes #11183 * fix: update topics:recent zset when rescheduling a topic * fix: dupe quote button, increase delay, hide immediately on empty selection * fix: navigator not showing up on first load * refactor: remove glance assorted fixes to navigator dont reduce remaning count if user scrolls down and up quickly only call topic.navigatorCallback when index changes * more sanity checks for bookmark dont allow setting bookmark higher than topic postcount * closes #11218, 🚋 * Revert "fix: update topics:recent zset when rescheduling a topic" This reverts commit737973cca9. * fix: #11306, show proper error if queued post doesn't exist was showing no-privileges if someone else accepted the post * https://github.com/NodeBB/NodeBB/issues/11307 dont use li * chore: up harmony * chore: bump version string * fix: copy paste fail * feat: closes #7382, tag filtering add client side support for filtering by tags on /category, /recent and /unread * chore: up harmony * chore: up harmony * Revert "fix: add back req.query fallback for backwards compatibility" [breaking] This reverts commitcf6cc2c454. This commit is no longer required as passing in a CSRF token via query parameter is no longer supported as of NodeBB v3.x This is a breaking change. * fix: pass csrf token in form data, re: NodeBB/NodeBB#11309 * chore: up deps * fix: tests, use x-csrf-token query param removed * test: fix csrf_token * lint: remove unused * feat: add itemprop="image" to avatar helper * fix: get chat upload button in chat modal * breaking: remove deprecated socket.io methods * test: update messaging tests to not use sockets * fix: parent post links * fix: prevent post tooltip if mouse leaves before data/tpl is loaded * chore: up harmony * chore: up harmony * chore: up harmony * chore: up harmony * fix: nested replies indices * fix(deps): bump 2factor * feat: add loggedIn user to all api routes * chore: up themes * refactor: audit admin v3 write api routes as per #11321 * refactor: audit category v3 write api routes as per #11321 [breaking] docs: fix open api spec for #11321 * refactor: audit chat v3 write api routes as per #11321 * refactor: audit files v3 write api routes as per #11321 * refactor: audit flags v3 write api routes as per #11321 * refactor: audit posts v3 write api routes as per #11321 * refactor: audit topics v3 write api routes as per #11321 * refactor: audit users v3 write api routes as per #11321 * fix: lang string * remove min height * fix: empty topic/labels taking up space * fix: tag filtering when changing filter to watched topics or changing popular time limit to month * chore: up harmony * fix: closes #11354, show no post error if queued post already accepted/rejected * test: #11354 * test: #11354 * fix(deps): bump 2factor * fix: #11357 clear cache on thumb remove * fix: thumb remove on windows, closes #11357 * test: openapi for thumbs * test: fix openapi --------- Co-authored-by: Julian Lam <julian@nodebb.org> Co-authored-by: Opliko <opliko.reg@protonmail.com>
729 lines
17 KiB
JavaScript
729 lines
17 KiB
JavaScript
'use strict';
|
|
|
|
|
|
// add default escape function for escaping HTML entities
|
|
const escapeCharMap = Object.freeze({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": ''',
|
|
'`': '`',
|
|
'=': '=',
|
|
});
|
|
function replaceChar(c) {
|
|
return escapeCharMap[c];
|
|
}
|
|
const escapeChars = /[&<>"'`=]/g;
|
|
|
|
const HTMLEntities = Object.freeze({
|
|
amp: '&',
|
|
gt: '>',
|
|
lt: '<',
|
|
quot: '"',
|
|
apos: "'",
|
|
AElig: 198,
|
|
Aacute: 193,
|
|
Acirc: 194,
|
|
Agrave: 192,
|
|
Aring: 197,
|
|
Atilde: 195,
|
|
Auml: 196,
|
|
Ccedil: 199,
|
|
ETH: 208,
|
|
Eacute: 201,
|
|
Ecirc: 202,
|
|
Egrave: 200,
|
|
Euml: 203,
|
|
Iacute: 205,
|
|
Icirc: 206,
|
|
Igrave: 204,
|
|
Iuml: 207,
|
|
Ntilde: 209,
|
|
Oacute: 211,
|
|
Ocirc: 212,
|
|
Ograve: 210,
|
|
Oslash: 216,
|
|
Otilde: 213,
|
|
Ouml: 214,
|
|
THORN: 222,
|
|
Uacute: 218,
|
|
Ucirc: 219,
|
|
Ugrave: 217,
|
|
Uuml: 220,
|
|
Yacute: 221,
|
|
aacute: 225,
|
|
acirc: 226,
|
|
aelig: 230,
|
|
agrave: 224,
|
|
aring: 229,
|
|
atilde: 227,
|
|
auml: 228,
|
|
ccedil: 231,
|
|
eacute: 233,
|
|
ecirc: 234,
|
|
egrave: 232,
|
|
eth: 240,
|
|
euml: 235,
|
|
iacute: 237,
|
|
icirc: 238,
|
|
igrave: 236,
|
|
iuml: 239,
|
|
ntilde: 241,
|
|
oacute: 243,
|
|
ocirc: 244,
|
|
ograve: 242,
|
|
oslash: 248,
|
|
otilde: 245,
|
|
ouml: 246,
|
|
szlig: 223,
|
|
thorn: 254,
|
|
uacute: 250,
|
|
ucirc: 251,
|
|
ugrave: 249,
|
|
uuml: 252,
|
|
yacute: 253,
|
|
yuml: 255,
|
|
copy: 169,
|
|
reg: 174,
|
|
nbsp: 160,
|
|
iexcl: 161,
|
|
cent: 162,
|
|
pound: 163,
|
|
curren: 164,
|
|
yen: 165,
|
|
brvbar: 166,
|
|
sect: 167,
|
|
uml: 168,
|
|
ordf: 170,
|
|
laquo: 171,
|
|
not: 172,
|
|
shy: 173,
|
|
macr: 175,
|
|
deg: 176,
|
|
plusmn: 177,
|
|
sup1: 185,
|
|
sup2: 178,
|
|
sup3: 179,
|
|
acute: 180,
|
|
micro: 181,
|
|
para: 182,
|
|
middot: 183,
|
|
cedil: 184,
|
|
ordm: 186,
|
|
raquo: 187,
|
|
frac14: 188,
|
|
frac12: 189,
|
|
frac34: 190,
|
|
iquest: 191,
|
|
times: 215,
|
|
divide: 247,
|
|
'OElig;': 338,
|
|
'oelig;': 339,
|
|
'Scaron;': 352,
|
|
'scaron;': 353,
|
|
'Yuml;': 376,
|
|
'fnof;': 402,
|
|
'circ;': 710,
|
|
'tilde;': 732,
|
|
'Alpha;': 913,
|
|
'Beta;': 914,
|
|
'Gamma;': 915,
|
|
'Delta;': 916,
|
|
'Epsilon;': 917,
|
|
'Zeta;': 918,
|
|
'Eta;': 919,
|
|
'Theta;': 920,
|
|
'Iota;': 921,
|
|
'Kappa;': 922,
|
|
'Lambda;': 923,
|
|
'Mu;': 924,
|
|
'Nu;': 925,
|
|
'Xi;': 926,
|
|
'Omicron;': 927,
|
|
'Pi;': 928,
|
|
'Rho;': 929,
|
|
'Sigma;': 931,
|
|
'Tau;': 932,
|
|
'Upsilon;': 933,
|
|
'Phi;': 934,
|
|
'Chi;': 935,
|
|
'Psi;': 936,
|
|
'Omega;': 937,
|
|
'alpha;': 945,
|
|
'beta;': 946,
|
|
'gamma;': 947,
|
|
'delta;': 948,
|
|
'epsilon;': 949,
|
|
'zeta;': 950,
|
|
'eta;': 951,
|
|
'theta;': 952,
|
|
'iota;': 953,
|
|
'kappa;': 954,
|
|
'lambda;': 955,
|
|
'mu;': 956,
|
|
'nu;': 957,
|
|
'xi;': 958,
|
|
'omicron;': 959,
|
|
'pi;': 960,
|
|
'rho;': 961,
|
|
'sigmaf;': 962,
|
|
'sigma;': 963,
|
|
'tau;': 964,
|
|
'upsilon;': 965,
|
|
'phi;': 966,
|
|
'chi;': 967,
|
|
'psi;': 968,
|
|
'omega;': 969,
|
|
'thetasym;': 977,
|
|
'upsih;': 978,
|
|
'piv;': 982,
|
|
'ensp;': 8194,
|
|
'emsp;': 8195,
|
|
'thinsp;': 8201,
|
|
'zwnj;': 8204,
|
|
'zwj;': 8205,
|
|
'lrm;': 8206,
|
|
'rlm;': 8207,
|
|
'ndash;': 8211,
|
|
'mdash;': 8212,
|
|
'lsquo;': 8216,
|
|
'rsquo;': 8217,
|
|
'sbquo;': 8218,
|
|
'ldquo;': 8220,
|
|
'rdquo;': 8221,
|
|
'bdquo;': 8222,
|
|
'dagger;': 8224,
|
|
'Dagger;': 8225,
|
|
'bull;': 8226,
|
|
'hellip;': 8230,
|
|
'permil;': 8240,
|
|
'prime;': 8242,
|
|
'Prime;': 8243,
|
|
'lsaquo;': 8249,
|
|
'rsaquo;': 8250,
|
|
'oline;': 8254,
|
|
'frasl;': 8260,
|
|
'euro;': 8364,
|
|
'image;': 8465,
|
|
'weierp;': 8472,
|
|
'real;': 8476,
|
|
'trade;': 8482,
|
|
'alefsym;': 8501,
|
|
'larr;': 8592,
|
|
'uarr;': 8593,
|
|
'rarr;': 8594,
|
|
'darr;': 8595,
|
|
'harr;': 8596,
|
|
'crarr;': 8629,
|
|
'lArr;': 8656,
|
|
'uArr;': 8657,
|
|
'rArr;': 8658,
|
|
'dArr;': 8659,
|
|
'hArr;': 8660,
|
|
'forall;': 8704,
|
|
'part;': 8706,
|
|
'exist;': 8707,
|
|
'empty;': 8709,
|
|
'nabla;': 8711,
|
|
'isin;': 8712,
|
|
'notin;': 8713,
|
|
'ni;': 8715,
|
|
'prod;': 8719,
|
|
'sum;': 8721,
|
|
'minus;': 8722,
|
|
'lowast;': 8727,
|
|
'radic;': 8730,
|
|
'prop;': 8733,
|
|
'infin;': 8734,
|
|
'ang;': 8736,
|
|
'and;': 8743,
|
|
'or;': 8744,
|
|
'cap;': 8745,
|
|
'cup;': 8746,
|
|
'int;': 8747,
|
|
'there4;': 8756,
|
|
'sim;': 8764,
|
|
'cong;': 8773,
|
|
'asymp;': 8776,
|
|
'ne;': 8800,
|
|
'equiv;': 8801,
|
|
'le;': 8804,
|
|
'ge;': 8805,
|
|
'sub;': 8834,
|
|
'sup;': 8835,
|
|
'nsub;': 8836,
|
|
'sube;': 8838,
|
|
'supe;': 8839,
|
|
'oplus;': 8853,
|
|
'otimes;': 8855,
|
|
'perp;': 8869,
|
|
'sdot;': 8901,
|
|
'lceil;': 8968,
|
|
'rceil;': 8969,
|
|
'lfloor;': 8970,
|
|
'rfloor;': 8971,
|
|
'lang;': 9001,
|
|
'rang;': 9002,
|
|
'loz;': 9674,
|
|
'spades;': 9824,
|
|
'clubs;': 9827,
|
|
'hearts;': 9829,
|
|
'diams;': 9830,
|
|
});
|
|
|
|
/* eslint-disable no-redeclare */
|
|
const utils = {
|
|
// https://github.com/substack/node-ent/blob/master/index.js
|
|
decodeHTMLEntities: function (html) {
|
|
return String(html)
|
|
.replace(/&#(\d+);?/g, function (_, code) {
|
|
return String.fromCharCode(code);
|
|
})
|
|
.replace(/&#[xX]([A-Fa-f0-9]+);?/g, function (_, hex) {
|
|
return String.fromCharCode(parseInt(hex, 16));
|
|
})
|
|
.replace(/&([^;\W]+;?)/g, function (m, e) {
|
|
const ee = e.replace(/;$/, '');
|
|
const target = HTMLEntities[e] || (e.match(/;$/) && HTMLEntities[ee]);
|
|
|
|
if (typeof target === 'number') {
|
|
return String.fromCharCode(target);
|
|
} else if (typeof target === 'string') {
|
|
return target;
|
|
}
|
|
|
|
return m;
|
|
});
|
|
},
|
|
// https://github.com/jprichardson/string.js/blob/master/lib/string.js
|
|
stripHTMLTags: function (str, tags) {
|
|
const pattern = (tags || ['']).join('|');
|
|
return String(str).replace(new RegExp('<(\\/)?(' + (pattern || '[^\\s>]+') + ')(\\s+[^<>]*?)?\\s*(\\/)?>', 'gi'), '');
|
|
},
|
|
|
|
cleanUpTag: function (tag, maxLength) {
|
|
if (typeof tag !== 'string' || !tag.length) {
|
|
return '';
|
|
}
|
|
|
|
tag = tag.trim().toLowerCase();
|
|
// see https://github.com/NodeBB/NodeBB/issues/4378
|
|
tag = tag.replace(/\u202E/gi, '');
|
|
tag = tag.replace(/[,/#!$^*;:{}=_`<>'"~()?|]/g, '');
|
|
tag = tag.slice(0, maxLength || 15).trim();
|
|
const matches = tag.match(/^[.-]*(.+?)[.-]*$/);
|
|
if (matches && matches.length > 1) {
|
|
tag = matches[1];
|
|
}
|
|
return tag;
|
|
},
|
|
|
|
removePunctuation: function (str) {
|
|
return str.replace(/[.,-/#!$%^&*;:{}=\-_`<>'"~()?]/g, '');
|
|
},
|
|
|
|
isEmailValid: function (email) {
|
|
return typeof email === 'string' && email.length && email.indexOf('@') !== -1 && email.indexOf(',') === -1 && email.indexOf(';') === -1;
|
|
},
|
|
|
|
isUserNameValid: function (name) {
|
|
return (name && name !== '' && (/^['" \-+.*[\]0-9\u00BF-\u1FFF\u2C00-\uD7FF\w]+$/.test(name)));
|
|
},
|
|
|
|
isPasswordValid: function (password) {
|
|
return typeof password === 'string' && password.length;
|
|
},
|
|
|
|
isNumber: function (n) {
|
|
// `isFinite('') === true` so isNan parseFloat check is necessary
|
|
return !isNaN(parseFloat(n)) && isFinite(n);
|
|
},
|
|
|
|
languageKeyRegex: /\[\[[\w]+:.+\]\]/,
|
|
hasLanguageKey: function (input) {
|
|
return utils.languageKeyRegex.test(input);
|
|
},
|
|
userLangToTimeagoCode: function (userLang) {
|
|
const mapping = {
|
|
'en-GB': 'en',
|
|
'en-US': 'en',
|
|
'fa-IR': 'fa',
|
|
'pt-BR': 'pt-br',
|
|
nb: 'no',
|
|
};
|
|
return mapping.hasOwnProperty(userLang) ? mapping[userLang] : userLang;
|
|
},
|
|
// shallow objects merge
|
|
merge: function () {
|
|
const result = {};
|
|
let obj;
|
|
let keys;
|
|
for (let i = 0; i < arguments.length; i += 1) {
|
|
obj = arguments[i] || {};
|
|
keys = Object.keys(obj);
|
|
for (let j = 0; j < keys.length; j += 1) {
|
|
result[keys[j]] = obj[keys[j]];
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
|
|
fileExtension: function (path) {
|
|
return ('' + path).split('.').pop();
|
|
},
|
|
|
|
extensionMimeTypeMap: {
|
|
bmp: 'image/bmp',
|
|
cmx: 'image/x-cmx',
|
|
cod: 'image/cis-cod',
|
|
gif: 'image/gif',
|
|
ico: 'image/x-icon',
|
|
ief: 'image/ief',
|
|
jfif: 'image/pipeg',
|
|
jpe: 'image/jpeg',
|
|
jpeg: 'image/jpeg',
|
|
jpg: 'image/jpeg',
|
|
png: 'image/png',
|
|
pbm: 'image/x-portable-bitmap',
|
|
pgm: 'image/x-portable-graymap',
|
|
pnm: 'image/x-portable-anymap',
|
|
ppm: 'image/x-portable-pixmap',
|
|
ras: 'image/x-cmu-raster',
|
|
rgb: 'image/x-rgb',
|
|
svg: 'image/svg+xml',
|
|
tif: 'image/tiff',
|
|
tiff: 'image/tiff',
|
|
xbm: 'image/x-xbitmap',
|
|
xpm: 'image/x-xpixmap',
|
|
xwd: 'image/x-xwindowdump',
|
|
},
|
|
|
|
fileMimeType: function (path) {
|
|
return utils.extensionToMimeType(utils.fileExtension(path));
|
|
},
|
|
|
|
extensionToMimeType: function (extension) {
|
|
return utils.extensionMimeTypeMap.hasOwnProperty(extension) ? utils.extensionMimeTypeMap[extension] : '*';
|
|
},
|
|
|
|
isPromise: function (object) {
|
|
// https://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise#comment97339131_27746324
|
|
return object && typeof object.then === 'function';
|
|
},
|
|
|
|
promiseParallel: function (obj) {
|
|
const keys = Object.keys(obj);
|
|
return Promise.all(
|
|
keys.map(function (k) { return obj[k]; })
|
|
).then(function (results) {
|
|
const data = {};
|
|
keys.forEach(function (k, i) {
|
|
data[k] = results[i];
|
|
});
|
|
return data;
|
|
});
|
|
},
|
|
|
|
// https://github.com/sindresorhus/is-absolute-url
|
|
isAbsoluteUrlRE: /^[a-zA-Z][a-zA-Z\d+\-.]*:/,
|
|
isWinPathRE: /^[a-zA-Z]:\\/,
|
|
isAbsoluteUrl: function (url) {
|
|
if (utils.isWinPathRE.test(url)) {
|
|
return false;
|
|
}
|
|
return utils.isAbsoluteUrlRE.test(url);
|
|
},
|
|
|
|
isRelativeUrl: function (url) {
|
|
return !utils.isAbsoluteUrl(url);
|
|
},
|
|
|
|
makeNumberHumanReadable: function (num, toFixed = 1) {
|
|
const n = parseInt(num, 10);
|
|
if (!n) {
|
|
return num;
|
|
}
|
|
if (n > 999999) {
|
|
return (n / 1000000).toFixed(toFixed) + 'm';
|
|
} else if (n > 999) {
|
|
return (n / 1000).toFixed(toFixed) + 'k';
|
|
}
|
|
return n;
|
|
},
|
|
|
|
// takes a string like 1000 and returns 1,000
|
|
addCommas: function (text) {
|
|
return String(text).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');
|
|
},
|
|
|
|
toISOString: function (timestamp) {
|
|
if (!timestamp || !Date.prototype.toISOString) {
|
|
return '';
|
|
}
|
|
|
|
// Prevent too-high values to be passed to Date object
|
|
timestamp = Math.min(timestamp, 8640000000000000);
|
|
|
|
try {
|
|
return new Date(parseInt(timestamp, 10)).toISOString();
|
|
} catch (e) {
|
|
return timestamp;
|
|
}
|
|
},
|
|
|
|
tags: ['a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'b', 'base', 'basefont',
|
|
'bdi', 'bdo', 'big', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
|
|
'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed',
|
|
'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link',
|
|
'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option',
|
|
'output', 'p', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select',
|
|
'small', 'source', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot',
|
|
'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'const', 'video', 'wbr'],
|
|
|
|
stripTags: ['abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'base', 'basefont',
|
|
'bdi', 'bdo', 'big', 'blink', 'body', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
|
|
'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed',
|
|
'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
'head', 'header', 'hr', 'html', 'iframe', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link',
|
|
'map', 'mark', 'marquee', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option',
|
|
'output', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select',
|
|
'source', 'span', 'strike', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot',
|
|
'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'const', 'video', 'wbr'],
|
|
|
|
escapeRegexChars: function (text) {
|
|
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
|
},
|
|
|
|
escapeHTML: function (str) {
|
|
if (str == null) {
|
|
return '';
|
|
}
|
|
if (!str) {
|
|
return String(str);
|
|
}
|
|
|
|
return str.toString().replace(escapeChars, replaceChar);
|
|
},
|
|
|
|
isAndroidBrowser: function () {
|
|
// http://stackoverflow.com/questions/9286355/how-to-detect-only-the-native-android-browser
|
|
const nua = navigator.userAgent;
|
|
return ((nua.indexOf('Mozilla/5.0') > -1 && nua.indexOf('Android ') > -1 && nua.indexOf('AppleWebKit') > -1) && !(nua.indexOf('Chrome') > -1));
|
|
},
|
|
|
|
isTouchDevice: function () {
|
|
return 'ontouchstart' in document.documentElement;
|
|
},
|
|
|
|
getHoursArray: function () {
|
|
const currentHour = new Date().getHours();
|
|
const labels = [];
|
|
|
|
for (let i = currentHour, ii = currentHour - 24; i > ii; i -= 1) {
|
|
const hour = i < 0 ? 24 + i : i;
|
|
labels.push(hour + ':00');
|
|
}
|
|
|
|
return labels.reverse();
|
|
},
|
|
|
|
getDaysArray: function (from, amount) {
|
|
const currentDay = new Date(parseInt(from, 10) || Date.now()).getTime();
|
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
const labels = [];
|
|
let tmpDate;
|
|
|
|
for (let x = (amount || 30) - 1; x >= 0; x -= 1) {
|
|
tmpDate = new Date(currentDay - (1000 * 60 * 60 * 24 * x));
|
|
labels.push(months[tmpDate.getMonth()] + ' ' + tmpDate.getDate());
|
|
}
|
|
|
|
return labels;
|
|
},
|
|
|
|
/* Retrieved from http://stackoverflow.com/a/7557433 @ 27 Mar 2016 */
|
|
isElementInViewport: function (el) {
|
|
// special bonus for those using jQuery
|
|
if (typeof jQuery === 'function' && el instanceof jQuery) {
|
|
el = el[0];
|
|
}
|
|
|
|
const rect = el.getBoundingClientRect();
|
|
|
|
return (
|
|
rect.top >= 0 &&
|
|
rect.left >= 0 &&
|
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */
|
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */
|
|
);
|
|
},
|
|
|
|
// get all the url params in a single key/value hash
|
|
params: function (options = {}) {
|
|
let url;
|
|
if (options.url && !options.url.startsWith('http')) {
|
|
// relative path passed in
|
|
options.url = options.url.replace(new RegExp(`/?${config.relative_path.slice(1)}/`, 'g'), '');
|
|
url = new URL(document.location);
|
|
url.pathname = options.url;
|
|
} else {
|
|
url = new URL(options.url || document.location);
|
|
}
|
|
let params = url.searchParams;
|
|
|
|
if (options.full) { // return URLSearchParams object
|
|
return params;
|
|
}
|
|
|
|
// Handle arrays passed in query string (Object.fromEntries does not)
|
|
const arrays = {};
|
|
params.forEach((value, key) => {
|
|
if (!key.endsWith('[]')) {
|
|
return;
|
|
}
|
|
|
|
key = key.slice(0, -2);
|
|
arrays[key] = arrays[key] || [];
|
|
arrays[key].push(utils.toType(value));
|
|
});
|
|
Object.keys(arrays).forEach((key) => {
|
|
params.delete(`${key}[]`);
|
|
});
|
|
|
|
// Backwards compatibility with v1.x -- all values passed through utils.toType()
|
|
params = Object.fromEntries(params);
|
|
Object.keys(params).forEach((key) => {
|
|
params[key] = utils.toType(params[key]);
|
|
});
|
|
|
|
return { ...params, ...arrays };
|
|
},
|
|
|
|
param: function (key) {
|
|
return this.params()[key];
|
|
},
|
|
|
|
urlToLocation: function (url) {
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
return a;
|
|
},
|
|
|
|
// return boolean if string 'true' or string 'false', or if a parsable string which is a number
|
|
// also supports JSON object and/or arrays parsing
|
|
toType: function (str) {
|
|
const type = typeof str;
|
|
if (type !== 'string') {
|
|
return str;
|
|
}
|
|
const nb = parseFloat(str);
|
|
if (!isNaN(nb) && isFinite(str)) {
|
|
return nb;
|
|
}
|
|
if (str === 'false') {
|
|
return false;
|
|
}
|
|
if (str === 'true') {
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
str = JSON.parse(str);
|
|
} catch (e) {}
|
|
|
|
return str;
|
|
},
|
|
|
|
// Safely get/set chained properties on an object
|
|
// set example: utils.props(A, 'a.b.c.d', 10) // sets A to {a: {b: {c: {d: 10}}}}, and returns 10
|
|
// get example: utils.props(A, 'a.b.c') // returns {d: 10}
|
|
// get example: utils.props(A, 'a.b.c.foo.bar') // returns undefined without throwing a TypeError
|
|
// credits to github.com/gkindel
|
|
props: function (obj, props, value) {
|
|
if (obj === undefined) {
|
|
obj = window;
|
|
}
|
|
if (props == null) {
|
|
return undefined;
|
|
}
|
|
const i = props.indexOf('.');
|
|
if (i === -1) {
|
|
if (value !== undefined) {
|
|
obj[props] = value;
|
|
}
|
|
return obj[props];
|
|
}
|
|
const prop = props.slice(0, i);
|
|
const newProps = props.slice(i + 1);
|
|
|
|
if (props !== undefined && !(obj[prop] instanceof Object)) {
|
|
obj[prop] = {};
|
|
}
|
|
|
|
return utils.props(obj[prop], newProps, value);
|
|
},
|
|
|
|
isInternalURI: function (targetLocation, referenceLocation, relative_path) {
|
|
return targetLocation.host === '' || // Relative paths are always internal links
|
|
(
|
|
targetLocation.host === referenceLocation.host &&
|
|
// Otherwise need to check if protocol and host match
|
|
targetLocation.protocol === referenceLocation.protocol &&
|
|
// Subfolder installs need this additional check
|
|
(relative_path.length > 0 ? targetLocation.pathname.indexOf(relative_path) === 0 : true)
|
|
);
|
|
},
|
|
|
|
rtrim: function (str) {
|
|
return str.replace(/\s+$/g, '');
|
|
},
|
|
|
|
debounce: function (func, wait, immediate) {
|
|
// modified from https://davidwalsh.name/javascript-debounce-function
|
|
let timeout;
|
|
return function () {
|
|
const context = this;
|
|
const args = arguments;
|
|
const later = function () {
|
|
timeout = null;
|
|
if (!immediate) {
|
|
func.apply(context, args);
|
|
}
|
|
};
|
|
const callNow = immediate && !timeout;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
if (callNow) {
|
|
func.apply(context, args);
|
|
}
|
|
};
|
|
},
|
|
throttle: function (func, wait, immediate) {
|
|
let timeout;
|
|
return function () {
|
|
const context = this;
|
|
const args = arguments;
|
|
const later = function () {
|
|
timeout = null;
|
|
if (!immediate) {
|
|
func.apply(context, args);
|
|
}
|
|
};
|
|
const callNow = immediate && !timeout;
|
|
if (!timeout) {
|
|
timeout = setTimeout(later, wait);
|
|
}
|
|
if (callNow) {
|
|
func.apply(context, args);
|
|
}
|
|
};
|
|
},
|
|
generateSaveId: function (uid) {
|
|
return ['composer', uid, Date.now()].join(':');
|
|
},
|
|
};
|
|
|
|
module.exports = utils;
|