You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1577 rivejä
38 KiB

  1. // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. // MIT License. See license.txt
  3. import deep_equal from "fast-deep-equal";
  4. frappe.provide("frappe.utils");
  5. // Array de duplicate
  6. if (!Array.prototype.uniqBy) {
  7. Object.defineProperty(Array.prototype, 'uniqBy', {
  8. value: function (key) {
  9. var seen = {};
  10. return this.filter(function (item) {
  11. var k = key(item);
  12. return k in seen ? false : (seen[k] = true);
  13. });
  14. }
  15. });
  16. Object.defineProperty(Array.prototype, 'move', {
  17. value: function(from, to) {
  18. this.splice(to, 0, this.splice(from, 1)[0]);
  19. }
  20. });
  21. }
  22. // Python's dict.setdefault ported for JS objects
  23. Object.defineProperty(Object.prototype, "setDefault", {
  24. value: function(key, default_value) {
  25. if (!(key in this)) this[key] = default_value;
  26. return this[key];
  27. },
  28. writable: true
  29. });
  30. // Pluralize
  31. String.prototype.plural = function(revert) {
  32. const plural = {
  33. "(quiz)$": "$1zes",
  34. "^(ox)$": "$1en",
  35. "([m|l])ouse$": "$1ice",
  36. "(matr|vert|ind)ix|ex$": "$1ices",
  37. "(x|ch|ss|sh)$": "$1es",
  38. "([^aeiouy]|qu)y$": "$1ies",
  39. "(hive)$": "$1s",
  40. "(?:([^f])fe|([lr])f)$": "$1$2ves",
  41. "(shea|lea|loa|thie)f$": "$1ves",
  42. sis$: "ses",
  43. "([ti])um$": "$1a",
  44. "(tomat|potat|ech|her|vet)o$": "$1oes",
  45. "(bu)s$": "$1ses",
  46. "(alias)$": "$1es",
  47. "(octop)us$": "$1i",
  48. "(ax|test)is$": "$1es",
  49. "(us)$": "$1es",
  50. "([^s]+)$": "$1s",
  51. };
  52. const singular = {
  53. "(quiz)zes$": "$1",
  54. "(matr)ices$": "$1ix",
  55. "(vert|ind)ices$": "$1ex",
  56. "^(ox)en$": "$1",
  57. "(alias)es$": "$1",
  58. "(octop|vir)i$": "$1us",
  59. "(cris|ax|test)es$": "$1is",
  60. "(shoe)s$": "$1",
  61. "(o)es$": "$1",
  62. "(bus)es$": "$1",
  63. "([m|l])ice$": "$1ouse",
  64. "(x|ch|ss|sh)es$": "$1",
  65. "(m)ovies$": "$1ovie",
  66. "(s)eries$": "$1eries",
  67. "([^aeiouy]|qu)ies$": "$1y",
  68. "([lr])ves$": "$1f",
  69. "(tive)s$": "$1",
  70. "(hive)s$": "$1",
  71. "(li|wi|kni)ves$": "$1fe",
  72. "(shea|loa|lea|thie)ves$": "$1f",
  73. "(^analy)ses$": "$1sis",
  74. "((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$":
  75. "$1$2sis",
  76. "([ti])a$": "$1um",
  77. "(n)ews$": "$1ews",
  78. "(h|bl)ouses$": "$1ouse",
  79. "(corpse)s$": "$1",
  80. "(us)es$": "$1",
  81. s$: "",
  82. };
  83. const irregular = {
  84. move: "moves",
  85. foot: "feet",
  86. goose: "geese",
  87. sex: "sexes",
  88. child: "children",
  89. man: "men",
  90. tooth: "teeth",
  91. person: "people",
  92. };
  93. const uncountable = [
  94. "sheep",
  95. "fish",
  96. "deer",
  97. "moose",
  98. "series",
  99. "species",
  100. "money",
  101. "rice",
  102. "information",
  103. "equipment",
  104. ];
  105. // save some time in the case that singular and plural are the same
  106. if (uncountable.indexOf(this.toLowerCase()) >= 0) return this;
  107. // check for irregular forms
  108. let word;
  109. let pattern;
  110. let replace;
  111. for (word in irregular) {
  112. if (revert) {
  113. pattern = new RegExp(irregular[word] + "$", "i");
  114. replace = word;
  115. } else {
  116. pattern = new RegExp(word + "$", "i");
  117. replace = irregular[word];
  118. }
  119. if (pattern.test(this)) return this.replace(pattern, replace);
  120. }
  121. let array;
  122. if (revert) array = singular;
  123. else array = plural;
  124. // check for matches using regular expressions
  125. let reg;
  126. for (reg in array) {
  127. pattern = new RegExp(reg, "i");
  128. if (pattern.test(this)) return this.replace(pattern, array[reg]);
  129. }
  130. return this;
  131. };
  132. Object.assign(frappe.utils, {
  133. get_random: function(len) {
  134. var text = "";
  135. var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
  136. for ( var i=0; i < len; i++ )
  137. text += possible.charAt(Math.floor(Math.random() * possible.length));
  138. return text;
  139. },
  140. get_file_link: function(filename) {
  141. filename = cstr(filename);
  142. if (frappe.utils.is_url(filename)) {
  143. return filename;
  144. } else if (filename.indexOf("/")===-1) {
  145. return "files/" + filename;
  146. } else {
  147. return filename;
  148. }
  149. },
  150. replace_newlines(t) {
  151. return t?t.replace(/\n/g, '<br>'):'';
  152. },
  153. is_html: function(txt) {
  154. if (!txt) return false;
  155. if (txt.indexOf("<br>")==-1 && txt.indexOf("<p")==-1
  156. && txt.indexOf("<img")==-1 && txt.indexOf("<div")==-1 && !txt.includes('<span')) {
  157. return false;
  158. }
  159. return true;
  160. },
  161. is_mac: function() {
  162. return window.navigator.platform === 'MacIntel';
  163. },
  164. is_xs: function() {
  165. return $(document).width() < 768;
  166. },
  167. is_sm: function() {
  168. return $(document).width() < 991 && $(document).width() >= 768;
  169. },
  170. is_md: function() {
  171. return $(document).width() < 1199 && $(document).width() >= 991;
  172. },
  173. is_json: function(str) {
  174. try {
  175. JSON.parse(str);
  176. } catch (e) {
  177. return false;
  178. }
  179. return true;
  180. },
  181. parse_json: function(str) {
  182. let parsed_json = '';
  183. try {
  184. parsed_json = JSON.parse(str);
  185. } catch (e) {
  186. return str;
  187. }
  188. return parsed_json;
  189. },
  190. strip_whitespace: function(html) {
  191. return (html || "").replace(/<p>\s*<\/p>/g, "").replace(/<br>(\s*<br>\s*)+/g, "<br><br>");
  192. },
  193. encode_tags: function(html) {
  194. var tagsToReplace = {
  195. '&': '&amp;',
  196. '<': '&lt;',
  197. '>': '&gt;'
  198. };
  199. function replaceTag(tag) {
  200. return tagsToReplace[tag] || tag;
  201. }
  202. return html.replace(/[&<>]/g, replaceTag);
  203. },
  204. strip_original_content: function(txt) {
  205. var out = [],
  206. part = [],
  207. newline = txt.indexOf("<br>")===-1 ? "\n" : "<br>";
  208. $.each(txt.split(newline), function(i, t) {
  209. var tt = strip(t);
  210. if (tt && (tt.substr(0, 1)===">" || tt.substr(0, 4)==="&gt;")) {
  211. part.push(t);
  212. } else {
  213. out = out.concat(part);
  214. out.push(t);
  215. part = [];
  216. }
  217. });
  218. return out.join(newline);
  219. },
  220. escape_html: function(txt) {
  221. let escape_html_mapping = {
  222. '&': '&amp;',
  223. '<': '&lt;',
  224. '>': '&gt;',
  225. '"': '&quot;',
  226. "'": '&#39;',
  227. '/': '&#x2F;',
  228. '`': '&#x60;',
  229. '=': '&#x3D;'
  230. };
  231. return String(txt).replace(
  232. /[&<>"'`=/]/g,
  233. char => escape_html_mapping[char] || char
  234. );
  235. },
  236. unescape_html: function(txt) {
  237. let unescape_html_mapping = {
  238. '&amp;': '&',
  239. '&lt;': '<',
  240. '&gt;': '>',
  241. '&quot;': '"',
  242. '&#39;': "'",
  243. '&#x2F;': '/',
  244. '&#x60;': '`',
  245. '&#x3D;': '='
  246. };
  247. return String(txt).replace(
  248. /&amp;|&lt;|&gt;|&quot;|&#39;|&#x2F;|&#x60;|&#x3D;/g,
  249. char => unescape_html_mapping[char] || char
  250. );
  251. },
  252. html2text: function(html) {
  253. let d = document.createElement('div');
  254. d.innerHTML = html;
  255. return d.textContent;
  256. },
  257. is_url: function(txt) {
  258. return txt.toLowerCase().substr(0, 7)=='http://'
  259. || txt.toLowerCase().substr(0, 8)=='https://';
  260. },
  261. to_title_case: function(string, with_space=false) {
  262. let titlecased_string = string.toLowerCase().replace(/(?:^|[\s-/])\w/g, function(match) {
  263. return match.toUpperCase();
  264. });
  265. let replace_with = with_space ? ' ' : '';
  266. return titlecased_string.replace(/-|_/g, replace_with);
  267. },
  268. toggle_blockquote: function(txt) {
  269. if (!txt) return txt;
  270. var content = $("<div></div>").html(txt);
  271. content.find("blockquote").parent("blockquote").addClass("hidden")
  272. .before('<p><a class="text-muted btn btn-default toggle-blockquote" style="padding: 2px 7px 0px; line-height: 1;"> \
  273. • • • \
  274. </a></p>');
  275. return content.html();
  276. },
  277. scroll_to: function(element, animate=true, additional_offset,
  278. element_to_be_scrolled, callback, highlight_element=false) {
  279. if (frappe.flags.disable_auto_scroll) return;
  280. element_to_be_scrolled = element_to_be_scrolled || $("html, body");
  281. let scroll_top = 0;
  282. if (element) {
  283. // If a number is passed, just subtract the offset,
  284. // otherwise calculate scroll position from element
  285. scroll_top = typeof element == "number"
  286. ? element - cint(additional_offset)
  287. : this.get_scroll_position(element, additional_offset);
  288. }
  289. if (scroll_top < 0) {
  290. scroll_top = 0;
  291. }
  292. // already there
  293. if (scroll_top == element_to_be_scrolled.scrollTop()) {
  294. return;
  295. }
  296. if (animate) {
  297. element_to_be_scrolled.animate({
  298. scrollTop: scroll_top
  299. }).promise().then(() => {
  300. if (highlight_element) {
  301. $(element).addClass('highlight');
  302. document.addEventListener("click", function() {
  303. $(element).removeClass('highlight');
  304. }, {once: true});
  305. }
  306. callback && callback();
  307. });
  308. } else {
  309. element_to_be_scrolled.scrollTop(scroll_top);
  310. }
  311. },
  312. get_scroll_position: function(element, additional_offset) {
  313. let header_offset = $(".navbar").height() + $(".page-head:visible").height() || $(".navbar").height();
  314. let scroll_top = $(element).offset().top - header_offset - cint(additional_offset);
  315. return scroll_top;
  316. },
  317. filter_dict: function(dict, filters) {
  318. var ret = [];
  319. if (typeof filters=='string') {
  320. return [dict[filters]];
  321. }
  322. $.each(dict, function(i, d) {
  323. for (var key in filters) {
  324. if ($.isArray(filters[key])) {
  325. if (filters[key][0]=="in") {
  326. if (filters[key][1].indexOf(d[key])==-1)
  327. return;
  328. } else if (filters[key][0]=="not in") {
  329. if (filters[key][1].indexOf(d[key])!=-1)
  330. return;
  331. } else if (filters[key][0]=="<") {
  332. if (!(d[key] < filters[key])) return;
  333. } else if (filters[key][0]=="<=") {
  334. if (!(d[key] <= filters[key])) return;
  335. } else if (filters[key][0]==">") {
  336. if (!(d[key] > filters[key])) return;
  337. } else if (filters[key][0]==">=") {
  338. if (!(d[key] >= filters[key])) return;
  339. }
  340. } else {
  341. if (d[key]!=filters[key]) return;
  342. }
  343. }
  344. ret.push(d);
  345. });
  346. return ret;
  347. },
  348. comma_or: function(list) {
  349. return frappe.utils.comma_sep(list, " " + __("or") + " ");
  350. },
  351. comma_and: function(list) {
  352. return frappe.utils.comma_sep(list, " " + __("and") + " ");
  353. },
  354. comma_sep: function(list, sep) {
  355. if (list instanceof Array) {
  356. if (list.length==0) {
  357. return "";
  358. } else if (list.length==1) {
  359. return list[0];
  360. } else {
  361. return list.slice(0, list.length-1).join(", ") + sep + list.slice(-1)[0];
  362. }
  363. } else {
  364. return list;
  365. }
  366. },
  367. set_footnote: function(footnote_area, wrapper, txt) {
  368. if (!footnote_area) {
  369. footnote_area = $('<div class="text-muted footnote-area level">')
  370. .appendTo(wrapper);
  371. }
  372. if (txt) {
  373. footnote_area.html(txt);
  374. } else {
  375. footnote_area.remove();
  376. footnote_area = null;
  377. }
  378. return footnote_area;
  379. },
  380. get_args_dict_from_url: function(txt) {
  381. var args = {};
  382. $.each(decodeURIComponent(txt).split("&"), function(i, arg) {
  383. arg = arg.split("=");
  384. args[arg[0]] = arg[1];
  385. });
  386. return args;
  387. },
  388. get_url_from_dict: function(args) {
  389. return $.map(args, function(val, key) {
  390. if (val!==null)
  391. return encodeURIComponent(key)+"="+encodeURIComponent(val);
  392. else
  393. return null;
  394. }).join("&") || "";
  395. },
  396. validate_type: function ( val, type ) {
  397. // from https://github.com/guillaumepotier/Parsley.js/blob/master/parsley.js#L81
  398. var regExp;
  399. switch ( type ) {
  400. case "phone":
  401. regExp = /^([0-9 +_\-,.*#()]){1,20}$/;
  402. break;
  403. case "name":
  404. regExp = /^[\w][\w'-]*([ \w][\w'-]+)*$/;
  405. break;
  406. case "number":
  407. regExp = /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/;
  408. break;
  409. case "digits":
  410. regExp = /^\d+$/;
  411. break;
  412. case "alphanum":
  413. regExp = /^\w+$/;
  414. break;
  415. case "email":
  416. // from https://emailregex.com/
  417. 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,}))$/;
  418. break;
  419. case "url":
  420. regExp = /^((([A-Za-z0-9.+-]+:(?:\/\/)?)(?:[-;:&=\+\,\w]@)?[A-Za-z0-9.-]+(:[0-9]+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/i;
  421. break;
  422. case "dateIso":
  423. regExp = /^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$/;
  424. break;
  425. default:
  426. return false;
  427. }
  428. // test regExp if not null
  429. return '' !== val ? regExp.test( val ) : false;
  430. },
  431. guess_style: function(text, default_style, _colour) {
  432. var style = default_style || "default";
  433. var colour = "gray";
  434. if (text) {
  435. if (has_words(["Pending", "Review", "Medium", "Not Approved"], text)) {
  436. style = "warning";
  437. colour = "orange";
  438. } else if (has_words(["Open", "Urgent", "High", "Failed", "Rejected", "Error"], text)) {
  439. style = "danger";
  440. colour = "red";
  441. } else if (has_words(["Closed", "Finished", "Converted", "Completed", "Complete", "Confirmed",
  442. "Approved", "Yes", "Active", "Available", "Paid", "Success"], text)) {
  443. style = "success";
  444. colour = "green";
  445. } else if (has_words(["Submitted"], text)) {
  446. style = "info";
  447. colour = "blue";
  448. }
  449. }
  450. return _colour ? colour : style;
  451. },
  452. guess_colour: function(text) {
  453. return frappe.utils.guess_style(text, null, true);
  454. },
  455. get_indicator_color: function(state) {
  456. return frappe.db.get_list('Workflow State', {filters: {name: state}, fields: ['name', 'style']}).then(res => {
  457. const state = res[0];
  458. if (!state.style) {
  459. return frappe.utils.guess_colour(state.name);
  460. }
  461. const style = state.style;
  462. const colour_map = {
  463. "Success": "green",
  464. "Warning": "orange",
  465. "Danger": "red",
  466. "Primary": "blue",
  467. };
  468. return colour_map[style];
  469. });
  470. },
  471. sort: function(list, key, compare_type, reverse) {
  472. if (!list || list.length < 2)
  473. return list || [];
  474. var sort_fn = {
  475. "string": function(a, b) {
  476. return cstr(a[key]).localeCompare(cstr(b[key]));
  477. },
  478. "number": function(a, b) {
  479. return flt(a[key]) - flt(b[key]);
  480. }
  481. };
  482. if (!compare_type)
  483. compare_type = typeof list[0][key]==="string" ? "string" : "number";
  484. list.sort(sort_fn[compare_type]);
  485. if (reverse) {
  486. list.reverse();
  487. }
  488. return list;
  489. },
  490. unique: function(list) {
  491. var dict = {},
  492. arr = [];
  493. for (var i=0, l=list.length; i < l; i++) {
  494. if (!(list[i] in dict)) {
  495. dict[list[i]] = null;
  496. arr.push(list[i]);
  497. }
  498. }
  499. return arr;
  500. },
  501. remove_nulls: function(list) {
  502. var new_list = [];
  503. for (var i=0, l=list.length; i < l; i++) {
  504. if (!is_null(list[i])) {
  505. new_list.push(list[i]);
  506. }
  507. }
  508. return new_list;
  509. },
  510. all: function(lst) {
  511. for (var i=0, l=lst.length; i<l; i++) {
  512. if (!lst[i]) {
  513. return false;
  514. }
  515. }
  516. return true;
  517. },
  518. dict: function(keys, values) {
  519. // make dictionaries from keys and values
  520. var out = [];
  521. $.each(values, function(row_idx, row) {
  522. var new_row = {};
  523. $.each(keys, function(key_idx, key) {
  524. new_row[key] = row[key_idx];
  525. });
  526. out.push(new_row);
  527. });
  528. return out;
  529. },
  530. sum: function(list) {
  531. return list.reduce(function(previous_value, current_value) {
  532. return flt(previous_value) + flt(current_value);
  533. }, 0.0);
  534. },
  535. arrays_equal: function(arr1, arr2) {
  536. if (!arr1 || !arr2) {
  537. return false;
  538. }
  539. if (arr1.length != arr2.length) {
  540. return false;
  541. }
  542. for (var i = 0; i < arr1.length; i++) {
  543. if ($.isArray(arr1[i])) {
  544. if (!frappe.utils.arrays_equal(arr1[i], arr2[i])) {
  545. return false;
  546. }
  547. } else if (arr1[i] !== arr2[i]) {
  548. return false;
  549. }
  550. }
  551. return true;
  552. },
  553. intersection: function(a, b) {
  554. // from stackoverflow: http://stackoverflow.com/questions/1885557/simplest-code-for-array-intersection-in-javascript
  555. /* finds the intersection of
  556. * two arrays in a simple fashion.
  557. *
  558. * PARAMS
  559. * a - first array, must already be sorted
  560. * b - second array, must already be sorted
  561. *
  562. * NOTES
  563. *
  564. * Should have O(n) operations, where n is
  565. * n = MIN(a.length(), b.length())
  566. */
  567. var ai=0, bi=0;
  568. var result = new Array();
  569. // sorted copies
  570. a = ([].concat(a)).sort();
  571. b = ([].concat(b)).sort();
  572. while ( ai < a.length && bi < b.length ) {
  573. if (a[ai] < b[bi] ) {
  574. ai++;
  575. } else if (a[ai] > b[bi] ) {
  576. bi++;
  577. } else {
  578. /* they're equal */
  579. result.push(a[ai]);
  580. ai++;
  581. bi++;
  582. }
  583. }
  584. return result;
  585. },
  586. resize_image: function(reader, callback, max_width, max_height) {
  587. var tempImg = new Image();
  588. if (!max_width) max_width = 600;
  589. if (!max_height) max_height = 400;
  590. tempImg.src = reader.result;
  591. tempImg.onload = function() {
  592. var tempW = tempImg.width;
  593. var tempH = tempImg.height;
  594. if (tempW > tempH) {
  595. if (tempW > max_width) {
  596. tempH *= max_width / tempW;
  597. tempW = max_width;
  598. }
  599. } else {
  600. if (tempH > max_height) {
  601. tempW *= max_height / tempH;
  602. tempH = max_height;
  603. }
  604. }
  605. var canvas = document.createElement('canvas');
  606. canvas.width = tempW;
  607. canvas.height = tempH;
  608. var ctx = canvas.getContext("2d");
  609. ctx.drawImage(this, 0, 0, tempW, tempH);
  610. var dataURL = canvas.toDataURL("image/jpeg");
  611. setTimeout(function() {
  612. callback(dataURL);
  613. }, 10 );
  614. };
  615. },
  616. csv_to_array: function (strData, strDelimiter) {
  617. // Check to see if the delimiter is defined. If not,
  618. // then default to comma.
  619. strDelimiter = (strDelimiter || ",");
  620. // Create a regular expression to parse the CSV values.
  621. var objPattern = new RegExp(
  622. (
  623. // Delimiters.
  624. "(\\" + strDelimiter + "|\\r?\\n|\\r|^)" +
  625. // Quoted fields.
  626. "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" +
  627. // Standard fields.
  628. "([^\"\\" + strDelimiter + "\\r\\n]*))"
  629. ),
  630. "gi"
  631. );
  632. // Create an array to hold our data. Give the array
  633. // a default empty first row.
  634. var arrData = [[]];
  635. // Create an array to hold our individual pattern
  636. // matching groups.
  637. var arrMatches = null;
  638. // Keep looping over the regular expression matches
  639. // until we can no longer find a match.
  640. while ((arrMatches = objPattern.exec( strData ))) {
  641. // Get the delimiter that was found.
  642. var strMatchedDelimiter = arrMatches[ 1 ];
  643. // Check to see if the given delimiter has a length
  644. // (is not the start of string) and if it matches
  645. // field delimiter. If id does not, then we know
  646. // that this delimiter is a row delimiter.
  647. if (
  648. strMatchedDelimiter.length &&
  649. strMatchedDelimiter !== strDelimiter
  650. ) {
  651. // Since we have reached a new row of data,
  652. // add an empty row to our data array.
  653. arrData.push( [] );
  654. }
  655. var strMatchedValue;
  656. // Now that we have our delimiter out of the way,
  657. // let's check to see which kind of value we
  658. // captured (quoted or unquoted).
  659. if (arrMatches[ 2 ]) {
  660. // We found a quoted value. When we capture
  661. // this value, unescape any double quotes.
  662. strMatchedValue = arrMatches[ 2 ].replace(
  663. new RegExp( "\"\"", "g" ),
  664. "\""
  665. );
  666. } else {
  667. // We found a non-quoted value.
  668. strMatchedValue = arrMatches[ 3 ];
  669. }
  670. // Now that we have our value string, let's add
  671. // it to the data array.
  672. arrData[ arrData.length - 1 ].push( strMatchedValue );
  673. }
  674. // Return the parsed data.
  675. return ( arrData );
  676. },
  677. warn_page_name_change: function() {
  678. frappe.msgprint(__("Note: Changing the Page Name will break previous URL to this page."));
  679. },
  680. notify: function(subject, body, route, onclick) {
  681. console.log('push notifications are evil and deprecated');
  682. },
  683. set_title: function(title) {
  684. frappe._original_title = title;
  685. if (frappe._title_prefix) {
  686. title = frappe._title_prefix + " " + title.replace(/<[^>]*>/g, "");
  687. }
  688. document.title = title;
  689. // save for re-routing
  690. const sub_path = frappe.router.get_sub_path();
  691. frappe.route_titles[sub_path] = title;
  692. },
  693. set_title_prefix: function(prefix) {
  694. frappe._title_prefix = prefix;
  695. // reset the original title
  696. frappe.utils.set_title(frappe._original_title);
  697. },
  698. is_image_file: function(filename) {
  699. if (!filename) return false;
  700. // url can have query params
  701. filename = filename.split('?')[0];
  702. return (/\.(gif|jpg|jpeg|tiff|png|svg)$/i).test(filename);
  703. },
  704. play_sound: function(name) {
  705. try {
  706. if (frappe.boot.user.mute_sounds) {
  707. return;
  708. }
  709. var audio = $("#sound-" + name)[0];
  710. audio.volume = audio.getAttribute("volume");
  711. audio.play();
  712. } catch (e) {
  713. console.log("Cannot play sound", name, e);
  714. // pass
  715. }
  716. },
  717. split_emails: function(txt) {
  718. var email_list = [];
  719. if (!txt) {
  720. return email_list;
  721. }
  722. // emails can be separated by comma or newline
  723. txt.split(/[,\n](?=(?:[^"]|"[^"]*")*$)/g).forEach(function(email) {
  724. email = email.trim();
  725. if (email) {
  726. email_list.push(email);
  727. }
  728. });
  729. return email_list;
  730. },
  731. supportsES6: function() {
  732. try {
  733. new Function("(a = 0) => a");
  734. return true;
  735. } catch (err) {
  736. return false;
  737. }
  738. }(),
  739. throttle: function (func, wait, options) {
  740. var context, args, result;
  741. var timeout = null;
  742. var previous = 0;
  743. if (!options) options = {};
  744. let later = function () {
  745. previous = options.leading === false ? 0 : Date.now();
  746. timeout = null;
  747. result = func.apply(context, args);
  748. if (!timeout) context = args = null;
  749. };
  750. return function () {
  751. var now = Date.now();
  752. if (!previous && options.leading === false) previous = now;
  753. let remaining = wait - (now - previous);
  754. context = this;
  755. args = arguments;
  756. if (remaining <= 0 || remaining > wait) {
  757. if (timeout) {
  758. clearTimeout(timeout);
  759. timeout = null;
  760. }
  761. previous = now;
  762. result = func.apply(context, args);
  763. if (!timeout) context = args = null;
  764. } else if (!timeout && options.trailing !== false) {
  765. timeout = setTimeout(later, remaining);
  766. }
  767. return result;
  768. };
  769. },
  770. debounce: function(func, wait, immediate) {
  771. var timeout;
  772. return function() {
  773. var context = this, args = arguments;
  774. var later = function() {
  775. timeout = null;
  776. if (!immediate) func.apply(context, args);
  777. };
  778. var callNow = immediate && !timeout;
  779. clearTimeout(timeout);
  780. timeout = setTimeout(later, wait);
  781. if (callNow) func.apply(context, args);
  782. };
  783. },
  784. get_form_link: function(doctype, name, html=false, display_text=null, query_params_obj=null) {
  785. display_text = display_text || name;
  786. name = encodeURIComponent(name);
  787. let route = `/app/${encodeURIComponent(doctype.toLowerCase().replace(/ /g, '-'))}/${name}`;
  788. if (query_params_obj) {
  789. route += frappe.utils.make_query_string(query_params_obj);
  790. }
  791. if (html) {
  792. return `<a href="${route}">${display_text}</a>`;
  793. }
  794. return route;
  795. },
  796. get_route_label(route_str) {
  797. let route = route_str.split('/');
  798. if (route[2] === 'Report' || route[0] === 'query-report') {
  799. return __('{0} Report', [route[3] || route[1]]);
  800. }
  801. if (route[0] === 'List') {
  802. return __('{0} List', [route[1]]);
  803. }
  804. if (route[0] === 'modules') {
  805. return __('{0} Modules', [route[1]]);
  806. }
  807. if (route[0] === 'dashboard') {
  808. return __('{0} Dashboard', [route[1]]);
  809. }
  810. return __(frappe.utils.to_title_case(route[0], true));
  811. },
  812. report_column_total: function(values, column, type) {
  813. if (column.column.disable_total) {
  814. return '';
  815. } else if (values.length > 0) {
  816. if (column.column.fieldtype == "Percent" || type === "mean") {
  817. return values.reduce((a, b) => a + flt(b)) / values.length;
  818. } else if (column.column.fieldtype == "Int") {
  819. return values.reduce((a, b) => a + cint(b));
  820. } else if (frappe.model.is_numeric_field(column.column.fieldtype)) {
  821. return values.reduce((a, b) => a + flt(b));
  822. } else {
  823. return null;
  824. }
  825. } else {
  826. return null;
  827. }
  828. },
  829. setup_search($wrapper, el_class, text_class, data_attr) {
  830. const $search_input = $wrapper.find('[data-element="search"]').show();
  831. $search_input.focus().val('');
  832. const $elements = $wrapper.find(el_class).show();
  833. $search_input.off('keyup').on('keyup', () => {
  834. let text_filter = $search_input.val().toLowerCase();
  835. // Replace trailing and leading spaces
  836. text_filter = text_filter.replace(/^\s+|\s+$/g, '');
  837. for (let i = 0; i < $elements.length; i++) {
  838. const text_element = $elements.eq(i).find(text_class);
  839. const text = text_element.text().toLowerCase();
  840. let name = '';
  841. if (data_attr && text_element.attr(data_attr)) {
  842. name = text_element.attr(data_attr).toLowerCase();
  843. }
  844. if (text.includes(text_filter) || name.includes(text_filter)) {
  845. $elements.eq(i).css('display', '');
  846. } else {
  847. $elements.eq(i).css('display', 'none');
  848. }
  849. }
  850. });
  851. },
  852. deep_equal(a, b) {
  853. return deep_equal(a, b);
  854. },
  855. file_name_ellipsis(filename, length) {
  856. let first_part_length = length * 2 / 3;
  857. let last_part_length = length - first_part_length;
  858. let parts = filename.split('.');
  859. let extn = parts.pop();
  860. let name = parts.join('');
  861. let first_part = name.slice(0, first_part_length);
  862. let last_part = name.slice(-last_part_length);
  863. if (name.length > length) {
  864. return `${first_part}...${last_part}.${extn}`;
  865. } else {
  866. return filename;
  867. }
  868. },
  869. get_decoded_string(dataURI) {
  870. // decodes base64 to string
  871. let parts = dataURI.split(',');
  872. const encoded_data = parts[1];
  873. let decoded = atob(encoded_data);
  874. try {
  875. const escaped = escape(decoded);
  876. decoded = decodeURIComponent(escaped);
  877. } catch (e) {
  878. // pass decodeURIComponent failure
  879. // just return atob response
  880. }
  881. return decoded;
  882. },
  883. copy_to_clipboard(string) {
  884. const show_success_alert = () => {
  885. frappe.show_alert({
  886. indicator: 'green',
  887. message: __('Copied to clipboard.')
  888. });
  889. };
  890. if (navigator.clipboard && window.isSecureContext) {
  891. navigator.clipboard.writeText(string).then(show_success_alert);
  892. } else {
  893. let input = $("<textarea>");
  894. $("body").append(input);
  895. input.val(string).select();
  896. document.execCommand("copy");
  897. show_success_alert();
  898. input.remove();
  899. }
  900. },
  901. is_rtl(lang=null) {
  902. return ["ar", "he", "fa", "ps"].includes(lang || frappe.boot.lang);
  903. },
  904. bind_actions_with_object($el, object) {
  905. // remove previously bound event
  906. $($el).off('click.class_actions');
  907. // attach new event
  908. $($el).on('click.class_actions', '[data-action]', e => {
  909. let $target = $(e.currentTarget);
  910. let action = $target.data('action');
  911. let method = object[action];
  912. method ? object[action](e, $target) : null;
  913. });
  914. return $el;
  915. },
  916. eval(code, context={}) {
  917. let variable_names = Object.keys(context);
  918. let variables = Object.values(context);
  919. code = `let out = ${code}; return out`;
  920. try {
  921. let expression_function = new Function(...variable_names, code);
  922. return expression_function(...variables);
  923. } catch (error) {
  924. console.log('Error evaluating the following expression:'); // eslint-disable-line no-console
  925. console.error(code); // eslint-disable-line no-console
  926. throw error;
  927. }
  928. },
  929. get_browser() {
  930. let ua = navigator.userAgent;
  931. let tem;
  932. let M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
  933. if (/trident/i.test(M[1])) {
  934. tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
  935. return { name: "IE", version: tem[1] || "" };
  936. }
  937. if (M[1] === "Chrome") {
  938. tem = ua.match(/\bOPR|Edge\/(\d+)/);
  939. if (tem != null) {
  940. return { name: "Opera", version: tem[1] };
  941. }
  942. }
  943. M = M[2]
  944. ? [M[1], M[2]]
  945. : [navigator.appName, navigator.appVersion, "-?"];
  946. if ((tem = ua.match(/version\/(\d+)/i)) != null) {
  947. M.splice(1, 1, tem[1]);
  948. }
  949. return {
  950. name: M[0],
  951. version: M[1],
  952. };
  953. },
  954. get_formatted_duration(value, duration_options=null) {
  955. let duration = '';
  956. if (!duration_options) {
  957. duration_options = {
  958. hide_days: 0,
  959. hide_seconds: 0
  960. };
  961. }
  962. if (value) {
  963. let total_duration = frappe.utils.seconds_to_duration(value, duration_options);
  964. if (total_duration.days) {
  965. duration += total_duration.days + __('d', null, 'Days (Field: Duration)');
  966. }
  967. if (total_duration.hours) {
  968. duration += (duration.length ? " " : "");
  969. duration += total_duration.hours + __('h', null, 'Hours (Field: Duration)');
  970. }
  971. if (total_duration.minutes) {
  972. duration += (duration.length ? " " : "");
  973. duration += total_duration.minutes + __('m', null, 'Minutes (Field: Duration)');
  974. }
  975. if (total_duration.seconds) {
  976. duration += (duration.length ? " " : "");
  977. duration += total_duration.seconds + __('s', null, 'Seconds (Field: Duration)');
  978. }
  979. }
  980. return duration;
  981. },
  982. seconds_to_duration(seconds, duration_options) {
  983. const round = seconds > 0 ? Math.floor : Math.ceil;
  984. const total_duration = {
  985. days: round(seconds / 86400), // 60 * 60 * 24
  986. hours: round(seconds % 86400 / 3600),
  987. minutes: round(seconds % 3600 / 60),
  988. seconds: round(seconds % 60)
  989. };
  990. if (duration_options && duration_options.hide_days) {
  991. total_duration.hours = round(seconds / 3600);
  992. total_duration.days = 0;
  993. }
  994. return total_duration;
  995. },
  996. duration_to_seconds(days=0, hours=0, minutes=0, seconds=0) {
  997. let value = 0;
  998. if (days) {
  999. value += days * 24 * 60 * 60;
  1000. }
  1001. if (hours) {
  1002. value += hours * 60 * 60;
  1003. }
  1004. if (minutes) {
  1005. value += minutes * 60;
  1006. }
  1007. if (seconds) {
  1008. value += seconds;
  1009. }
  1010. return value;
  1011. },
  1012. get_duration_options: function(docfield) {
  1013. let duration_options = {
  1014. hide_days: docfield.hide_days,
  1015. hide_seconds: docfield.hide_seconds
  1016. };
  1017. return duration_options;
  1018. },
  1019. get_number_system: function (country) {
  1020. let number_system_map = {
  1021. 'India':
  1022. [{
  1023. divisor: 1.0e+7,
  1024. symbol: 'Cr'
  1025. },
  1026. {
  1027. divisor: 1.0e+5,
  1028. symbol: 'Lakh'
  1029. },
  1030. {
  1031. divisor: 1.0e+3,
  1032. symbol: 'K',
  1033. }
  1034. ],
  1035. '':
  1036. [{
  1037. divisor: 1.0e+12,
  1038. symbol: 'T'
  1039. },
  1040. {
  1041. divisor: 1.0e+9,
  1042. symbol: 'B'
  1043. },
  1044. {
  1045. divisor: 1.0e+6,
  1046. symbol: 'M'
  1047. },
  1048. {
  1049. divisor: 1.0e+3,
  1050. symbol: 'K',
  1051. }]
  1052. };
  1053. if (!Object.keys(number_system_map).includes(country)) country = '';
  1054. return number_system_map[country];
  1055. },
  1056. map_defaults: {
  1057. center: [19.0800, 72.8961],
  1058. zoom: 13,
  1059. tiles: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
  1060. options: {
  1061. attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
  1062. }
  1063. },
  1064. icon(icon_name, size="sm", icon_class="", icon_style="", svg_class="") {
  1065. let size_class = "";
  1066. if (typeof size == "object") {
  1067. icon_style += ` width: ${size.width}; height: ${size.height}`;
  1068. } else {
  1069. size_class = `icon-${size}`;
  1070. }
  1071. return `<svg class="icon ${svg_class} ${size_class}" style="${icon_style}">
  1072. <use class="${icon_class}" href="#icon-${icon_name}"></use>
  1073. </svg>`;
  1074. },
  1075. flag(country_code) {
  1076. return `<img
  1077. src="https://flagcdn.com/${country_code}.svg"
  1078. width="20" height="15">`;
  1079. },
  1080. make_chart(wrapper, custom_options={}) {
  1081. let chart_args = {
  1082. type: 'bar',
  1083. colors: ['light-blue'],
  1084. axisOptions: {
  1085. xIsSeries: 1,
  1086. shortenYAxisNumbers: 1,
  1087. xAxisMode: 'tick',
  1088. numberFormatter: frappe.utils.format_chart_axis_number,
  1089. }
  1090. };
  1091. for (let key in custom_options) {
  1092. if (typeof chart_args[key] === 'object' && typeof custom_options[key] === 'object') {
  1093. chart_args[key] = Object.assign(chart_args[key], custom_options[key]);
  1094. } else {
  1095. chart_args[key] = custom_options[key];
  1096. }
  1097. }
  1098. return new frappe.Chart(wrapper, chart_args);
  1099. },
  1100. format_chart_axis_number(label, country) {
  1101. const default_country = frappe.sys_defaults.country;
  1102. return frappe.utils.shorten_number(label, country || default_country, 3);
  1103. },
  1104. generate_route(item) {
  1105. const type = item.type.toLowerCase();
  1106. if (type === "doctype") {
  1107. item.doctype = item.name;
  1108. }
  1109. let route = "";
  1110. if (!item.route) {
  1111. if (item.link) {
  1112. route = strip(item.link, "#");
  1113. } else if (type === "doctype") {
  1114. let doctype_slug = frappe.router.slug(item.doctype);
  1115. if (frappe.model.is_single(item.doctype)) {
  1116. route = doctype_slug;
  1117. } else {
  1118. if (!item.doc_view) {
  1119. if (frappe.model.is_tree(item.doctype)) {
  1120. item.doc_view = "Tree";
  1121. } else {
  1122. item.doc_view = "List";
  1123. }
  1124. }
  1125. switch (item.doc_view) {
  1126. case "List":
  1127. if (item.filters) {
  1128. frappe.route_options = item.filters;
  1129. }
  1130. route = doctype_slug;
  1131. break;
  1132. case "Tree":
  1133. route = `${doctype_slug}/view/tree`;
  1134. break;
  1135. case "Report Builder":
  1136. route = `${doctype_slug}/view/report`;
  1137. break;
  1138. case "Dashboard":
  1139. route = `${doctype_slug}/view/dashboard`;
  1140. break;
  1141. case "New":
  1142. route = `${doctype_slug}/new`;
  1143. break;
  1144. case "Calendar":
  1145. route = `${doctype_slug}/view/calendar/default`;
  1146. break;
  1147. default:
  1148. frappe.throw({ message: __("Not a valid view:") + item.doc_view, title: __("Unknown View") });
  1149. route = "";
  1150. }
  1151. }
  1152. } else if (type === "report") {
  1153. if (item.is_query_report) {
  1154. route = "query-report/" + item.name;
  1155. } else if (!item.doctype) {
  1156. route = "/report/" + item.name;
  1157. } else {
  1158. route = frappe.router.slug(item.doctype) + "/view/report/" + item.name;
  1159. }
  1160. } else if (type === "page") {
  1161. route = item.name;
  1162. } else if (type === "dashboard") {
  1163. route = `dashboard-view/${item.name}`;
  1164. }
  1165. } else {
  1166. route = item.route;
  1167. }
  1168. if (item.route_options) {
  1169. route +=
  1170. "?" +
  1171. $.map(item.route_options, function (value, key) {
  1172. return (
  1173. encodeURIComponent(key) + "=" + encodeURIComponent(value)
  1174. );
  1175. }).join("&");
  1176. }
  1177. // if(type==="page" || type==="help" || type==="report" ||
  1178. // (item.doctype && frappe.model.can_read(item.doctype))) {
  1179. // item.shown = true;
  1180. // }
  1181. return `/app/${route}`;
  1182. },
  1183. shorten_number: function (number, country, min_length=4, max_no_of_decimals=2) {
  1184. /* returns the number as an abbreviated string
  1185. * PARAMS
  1186. * number - number to be shortened
  1187. * country - country that determines the numnber system to be used
  1188. * min_length - length below which the number will not be shortened
  1189. * max_no_of_decimals - max number of decimals of the shortened number
  1190. */
  1191. // return number if total digits is lesser than min_length
  1192. const len = String(number).match(/\d/g).length;
  1193. if (len < min_length) return number.toString();
  1194. const number_system = this.get_number_system(country);
  1195. let x = Math.abs(Math.round(number));
  1196. for (const map of number_system) {
  1197. if (x >= map.divisor) {
  1198. let result = number/map.divisor;
  1199. const no_of_decimals = this.get_number_of_decimals(result);
  1200. /*
  1201. If no_of_decimals is greater than max_no_of_decimals,
  1202. round the number to max_no_of_decimals
  1203. */
  1204. result = no_of_decimals > max_no_of_decimals
  1205. ? result.toFixed(max_no_of_decimals)
  1206. : result;
  1207. return result + ' ' + map.symbol;
  1208. }
  1209. }
  1210. return number.toFixed(max_no_of_decimals);
  1211. },
  1212. get_number_of_decimals: function (number) {
  1213. if (Math.floor(number) === number) return 0;
  1214. return number.toString().split(".")[1].length || 0;
  1215. },
  1216. build_summary_item(summary) {
  1217. if (summary.type == "separator") {
  1218. return $(`<div class="summary-separator">
  1219. <div class="summary-value ${summary.color ? summary.color.toLowerCase() : 'text-muted'}">${summary.value}</div>
  1220. </div>`);
  1221. }
  1222. let df = { fieldtype: summary.datatype };
  1223. let doc = null;
  1224. if (summary.datatype == "Currency") {
  1225. df.options = "currency";
  1226. doc = { currency: summary.currency };
  1227. }
  1228. let value = frappe.format(summary.value, df, { only_value: true }, doc);
  1229. let color = summary.indicator ? summary.indicator.toLowerCase()
  1230. : summary.color ? summary.color.toLowerCase() : '';
  1231. return $(`<div class="summary-item">
  1232. <span class="summary-label">${summary.label}</span>
  1233. <div class="summary-value ${color}">${value}</div>
  1234. </div>`);
  1235. },
  1236. print(doctype, docname, print_format, letterhead, lang_code) {
  1237. let w = window.open(
  1238. frappe.urllib.get_full_url(
  1239. '/printview?doctype=' +
  1240. encodeURIComponent(doctype) +
  1241. '&name=' +
  1242. encodeURIComponent(docname) +
  1243. '&trigger_print=1' +
  1244. '&format=' +
  1245. encodeURIComponent(print_format) +
  1246. '&no_letterhead=' +
  1247. (letterhead ? '0' : '1') +
  1248. '&letterhead=' +
  1249. encodeURIComponent(letterhead) +
  1250. (lang_code ? '&_lang=' + lang_code : '')
  1251. )
  1252. );
  1253. if (!w) {
  1254. frappe.msgprint(__('Please enable pop-ups'));
  1255. return;
  1256. }
  1257. },
  1258. get_clipboard_data(clipboard_paste_event) {
  1259. let e = clipboard_paste_event;
  1260. let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
  1261. return clipboard_data.getData('Text');
  1262. },
  1263. add_custom_button(html, action, class_name = "", title="", btn_type, wrapper, prepend) {
  1264. if (!btn_type) btn_type = 'btn-secondary';
  1265. let button = $(
  1266. `<button class="btn ${btn_type} btn-xs ${class_name}" title="${title}">${html}</button>`
  1267. );
  1268. button.click(event => {
  1269. event.stopPropagation();
  1270. action && action(event);
  1271. });
  1272. !prepend && button.appendTo(wrapper);
  1273. prepend && wrapper.prepend(button);
  1274. },
  1275. sleep(time) {
  1276. return new Promise((resolve) => setTimeout(resolve, time));
  1277. },
  1278. parse_array(array) {
  1279. if (array && array.length !== 0) {
  1280. return array;
  1281. }
  1282. return undefined;
  1283. },
  1284. // simple implementation of python's range
  1285. range(start, end) {
  1286. if (!end) {
  1287. end = start;
  1288. start = 0;
  1289. }
  1290. let arr = [];
  1291. for (let i = start; i < end; i++) {
  1292. arr.push(i);
  1293. }
  1294. return arr;
  1295. },
  1296. get_link_title(doctype, name) {
  1297. if (!doctype || !name || !frappe._link_titles) {
  1298. return;
  1299. }
  1300. return frappe._link_titles[doctype + "::" + name];
  1301. },
  1302. add_link_title(doctype, name, value) {
  1303. if (!doctype || !name) {
  1304. return;
  1305. }
  1306. if (!frappe._link_titles) {
  1307. // for link titles
  1308. frappe._link_titles = {};
  1309. }
  1310. frappe._link_titles[doctype + "::" + name] = value;
  1311. },
  1312. fetch_link_title(doctype, name) {
  1313. try {
  1314. return frappe.xcall("frappe.desk.search.get_link_title", {
  1315. "doctype": doctype,
  1316. "docname": name
  1317. }).then(title => {
  1318. frappe.utils.add_link_title(doctype, name, title);
  1319. return title;
  1320. });
  1321. } catch (error) {
  1322. console.log('Error while fetching link title.'); // eslint-disable-line
  1323. console.log(error); // eslint-disable-line
  1324. return Promise.resolve(name);
  1325. }
  1326. },
  1327. only_allow_num_decimal(input) {
  1328. input.on('input', (e) => {
  1329. let self = $(e.target);
  1330. self.val(self.val().replace(/[^0-9.]/g, ''));
  1331. if ((e.which != 46 || self.val().indexOf('.') != -1) && (e.which < 48 || e.which > 57)) {
  1332. e.preventDefault();
  1333. }
  1334. });
  1335. },
  1336. string_to_boolean(string) {
  1337. switch (string.toLowerCase().trim()) {
  1338. case "t": case "true": case "y": case "yes": case "1": return true;
  1339. case "f": case "false": case "n": case "no": case "0": case null: return false;
  1340. default: return string;
  1341. }
  1342. },
  1343. get_filter_as_json(filters) {
  1344. // convert filter array to json
  1345. let filter = null;
  1346. if (filters.length) {
  1347. filter = {};
  1348. filters.forEach(arr => {
  1349. filter[arr[1]] = [arr[2], arr[3]];
  1350. });
  1351. filter = JSON.stringify(filter);
  1352. }
  1353. return filter;
  1354. },
  1355. get_filter_from_json(filter_json, doctype) {
  1356. // convert json to filter array
  1357. if (filter_json) {
  1358. if (!filter_json.length) {
  1359. return [];
  1360. }
  1361. const filters_json = new Function(`return ${filter_json}`)();
  1362. if (!doctype) {
  1363. // e.g. return {
  1364. // priority: (2) ['=', 'Medium'],
  1365. // status: (2) ['=', 'Open']
  1366. // }
  1367. return filters_json || [];
  1368. }
  1369. // e.g. return [
  1370. // ['ToDo', 'status', '=', 'Open', false],
  1371. // ['ToDo', 'priority', '=', 'Medium', false]
  1372. // ]
  1373. return Object.keys(filters_json).map(filter => {
  1374. let val = filters_json[filter];
  1375. return [doctype, filter, val[0], val[1], false];
  1376. });
  1377. }
  1378. },
  1379. load_video_player() {
  1380. return frappe.require("video_player.bundle.js");
  1381. },
  1382. is_current_user(user) {
  1383. return user === frappe.session.user;
  1384. },
  1385. debug: {
  1386. watch_property(obj, prop, callback=console.trace) {
  1387. if (!frappe.boot.developer_mode) {
  1388. return;
  1389. }
  1390. console.warn("Adding property watcher, make sure to remove it after debugging.");
  1391. // Adapted from https://stackoverflow.com/a/11658693
  1392. // Reused under CC-BY-SA 4.0
  1393. // changes: variable names are changed for consistency with our codebase
  1394. const private_prop = "$_" + prop + "_$";
  1395. obj[private_prop] = obj[prop];
  1396. Object.defineProperty(obj, prop, {
  1397. get: function() {
  1398. return obj[private_prop];
  1399. },
  1400. set: function(value) {
  1401. callback();
  1402. obj[private_prop] = value;
  1403. },
  1404. });
  1405. },
  1406. }
  1407. });