Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 
 

572 строки
16 KiB

  1. frappe.provide('frappe.search');
  2. frappe.search.utils = {
  3. setup_recent: function() {
  4. this.recent = JSON.parse(frappe.boot.user.recent || "[]") || [];
  5. },
  6. get_recent_pages: function(keywords) {
  7. var me = this, values = [], options = [];
  8. function find(list, keywords, process) {
  9. list.forEach(function(item, i) {
  10. var _item = ($.isArray(item)) ? item[0] : item;
  11. _item = __(_item || '').toLowerCase().replace(/-/g, " ");
  12. if(keywords===_item || _item.indexOf(keywords) !== -1) {
  13. var option = process(item);
  14. if(option) {
  15. if($.isPlainObject(option)) {
  16. option = [option];
  17. }
  18. option.forEach(function(o) { o.match = item; });
  19. options = option.concat(options);
  20. }
  21. }
  22. });
  23. }
  24. me.recent.forEach(function(doctype, i) {
  25. values.push([doctype[1], ['Form', doctype[0], doctype[1]]]);
  26. });
  27. values = values.reverse();
  28. frappe.route_history.forEach(function(route, i) {
  29. if(route[0]==='Form') {
  30. values.push([route[2], route]);
  31. }
  32. else if(in_list(['List', 'Report', 'Tree', 'modules', 'query-report'], route[0])) {
  33. if(route[1]) {
  34. values.push([route[1], route]);
  35. }
  36. }
  37. else if(route[0]) {
  38. values.push([frappe.route_titles[route[0]] || route[0], route]);
  39. }
  40. });
  41. find(values, keywords, function(match) {
  42. var out = {
  43. route: match[1]
  44. }
  45. if(match[1][0]==='Form') {
  46. if(match[1][1] !== match[1][2]) {
  47. out.label = __(match[1][1]) + " " + match[1][2].bold();
  48. out.value = __(match[1][1]) + " " + match[1][2];
  49. } else {
  50. out.label = __(match[1][1]).bold();
  51. out.value = __(match[1][1]);
  52. }
  53. } else if(in_list(['List', 'Report', 'Tree', 'modules', 'query-report'], match[1][0])) {
  54. var type = match[1][0], label = type;
  55. if(type==='modules') label = 'Module';
  56. else if(type==='query-report') label = 'Report';
  57. out.label = __(match[1][1]).bold() + " " + __(label);
  58. out.value = __(match[1][1]) + " " + __(label);
  59. } else {
  60. out.label = match[0].bold();
  61. out.value = match[0];
  62. }
  63. out.index = 80;
  64. return out;
  65. });
  66. return options;
  67. },
  68. get_search_in_list: function(keywords) {
  69. var me = this;
  70. var out = [];
  71. if(in_list(keywords.split(" "), "in") && (keywords.slice(-2) !== "in")) {
  72. var parts = keywords.split(" in ");
  73. frappe.boot.user.can_read.forEach(function (item) {
  74. if(frappe.boot.user.can_search.includes(item)) {
  75. var level = me.fuzzy_search(parts[1], item);
  76. if(level) {
  77. out.push({
  78. type: "In List",
  79. label: __('Find {0} in {1}', [__(parts[0]), me.bolden_match_part(__(item), parts[1])]),
  80. value: __('Find {0} in {1}', [__(parts[0]), __(item)]),
  81. route_options: {"name": ["like", "%" + parts[0] + "%"]},
  82. index: 1 + level,
  83. route: ["List", item]
  84. });
  85. }
  86. }
  87. });
  88. }
  89. return out;
  90. },
  91. get_creatables: function(keywords) {
  92. var me = this;
  93. var out = [];
  94. var firstKeyword = keywords.split(" ")[0];
  95. if(firstKeyword.toLowerCase() === __("new")) {
  96. frappe.boot.user.can_create.forEach(function (item) {
  97. var level = me.fuzzy_search(keywords.substr(4), item);
  98. if(level) {
  99. out.push({
  100. type: "New",
  101. label: __("New {0}", [me.bolden_match_part(__(item), keywords.substr(4))]),
  102. value: __("New {0}", [__(item)]),
  103. index: 1 + level,
  104. match: item,
  105. onclick: function() { frappe.new_doc(item, true); }
  106. });
  107. }
  108. });
  109. }
  110. return out;
  111. },
  112. get_doctypes: function(keywords) {
  113. var me = this;
  114. var out = [];
  115. var level, target;
  116. var option = function(type, route, order) {
  117. return {
  118. type: type,
  119. label: me.bolden_match_part(__(target), keywords) + " " + __(type),
  120. value: __(target) + " " + __(type),
  121. index: level + order,
  122. match: target,
  123. route: route,
  124. }
  125. };
  126. frappe.boot.user.can_read.forEach(function (item) {
  127. level = me.fuzzy_search(keywords, item);
  128. if (level) {
  129. target = item;
  130. if (in_list(frappe.boot.single_types, item)) {
  131. out.push(option("", ["Form", item, item], 0.05));
  132. } else if (frappe.boot.user.can_search.includes(item)) {
  133. // include 'making new' option
  134. if (in_list(frappe.boot.user.can_create, item)) {
  135. var match = item;
  136. out.push({
  137. type: "New",
  138. label: __("New {0}", [me.bolden_match_part(__(item), keywords)]),
  139. value: __("New {0}", [__(item)]),
  140. index: level + 0.015,
  141. match: item,
  142. onclick: function () { frappe.new_doc(match, true); }
  143. });
  144. }
  145. if (in_list(frappe.boot.treeviews, item)) {
  146. out.push(option("Tree", ["Tree", item], 0.05));
  147. } else {
  148. out.push(option("List", ["List", item], 0.05));
  149. if (frappe.model.can_get_report(item)) {
  150. out.push(option("Report", ["Report", item], 0.04));
  151. }
  152. if (frappe.boot.calendars.indexOf(item) !== -1) {
  153. out.push(option("Calendar", ["List", item, "Calendar"], 0.03));
  154. out.push(option("Gantt", ["List", item, "Gantt"], 0.02));
  155. }
  156. }
  157. }
  158. }
  159. });
  160. return out;
  161. },
  162. get_reports: function(keywords) {
  163. var me = this;
  164. var out = [];
  165. var route;
  166. Object.keys(frappe.boot.user.all_reports).forEach(function(item) {
  167. var level = me.fuzzy_search(keywords, item);
  168. if(level > 0) {
  169. var report = frappe.boot.user.all_reports[item];
  170. if(report.report_type == "Report Builder")
  171. route = ["Report", report.ref_doctype, item];
  172. else
  173. route = ["query-report", item];
  174. out.push({
  175. type: "Report",
  176. label: __("Report {0}" , [me.bolden_match_part(__(item), keywords)]),
  177. value: __("Report {0}" , [__(item)]),
  178. index: level,
  179. route: route
  180. });
  181. }
  182. });
  183. return out;
  184. },
  185. get_pages: function(keywords) {
  186. var me = this;
  187. var out = [];
  188. this.pages = {};
  189. $.each(frappe.boot.page_info, function(name, p) {
  190. me.pages[p.title] = p;
  191. p.name = name;
  192. });
  193. Object.keys(this.pages).forEach(function(item) {
  194. var level = me.fuzzy_search(keywords, item);
  195. if(level) {
  196. var page = me.pages[item];
  197. out.push({
  198. type: "Page",
  199. label: __("Open {0}", [me.bolden_match_part(__(item), keywords)]),
  200. value: __("Open {0}", [__(item)]),
  201. match: item,
  202. index: level,
  203. route: [page.route || page.name]
  204. });
  205. }
  206. });
  207. var target = 'Calendar';
  208. if(__('calendar').indexOf(keywords.toLowerCase()) === 0) {
  209. out.push({
  210. type: "Calendar",
  211. value: __("Open {0}", [__(target)]),
  212. index: me.fuzzy_search(keywords, 'Calendar'),
  213. match: target,
  214. route: ['List', 'Event', target],
  215. });
  216. }
  217. if(__('email inbox').indexOf(keywords.toLowerCase()) === 0) {
  218. out.push({
  219. type: "Inbox",
  220. value: __("Open {0}", [__('Email Inbox')]),
  221. index: me.fuzzy_search(keywords, 'email inbox'),
  222. match: target,
  223. route: ['List', 'Communication', 'Inbox'],
  224. });
  225. }
  226. return out;
  227. },
  228. get_modules: function(keywords) {
  229. var me = this;
  230. var out = [];
  231. Object.keys(frappe.modules).forEach(function(item) {
  232. var level = me.fuzzy_search(keywords, item);
  233. if(level > 0) {
  234. var module = frappe.modules[item];
  235. if(module._doctype) return;
  236. var ret = {
  237. type: "Module",
  238. label: __("Open {0}", [me.bolden_match_part(__(item), keywords)]),
  239. value: __("Open {0}", [__(item)]),
  240. index: level,
  241. }
  242. if(module.link) {
  243. ret.route = [module.link];
  244. } else {
  245. ret.route = ["Module", item];
  246. }
  247. out.push(ret);
  248. }
  249. });
  250. return out;
  251. },
  252. get_global_results: function (keywords, start, limit, doctype = "") {
  253. var me = this;
  254. function get_results_sets(data) {
  255. var results_sets = [], result, set;
  256. function get_existing_set (doctype) {
  257. return results_sets.find(function(set) {
  258. return set.title === doctype;
  259. });
  260. }
  261. function make_description(content, doc_name) {
  262. var parts = content.split(" ||| ");
  263. var result_max_length = 300;
  264. var field_length = 120;
  265. var fields = [];
  266. var result_current_length = 0;
  267. var field_text = "";
  268. for(var i = 0; i < parts.length; i++) {
  269. var part = parts[i];
  270. if(part.toLowerCase().indexOf(keywords) !== -1) {
  271. // If the field contains the keyword
  272. if(part.indexOf(' &&& ') !== -1) {
  273. var colon_index = part.indexOf(' &&& ');
  274. var field_value = part.slice(colon_index + 5);
  275. } else {
  276. var colon_index = part.indexOf(' : ');
  277. var field_value = part.slice(colon_index + 3);
  278. }
  279. if(field_value.length > field_length) {
  280. // If field value exceeds field_length, find the keyword in it
  281. // and trim field value by half the field_length at both sides
  282. // ellipsify if necessary
  283. var field_data = "";
  284. var index = field_value.indexOf(keywords);
  285. field_data += index < field_length/2 ? field_value.slice(0, index)
  286. : '...' + field_value.slice(index - field_length/2, index)
  287. field_data += field_value.slice(index, index + field_length/2);
  288. field_data += index + field_length/2 < field_value.length ? "..." : "";
  289. field_value = field_data;
  290. }
  291. var field_name = part.slice(0, colon_index);
  292. // Find remaining result_length and add field length to result_current_length
  293. var remaining_length = result_max_length - result_current_length;
  294. result_current_length += field_name.length + field_value.length + 2;
  295. if(result_current_length < result_max_length) {
  296. // We have room, push the entire field
  297. field_text = '<span class="field-name text-muted">' +
  298. me.bolden_match_part(field_name, keywords) + ': </span> ' +
  299. me.bolden_match_part(field_value, keywords);
  300. if(fields.indexOf(field_text) === -1 && doc_name !== field_value) {
  301. fields.push(field_text);
  302. }
  303. } else {
  304. // Not enough room
  305. if(field_name.length < remaining_length){
  306. // Ellipsify (trim at word end) and push
  307. remaining_length -= field_name.length;
  308. field_text = '<span class="field-name text-muted">' +
  309. me.bolden_match_part(field_name, keywords) + ': </span> ';
  310. field_value = field_value.slice(0, remaining_length);
  311. field_value = field_value.slice(0, field_value.lastIndexOf(' ')) + ' ...';
  312. field_text += me.bolden_match_part(field_value, keywords);
  313. fields.push(field_text);
  314. } else {
  315. // No room for even the field name, skip
  316. fields.push('...');
  317. }
  318. break;
  319. }
  320. }
  321. }
  322. return fields.join(', ');
  323. }
  324. data.forEach(function(d) {
  325. // more properties
  326. result = {
  327. label: d.name,
  328. value: d.name,
  329. description: make_description(d.content, d.name),
  330. route: ['Form', d.doctype, d.name],
  331. }
  332. if(d.image || d.image === null){
  333. result.image = d.image;
  334. }
  335. set = get_existing_set(d.doctype);
  336. if(set) {
  337. set.results.push(result);
  338. } else {
  339. set = {
  340. title: d.doctype,
  341. results: [result],
  342. fetch_type: "Global"
  343. }
  344. results_sets.push(set);
  345. }
  346. });
  347. return results_sets;
  348. }
  349. return new Promise(function(resolve, reject) {
  350. frappe.call({
  351. method: "frappe.utils.global_search.search",
  352. args: {
  353. text: keywords,
  354. start: start,
  355. limit: limit,
  356. doctype: doctype
  357. },
  358. callback: function(r) {
  359. if(r.message) {
  360. resolve(get_results_sets(r.message));
  361. } else {
  362. resolve([]);
  363. }
  364. }
  365. });
  366. });
  367. },
  368. get_help_results: function(keywords) {
  369. function get_results_set(data) {
  370. var result;
  371. var set = {
  372. title: "Help",
  373. fetch_type: "Help",
  374. results: []
  375. }
  376. data.forEach(function(d) {
  377. // more properties
  378. result = {
  379. label: d[0],
  380. value: d[0],
  381. description: d[1],
  382. data_path: d[2],
  383. onclick: function() {
  384. }
  385. }
  386. set.results.push(result);
  387. });
  388. return [set];
  389. }
  390. return new Promise(function(resolve, reject) {
  391. frappe.call({
  392. method: "frappe.utils.help.get_help",
  393. args: {
  394. text: keywords
  395. },
  396. callback: function(r) {
  397. if(r.message) {
  398. resolve(get_results_set(r.message));
  399. } else {
  400. resolve([]);
  401. }
  402. }
  403. });
  404. });
  405. },
  406. get_nav_results: function(keywords) {
  407. function sort_uniques(array) {
  408. var routes = [], out = [];
  409. array.forEach(function(d) {
  410. if(d.route) {
  411. if(d.route[0] === "List" && d.route[2]) {
  412. d.route.splice(2);
  413. }
  414. var str_route = d.route.join('/');
  415. if(routes.indexOf(str_route) === -1) {
  416. routes.push(str_route);
  417. out.push(d);
  418. } else {
  419. var old = routes.indexOf(str_route);
  420. if(out[old].index > d.index) {
  421. out[old] = d;
  422. }
  423. }
  424. } else {
  425. out.push(d);
  426. }
  427. });
  428. return out.sort(function(a, b) {
  429. return b.index - a.index;
  430. });
  431. }
  432. var lists = [], setup = [];
  433. var all_doctypes = sort_uniques(this.get_doctypes(keywords));
  434. all_doctypes.forEach(function(d) {
  435. if(d.type === "") {
  436. setup.push(d);
  437. } else {
  438. lists.push(d);
  439. }
  440. });
  441. var in_keyword = keywords.split(" in ")[0];
  442. return [
  443. {title: "Recents", fetch_type: "Nav", results: sort_uniques(this.get_recent_pages(keywords))},
  444. {title: "Create a new ...", fetch_type: "Nav", results: sort_uniques(this.get_creatables(keywords))},
  445. {title: "Find '" + in_keyword + "' in ... ", fetch_type: "Nav", results: sort_uniques(this.get_search_in_list(keywords))},
  446. {title: "Lists", fetch_type: "Nav", results: lists},
  447. {title: "Reports", fetch_type: "Nav", results: sort_uniques(this.get_reports(keywords))},
  448. {title: "Administration", fetch_type: "Nav", results: sort_uniques(this.get_pages(keywords))},
  449. {title: "Modules", fetch_type: "Nav", results: sort_uniques(this.get_modules(keywords))},
  450. {title: "Setup", fetch_type: "Nav", results: setup},
  451. ]
  452. },
  453. fuzzy_search: function(keywords, _item) {
  454. // Returns 10 for case-perfect contain, 0 for not found
  455. // 9 for perfect contain,
  456. // 0 - 6 for fuzzy contain
  457. // **Specific use-case step**
  458. var item = __(_item || '').replace(/-/g, " ");
  459. var ilen = item.length;
  460. var klen = keywords.length;
  461. var length_ratio = klen/ilen;
  462. var max_skips = 3, max_mismatch_len = 2;
  463. if (klen > ilen) { return 0; }
  464. if(keywords === item || item.toLowerCase().indexOf(keywords) === 0) {
  465. return 10 + length_ratio;
  466. }
  467. if (item.indexOf(keywords) !== -1 && keywords !== keywords.toLowerCase()) {
  468. return 9 + length_ratio;
  469. }
  470. item = item.toLowerCase();
  471. keywords = keywords.toLowerCase();
  472. if (item.indexOf(keywords) !== -1) {
  473. return 8 + length_ratio;
  474. }
  475. var skips = 0, mismatches = 0;
  476. outer: for (var i = 0, j = 0; i < klen; i++) {
  477. if(mismatches !== 0) skips++;
  478. if(skips > max_skips) return 0;
  479. var k_ch = keywords.charCodeAt(i);
  480. mismatches = 0;
  481. while (j < ilen) {
  482. if (item.charCodeAt(j++) === k_ch) {
  483. continue outer;
  484. }
  485. if(++mismatches > max_mismatch_len) return 0 ;
  486. }
  487. return 0;
  488. }
  489. // Since indexOf didn't pass, there will be atleast 1 skip
  490. // hence no divide by zero, but just to be safe
  491. if((skips + mismatches) > 0) {
  492. return (5 + length_ratio)/(skips + mismatches);
  493. } else {
  494. return 0;
  495. }
  496. },
  497. bolden_match_part: function(str, subseq) {
  498. var rendered = "";
  499. if(this.fuzzy_search(subseq, str) === 0) {
  500. return str;
  501. } else if(this.fuzzy_search(subseq, str) > 6) {
  502. var regEx = new RegExp("("+ subseq +")", "ig");
  503. return str.replace(regEx, '<b>$1</b>');
  504. } else {
  505. var str_orig = str;
  506. var str = str.toLowerCase();
  507. var str_len = str.length;
  508. var subseq = subseq.toLowerCase();
  509. outer: for(var i = 0, j = 0; i < subseq.length; i++) {
  510. var sub_ch = subseq.charCodeAt(i);
  511. while(j < str_len) {
  512. if(str.charCodeAt(j) === sub_ch) {
  513. var str_char = str_orig.charAt(j);
  514. if(str_char === str_char.toLowerCase()) {
  515. rendered += '<b>' + subseq.charAt(i) + '</b>';
  516. } else {
  517. rendered += '<b>' + subseq.charAt(i).toUpperCase() + '</b>';
  518. }
  519. j++;
  520. continue outer;
  521. }
  522. rendered += str_orig.charAt(j);
  523. j++;
  524. }
  525. return str_orig;
  526. }
  527. rendered += str_orig.slice(j);
  528. return rendered;
  529. }
  530. },
  531. }