2185 lines
55 KiB
JavaScript
2185 lines
55 KiB
JavaScript
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
// MIT License. See license.txt
|
|
|
|
import deep_equal from "fast-deep-equal";
|
|
import number_systems from "./number_systems";
|
|
|
|
frappe.provide("frappe.utils");
|
|
|
|
const eval_function_cache = new Map();
|
|
|
|
// Array de duplicate
|
|
if (!Array.prototype.uniqBy) {
|
|
Object.defineProperty(Array.prototype, "uniqBy", {
|
|
value: function (key) {
|
|
var seen = {};
|
|
return this.filter(function (item) {
|
|
var k = key(item);
|
|
return k in seen ? false : (seen[k] = true);
|
|
});
|
|
},
|
|
});
|
|
Object.defineProperty(Array.prototype, "move", {
|
|
value: function (from, to) {
|
|
this.splice(to, 0, this.splice(from, 1)[0]);
|
|
},
|
|
});
|
|
}
|
|
|
|
// Python's dict.setdefault ported for JS objects
|
|
Object.defineProperty(Object.prototype, "setDefault", {
|
|
value: function (key, default_value) {
|
|
if (!(key in this)) this[key] = default_value;
|
|
return this[key];
|
|
},
|
|
writable: true,
|
|
});
|
|
|
|
// Pluralize
|
|
String.prototype.plural = function (revert) {
|
|
const plural = {
|
|
"(quiz)$": "$1zes",
|
|
"^(ox)$": "$1en",
|
|
"([m|l])ouse$": "$1ice",
|
|
"(matr|vert|ind)ix|ex$": "$1ices",
|
|
"(x|ch|ss|sh)$": "$1es",
|
|
"([^aeiouy]|qu)y$": "$1ies",
|
|
"(hive)$": "$1s",
|
|
"(?:([^f])fe|([lr])f)$": "$1$2ves",
|
|
"(shea|lea|loa|thie)f$": "$1ves",
|
|
sis$: "ses",
|
|
"([ti])um$": "$1a",
|
|
"(tomat|potat|ech|her|vet)o$": "$1oes",
|
|
"(bu)s$": "$1ses",
|
|
"(alias)$": "$1es",
|
|
"(octop)us$": "$1i",
|
|
"(ax|test)is$": "$1es",
|
|
"(us)$": "$1es",
|
|
"(f)oot$": "$1eet",
|
|
"(g)oose$": "$1eese",
|
|
"(sex)$": "$1es",
|
|
"(child)$": "$1ren",
|
|
"(m)an$": "$1en",
|
|
"(t)ooth$": "$1eeth",
|
|
"(pe)rson$": "$1ople",
|
|
"([^s]+)$": "$1s",
|
|
};
|
|
|
|
const singular = {
|
|
"(quiz)zes$": "$1",
|
|
"(matr)ices$": "$1ix",
|
|
"(vert|ind)ices$": "$1ex",
|
|
"^(ox)en$": "$1",
|
|
"(alias)es$": "$1",
|
|
"(octop|vir)i$": "$1us",
|
|
"(cris|ax|test)es$": "$1is",
|
|
"(shoe)s$": "$1",
|
|
"(o)es$": "$1",
|
|
"(bus)es$": "$1",
|
|
"([m|l])ice$": "$1ouse",
|
|
"(x|ch|ss|sh)es$": "$1",
|
|
"(m)ovies$": "$1ovie",
|
|
"(s)eries$": "$1eries",
|
|
"([^aeiouy]|qu)ies$": "$1y",
|
|
"([lr])ves$": "$1f",
|
|
"(tive)s$": "$1",
|
|
"(hive)s$": "$1",
|
|
"(li|wi|kni)ves$": "$1fe",
|
|
"(shea|loa|lea|thie)ves$": "$1f",
|
|
"(^analy)ses$": "$1sis",
|
|
"((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$": "$1$2sis",
|
|
"([ti])a$": "$1um",
|
|
"(n)ews$": "$1ews",
|
|
"(h|bl)ouses$": "$1ouse",
|
|
"(corpse)s$": "$1",
|
|
"(us)es$": "$1",
|
|
"(f)eet$": "$1oot",
|
|
"(g)eese$": "$1oose",
|
|
"(sex)es$": "$1",
|
|
"(child)ren$": "$1",
|
|
"(m)en$": "$1an",
|
|
"(t)eeth$": "$1ooth",
|
|
"(pe)ople$": "$1rson",
|
|
s$: "",
|
|
};
|
|
|
|
const uncountable = [
|
|
"sheep",
|
|
"fish",
|
|
"deer",
|
|
"moose",
|
|
"series",
|
|
"species",
|
|
"money",
|
|
"rice",
|
|
"information",
|
|
"equipment",
|
|
];
|
|
|
|
// save some time in the case that singular and plural are the same
|
|
if (uncountable.indexOf(this.toLowerCase()) >= 0) return this;
|
|
|
|
// check for matches using regular expressions
|
|
const array = revert ? singular : plural;
|
|
|
|
let reg;
|
|
for (reg in array) {
|
|
const pattern = new RegExp(reg, "i");
|
|
|
|
if (pattern.test(this)) return this.replace(pattern, array[reg]);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
Object.assign(frappe.utils, {
|
|
get_random: function (len) {
|
|
var text = "";
|
|
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
|
|
for (var i = 0; i < len; i++)
|
|
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
|
|
|
return text;
|
|
},
|
|
get_file_link: function (filename) {
|
|
filename = cstr(filename);
|
|
if (frappe.utils.is_url(filename)) {
|
|
return filename;
|
|
} else if (filename.indexOf("/") === -1) {
|
|
return "files/" + filename;
|
|
} else {
|
|
return filename;
|
|
}
|
|
},
|
|
replace_newlines(t) {
|
|
return t ? t.replace(/\n/g, "<br>") : "";
|
|
},
|
|
is_html: function (txt) {
|
|
if (!txt) return false;
|
|
|
|
const doc = new DOMParser().parseFromString(txt, "text/html");
|
|
const nodes = doc.body.childNodes || [];
|
|
|
|
// check if any of the nodes are element nodes
|
|
// Ref: https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
|
|
return [...nodes].some((node) => node.nodeType === 1);
|
|
},
|
|
is_mac: function () {
|
|
return window.navigator.platform === "MacIntel";
|
|
},
|
|
is_xs: function () {
|
|
return $(document).width() < 768;
|
|
},
|
|
is_sm: function () {
|
|
return $(document).width() < 991 && $(document).width() >= 768;
|
|
},
|
|
is_md: function () {
|
|
return $(document).width() < 1199 && $(document).width() >= 991;
|
|
},
|
|
is_json: function (str) {
|
|
try {
|
|
JSON.parse(str);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
parse_json: function (str) {
|
|
let parsed_json = "";
|
|
try {
|
|
parsed_json = JSON.parse(str);
|
|
} catch (e) {
|
|
return str;
|
|
}
|
|
return parsed_json;
|
|
},
|
|
strip_whitespace: function (html) {
|
|
return (html || "").replace(/<p>\s*<\/p>/g, "").replace(/<br>(\s*<br>\s*)+/g, "<br><br>");
|
|
},
|
|
encode_tags: function (html) {
|
|
var tagsToReplace = {
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
};
|
|
|
|
function replaceTag(tag) {
|
|
return tagsToReplace[tag] || tag;
|
|
}
|
|
|
|
return html.replace(/[&<>]/g, replaceTag);
|
|
},
|
|
strip_original_content: function (txt) {
|
|
var out = [],
|
|
part = [],
|
|
newline = txt.indexOf("<br>") === -1 ? "\n" : "<br>";
|
|
|
|
$.each(txt.split(newline), function (i, t) {
|
|
var tt = strip(t);
|
|
if (tt && (tt.substr(0, 1) === ">" || tt.substr(0, 4) === ">")) {
|
|
part.push(t);
|
|
} else {
|
|
out = out.concat(part);
|
|
out.push(t);
|
|
part = [];
|
|
}
|
|
});
|
|
return out.join(newline);
|
|
},
|
|
|
|
escape_html: function (txt) {
|
|
if (!txt) return "";
|
|
let escape_html_mapping = {
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'",
|
|
"`": "`",
|
|
"=": "=",
|
|
};
|
|
|
|
return String(txt).replace(/[&<>"'`=]/g, (char) => escape_html_mapping[char] || char);
|
|
},
|
|
|
|
unescape_html: function (txt) {
|
|
let unescape_html_mapping = {
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
""": '"',
|
|
"'": "'",
|
|
"`": "`",
|
|
"=": "=",
|
|
};
|
|
|
|
return String(txt).replace(
|
|
/&|<|>|"|'|`|=/g,
|
|
(char) => unescape_html_mapping[char] || char
|
|
);
|
|
},
|
|
|
|
html2text: function (html) {
|
|
const parser = new DOMParser();
|
|
const dom = parser.parseFromString(html, "text/html");
|
|
return dom.body.textContent;
|
|
},
|
|
|
|
is_url: function (txt) {
|
|
return (
|
|
txt.toLowerCase().substr(0, 7) == "http://" ||
|
|
txt.toLowerCase().substr(0, 8) == "https://"
|
|
);
|
|
},
|
|
to_title_case: function (string, with_space = false) {
|
|
let titlecased_string = string.toLowerCase().replace(/(?:^|[\s-/])\w/g, function (match) {
|
|
return match.toUpperCase();
|
|
});
|
|
|
|
let replace_with = with_space ? " " : "";
|
|
|
|
return titlecased_string.replace(/-|_/g, replace_with);
|
|
},
|
|
toggle_blockquote: function (txt) {
|
|
if (!txt) return txt;
|
|
|
|
var content = $("<div></div>").html(txt);
|
|
content
|
|
.find("blockquote")
|
|
.parent("blockquote")
|
|
.addClass("hidden")
|
|
.before(
|
|
'<p><a class="text-muted btn btn-default toggle-blockquote" style="padding: 2px 7px 0px; line-height: 1;"> \
|
|
• • • \
|
|
</a></p>'
|
|
);
|
|
return content.html();
|
|
},
|
|
scroll_page_to_top() {
|
|
$(".main-section").scrollTop(0);
|
|
},
|
|
scroll_to: function (
|
|
element,
|
|
animate = true,
|
|
additional_offset,
|
|
element_to_be_scrolled,
|
|
callback,
|
|
highlight_element = false
|
|
) {
|
|
if (frappe.flags.disable_auto_scroll) return;
|
|
|
|
element_to_be_scrolled = element_to_be_scrolled || $("html, body");
|
|
let scroll_top = 0;
|
|
if (element) {
|
|
// If a number is passed, just subtract the offset,
|
|
// otherwise calculate scroll position from element
|
|
scroll_top =
|
|
typeof element == "number"
|
|
? element - cint(additional_offset)
|
|
: this.get_scroll_position(element, additional_offset);
|
|
}
|
|
|
|
if (scroll_top < 0) {
|
|
scroll_top = 0;
|
|
}
|
|
|
|
const highlight = () => {
|
|
if (highlight_element) {
|
|
$(element).addClass("highlight");
|
|
document.addEventListener(
|
|
"click",
|
|
function () {
|
|
$(element).removeClass("highlight");
|
|
},
|
|
{ once: true }
|
|
);
|
|
}
|
|
};
|
|
// already there
|
|
if (scroll_top == element_to_be_scrolled.scrollTop()) {
|
|
return highlight();
|
|
}
|
|
|
|
if (animate) {
|
|
element_to_be_scrolled
|
|
.animate({
|
|
scrollTop: scroll_top,
|
|
})
|
|
.promise()
|
|
.then(() => {
|
|
highlight();
|
|
callback && callback();
|
|
});
|
|
} else {
|
|
element_to_be_scrolled.scrollTop(scroll_top);
|
|
}
|
|
},
|
|
get_scroll_position: function (element, additional_offset) {
|
|
let header_offset =
|
|
$(".navbar").height() + $(".page-head:visible").height() || $(".navbar").height();
|
|
return $(element).offset().top - header_offset - cint(additional_offset);
|
|
},
|
|
filter_dict: function (dict, filters) {
|
|
var ret = [];
|
|
if (typeof filters == "string") {
|
|
return [dict[filters]];
|
|
}
|
|
$.each(dict, function (i, d) {
|
|
for (var key in filters) {
|
|
if ($.isArray(filters[key])) {
|
|
if (filters[key][0] == "in") {
|
|
if (filters[key][1].indexOf(d[key]) == -1) return;
|
|
} else if (filters[key][0] == "not in") {
|
|
if (filters[key][1].indexOf(d[key]) != -1) return;
|
|
} else if (filters[key][0] == "<") {
|
|
if (!(d[key] < filters[key])) return;
|
|
} else if (filters[key][0] == "<=") {
|
|
if (!(d[key] <= filters[key])) return;
|
|
} else if (filters[key][0] == ">") {
|
|
if (!(d[key] > filters[key])) return;
|
|
} else if (filters[key][0] == ">=") {
|
|
if (!(d[key] >= filters[key])) return;
|
|
}
|
|
} else {
|
|
if (d[key] != filters[key]) return;
|
|
}
|
|
}
|
|
ret.push(d);
|
|
});
|
|
return ret;
|
|
},
|
|
comma_or: function (list) {
|
|
return frappe.utils.comma_sep(list, " " + __("or") + " ");
|
|
},
|
|
comma_and: function (list) {
|
|
return frappe.utils.comma_sep(list, " " + __("and") + " ");
|
|
},
|
|
comma_sep: function (list, sep) {
|
|
if (list instanceof Array) {
|
|
if (list.length == 0) {
|
|
return "";
|
|
} else if (list.length == 1) {
|
|
return list[0];
|
|
} else {
|
|
return list.slice(0, list.length - 1).join(", ") + sep + list.slice(-1)[0];
|
|
}
|
|
} else {
|
|
return list;
|
|
}
|
|
},
|
|
set_footnote: function (footnote_area, wrapper, txt) {
|
|
if (!footnote_area) {
|
|
footnote_area = $('<div class="text-muted footnote-area level">').appendTo(wrapper);
|
|
}
|
|
|
|
if (txt) {
|
|
footnote_area.html(txt);
|
|
} else {
|
|
footnote_area.remove();
|
|
footnote_area = null;
|
|
}
|
|
return footnote_area;
|
|
},
|
|
get_args_dict_from_url: function (txt) {
|
|
var args = {};
|
|
$.each(decodeURIComponent(txt).split("&"), function (i, arg) {
|
|
arg = arg.split("=");
|
|
args[arg[0]] = arg[1];
|
|
});
|
|
return args;
|
|
},
|
|
get_url_from_dict: function (args) {
|
|
return (
|
|
$.map(args, function (val, key) {
|
|
if (val !== null) return encodeURIComponent(key) + "=" + encodeURIComponent(val);
|
|
else return null;
|
|
}).join("&") || ""
|
|
);
|
|
},
|
|
validate_type: function (val, type) {
|
|
// from https://github.com/guillaumepotier/Parsley.js/blob/master/parsley.js#L81
|
|
var regExp;
|
|
|
|
switch (type) {
|
|
case "phone":
|
|
regExp = /^([0-9 +_\-,.*#()]){1,20}$/;
|
|
break;
|
|
case "name":
|
|
regExp = /^[\w][\w'-]*([ \w][\w'-]+)*$/;
|
|
break;
|
|
case "number":
|
|
regExp = /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/;
|
|
break;
|
|
case "digits":
|
|
regExp = /^\d+$/;
|
|
break;
|
|
case "alphanum":
|
|
regExp = /^\w+$/;
|
|
break;
|
|
case "email":
|
|
// from https://emailregex.com/
|
|
regExp =
|
|
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
|
break;
|
|
case "url":
|
|
regExp =
|
|
/^((([A-Za-z0-9.+-]+:(?:\/\/)?)(?:[-;:&=\+\,\w]@)?[A-Za-z0-9.-]+(:[0-9]+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/i; // eslint-disable-line
|
|
break;
|
|
case "dateIso":
|
|
regExp = /^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$/;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
// test regExp if not null
|
|
return "" !== val ? regExp.test(val) : false;
|
|
},
|
|
guess_style: function (text, default_style, _colour) {
|
|
var style = default_style || "default";
|
|
var colour = "gray";
|
|
if (text) {
|
|
text = cstr(text);
|
|
if (has_words(["Pending", "Review", "Medium", "Not Approved"], text)) {
|
|
style = "warning";
|
|
colour = "orange";
|
|
} else if (
|
|
has_words(["Open", "Urgent", "High", "Failed", "Rejected", "Error"], text)
|
|
) {
|
|
style = "danger";
|
|
colour = "red";
|
|
} else if (
|
|
has_words(
|
|
[
|
|
"Closed",
|
|
"Finished",
|
|
"Converted",
|
|
"Completed",
|
|
"Complete",
|
|
"Confirmed",
|
|
"Approved",
|
|
"Yes",
|
|
"Active",
|
|
"Available",
|
|
"Paid",
|
|
"Success",
|
|
],
|
|
text
|
|
)
|
|
) {
|
|
style = "success";
|
|
colour = "green";
|
|
} else if (has_words(["Submitted"], text)) {
|
|
style = "info";
|
|
colour = "blue";
|
|
}
|
|
}
|
|
return _colour ? colour : style;
|
|
},
|
|
|
|
guess_colour: function (text) {
|
|
return frappe.utils.guess_style(text, null, true);
|
|
},
|
|
|
|
get_indicator_color: function (state) {
|
|
return frappe.db
|
|
.get_list("Workflow State", { filters: { name: state }, fields: ["name", "style"] })
|
|
.then((res) => {
|
|
const state = res[0];
|
|
if (!state.style) {
|
|
return frappe.utils.guess_colour(state.name);
|
|
}
|
|
const style = state.style;
|
|
const colour_map = {
|
|
Success: "green",
|
|
Warning: "orange",
|
|
Danger: "red",
|
|
Primary: "blue",
|
|
};
|
|
|
|
return colour_map[style];
|
|
});
|
|
},
|
|
|
|
sort: function (list, key, compare_type, reverse) {
|
|
if (!list || list.length < 2) return list || [];
|
|
|
|
var sort_fn = {
|
|
string: function (a, b) {
|
|
return cstr(a[key]).localeCompare(cstr(b[key]));
|
|
},
|
|
number: function (a, b) {
|
|
return flt(a[key]) - flt(b[key]);
|
|
},
|
|
};
|
|
|
|
if (!compare_type) compare_type = typeof list[0][key] === "string" ? "string" : "number";
|
|
|
|
list.sort(sort_fn[compare_type]);
|
|
|
|
if (reverse) {
|
|
list.reverse();
|
|
}
|
|
|
|
return list;
|
|
},
|
|
|
|
unique: function (list) {
|
|
var dict = {},
|
|
arr = [];
|
|
for (var i = 0, l = list.length; i < l; i++) {
|
|
if (!(list[i] in dict)) {
|
|
dict[list[i]] = null;
|
|
arr.push(list[i]);
|
|
}
|
|
}
|
|
return arr;
|
|
},
|
|
|
|
remove_nulls: function (list) {
|
|
var new_list = [];
|
|
for (var i = 0, l = list.length; i < l; i++) {
|
|
if (!is_null(list[i])) {
|
|
new_list.push(list[i]);
|
|
}
|
|
}
|
|
return new_list;
|
|
},
|
|
|
|
all: function (lst) {
|
|
for (var i = 0, l = lst.length; i < l; i++) {
|
|
if (!lst[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
dict: function (keys, values) {
|
|
// make dictionaries from keys and values
|
|
var out = [];
|
|
$.each(values, function (row_idx, row) {
|
|
var new_row = {};
|
|
$.each(keys, function (key_idx, key) {
|
|
new_row[key] = row[key_idx];
|
|
});
|
|
out.push(new_row);
|
|
});
|
|
return out;
|
|
},
|
|
|
|
sum: function (list) {
|
|
return list.reduce(function (previous_value, current_value) {
|
|
return flt(previous_value) + flt(current_value);
|
|
}, 0.0);
|
|
},
|
|
|
|
arrays_equal: function (arr1, arr2) {
|
|
if (!arr1 || !arr2) {
|
|
return false;
|
|
}
|
|
if (arr1.length != arr2.length) {
|
|
return false;
|
|
}
|
|
for (var i = 0; i < arr1.length; i++) {
|
|
if ($.isArray(arr1[i])) {
|
|
if (!frappe.utils.arrays_equal(arr1[i], arr2[i])) {
|
|
return false;
|
|
}
|
|
} else if (arr1[i] !== arr2[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
intersection: function (a, b) {
|
|
// from stackoverflow: http://stackoverflow.com/questions/1885557/simplest-code-for-array-intersection-in-javascript
|
|
/* finds the intersection of
|
|
* two arrays in a simple fashion.
|
|
*
|
|
* PARAMS
|
|
* a - first array, must already be sorted
|
|
* b - second array, must already be sorted
|
|
*
|
|
* NOTES
|
|
*
|
|
* Should have O(n) operations, where n is
|
|
* n = MIN(a.length(), b.length())
|
|
*/
|
|
var ai = 0,
|
|
bi = 0;
|
|
var result = new Array();
|
|
|
|
// sorted copies
|
|
a = [].concat(a).sort();
|
|
b = [].concat(b).sort();
|
|
|
|
while (ai < a.length && bi < b.length) {
|
|
if (a[ai] < b[bi]) {
|
|
ai++;
|
|
} else if (a[ai] > b[bi]) {
|
|
bi++;
|
|
} else {
|
|
/* they're equal */
|
|
result.push(a[ai]);
|
|
ai++;
|
|
bi++;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
resize_image: function (reader, callback, max_width, max_height) {
|
|
var tempImg = new Image();
|
|
if (!max_width) max_width = 600;
|
|
if (!max_height) max_height = 400;
|
|
tempImg.src = reader.result;
|
|
|
|
tempImg.onload = function () {
|
|
var tempW = tempImg.width;
|
|
var tempH = tempImg.height;
|
|
if (tempW > tempH) {
|
|
if (tempW > max_width) {
|
|
tempH *= max_width / tempW;
|
|
tempW = max_width;
|
|
}
|
|
} else {
|
|
if (tempH > max_height) {
|
|
tempW *= max_height / tempH;
|
|
tempH = max_height;
|
|
}
|
|
}
|
|
|
|
var canvas = document.createElement("canvas");
|
|
canvas.width = tempW;
|
|
canvas.height = tempH;
|
|
var ctx = canvas.getContext("2d");
|
|
ctx.drawImage(this, 0, 0, tempW, tempH);
|
|
var dataURL = canvas.toDataURL("image/jpeg");
|
|
setTimeout(function () {
|
|
callback(dataURL);
|
|
}, 10);
|
|
};
|
|
},
|
|
|
|
csv_to_array: function (strData, strDelimiter) {
|
|
// Check to see if the delimiter is defined. If not,
|
|
// then default to comma.
|
|
strDelimiter = strDelimiter || ",";
|
|
|
|
// Create a regular expression to parse the CSV values.
|
|
var objPattern = new RegExp(
|
|
// Delimiters.
|
|
"(\\" +
|
|
strDelimiter +
|
|
"|\\r?\\n|\\r|^)" +
|
|
// Quoted fields.
|
|
'(?:"([^"]*(?:""[^"]*)*)"|' +
|
|
// Standard fields.
|
|
'([^"\\' +
|
|
strDelimiter +
|
|
"\\r\\n]*))",
|
|
"gi"
|
|
);
|
|
|
|
// Create an array to hold our data. Give the array
|
|
// a default empty first row.
|
|
var arrData = [[]];
|
|
|
|
// Create an array to hold our individual pattern
|
|
// matching groups.
|
|
var arrMatches = null;
|
|
|
|
// Keep looping over the regular expression matches
|
|
// until we can no longer find a match.
|
|
while ((arrMatches = objPattern.exec(strData))) {
|
|
// Get the delimiter that was found.
|
|
var strMatchedDelimiter = arrMatches[1];
|
|
|
|
// Check to see if the given delimiter has a length
|
|
// (is not the start of string) and if it matches
|
|
// field delimiter. If id does not, then we know
|
|
// that this delimiter is a row delimiter.
|
|
if (strMatchedDelimiter.length && strMatchedDelimiter !== strDelimiter) {
|
|
// Since we have reached a new row of data,
|
|
// add an empty row to our data array.
|
|
arrData.push([]);
|
|
}
|
|
|
|
var strMatchedValue;
|
|
|
|
// Now that we have our delimiter out of the way,
|
|
// let's check to see which kind of value we
|
|
// captured (quoted or unquoted).
|
|
if (arrMatches[2]) {
|
|
// We found a quoted value. When we capture
|
|
// this value, unescape any double quotes.
|
|
strMatchedValue = arrMatches[2].replace(new RegExp('""', "g"), '"');
|
|
} else {
|
|
// We found a non-quoted value.
|
|
strMatchedValue = arrMatches[3];
|
|
}
|
|
|
|
// Now that we have our value string, let's add
|
|
// it to the data array.
|
|
arrData[arrData.length - 1].push(strMatchedValue);
|
|
}
|
|
|
|
// Return the parsed data.
|
|
return arrData;
|
|
},
|
|
|
|
warn_page_name_change: function () {
|
|
frappe.msgprint(__("Note: Changing the Page Name will break previous URL to this page."));
|
|
},
|
|
|
|
set_title: function (title) {
|
|
frappe._original_title = title;
|
|
if (frappe._title_prefix) {
|
|
title = frappe._title_prefix + " " + title.replace(/<[^>]*>/g, "");
|
|
}
|
|
document.title = title;
|
|
|
|
// save for re-routing
|
|
const sub_path = frappe.router.get_sub_path();
|
|
frappe.route_titles[sub_path] = title;
|
|
},
|
|
|
|
set_title_prefix: function (prefix) {
|
|
frappe._title_prefix = prefix;
|
|
|
|
// reset the original title
|
|
frappe.utils.set_title(frappe._original_title);
|
|
},
|
|
|
|
is_image_file: function (filename) {
|
|
if (!filename) return false;
|
|
// url can have query params
|
|
filename = filename.split("?")[0];
|
|
return /\.(gif|jpg|jpeg|tiff|png|svg)$/i.test(filename);
|
|
},
|
|
|
|
is_video_file: function (filename) {
|
|
if (!filename) return false;
|
|
// url can have query params
|
|
filename = filename.split("?")[0];
|
|
return /\.(mov|mp4|mkv|webm)$/i.test(filename);
|
|
},
|
|
|
|
play_sound: function (name) {
|
|
try {
|
|
if (frappe.boot.user.mute_sounds) {
|
|
return;
|
|
}
|
|
|
|
var audio = $("#sound-" + name)[0];
|
|
audio.volume = audio.getAttribute("volume");
|
|
if (!audio.paused) {
|
|
audio.currentTime = 0;
|
|
}
|
|
audio.play();
|
|
} catch (e) {
|
|
console.log("Cannot play sound", name, e);
|
|
// pass
|
|
}
|
|
},
|
|
split_emails: function (txt) {
|
|
var email_list = [];
|
|
|
|
if (!txt) {
|
|
return email_list;
|
|
}
|
|
|
|
// emails can be separated by comma or newline
|
|
txt.split(/[,\n](?=(?:[^"]|"[^"]*")*$)/g).forEach(function (email) {
|
|
email = email.trim();
|
|
if (email) {
|
|
email_list.push(email);
|
|
}
|
|
});
|
|
|
|
return email_list;
|
|
},
|
|
supportsES6: (function () {
|
|
try {
|
|
new Function("(a = 0) => a");
|
|
return true;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
})(),
|
|
throttle: function (func, wait, options) {
|
|
var context, args, result;
|
|
var timeout = null;
|
|
var previous = 0;
|
|
if (!options) options = {};
|
|
|
|
let later = function () {
|
|
previous = options.leading === false ? 0 : Date.now();
|
|
timeout = null;
|
|
result = func.apply(context, args);
|
|
if (!timeout) context = args = null;
|
|
};
|
|
|
|
return function () {
|
|
var now = Date.now();
|
|
if (!previous && options.leading === false) previous = now;
|
|
let remaining = wait - (now - previous);
|
|
context = this;
|
|
args = arguments;
|
|
if (remaining <= 0 || remaining > wait) {
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
timeout = null;
|
|
}
|
|
previous = now;
|
|
result = func.apply(context, args);
|
|
if (!timeout) context = args = null;
|
|
} else if (!timeout && options.trailing !== false) {
|
|
timeout = setTimeout(later, remaining);
|
|
}
|
|
return result;
|
|
};
|
|
},
|
|
debounce: function (func, wait, immediate) {
|
|
var timeout, context, args;
|
|
|
|
var later = function () {
|
|
timeout = null;
|
|
if (!immediate) func.apply(context, args);
|
|
};
|
|
|
|
var debounced = function () {
|
|
context = this;
|
|
args = arguments;
|
|
var callNow = immediate && !timeout;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
if (callNow) func.apply(context, args);
|
|
};
|
|
|
|
debounced.cancel = function () {
|
|
if (!timeout) return false;
|
|
|
|
clearTimeout(timeout);
|
|
timeout = null;
|
|
return true;
|
|
};
|
|
|
|
debounced.flush = function () {
|
|
if (!timeout) return false;
|
|
|
|
clearTimeout(timeout);
|
|
timeout = null;
|
|
func.apply(context, args);
|
|
return true;
|
|
};
|
|
|
|
return debounced;
|
|
},
|
|
get_form_link: function (
|
|
doctype,
|
|
name,
|
|
html = false,
|
|
display_text = null,
|
|
query_params_obj = null
|
|
) {
|
|
display_text = display_text || name;
|
|
name = encodeURIComponent(name);
|
|
let route = `/desk/${encodeURIComponent(
|
|
doctype.toLowerCase().replace(/ /g, "-")
|
|
)}/${name}`;
|
|
if (query_params_obj) {
|
|
route += frappe.utils.make_query_string(query_params_obj);
|
|
}
|
|
if (html) {
|
|
return `<a href="${route}">${display_text}</a>`;
|
|
}
|
|
return route;
|
|
},
|
|
get_route_label(route_str) {
|
|
let route = route_str.split("/");
|
|
|
|
if (route[2] === "Report" || route[0] === "query-report") {
|
|
return (__(route[3]) || __(route[1])).bold() + " " + __("Report");
|
|
}
|
|
if (route[0] === "List") {
|
|
return __(route[1]).bold() + " " + __("List");
|
|
}
|
|
if (route[0] === "modules") {
|
|
return __(route[1]).bold() + " " + __("Module");
|
|
}
|
|
if (route[0] === "Workspaces") {
|
|
return __(route[1]).bold() + " " + __("Workspace");
|
|
}
|
|
if (route[0] === "dashboard") {
|
|
return __(route[1]).bold() + " " + __("Dashboard");
|
|
}
|
|
return __(frappe.utils.to_title_case(__(route[0]), true));
|
|
},
|
|
report_column_total: function (values, column, type) {
|
|
if (column.column.disable_total) {
|
|
return "";
|
|
} else if (values.length > 0) {
|
|
if (column.column.fieldtype == "Percent" || type === "mean") {
|
|
return values.reduce((a, b) => flt(a) + flt(b)) / values.length;
|
|
} else if (column.column.fieldtype == "Int") {
|
|
return values.reduce((a, b) => cint(a) + cint(b));
|
|
} else if (frappe.model.is_numeric_field(column.column.fieldtype)) {
|
|
return values.reduce((a, b) => flt(a) + flt(b));
|
|
} else {
|
|
return null;
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
},
|
|
setup_search($wrapper, el_class, text_class, data_attr) {
|
|
const $search_input = $wrapper.find('[data-element="search"]').show();
|
|
$search_input.focus().val("");
|
|
const $elements = $wrapper.find(el_class).show();
|
|
const $multichecks = $wrapper.find('[data-fieldtype="MultiCheck"]');
|
|
|
|
let $no_results = $wrapper.find(".no-results-message");
|
|
if (!$no_results.length) {
|
|
$no_results = $(`
|
|
<div class="no-results-message text-muted text-center" style="padding: 5px; display: none;">
|
|
${__("No values to show")}
|
|
</div>
|
|
`).appendTo($wrapper);
|
|
}
|
|
|
|
$no_results.hide();
|
|
|
|
const matches_filter = ($el, filter) => {
|
|
const $text_el = $el.find(text_class);
|
|
const text = $text_el.text().toLowerCase();
|
|
|
|
let name = "";
|
|
if (data_attr && $text_el.attr(data_attr)) {
|
|
name = $text_el.attr(data_attr).toLowerCase();
|
|
}
|
|
|
|
return text.includes(filter) || name.includes(filter);
|
|
};
|
|
|
|
$search_input.off("keyup").on("keyup", () => {
|
|
const text_filter = $search_input.val().toLowerCase().trim();
|
|
let any_visible = false;
|
|
|
|
$elements.each(function () {
|
|
const match = matches_filter($(this), text_filter);
|
|
$(this).toggle(match);
|
|
if (match) any_visible = true;
|
|
});
|
|
|
|
if ($multichecks.length) {
|
|
$multichecks.show();
|
|
|
|
$multichecks.each(function () {
|
|
const has_visible = $(this).find(el_class + ":visible").length;
|
|
$(this).toggle(!!has_visible);
|
|
});
|
|
}
|
|
|
|
if (text_filter) {
|
|
$no_results.toggle(!any_visible);
|
|
} else {
|
|
$no_results.hide();
|
|
}
|
|
});
|
|
},
|
|
setup_timer(start, end, $element) {
|
|
const increment = end > start;
|
|
let counter = start;
|
|
|
|
let interval = setInterval(() => {
|
|
increment ? counter++ : counter--;
|
|
if (increment ? counter > end : counter < end) {
|
|
clearInterval(interval);
|
|
return;
|
|
}
|
|
$element.text(counter);
|
|
}, 1000);
|
|
},
|
|
|
|
deep_equal(a, b) {
|
|
return deep_equal(a, b);
|
|
},
|
|
|
|
file_name_ellipsis(filename, length) {
|
|
let first_part_length = (length * 2) / 3;
|
|
let last_part_length = length - first_part_length;
|
|
let parts = filename.split(".");
|
|
let extn = parts.pop();
|
|
let name = parts.join("");
|
|
let first_part = name.slice(0, first_part_length);
|
|
let last_part = name.slice(-last_part_length);
|
|
if (name.length > length) {
|
|
return `${first_part}...${last_part}.${extn}`;
|
|
} else {
|
|
return filename;
|
|
}
|
|
},
|
|
get_decoded_string(dataURI) {
|
|
// decodes base64 to string
|
|
let parts = dataURI.split(",");
|
|
const encoded_data = parts[1];
|
|
let decoded = atob(encoded_data);
|
|
try {
|
|
const escaped = escape(decoded);
|
|
decoded = decodeURIComponent(escaped);
|
|
} catch (e) {
|
|
// pass decodeURIComponent failure
|
|
// just return atob response
|
|
}
|
|
return decoded;
|
|
},
|
|
copy_to_clipboard(string, message) {
|
|
const show_success_alert = () => {
|
|
frappe.show_alert({
|
|
indicator: "green",
|
|
message: message || __("Copied to clipboard."),
|
|
});
|
|
};
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
navigator.clipboard.writeText(string).then(show_success_alert);
|
|
} else {
|
|
let input = $("<textarea>");
|
|
$("body").append(input);
|
|
input.val(string).select();
|
|
|
|
document.execCommand("copy");
|
|
show_success_alert();
|
|
input.remove();
|
|
}
|
|
},
|
|
is_rtl(lang = null) {
|
|
return ["ar", "he", "fa", "ps"].includes(lang || frappe.boot.lang);
|
|
},
|
|
bind_actions_with_object($el, object) {
|
|
// remove previously bound event
|
|
$($el).off("click.class_actions");
|
|
// attach new event
|
|
$($el).on("click.class_actions", "[data-action]", (e) => {
|
|
let $target = $(e.currentTarget);
|
|
let action = $target.data("action");
|
|
let method = object[action];
|
|
method ? object[action](e, $target) : null;
|
|
});
|
|
|
|
return $el;
|
|
},
|
|
|
|
eval(code, context = {}) {
|
|
if (code.substr(0, 5) == "eval:") {
|
|
code = code.substr(5);
|
|
}
|
|
|
|
let variable_names = Object.keys(context);
|
|
let variables = Object.values(context);
|
|
|
|
// only cache expressions under 500 chars
|
|
const should_cache = code.length < 500;
|
|
const cache_key = should_cache ? code + "|" + variable_names.join(",") : null;
|
|
|
|
let expression_function = cache_key && eval_function_cache.get(cache_key);
|
|
|
|
if (!expression_function) {
|
|
const function_code = `let out = ${code}; return out`;
|
|
try {
|
|
expression_function = new Function(...variable_names, function_code);
|
|
} catch (error) {
|
|
console.log("Error evaluating the following expression:");
|
|
console.error(function_code);
|
|
throw error;
|
|
}
|
|
|
|
if (cache_key) {
|
|
eval_function_cache.set(cache_key, expression_function);
|
|
}
|
|
}
|
|
|
|
try {
|
|
return expression_function(...variables);
|
|
} catch (error) {
|
|
console.log("Error executing the following expression:");
|
|
console.error(code);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
get_browser() {
|
|
let ua = navigator.userAgent;
|
|
let tem;
|
|
let M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
|
|
|
|
if (/trident/i.test(M[1])) {
|
|
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
|
|
return { name: "IE", version: tem[1] || "" };
|
|
}
|
|
if (M[1] === "Chrome") {
|
|
tem = ua.match(/\bOPR|Edge\/(\d+)/);
|
|
if (tem != null) {
|
|
return { name: "Opera", version: tem[1] };
|
|
}
|
|
}
|
|
M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, "-?"];
|
|
if ((tem = ua.match(/version\/(\d+)/i)) != null) {
|
|
M.splice(1, 1, tem[1]);
|
|
}
|
|
return {
|
|
name: M[0],
|
|
version: M[1],
|
|
};
|
|
},
|
|
|
|
get_formatted_duration(value, duration_options = null) {
|
|
let duration = "";
|
|
if (!duration_options) {
|
|
duration_options = {
|
|
hide_days: 0,
|
|
hide_seconds: 0,
|
|
};
|
|
}
|
|
if (value) {
|
|
let total_duration = frappe.utils.seconds_to_duration(value, duration_options);
|
|
|
|
if (total_duration.days && duration_options.hide_days !== 1) {
|
|
duration += total_duration.days + __("d", null, "Days (Field: Duration)");
|
|
}
|
|
if (total_duration.hours) {
|
|
duration += duration.length ? " " : "";
|
|
duration += total_duration.hours + __("h", null, "Hours (Field: Duration)");
|
|
}
|
|
if (total_duration.minutes) {
|
|
duration += duration.length ? " " : "";
|
|
duration += total_duration.minutes + __("m", null, "Minutes (Field: Duration)");
|
|
}
|
|
if (total_duration.seconds && duration_options.hide_seconds !== 1) {
|
|
duration += duration.length ? " " : "";
|
|
duration += total_duration.seconds + __("s", null, "Seconds (Field: Duration)");
|
|
}
|
|
}
|
|
return duration;
|
|
},
|
|
|
|
get_formatted_iban(value) {
|
|
if (!value || ["BI", "SV", "EG", "LY"].some((country) => value.startsWith(country))) {
|
|
return value;
|
|
}
|
|
|
|
return value.replaceAll(" ", "").replace(/(.{4})(?=.)/g, "$1 ");
|
|
},
|
|
|
|
seconds_to_duration(seconds, duration_options) {
|
|
const floor = seconds > 0 ? Math.floor : Math.ceil;
|
|
const total_duration = {
|
|
days: floor(seconds / 86400), // 60 * 60 * 24
|
|
hours: floor((seconds % 86400) / 3600),
|
|
minutes: floor((seconds % 3600) / 60),
|
|
seconds: floor(seconds % 60),
|
|
};
|
|
|
|
if (duration_options && duration_options.hide_days) {
|
|
total_duration.hours = floor(seconds / 3600);
|
|
total_duration.days = 0;
|
|
}
|
|
|
|
if (duration_options && duration_options.hide_seconds) {
|
|
total_duration.minutes += Math.round(total_duration.seconds / 60);
|
|
total_duration.seconds = 0;
|
|
}
|
|
|
|
return total_duration;
|
|
},
|
|
|
|
duration_to_seconds(days = 0, hours = 0, minutes = 0, seconds = 0) {
|
|
let value = 0;
|
|
if (days) {
|
|
value += days * 24 * 60 * 60;
|
|
}
|
|
if (hours) {
|
|
value += hours * 60 * 60;
|
|
}
|
|
if (minutes) {
|
|
value += minutes * 60;
|
|
}
|
|
if (seconds) {
|
|
value += seconds;
|
|
}
|
|
return value;
|
|
},
|
|
|
|
get_duration_options: function (docfield) {
|
|
return {
|
|
hide_days: docfield.hide_days,
|
|
hide_seconds: docfield.hide_seconds,
|
|
};
|
|
},
|
|
|
|
get_number_system: function (country) {
|
|
if (["Bangladesh", "India", "Myanmar", "Pakistan"].includes(country)) {
|
|
return number_systems.indian;
|
|
} else if (country == "Nepal") {
|
|
return number_systems.nepalese;
|
|
} else {
|
|
return number_systems.default;
|
|
}
|
|
},
|
|
|
|
map_defaults: {
|
|
center: [19.08, 72.8961],
|
|
zoom: 13,
|
|
tiles: {
|
|
default_tile: {
|
|
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
|
options: {
|
|
attribution:
|
|
'© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
|
|
},
|
|
},
|
|
satellite_tile: {
|
|
url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
|
|
options: {
|
|
attribution: "© Esri © OpenStreetMap Contributors",
|
|
},
|
|
},
|
|
labels_tail: {
|
|
url: "https://tiles.stadiamaps.com/tiles/stamen_toner_labels/{z}/{x}/{y}{r}.png",
|
|
options: {
|
|
attribution:
|
|
'© <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> © <a href="https://www.stamen.com/" target="_blank">Stamen Design</a> © <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a>',
|
|
},
|
|
},
|
|
terrain_lines_tail: {
|
|
url: "https://tiles.stadiamaps.com/tiles/stamen_terrain_lines/{z}/{x}/{y}{r}.png",
|
|
options: {
|
|
attribution:
|
|
'© <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> © <a href="https://www.stamen.com/" target="_blank">Stamen Design</a> © <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a>',
|
|
},
|
|
},
|
|
},
|
|
image_path: "/assets/frappe/images/leaflet/",
|
|
},
|
|
get_route_for_icon(desktop_icon) {
|
|
let route;
|
|
if (!desktop_icon) return;
|
|
let item = {};
|
|
if (desktop_icon.link_type == "External" && desktop_icon.link) {
|
|
route = desktop_icon.link;
|
|
} else {
|
|
let sidebar = frappe.boot.workspace_sidebar_item[desktop_icon.label.toLowerCase()];
|
|
if (desktop_icon.link_type == "Workspace Sidebar" && sidebar) {
|
|
let first_link = sidebar.items.find((i) => i.type == "Link");
|
|
if (first_link) {
|
|
if (first_link.link_type === "Report") {
|
|
let args = {
|
|
type: first_link.link_type,
|
|
name: first_link.link_to,
|
|
};
|
|
|
|
if (first_link.report || !frappe.app.sidebar.editor.edit_mode) {
|
|
args.is_query_report =
|
|
first_link.report.report_type === "Query Report" ||
|
|
first_link.report.report_type == "Script Report";
|
|
args.report_ref_doctype = first_link.report.ref_doctype;
|
|
}
|
|
|
|
route = frappe.utils.generate_route(args);
|
|
} else if (first_link.link_type == "Workspace") {
|
|
let workspaces = frappe.workspaces[frappe.router.slug(first_link.link_to)];
|
|
if (workspaces) {
|
|
if (workspaces.public) {
|
|
route = "/desk/" + frappe.router.slug(first_link.link_to);
|
|
} else {
|
|
route = "/desk/private/" + frappe.router.slug(workspaces.title);
|
|
}
|
|
}
|
|
|
|
if (first_link.route) {
|
|
route = first_link.route;
|
|
}
|
|
} else if (first_link.link_type === "URL") {
|
|
route = first_link.url;
|
|
} else if (first_link.link_type == "Page" && first_link.route_options) {
|
|
route = frappe.utils.generate_route({
|
|
type: first_link.link_type,
|
|
name: first_link.link_to,
|
|
route_options: JSON.parse(first_link.route_options),
|
|
});
|
|
} else {
|
|
route = frappe.utils.generate_route({
|
|
type: first_link.link_type,
|
|
name: first_link.link_to,
|
|
tab: first_link.tab,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return route;
|
|
},
|
|
desktop_icon(label, color, size) {
|
|
let letter = label.charAt(0).toUpperCase();
|
|
let icon_size = size ? size : "md";
|
|
let opacity_hex = "1A";
|
|
let icon_html = $(`
|
|
<div class="icon-container">
|
|
<svg fill="currentColor" class="desktop-alphabet icon text-ink-gray-7 icon-${icon_size}" stroke=none style="" aria-hidden="true">
|
|
<use class="" href="#${letter}"></use>
|
|
</svg>
|
|
</div>
|
|
`);
|
|
let pallete_color = this.desktop_pallete[color || "blue"];
|
|
let bg_color = pallete_color + opacity_hex;
|
|
let stroke_color = pallete_color;
|
|
if (frappe.boot.desktop_icon_style == "Solid") {
|
|
bg_color = stroke_color;
|
|
stroke_color = "var(--white)";
|
|
}
|
|
icon_html.css("backgroundColor", bg_color);
|
|
icon_html.find("svg").css("color", stroke_color);
|
|
return icon_html.get(0).outerHTML;
|
|
},
|
|
desktop_pallete: {
|
|
blue: "#0981E3",
|
|
gray: "#7B808A",
|
|
},
|
|
desktop_bg_color(color_name) {
|
|
let color_value = this.desktop_pallete[color_name];
|
|
color_value + "";
|
|
},
|
|
icon(
|
|
icon_name,
|
|
size = "sm",
|
|
icon_class = "",
|
|
icon_style = "",
|
|
svg_class = "",
|
|
current_color = false,
|
|
stroke_color = null
|
|
) {
|
|
if (frappe.utils.is_emoji(icon_name)) {
|
|
return `<span>${icon_name}</span>`;
|
|
}
|
|
let size_class = "";
|
|
let is_espresso = icon_name.startsWith("es-");
|
|
|
|
icon_name = is_espresso ? `${"#" + icon_name}` : `${"#icon-" + icon_name}`;
|
|
if (typeof size == "object") {
|
|
icon_style += ` width: ${size.width}; height: ${size.height}`;
|
|
} else {
|
|
size_class = `icon-${size}`;
|
|
}
|
|
let $svg = `<svg class="${
|
|
is_espresso
|
|
? icon_name.startsWith("es-solid")
|
|
? "es-icon es-solid"
|
|
: "es-icon es-line"
|
|
: "icon"
|
|
} ${svg_class} ${size_class}"
|
|
${current_color ? 'stroke="currentColor"' : ""}
|
|
${stroke_color ? `stroke="${stroke_color}"` : ""}
|
|
style="${icon_style}" aria-hidden="true">
|
|
<use class="${icon_class}" href="${icon_name}"
|
|
${stroke_color ? `stroke="${stroke_color}"` : ""}
|
|
>
|
|
</use>
|
|
</svg>`;
|
|
|
|
return $svg;
|
|
},
|
|
|
|
flag(country_code) {
|
|
return `<img loading="lazy" src="https://flagcdn.com/${country_code}.svg" width="20" height="15">`;
|
|
},
|
|
|
|
is_emoji(emoji_name) {
|
|
let emojiList = gemoji.map((emoji) => emoji.emoji);
|
|
return emojiList.includes(emoji_name);
|
|
},
|
|
|
|
get_desktop_icon(icon_name, variant) {
|
|
let exists = false;
|
|
let icon_data = this.get_desktop_icon_by_label(icon_name);
|
|
variant = variant.toLowerCase();
|
|
if (!icon_data?.app) return exists;
|
|
let app_name = icon_data.app;
|
|
let icon_url = `assets/${app_name}/icons/desktop_icons/${variant}/${frappe.scrub(
|
|
icon_name
|
|
)}.svg`;
|
|
|
|
if (
|
|
frappe.boot.desktop_icon_urls[app_name] &&
|
|
frappe.boot.desktop_icon_urls[app_name][variant].includes(icon_url)
|
|
) {
|
|
return `/${icon_url}`;
|
|
}
|
|
return exists;
|
|
},
|
|
|
|
desktop_icon_exists(app_name, url) {
|
|
let exists = false;
|
|
if (frappe.boot.desktop_icon_urls[app_name].includes(url)) exists = true;
|
|
return exists;
|
|
},
|
|
get_desktop_icon_by_label(title, filters) {
|
|
if (!filters) {
|
|
return frappe.boot.desktop_icons.find((f) => f.label === title);
|
|
} else {
|
|
return frappe.boot.desktop_icons.find((f) => {
|
|
return (
|
|
f.label === title &&
|
|
Object.keys(filters).every((key) => f[key] === filters[key])
|
|
);
|
|
});
|
|
}
|
|
},
|
|
|
|
make_chart(wrapper, custom_options = {}) {
|
|
let chart_args = {
|
|
type: "bar",
|
|
colors: ["light-blue"],
|
|
axisOptions: {
|
|
xIsSeries: 1,
|
|
shortenYAxisNumbers: 1,
|
|
xAxisMode: "tick",
|
|
numberFormatter: frappe.utils.format_chart_axis_number,
|
|
},
|
|
};
|
|
|
|
for (let key in custom_options) {
|
|
if (typeof chart_args[key] === "object" && typeof custom_options[key] === "object") {
|
|
chart_args[key] = Object.assign(chart_args[key], custom_options[key]);
|
|
} else {
|
|
chart_args[key] = custom_options[key];
|
|
}
|
|
}
|
|
frappe.utils.set_space_label_ratio(chart_args);
|
|
return new frappe.Chart(wrapper, chart_args);
|
|
},
|
|
|
|
format_chart_axis_number(label, country) {
|
|
const default_country = frappe.sys_defaults.country;
|
|
return frappe.utils.shorten_number(label, country || default_country, 3);
|
|
},
|
|
set_space_label_ratio(chart_args) {
|
|
if (chart_args.data.labels.length > 10) {
|
|
chart_args["axisOptions"]["seriesLabelSpaceRatio"] = 0.9;
|
|
}
|
|
},
|
|
generate_route(item) {
|
|
const type = item.type.toLowerCase();
|
|
if (type === "doctype") {
|
|
item.doctype = item.name;
|
|
}
|
|
let route = "";
|
|
if (!item.route) {
|
|
if (item.link) {
|
|
route = strip(item.link, "#");
|
|
} else if (type === "doctype") {
|
|
let doctype_slug = frappe.router.slug(item.doctype);
|
|
|
|
if (frappe.model.is_single(item.doctype)) {
|
|
route = `${doctype_slug}/${item.doctype}`;
|
|
} else {
|
|
switch (item.doc_view) {
|
|
case "List":
|
|
if (item.filters) {
|
|
frappe.route_options = item.filters;
|
|
}
|
|
route = `${doctype_slug}/view/list`;
|
|
break;
|
|
case "Tree":
|
|
route = `${doctype_slug}/view/tree`;
|
|
break;
|
|
case "Report Builder":
|
|
route = `${doctype_slug}/view/report`;
|
|
break;
|
|
case "Dashboard":
|
|
route = `${doctype_slug}/view/dashboard`;
|
|
break;
|
|
case "New":
|
|
route = `${doctype_slug}/new`;
|
|
break;
|
|
case "Calendar":
|
|
route = `${doctype_slug}/view/calendar/default`;
|
|
break;
|
|
case "Kanban":
|
|
route = `${doctype_slug}/view/kanban`;
|
|
if (item.kanban_board) {
|
|
route += `/${item.kanban_board}`;
|
|
}
|
|
break;
|
|
case "Image":
|
|
route = `${doctype_slug}/view/image`;
|
|
break;
|
|
default:
|
|
route = doctype_slug;
|
|
}
|
|
}
|
|
if (item.tab) {
|
|
route += `#${item.tab}`;
|
|
}
|
|
} else if (type === "report") {
|
|
if (item.is_query_report) {
|
|
route = "query-report/" + item.name;
|
|
} else if (!item.is_query_report && item.report_ref_doctype) {
|
|
route =
|
|
frappe.router.slug(item.report_ref_doctype) + "/view/report/" + item.name;
|
|
} else {
|
|
route = "report/" + item.name;
|
|
}
|
|
} else if (type === "page") {
|
|
route = item.name;
|
|
} else if (type === "dashboard") {
|
|
route = `dashboard-view/${item.name}`;
|
|
}
|
|
} else {
|
|
route = item.route;
|
|
}
|
|
|
|
if (item.route_options) {
|
|
route +=
|
|
"?" +
|
|
$.map(item.route_options, function (value, key) {
|
|
return encodeURIComponent(key) + "=" + encodeURIComponent(value);
|
|
}).join("&");
|
|
}
|
|
|
|
// if(type==="page" || type==="help" || type==="report" ||
|
|
// (item.doctype && frappe.model.can_read(item.doctype))) {
|
|
// item.shown = true;
|
|
// }
|
|
return `/desk/${route}`;
|
|
},
|
|
|
|
shorten_number: function (number, country, min_length = 4, max_no_of_decimals = 2) {
|
|
/* returns the number as an abbreviated string
|
|
* PARAMS
|
|
* number - number to be shortened
|
|
* country - country that determines the numnber system to be used
|
|
* min_length - length below which the number will not be shortened
|
|
* max_no_of_decimals - max number of decimals of the shortened number
|
|
*/
|
|
|
|
// return number if total digits is lesser than min_length
|
|
const len = String(number).match(/\d/g).length;
|
|
if (len < min_length) {
|
|
return number.toString();
|
|
}
|
|
|
|
const number_system = this.get_number_system(country);
|
|
let x = Math.abs(Math.round(number));
|
|
|
|
// if rounding was sufficient to get below min_length, return the rounded number
|
|
const x_string = x.toString();
|
|
if (x_string.length < min_length) {
|
|
return x_string;
|
|
}
|
|
|
|
for (const map of number_system) {
|
|
if (x >= map.divisor) {
|
|
let result = number / map.divisor;
|
|
const no_of_decimals = this.get_number_of_decimals(result);
|
|
/*
|
|
If no_of_decimals is greater than max_no_of_decimals,
|
|
round the number to max_no_of_decimals
|
|
*/
|
|
result =
|
|
no_of_decimals > max_no_of_decimals
|
|
? result.toFixed(max_no_of_decimals)
|
|
: result;
|
|
return result + " " + map.symbol;
|
|
}
|
|
}
|
|
|
|
return number.toFixed(max_no_of_decimals);
|
|
},
|
|
|
|
get_number_of_decimals: function (number) {
|
|
if (Math.floor(number) === number) return 0;
|
|
return number.toString().split(".")[1].length || 0;
|
|
},
|
|
|
|
build_summary_item(summary) {
|
|
if (summary.type == "separator") {
|
|
return $(`<div class="summary-separator">
|
|
<div class="summary-value ${summary.color ? summary.color.toLowerCase() : "text-muted"}">${
|
|
summary.value
|
|
}</div>
|
|
</div>`);
|
|
}
|
|
let df = { fieldtype: summary.datatype };
|
|
let doc = null;
|
|
if (summary.datatype == "Currency") {
|
|
df.options = "currency";
|
|
doc = { currency: summary.currency };
|
|
}
|
|
|
|
let value = frappe.format(summary.value, df, { only_value: true }, doc);
|
|
let color = summary.indicator
|
|
? summary.indicator.toLowerCase()
|
|
: summary.color
|
|
? summary.color.toLowerCase()
|
|
: "";
|
|
|
|
return $(`<div class="summary-item">
|
|
<span class="summary-label">${__(summary.label)}</span>
|
|
<div class="summary-value ${color}">${value}</div>
|
|
</div>`);
|
|
},
|
|
|
|
print(doctype, docname, print_format, letterhead, lang_code) {
|
|
let w = window.open(
|
|
frappe.urllib.get_full_url(
|
|
"/printview?doctype=" +
|
|
encodeURIComponent(doctype) +
|
|
"&name=" +
|
|
encodeURIComponent(docname) +
|
|
"&trigger_print=1" +
|
|
"&format=" +
|
|
encodeURIComponent(print_format) +
|
|
"&no_letterhead=" +
|
|
(letterhead ? "0" : "1") +
|
|
"&letterhead=" +
|
|
encodeURIComponent(letterhead) +
|
|
(lang_code ? "&_lang=" + lang_code : "")
|
|
)
|
|
);
|
|
|
|
if (!w) {
|
|
frappe.msgprint(__("Please enable pop-ups"));
|
|
return;
|
|
}
|
|
},
|
|
|
|
get_clipboard_data(clipboard_paste_event) {
|
|
let e = clipboard_paste_event;
|
|
let clipboard_data =
|
|
e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
|
|
return clipboard_data.getData("Text");
|
|
},
|
|
|
|
add_custom_button(html, action, class_name = "", title = "", btn_type, wrapper, prepend) {
|
|
if (!btn_type) btn_type = "btn-secondary";
|
|
let button = $(
|
|
`<button class="btn ${btn_type} btn-xs ${class_name}" title="${title}">${html}</button>`
|
|
);
|
|
button.click((event) => {
|
|
event.stopPropagation();
|
|
action && action(event);
|
|
});
|
|
!prepend && button.appendTo(wrapper);
|
|
prepend && wrapper.prepend(button);
|
|
},
|
|
|
|
add_select_group_button(wrapper, actions, btn_type, icon = "", prepend) {
|
|
// actions = [{
|
|
// label: "Action 1",
|
|
// description: "Description 1", (optional)
|
|
// action: () => {},
|
|
// },
|
|
// {
|
|
// label: "Action 2",
|
|
// description: "Description 2", (optional)
|
|
// action: () => {},
|
|
// }]
|
|
let selected_action = actions[0];
|
|
|
|
let $select_group_button = $(`
|
|
<div class="btn-group select-group-btn">
|
|
<button type="button" class="btn ${btn_type} btn-sm selected-button">
|
|
<span class="left-icon">${icon && frappe.utils.icon(icon, "xs")}</span>
|
|
<span class="label">${selected_action.label}</span>
|
|
</button>
|
|
|
|
<button type="button" class="btn ${btn_type} btn-sm dropdown-toggle dropdown-toggle-split" data-toggle="dropdown">
|
|
${frappe.utils.icon("down", "xs")}
|
|
</button>
|
|
|
|
<ul class="dropdown-menu dropdown-menu-right" role="menu"></ul>
|
|
</div>
|
|
`);
|
|
|
|
actions.forEach((action) => {
|
|
$(`<li>
|
|
<a class="dropdown-item flex">
|
|
<div class="tick-icon mr-2">${frappe.utils.icon("check", "xs")}</div>
|
|
<div>
|
|
<div class="item-label">${action.label}</div>
|
|
<div class="item-description text-muted small">${action.description || ""}</div>
|
|
</div>
|
|
</a>
|
|
</li>`)
|
|
.appendTo($select_group_button.find(".dropdown-menu"))
|
|
.click((e) => {
|
|
selected_action = action;
|
|
$select_group_button.find(".selected-button .label").text(action.label);
|
|
|
|
$(e.currentTarget).find(".tick-icon").addClass("selected");
|
|
$(e.currentTarget).siblings().find(".tick-icon").removeClass("selected");
|
|
});
|
|
});
|
|
|
|
$select_group_button.find(".dropdown-menu li:first-child .tick-icon").addClass("selected");
|
|
|
|
$select_group_button.find(".selected-button").click((event) => {
|
|
event.stopPropagation();
|
|
selected_action.action && selected_action.action(event);
|
|
});
|
|
|
|
!prepend && $select_group_button.appendTo(wrapper);
|
|
prepend && wrapper.prepend($select_group_button);
|
|
|
|
return $select_group_button;
|
|
},
|
|
|
|
sleep(time) {
|
|
return new Promise((resolve) => setTimeout(resolve, time));
|
|
},
|
|
|
|
parse_array(array) {
|
|
if (array && array.length !== 0) {
|
|
return array;
|
|
}
|
|
return undefined;
|
|
},
|
|
|
|
// simple implementation of python's range
|
|
range(start, end) {
|
|
if (!end) {
|
|
end = start;
|
|
start = 0;
|
|
}
|
|
let arr = [];
|
|
for (let i = start; i < end; i++) {
|
|
arr.push(i);
|
|
}
|
|
return arr;
|
|
},
|
|
|
|
get_link_title(doctype, name) {
|
|
if (!doctype || !name || !frappe._link_titles) {
|
|
return;
|
|
}
|
|
|
|
return frappe._link_titles[doctype + "::" + name];
|
|
},
|
|
|
|
add_link_title(doctype, name, value) {
|
|
if (!doctype || !name) {
|
|
return;
|
|
}
|
|
|
|
if (!frappe._link_titles) {
|
|
// for link titles
|
|
frappe._link_titles = {};
|
|
}
|
|
|
|
frappe._link_titles[doctype + "::" + name] = value;
|
|
},
|
|
|
|
fetch_link_title(doctype, name) {
|
|
if (!doctype || !name) {
|
|
return;
|
|
}
|
|
try {
|
|
return frappe
|
|
.xcall("frappe.desk.search.get_link_title", {
|
|
doctype: doctype,
|
|
docname: name,
|
|
})
|
|
.then((title) => {
|
|
frappe.utils.add_link_title(doctype, name, title);
|
|
return title;
|
|
});
|
|
} catch (error) {
|
|
console.log("Error while fetching link title.");
|
|
console.log(error);
|
|
return Promise.resolve(name);
|
|
}
|
|
},
|
|
|
|
only_allow_num_decimal(input) {
|
|
input.on("input", (e) => {
|
|
let self = $(e.target);
|
|
self.val(self.val().replace(/[^0-9.\-]/g, ""));
|
|
if (
|
|
(e.which != 46 || self.val().indexOf(".") != -1) &&
|
|
(e.which < 48 || e.which > 57)
|
|
) {
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
},
|
|
|
|
string_to_boolean(string) {
|
|
switch (string.toLowerCase().trim()) {
|
|
case "t":
|
|
case "true":
|
|
case "y":
|
|
case "yes":
|
|
case "1":
|
|
return true;
|
|
case "f":
|
|
case "false":
|
|
case "n":
|
|
case "no":
|
|
case "0":
|
|
case null:
|
|
return false;
|
|
default:
|
|
return string;
|
|
}
|
|
},
|
|
|
|
get_filter_as_json(filters) {
|
|
// convert filter array to json
|
|
let filter = null;
|
|
if (filters.length) {
|
|
filter = {};
|
|
filters.forEach((arr) => {
|
|
filter[arr[1]] = [arr[2], arr[3]];
|
|
});
|
|
filter = JSON.stringify(filter);
|
|
}
|
|
return filter;
|
|
},
|
|
|
|
process_filter_expression(filter) {
|
|
let filters = [];
|
|
filters = filter ? new Function(`return ${filter}`)() : [];
|
|
return this.cleanup_filters(filters);
|
|
},
|
|
cleanup_filters(filters) {
|
|
if (filters.length && filters[0].length == 5) {
|
|
filters.pop();
|
|
return filters;
|
|
}
|
|
return filters;
|
|
},
|
|
get_filter_from_json(filter_json, doctype) {
|
|
// convert json to filter array
|
|
if (filter_json) {
|
|
if (!filter_json.length) {
|
|
return [];
|
|
}
|
|
|
|
const filters_json = this.process_filter_expression(filter_json);
|
|
if (!doctype) {
|
|
// e.g. return {
|
|
// priority: (2) ['=', 'Medium'],
|
|
// status: (2) ['=', 'Open']
|
|
// }
|
|
|
|
// don't remove unless patch is created to convert all existing filters from object to array
|
|
// backward compatibility
|
|
if (Array.isArray(filters_json)) {
|
|
let filter = filters_json.reduce((acc, filter) => {
|
|
const field = filter[1];
|
|
const value = [filter[2], filter[3]];
|
|
|
|
// if we have multiple filters for the same field,
|
|
// we convert it into an array
|
|
if (acc[field]) {
|
|
acc[field].push(value);
|
|
} else {
|
|
acc[field] = [value];
|
|
}
|
|
return acc;
|
|
}, {});
|
|
return filter || [];
|
|
}
|
|
return filters_json || [];
|
|
}
|
|
|
|
// e.g. return [
|
|
// ['ToDo', 'status', '=', 'Open', false],
|
|
// ['ToDo', 'priority', '=', 'Medium', false]
|
|
// ]
|
|
if (Array.isArray(filters_json)) {
|
|
return filters_json;
|
|
}
|
|
// don't remove unless patch is created to convert all existing filters from object to array
|
|
// backward compatibility
|
|
return Object.keys(filters_json).map((filter) => {
|
|
let val = filters_json[filter];
|
|
return [doctype, filter, val[0], val[1], false];
|
|
});
|
|
}
|
|
},
|
|
|
|
load_video_player() {
|
|
return frappe.require("video_player.bundle.js");
|
|
},
|
|
|
|
is_current_user(user) {
|
|
return user === frappe.session.user;
|
|
},
|
|
|
|
debug: {
|
|
watch_property(obj, prop, callback = console.trace) {
|
|
console.warn("Adding property watcher, make sure to remove it after debugging.");
|
|
|
|
// Adapted from https://stackoverflow.com/a/11658693
|
|
// Reused under CC-BY-SA 4.0
|
|
// changes: variable names are changed for consistency with our codebase
|
|
const private_prop = "$_" + prop + "_$";
|
|
obj[private_prop] = obj[prop];
|
|
|
|
Object.defineProperty(obj, prop, {
|
|
get: function () {
|
|
return obj[private_prop];
|
|
},
|
|
set: function (value) {
|
|
callback();
|
|
obj[private_prop] = value;
|
|
},
|
|
});
|
|
},
|
|
},
|
|
generate_tracking_url() {
|
|
frappe.prompt(
|
|
[
|
|
{
|
|
fieldname: "url",
|
|
label: __("Web Page URL"),
|
|
fieldtype: "Data",
|
|
options: "URL",
|
|
reqd: 1,
|
|
default: localStorage.getItem("tracker_url:url"),
|
|
},
|
|
{
|
|
fieldname: "source",
|
|
label: __("Source"),
|
|
fieldtype: "Link",
|
|
reqd: 1,
|
|
options: "UTM Source",
|
|
description: "The referrer (e.g. google, newsletter)",
|
|
default: localStorage.getItem("tracker_url:source"),
|
|
},
|
|
{
|
|
fieldname: "campaign",
|
|
label: __("Campaign"),
|
|
fieldtype: "Link",
|
|
ignore_link_validation: 1,
|
|
options: "UTM Campaign",
|
|
default: localStorage.getItem("tracker_url:campaign"),
|
|
},
|
|
{
|
|
fieldname: "medium",
|
|
label: __("Medium"),
|
|
fieldtype: "Link",
|
|
options: "UTM Medium",
|
|
description: "Marketing medium (e.g. cpc, banner, email)",
|
|
default: localStorage.getItem("tracker_url:medium"),
|
|
},
|
|
{
|
|
fieldname: "content",
|
|
label: __("Content"),
|
|
fieldtype: "Data",
|
|
description: "Use to differentiate ad variants (e.g. A/B testing)",
|
|
default: localStorage.getItem("tracker_url:content"),
|
|
},
|
|
],
|
|
async function (data) {
|
|
let url = data.url;
|
|
localStorage.setItem("tracker_url:url", data.url);
|
|
|
|
const { message } = await frappe.db.get_value("UTM Source", data.source, "slug");
|
|
url += "?utm_source=" + encodeURIComponent(message.slug || data.source);
|
|
localStorage.setItem("tracker_url:source", data.source);
|
|
if (data.campaign) {
|
|
const { message } = await frappe.db.get_value(
|
|
"UTM Campaign",
|
|
data.campaign,
|
|
"slug"
|
|
);
|
|
url += "&utm_campaign=" + encodeURIComponent(message.slug || data.campaign);
|
|
localStorage.setItem("tracker_url:campaign", data.campaign);
|
|
}
|
|
if (data.medium) {
|
|
const { message } = await frappe.db.get_value(
|
|
"UTM Medium",
|
|
data.medium,
|
|
"slug"
|
|
);
|
|
url += "&utm_medium=" + encodeURIComponent(message.slug || data.medium);
|
|
localStorage.setItem("tracker_url:medium", data.medium);
|
|
}
|
|
if (data.content) {
|
|
url += "&utm_content=" + encodeURIComponent(data.content);
|
|
localStorage.setItem("tracker_url:content", data.content);
|
|
}
|
|
|
|
frappe.utils.copy_to_clipboard(url);
|
|
|
|
frappe.msgprint(
|
|
__("Tracking URL generated and copied to clipboard") +
|
|
": <br>" +
|
|
`<a href="${url}">${url.bold()}</a>`,
|
|
__("Here's your tracking URL")
|
|
);
|
|
},
|
|
__("Generate Tracking URL")
|
|
);
|
|
},
|
|
/**
|
|
* Checks if a value is empty.
|
|
*
|
|
* Returns false for: "hello", 0, 1, 3.1415, {"a": 1}, [1, 2, 3]
|
|
* Returns true for: "", null, undefined, {}, []
|
|
*
|
|
* @param {*} value - The value to check.
|
|
* @returns {boolean} - Returns `true` if the value is empty, `false` otherwise.
|
|
*/
|
|
is_empty(value) {
|
|
if (!value && value !== 0) return true;
|
|
|
|
if (typeof value === "object")
|
|
return (Array.isArray(value) ? value : Object.keys(value)).length === 0;
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Masks passwords in an object by replacing the values of keys containing
|
|
* "password" or "passphrase" with "*****".
|
|
*
|
|
* @param {Object} obj - The object to mask passwords in.
|
|
*/
|
|
mask_passwords(obj) {
|
|
const KEYWORDS_TO_MASK = ["password", "passphrase"];
|
|
for (const key of Object.keys(obj)) {
|
|
if (KEYWORDS_TO_MASK.some((keyword) => key.includes(keyword)) && obj[key]) {
|
|
obj[key] = "*****";
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Adds syntax highlighting to all <pre> tags in the given jQuery wrapper.
|
|
* Example wrapper:
|
|
*
|
|
* ```html
|
|
* <pre><code class="language-python">
|
|
* def add(a, b):
|
|
* return a + b
|
|
*
|
|
* print(add(1, 2))
|
|
*
|
|
* # Output: 3
|
|
* </code></pre>
|
|
* ```
|
|
*
|
|
* @param {jQuery} $wrapper - The jQuery wrapper to add syntax highlighting to.
|
|
*/
|
|
highlight_pre($wrapper) {
|
|
frappe.require("syntax_highlighting.bundle.js").then(() => {
|
|
$wrapper.find("pre").each(function () {
|
|
hljs.highlightElement(this);
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Check if current user can upload public files.
|
|
* @returns {boolean}
|
|
*/
|
|
can_upload_public_files() {
|
|
if (
|
|
Number(frappe.boot.sysdefaults?.only_allow_system_managers_to_upload_public_files) !==
|
|
1
|
|
) {
|
|
return true;
|
|
}
|
|
return frappe.user.has_role(["System Manager", "Administrator"]);
|
|
},
|
|
|
|
get_help_siblings() {
|
|
const navbar_settings = frappe.boot.navbar_settings;
|
|
let help_dropdown_items = [];
|
|
|
|
let custom_help_links = this.get_custom_help_links();
|
|
|
|
help_dropdown_items = custom_help_links.concat(help_dropdown_items);
|
|
|
|
navbar_settings.help_dropdown.forEach((element) => {
|
|
let dropdown_children = {
|
|
name: element.name,
|
|
label: element.item_label,
|
|
};
|
|
if (element.item_type === "Route") {
|
|
dropdown_children.url = element.route;
|
|
}
|
|
if (element.item_type === "Action") {
|
|
dropdown_children.onClick = function () {
|
|
frappe.utils.eval(element.action);
|
|
};
|
|
}
|
|
help_dropdown_items.push(dropdown_children);
|
|
});
|
|
|
|
return help_dropdown_items;
|
|
},
|
|
get_custom_help_links() {
|
|
let route = frappe.get_route_str();
|
|
let breadcrumbs = route.split("/");
|
|
|
|
let links = [];
|
|
for (let i = 0; i < breadcrumbs.length; i++) {
|
|
let r = route.split("/", i + 1);
|
|
let key = r.join("/");
|
|
let help_links = frappe.help.help_links[key] || [];
|
|
links = $.merge(links, help_links);
|
|
}
|
|
if (links.length) {
|
|
links.push({ is_divider: true });
|
|
}
|
|
return links;
|
|
},
|
|
});
|