This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/app/assets/javascripts/discourse/lib/utilities.js.es6
Penar Musaraj 1aaddf13a0 UX: remove iPad keyboard user setting from iOS 13
Removes setting for iOS devices that support Visual Viewport API.

On devices where it was previously enabled, it was causing some scrolling drift when invoking the composer.
2019-10-07 14:55:25 -04:00

701 lines
18 KiB
JavaScript

import { escape } from "pretty-text/sanitizer";
import toMarkdown from "discourse/lib/to-markdown";
const homepageSelector = "meta[name=discourse_current_homepage]";
export function translateSize(size) {
switch (size) {
case "tiny":
return 20;
case "small":
return 25;
case "medium":
return 32;
case "large":
return 45;
case "extra_large":
return 60;
case "huge":
return 120;
}
return size;
}
export function escapeExpression(string) {
// don't escape SafeStrings, since they're already safe
if (string instanceof Handlebars.SafeString) {
return string.toString();
}
return escape(string);
}
let _usernameFormatDelegate = username => username;
export function formatUsername(username) {
return _usernameFormatDelegate(username || "");
}
export function replaceFormatter(fn) {
_usernameFormatDelegate = fn;
}
export function avatarUrl(template, size) {
if (!template) {
return "";
}
const rawSize = getRawSize(translateSize(size));
return template.replace(/\{size\}/g, rawSize);
}
export function getRawSize(size) {
const pixelRatio = window.devicePixelRatio || 1;
return size * Math.min(3, Math.max(1, Math.round(pixelRatio)));
}
export function avatarImg(options, getURL) {
getURL = getURL || Discourse.getURLWithCDN;
const size = translateSize(options.size);
const url = avatarUrl(options.avatarTemplate, size);
// We won't render an invalid url
if (!url || url.length === 0) {
return "";
}
const classes =
"avatar" + (options.extraClasses ? " " + options.extraClasses : "");
const title = options.title
? " title='" + escapeExpression(options.title || "") + "'"
: "";
return (
"<img alt='' width='" +
size +
"' height='" +
size +
"' src='" +
getURL(url) +
"' class='" +
classes +
"'" +
title +
">"
);
}
export function tinyAvatar(avatarTemplate, options) {
return avatarImg(
_.merge({ avatarTemplate: avatarTemplate, size: "tiny" }, options)
);
}
export function postUrl(slug, topicId, postNumber) {
var url = Discourse.getURL("/t/");
if (slug) {
url += slug + "/";
} else {
url += "topic/";
}
url += topicId;
if (postNumber > 1) {
url += "/" + postNumber;
}
return url;
}
export function emailValid(email) {
// see: http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
const re = /^[a-zA-Z0-9!#$%&'*+\/=?\^_`{|}~\-]+(?:\.[a-zA-Z0-9!#$%&'\*+\/=?\^_`{|}~\-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?$/;
return re.test(email);
}
export function extractDomainFromUrl(url) {
if (url.indexOf("://") > -1) {
url = url.split("/")[2];
} else {
url = url.split("/")[0];
}
return url.split(":")[0];
}
export function selectedText() {
const selection = window.getSelection();
if (selection.isCollapsed) {
return "";
}
const $div = $("<div>");
for (let r = 0; r < selection.rangeCount; r++) {
const range = selection.getRangeAt(r);
const $ancestor = $(range.commonAncestorContainer);
// ensure we never quote text in the post menu area
const $postMenuArea = $ancestor.find(".post-menu-area")[0];
if ($postMenuArea) {
range.setEndBefore($postMenuArea);
}
$div.append(range.cloneContents());
}
return toMarkdown($div.html());
}
// Determine the row and col of the caret in an element
export function caretRowCol(el) {
var cp = caretPosition(el);
var rows = el.value.slice(0, cp).split("\n");
var rowNum = rows.length;
var colNum =
cp -
rows.splice(0, rowNum - 1).reduce(function(sum, row) {
return sum + row.length + 1;
}, 0);
return { rowNum: rowNum, colNum: colNum };
}
// Determine the position of the caret in an element
export function caretPosition(el) {
var r, rc, re;
if (el.selectionStart) {
return el.selectionStart;
}
if (document.selection) {
el.focus();
r = document.selection.createRange();
if (!r) return 0;
re = el.createTextRange();
rc = re.duplicate();
re.moveToBookmark(r.getBookmark());
rc.setEndPoint("EndToStart", re);
return rc.text.length;
}
return 0;
}
// Set the caret's position
export function setCaretPosition(ctrl, pos) {
var range;
if (ctrl.setSelectionRange) {
ctrl.focus();
ctrl.setSelectionRange(pos, pos);
return;
}
if (ctrl.createTextRange) {
range = ctrl.createTextRange();
range.collapse(true);
range.moveEnd("character", pos);
range.moveStart("character", pos);
return range.select();
}
}
export function validateUploadedFiles(files, opts) {
if (!files || files.length === 0) {
return false;
}
if (files.length > 1) {
bootbox.alert(I18n.t("post.errors.too_many_uploads"));
return false;
}
const upload = files[0];
// CHROME ONLY: if the image was pasted, sets its name to a default one
if (typeof Blob !== "undefined" && typeof File !== "undefined") {
if (
upload instanceof Blob &&
!(upload instanceof File) &&
upload.type === "image/png"
) {
upload.name = "image.png";
}
}
opts = opts || {};
opts.type = uploadTypeFromFileName(upload.name);
return validateUploadedFile(upload, opts);
}
export function validateUploadedFile(file, opts) {
if (opts.skipValidation) return true;
if (!authorizesOneOrMoreExtensions()) return false;
opts = opts || {};
const name = file && file.name;
if (!name) {
return false;
}
// check that the uploaded file is authorized
if (opts.allowStaffToUploadAnyFileInPm && opts.isPrivateMessage) {
if (Discourse.User.currentProp("staff")) {
return true;
}
}
if (opts.imagesOnly) {
if (!isAnImage(name) && !isAuthorizedImage(name)) {
bootbox.alert(
I18n.t("post.errors.upload_not_authorized", {
authorized_extensions: authorizedImagesExtensions()
})
);
return false;
}
} else if (opts.csvOnly) {
if (!/\.csv$/i.test(name)) {
bootbox.alert(I18n.t("user.invited.bulk_invite.error"));
return false;
}
} else {
if (!authorizesAllExtensions() && !isAuthorizedFile(name)) {
bootbox.alert(
I18n.t("post.errors.upload_not_authorized", {
authorized_extensions: authorizedExtensions()
})
);
return false;
}
}
if (!opts.bypassNewUserRestriction) {
// ensures that new users can upload a file
if (!Discourse.User.current().isAllowedToUploadAFile(opts.type)) {
bootbox.alert(
I18n.t(`post.errors.${opts.type}_upload_not_allowed_for_new_user`)
);
return false;
}
}
// everything went fine
return true;
}
const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|svg|ico)/i;
function extensionsToArray(exts) {
return exts
.toLowerCase()
.replace(/[\s\.]+/g, "")
.split("|")
.filter(ext => ext.indexOf("*") === -1);
}
function extensions() {
return extensionsToArray(Discourse.SiteSettings.authorized_extensions);
}
function staffExtensions() {
return extensionsToArray(
Discourse.SiteSettings.authorized_extensions_for_staff
);
}
function imagesExtensions() {
let exts = extensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext));
if (Discourse.User.currentProp("staff")) {
const staffExts = staffExtensions().filter(ext =>
IMAGES_EXTENSIONS_REGEX.test(ext)
);
exts = _.union(exts, staffExts);
}
return exts;
}
function extensionsRegex() {
return new RegExp("\\.(" + extensions().join("|") + ")$", "i");
}
function imagesExtensionsRegex() {
return new RegExp("\\.(" + imagesExtensions().join("|") + ")$", "i");
}
function staffExtensionsRegex() {
return new RegExp("\\.(" + staffExtensions().join("|") + ")$", "i");
}
function isAuthorizedFile(fileName) {
if (
Discourse.User.currentProp("staff") &&
staffExtensionsRegex().test(fileName)
) {
return true;
}
return extensionsRegex().test(fileName);
}
function isAuthorizedImage(fileName) {
return imagesExtensionsRegex().test(fileName);
}
export function authorizedExtensions() {
const exts = Discourse.User.currentProp("staff")
? [...extensions(), ...staffExtensions()]
: extensions();
return exts.filter(ext => ext.length > 0).join(", ");
}
export function authorizedImagesExtensions() {
return authorizesAllExtensions()
? "png, jpg, jpeg, gif, svg, ico"
: imagesExtensions().join(", ");
}
export function authorizesAllExtensions() {
return (
Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0 ||
(Discourse.SiteSettings.authorized_extensions_for_staff.indexOf("*") >= 0 &&
Discourse.User.currentProp("staff"))
);
}
export function authorizesOneOrMoreExtensions() {
if (authorizesAllExtensions()) return true;
return (
Discourse.SiteSettings.authorized_extensions.split("|").filter(ext => ext)
.length > 0
);
}
export function authorizesOneOrMoreImageExtensions() {
if (authorizesAllExtensions()) return true;
return imagesExtensions().length > 0;
}
export function isAnImage(path) {
return /\.(png|jpe?g|gif|svg|ico)$/i.test(path);
}
function uploadTypeFromFileName(fileName) {
return isAnImage(fileName) ? "image" : "attachment";
}
function isGUID(value) {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
value
);
}
function imageNameFromFileName(fileName) {
const split = fileName.split(".");
let name = split[split.length - 2];
if (exports.isAppleDevice() && isGUID(name)) {
name = I18n.t("upload_selector.default_image_alt_text");
}
return encodeURIComponent(name);
}
export function allowsImages() {
return (
authorizesAllExtensions() ||
IMAGES_EXTENSIONS_REGEX.test(authorizedExtensions())
);
}
export function allowsAttachments() {
return (
authorizesAllExtensions() ||
authorizedExtensions().split(", ").length > imagesExtensions().length
);
}
export function uploadIcon() {
return allowsAttachments() ? "upload" : "far-image";
}
export function uploadLocation(url) {
if (Discourse.CDN) {
url = Discourse.getURLWithCDN(url);
return /^\/\//.test(url) ? "http:" + url : url;
} else if (Discourse.S3BaseUrl) {
return "https:" + url;
} else {
var protocol = window.location.protocol + "//",
hostname = window.location.hostname,
port = window.location.port ? ":" + window.location.port : "";
return protocol + hostname + port + url;
}
}
export function getUploadMarkdown(upload) {
if (isAnImage(upload.original_filename)) {
const name = imageNameFromFileName(upload.original_filename);
return `![${name}|${upload.thumbnail_width}x${
upload.thumbnail_height
}](${upload.short_url || upload.url})`;
} else if (
!Discourse.SiteSettings.prevent_anons_from_downloading_files &&
/\.(mov|mp4|webm|ogv|mp3|ogg|wav|m4a)$/i.test(upload.original_filename)
) {
return uploadLocation(upload.url);
} else {
return `[${upload.original_filename}|attachment](${
upload.short_url
}) (${I18n.toHumanSize(upload.filesize)})`;
}
}
export function displayErrorForUpload(data) {
if (data.jqXHR) {
switch (data.jqXHR.status) {
// cancelled by the user
case 0:
return;
// entity too large, usually returned from the web server
case 413:
const type = uploadTypeFromFileName(data.files[0].name);
const max_size_kb = Discourse.SiteSettings[`max_${type}_size_kb`];
bootbox.alert(I18n.t("post.errors.file_too_large", { max_size_kb }));
return;
// the error message is provided by the server
case 422:
if (data.jqXHR.responseJSON.message) {
bootbox.alert(data.jqXHR.responseJSON.message);
} else {
bootbox.alert(data.jqXHR.responseJSON.errors.join("\n"));
}
return;
}
} else if (data.errors && data.errors.length > 0) {
bootbox.alert(data.errors.join("\n"));
return;
}
// otherwise, display a generic error message
bootbox.alert(I18n.t("post.errors.upload"));
}
export function defaultHomepage() {
let homepage = null;
let elem = _.first($(homepageSelector));
if (elem) {
homepage = elem.content;
}
if (!homepage) {
homepage = Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];
}
return homepage;
}
export function setDefaultHomepage(homepage) {
let elem = _.first($(homepageSelector));
if (elem) {
elem.content = homepage;
}
}
export function determinePostReplaceSelection({
selection,
needle,
replacement
}) {
const diff =
replacement.end - replacement.start - (needle.end - needle.start);
if (selection.end <= needle.start) {
// Selection ends (and starts) before needle.
return { start: selection.start, end: selection.end };
} else if (selection.start <= needle.start) {
// Selection starts before needle...
if (selection.end < needle.end) {
// ... and ends inside needle.
return { start: selection.start, end: needle.start };
} else {
// ... and spans needle completely.
return { start: selection.start, end: selection.end + diff };
}
} else if (selection.start < needle.end) {
// Selection starts inside needle...
if (selection.end <= needle.end) {
// ... and ends inside needle.
return { start: replacement.end, end: replacement.end };
} else {
// ... and spans end of needle.
return { start: replacement.end, end: selection.end + diff };
}
} else {
// Selection starts (and ends) behind needle.
return { start: selection.start + diff, end: selection.end + diff };
}
}
export function isAppleDevice() {
// IE has no DOMNodeInserted so can not get this hack despite saying it is like iPhone
// This will apply hack on all iDevices
const caps = Discourse.__container__.lookup("capabilities:main");
return caps.isIOS && !navigator.userAgent.match(/Trident/g);
}
let iPadDetected = undefined;
export function iOSWithVisualViewport() {
return isAppleDevice() && window.visualViewport !== undefined;
}
export function isiPad() {
if (iPadDetected === undefined) {
iPadDetected =
navigator.userAgent.match(/iPad/g) &&
!navigator.userAgent.match(/Trident/g);
}
return iPadDetected;
}
export function safariHacksDisabled() {
if (iOSWithVisualViewport()) return false;
let pref = localStorage.getItem("safari-hacks-disabled");
let result = false;
if (pref !== null) {
result = pref === "true";
}
return result;
}
const toArray = items => {
items = items || [];
if (!Array.isArray(items)) {
return Array.from(items);
}
return items;
};
export function clipboardData(e, canUpload) {
const clipboard =
e.clipboardData ||
e.originalEvent.clipboardData ||
e.delegatedEvent.originalEvent.clipboardData;
const types = toArray(clipboard.types);
let files = toArray(clipboard.files);
if (types.includes("Files") && files.length === 0) {
// for IE
files = toArray(clipboard.items).filter(i => i.kind === "file");
}
canUpload = files && canUpload && types.includes("Files");
const canUploadImage =
canUpload && files.filter(f => f.type.match("^image/"))[0];
const canPasteHtml =
Discourse.SiteSettings.enable_rich_text_paste &&
types.includes("text/html") &&
!canUploadImage;
return { clipboard, types, canUpload, canPasteHtml };
}
export function toNumber(input) {
return typeof input === "number" ? input : parseFloat(input);
}
export function isNumeric(input) {
return !isNaN(toNumber(input)) && isFinite(input);
}
export function fillMissingDates(data, startDate, endDate) {
const startMoment = moment(startDate, "YYYY-MM-DD");
const endMoment = moment(endDate, "YYYY-MM-DD");
const countDays = endMoment.diff(startMoment, "days");
let currentMoment = startMoment;
for (let i = 0; i <= countDays; i++) {
let date = data[i] ? moment(data[i].x, "YYYY-MM-DD") : null;
if (i === 0 && (!date || date.isAfter(startMoment))) {
data.splice(i, 0, { x: startMoment.format("YYYY-MM-DD"), y: 0 });
} else {
if (!date || date.isAfter(moment(currentMoment))) {
data.splice(i, 0, { x: currentMoment, y: 0 });
}
}
currentMoment = moment(currentMoment)
.add(1, "day")
.format("YYYY-MM-DD");
}
return data;
}
export function areCookiesEnabled() {
// see: https://github.com/Modernizr/Modernizr/blob/400db4043c22af98d46e1d2b9cbc5cb062791192/feature-detects/cookies.js
try {
document.cookie = "cookietest=1";
var ret = document.cookie.indexOf("cookietest=") !== -1;
document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT";
return ret;
} catch (e) {
return false;
}
}
export function isiOSPWA() {
return (
window.matchMedia("(display-mode: standalone)").matches &&
navigator.userAgent.match(/(iPad|iPhone|iPod)/g)
);
}
export function isAppWebview() {
return window.ReactNativeWebView !== undefined;
}
export function postRNWebviewMessage(prop, value) {
if (window.ReactNativeWebView !== undefined) {
window.ReactNativeWebView.postMessage(JSON.stringify({ [prop]: value }));
}
}
function reportToLogster(name, error) {
const data = {
message: `${name} theme/component is throwing errors`,
stacktrace: error.stack
};
Ember.$.ajax(`${Discourse.BaseUri}/logs/report_js_error`, {
data,
type: "POST",
cache: false
});
}
// this function is used in lib/theme_javascript_compiler.rb
export function rescueThemeError(name, error, api) {
/* eslint-disable-next-line no-console */
console.error(`"${name}" error:`, error);
reportToLogster(name, error);
const currentUser = api.getCurrentUser();
if (!currentUser || !currentUser.admin) {
return;
}
const path = `${Discourse.BaseUri}/admin/customize/themes`;
const message = I18n.t("themes.broken_theme_alert", {
theme: name,
path: `<a href="${path}">${path}</a>`
});
const alertDiv = document.createElement("div");
alertDiv.classList.add("broken-theme-alert");
alertDiv.innerHTML = `⚠️ ${message}`;
document.body.prepend(alertDiv);
}
// This prevents a mini racer crash
export default {};