No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 
 
 

2041 líneas
54 KiB

  1. // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. // MIT License. See license.txt
  3. frappe.ui.form.make_control = function (opts) {
  4. var control_class_name = "Control" + opts.df.fieldtype.replace(/ /g, "");
  5. if(frappe.ui.form[control_class_name]) {
  6. return new frappe.ui.form[control_class_name](opts);
  7. } else {
  8. console.log("Invalid Control Name: " + opts.df.fieldtype);
  9. }
  10. }
  11. frappe.ui.form.Control = Class.extend({
  12. init: function(opts) {
  13. $.extend(this, opts);
  14. this.make();
  15. // if developer_mode=1, show fieldname as tooltip
  16. if(frappe.boot.user && frappe.boot.user.name==="Administrator" &&
  17. frappe.boot.developer_mode===1 && this.$wrapper) {
  18. this.$wrapper.attr("title", __(this.df.fieldname));
  19. }
  20. if(this.render_input) {
  21. this.refresh();
  22. }
  23. },
  24. make: function() {
  25. this.make_wrapper();
  26. this.$wrapper
  27. .attr("data-fieldtype", this.df.fieldtype)
  28. .attr("data-fieldname", this.df.fieldname);
  29. this.wrapper = this.$wrapper.get(0);
  30. this.wrapper.fieldobj = this; // reference for event handlers
  31. },
  32. make_wrapper: function() {
  33. this.$wrapper = $("<div class='frappe-control'></div>").appendTo(this.parent);
  34. // alias
  35. this.wrapper = this.$wrapper;
  36. },
  37. toggle: function(show) {
  38. this.df.hidden = show ? 0 : 1;
  39. this.refresh();
  40. },
  41. // returns "Read", "Write" or "None"
  42. // as strings based on permissions
  43. get_status: function(explain) {
  44. if(!this.doctype && !this.docname) {
  45. // like in case of a dialog box
  46. if (cint(this.df.hidden)) {
  47. if(explain) console.log("By Hidden: None");
  48. return "None";
  49. } else if (cint(this.df.hidden_due_to_dependency)) {
  50. if(explain) console.log("By Hidden Dependency: None");
  51. return "None";
  52. } else if (cint(this.df.read_only)) {
  53. if(explain) console.log("By Read Only: Read");
  54. return "Read";
  55. }
  56. return "Write";
  57. }
  58. var status = frappe.perm.get_field_display_status(this.df,
  59. frappe.model.get_doc(this.doctype, this.docname), this.perm || (this.frm && this.frm.perm), explain);
  60. // hide if no value
  61. if (this.doctype && status==="Read" && !this.only_input
  62. && is_null(frappe.model.get_value(this.doctype, this.docname, this.df.fieldname))
  63. && !in_list(["HTML", "Image"], this.df.fieldtype)) {
  64. if(explain) console.log("By Hide Read-only, null fields: None");
  65. status = "None";
  66. }
  67. return status;
  68. },
  69. refresh: function() {
  70. this.disp_status = this.get_status();
  71. this.$wrapper
  72. && this.$wrapper.toggleClass("hide-control", this.disp_status=="None")
  73. && this.$wrapper.trigger("refresh");
  74. },
  75. get_doc: function() {
  76. return this.doctype && this.docname
  77. && locals[this.doctype] && locals[this.doctype][this.docname] || {};
  78. },
  79. get_model_value: function() {
  80. if(this.doc) {
  81. return this.doc[this.df.fieldname];
  82. }
  83. },
  84. set_value: function(value) {
  85. this.parse_validate_and_set_in_model(value);
  86. },
  87. parse_validate_and_set_in_model: function(value, e) {
  88. var me = this;
  89. if(this.inside_change_event) return;
  90. this.inside_change_event = true;
  91. if(this.parse) value = this.parse(value);
  92. var set = function(value) {
  93. me.set_model_value(value);
  94. me.inside_change_event = false;
  95. me.set_mandatory && me.set_mandatory(value);
  96. if(me.df.change || me.df.onchange) {
  97. // onchange event specified in df
  98. (me.df.change || me.df.onchange).apply(me, [e]);
  99. }
  100. }
  101. this.validate ? this.validate(value, set) : set(value);
  102. },
  103. get_parsed_value: function() {
  104. var me = this;
  105. if(this.get_status()==='Write') {
  106. return this.get_value ?
  107. (this.parse ? this.parse(this.get_value()) : this.get_value()) :
  108. undefined;
  109. } else if(this.get_status()==='Read') {
  110. return this.value || undefined;
  111. } else {
  112. return undefined;
  113. }
  114. },
  115. set_model_value: function(value) {
  116. if(this.doctype && this.docname) {
  117. if(frappe.model.set_value(this.doctype, this.docname, this.df.fieldname,
  118. value, this.df.fieldtype)) {
  119. this.last_value = value;
  120. }
  121. } else {
  122. if(this.doc) {
  123. this.doc[this.df.fieldname] = value;
  124. }
  125. this.set_input(value);
  126. }
  127. },
  128. set_focus: function() {
  129. if(this.$input) {
  130. this.$input.get(0).focus();
  131. return true;
  132. }
  133. }
  134. });
  135. frappe.ui.form.ControlHTML = frappe.ui.form.Control.extend({
  136. make: function() {
  137. this._super();
  138. var me = this;
  139. this.disp_area = this.wrapper;
  140. this.$wrapper.on("refresh", function() {
  141. var content = me.get_content();
  142. if(content) me.$wrapper.html(content);
  143. return false;
  144. });
  145. },
  146. get_content: function() {
  147. return this.df.options || "";
  148. },
  149. html: function(html) {
  150. this.$wrapper.html(html || this.get_content());
  151. },
  152. set_value: function(html) {
  153. if(html.appendTo) {
  154. // jquery object
  155. html.appendTo(this.$wrapper.empty());
  156. } else {
  157. // html
  158. this.df.options = html;
  159. this.html(html);
  160. }
  161. }
  162. });
  163. frappe.ui.form.ControlHeading = frappe.ui.form.ControlHTML.extend({
  164. get_content: function() {
  165. return "<h4>" + __(this.df.label) + "</h4>";
  166. }
  167. });
  168. frappe.ui.form.ControlImage = frappe.ui.form.Control.extend({
  169. make: function() {
  170. this._super();
  171. var me = this;
  172. this.$wrapper.css({"margin": "0px"});
  173. this.$body = $("<div></div>").appendTo(this.$wrapper)
  174. .css({"margin-bottom": "10px"})
  175. this.$wrapper.on("refresh", function() {
  176. var doc = null;
  177. me.$body.empty();
  178. var doc = me.get_doc();
  179. if(doc && me.df.options && doc[me.df.options]) {
  180. me.$img = $("<img src='"+doc[me.df.options]+"' class='img-responsive'>")
  181. .appendTo(me.$body);
  182. } else {
  183. me.$buffer = $("<div class='missing-image'><i class='octicon octicon-circle-slash'></i></div>")
  184. .appendTo(me.$body)
  185. }
  186. return false;
  187. });
  188. $('<div class="clearfix"></div>').appendTo(this.$wrapper);
  189. }
  190. });
  191. frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
  192. horizontal: true,
  193. make: function() {
  194. // parent element
  195. this._super();
  196. this.set_input_areas();
  197. // set description
  198. this.set_max_width();
  199. this.setup_update_on_refresh();
  200. },
  201. make_wrapper: function() {
  202. if(this.only_input) {
  203. this.$wrapper = $('<div class="form-group frappe-control">').appendTo(this.parent);
  204. } else {
  205. this.$wrapper = $('<div class="frappe-control">\
  206. <div class="form-group">\
  207. <div class="clearfix">\
  208. <label class="control-label" style="padding-right: 0px;"></label>\
  209. </div>\
  210. <div class="control-input-wrapper">\
  211. <div class="control-input"></div>\
  212. <div class="control-value like-disabled-input" style="display: none;"></div>\
  213. <p class="help-box small text-muted hidden-xs"></p>\
  214. </div>\
  215. </div>\
  216. </div>').appendTo(this.parent);
  217. }
  218. },
  219. toggle_label: function(show) {
  220. this.$wrapper.find(".control-label").toggleClass("hide", !show);
  221. },
  222. toggle_description: function(show) {
  223. this.$wrapper.find(".help-box").toggleClass("hide", !show);
  224. },
  225. set_input_areas: function() {
  226. if(this.only_input) {
  227. this.input_area = this.wrapper;
  228. } else {
  229. this.label_area = this.label_span = this.$wrapper.find("label").get(0);
  230. this.input_area = this.$wrapper.find(".control-input").get(0);
  231. // keep a separate display area to rendered formatted values
  232. // like links, currencies, HTMLs etc.
  233. this.disp_area = this.$wrapper.find(".control-value").get(0);
  234. }
  235. },
  236. set_max_width: function() {
  237. if(this.horizontal) {
  238. this.$wrapper.addClass("input-max-width");
  239. }
  240. },
  241. // update input value, label, description
  242. // display (show/hide/read-only),
  243. // mandatory style on refresh
  244. setup_update_on_refresh: function() {
  245. var me = this;
  246. var make_input = function() {
  247. if(!me.has_input) {
  248. me.make_input();
  249. if(me.df.on_make) {
  250. me.df.on_make(me);
  251. }
  252. }
  253. }
  254. var update_input = function() {
  255. if(me.doctype && me.docname) {
  256. me.set_input(me.value);
  257. } else {
  258. me.set_input(me.value || null);
  259. }
  260. }
  261. this.$wrapper.on("refresh", function() {
  262. if(me.disp_status != "None") {
  263. // refresh value
  264. if(me.doctype && me.docname) {
  265. me.value = frappe.model.get_value(me.doctype, me.docname, me.df.fieldname);
  266. }
  267. if(me.disp_status=="Write") {
  268. me.disp_area && $(me.disp_area).toggle(false);
  269. $(me.input_area).toggle(true);
  270. me.$input && me.$input.prop("disabled", false);
  271. make_input();
  272. update_input();
  273. } else {
  274. if(me.only_input) {
  275. make_input();
  276. update_input();
  277. } else {
  278. $(me.input_area).toggle(false);
  279. if (me.disp_area) {
  280. me.set_disp_area();
  281. $(me.disp_area).toggle(true);
  282. }
  283. }
  284. me.$input && me.$input.prop("disabled", true);
  285. }
  286. me.set_description();
  287. me.set_label();
  288. me.set_mandatory(me.value);
  289. me.set_bold();
  290. }
  291. return false;
  292. });
  293. },
  294. set_disp_area: function() {
  295. let value = this.get_value();
  296. if(in_list(["Currency", "Int", "Float"], this.df.fieldtype) && (this.value === 0 || value === 0)) {
  297. // to set the 0 value in readonly for currency, int, float field
  298. value = 0;
  299. } else {
  300. value = this.value || value;
  301. }
  302. this.disp_area && $(this.disp_area)
  303. .html(frappe.format(value, this.df, {no_icon:true, inline:true},
  304. this.doc || (this.frm && this.frm.doc)));
  305. },
  306. bind_change_event: function() {
  307. var me = this;
  308. this.$input && this.$input.on("change", this.change || function(e) {
  309. me.parse_validate_and_set_in_model(me.get_value(), e);
  310. });
  311. },
  312. bind_focusout: function() {
  313. // on touchscreen devices, scroll to top
  314. // so that static navbar and page head don't overlap the input
  315. if (frappe.dom.is_touchscreen()) {
  316. var me = this;
  317. this.$input && this.$input.on("focusout", function() {
  318. if (frappe.dom.is_touchscreen()) {
  319. frappe.utils.scroll_to(me.$wrapper);
  320. }
  321. });
  322. }
  323. },
  324. set_label: function(label) {
  325. if(label) this.df.label = label;
  326. if(this.only_input || this.df.label==this._label)
  327. return;
  328. var icon = "";
  329. this.label_span.innerHTML = (icon ? '<i class="'+icon+'"></i> ' : "") +
  330. __(this.df.label) || "&nbsp;";
  331. this._label = this.df.label;
  332. },
  333. set_description: function() {
  334. if(this.only_input || this.df.description===this._description)
  335. return;
  336. if(this.df.description) {
  337. this.$wrapper.find(".help-box").html(__(this.df.description));
  338. } else {
  339. this.set_empty_description();
  340. }
  341. this._description = this.df.description;
  342. },
  343. set_new_description: function(description) {
  344. this.$wrapper.find(".help-box").html(description);
  345. },
  346. set_empty_description: function() {
  347. this.$wrapper.find(".help-box").html("");
  348. },
  349. set_mandatory: function(value) {
  350. this.$wrapper.toggleClass("has-error", (this.df.reqd && is_null(value)) ? true : false);
  351. },
  352. set_bold: function() {
  353. if(this.$input) {
  354. this.$input.toggleClass("bold", !!(this.df.bold || this.df.reqd));
  355. }
  356. if(this.disp_area) {
  357. $(this.disp_area).toggleClass("bold", !!(this.df.bold || this.df.reqd));
  358. }
  359. }
  360. });
  361. frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({
  362. html_element: "input",
  363. input_type: "text",
  364. make_input: function() {
  365. if(this.$input) return;
  366. this.$input = $("<"+ this.html_element +">")
  367. .attr("type", this.input_type)
  368. .attr("autocomplete", "off")
  369. .addClass("input-with-feedback form-control")
  370. .prependTo(this.input_area)
  371. if (in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'],
  372. this.df.fieldtype)) {
  373. this.$input.attr("maxlength", this.df.length || 140);
  374. }
  375. this.set_input_attributes();
  376. this.input = this.$input.get(0);
  377. this.has_input = true;
  378. this.bind_change_event();
  379. this.bind_focusout();
  380. // somehow this event does not bubble up to document
  381. // after v7, if you can debug, remove this
  382. },
  383. set_input_attributes: function() {
  384. this.$input
  385. .attr("data-fieldtype", this.df.fieldtype)
  386. .attr("data-fieldname", this.df.fieldname)
  387. .attr("placeholder", this.df.placeholder || "")
  388. if(this.doctype) {
  389. this.$input.attr("data-doctype", this.doctype);
  390. }
  391. if(this.df.input_css) {
  392. this.$input.css(this.df.input_css);
  393. }
  394. if(this.df.input_class) {
  395. this.$input.addClass(this.df.input_class);
  396. }
  397. },
  398. set_input: function(value) {
  399. this.last_value = this.value;
  400. this.value = value;
  401. this.set_formatted_input(value);
  402. this.set_disp_area();
  403. this.set_mandatory && this.set_mandatory(value);
  404. },
  405. set_formatted_input: function(value) {
  406. this.$input && this.$input.val(this.format_for_input(value));
  407. },
  408. get_value: function() {
  409. return this.$input ? this.$input.val() : undefined;
  410. },
  411. format_for_input: function(val) {
  412. return val==null ? "" : val;
  413. },
  414. validate: function(v, callback) {
  415. if(this.df.options == 'Phone') {
  416. if(v+''=='') {
  417. callback("");
  418. return;
  419. }
  420. var v1 = ''
  421. // phone may start with + and must only have numbers later, '-' and ' ' are stripped
  422. v = v.replace(/ /g, '').replace(/-/g, '').replace(/\(/g, '').replace(/\)/g, '');
  423. // allow initial +,0,00
  424. if(v && v.substr(0,1)=='+') {
  425. v1 = '+'; v = v.substr(1);
  426. }
  427. if(v && v.substr(0,2)=='00') {
  428. v1 += '00'; v = v.substr(2);
  429. }
  430. if(v && v.substr(0,1)=='0') {
  431. v1 += '0'; v = v.substr(1);
  432. }
  433. v1 += cint(v) + '';
  434. callback(v1);
  435. } else if(this.df.options == 'Email') {
  436. if(v+''=='') {
  437. callback("");
  438. return;
  439. }
  440. var email_list = frappe.utils.split_emails(v);
  441. if (!email_list) {
  442. // invalid email
  443. callback("");
  444. } else {
  445. var invalid_email = false;
  446. email_list.forEach(function(email) {
  447. if (!validate_email(email)) {
  448. frappe.msgprint(__("Invalid Email: {0}", [email]));
  449. invalid_email = true;
  450. }
  451. });
  452. if (invalid_email) {
  453. // at least 1 invalid email
  454. callback("");
  455. } else {
  456. // all good
  457. callback(v);
  458. }
  459. }
  460. } else {
  461. callback(v);
  462. }
  463. }
  464. });
  465. frappe.ui.form.ControlReadOnly = frappe.ui.form.ControlData.extend({
  466. get_status: function(explain) {
  467. var status = this._super(explain);
  468. if(status==="Write")
  469. status = "Read";
  470. return;
  471. },
  472. });
  473. frappe.ui.form.ControlPassword = frappe.ui.form.ControlData.extend({
  474. input_type: "password"
  475. });
  476. frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({
  477. make: function() {
  478. this._super();
  479. // $(this.label_area).addClass('pull-right');
  480. // $(this.disp_area).addClass('text-right');
  481. },
  482. make_input: function() {
  483. var me = this;
  484. this._super();
  485. this.$input
  486. // .addClass("text-right")
  487. .on("focus", function() {
  488. setTimeout(function() {
  489. if(!document.activeElement) return;
  490. me.validate(document.activeElement.value, function(val) {
  491. document.activeElement.value = val;
  492. });
  493. document.activeElement.select()
  494. }, 100);
  495. return false;
  496. })
  497. },
  498. parse: function(value) {
  499. return cint(value, null);
  500. },
  501. validate: function(value, callback) {
  502. return callback(value);
  503. }
  504. });
  505. frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({
  506. parse: function(value) {
  507. return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision());
  508. },
  509. format_for_input: function(value) {
  510. var number_format;
  511. if (this.df.fieldtype==="Float" && this.df.options && this.df.options.trim()) {
  512. number_format = this.get_number_format();
  513. }
  514. var formatted_value = format_number(parseFloat(value), number_format, this.get_precision());
  515. return isNaN(parseFloat(value)) ? "" : formatted_value;
  516. },
  517. // even a float field can be formatted based on currency format instead of float format
  518. get_number_format: function() {
  519. var currency = frappe.meta.get_field_currency(this.df, this.get_doc());
  520. return get_number_format(currency);
  521. },
  522. get_precision: function() {
  523. // round based on field precision or float precision, else don't round
  524. return this.df.precision || cint(frappe.boot.sysdefaults.float_precision, null);
  525. }
  526. });
  527. frappe.ui.form.ControlCurrency = frappe.ui.form.ControlFloat.extend({
  528. format_for_input: function(value) {
  529. var formatted_value = format_number(parseFloat(value), this.get_number_format(), this.get_precision());
  530. return isNaN(parseFloat(value)) ? "" : formatted_value;
  531. },
  532. get_precision: function() {
  533. // always round based on field precision or currency's precision
  534. // this method is also called in this.parse()
  535. if (!this.df.precision) {
  536. if(frappe.boot.sysdefaults.currency_precision) {
  537. this.df.precision = frappe.boot.sysdefaults.currency_precision;
  538. } else {
  539. this.df.precision = get_number_format_info(this.get_number_format()).precision;
  540. }
  541. }
  542. return this.df.precision;
  543. }
  544. });
  545. frappe.ui.form.ControlPercent = frappe.ui.form.ControlFloat;
  546. frappe.ui.form.ControlDate = frappe.ui.form.ControlData.extend({
  547. make_input: function() {
  548. this._super();
  549. this.set_date_options();
  550. this.set_datepicker();
  551. this.set_t_for_today();
  552. },
  553. set_formatted_input: function(value) {
  554. this._super(value);
  555. if(value
  556. && ((this.last_value && this.last_value !== value)
  557. || (!this.datepicker.selectedDates.length))) {
  558. this.datepicker.selectDate(frappe.datetime.str_to_obj(value));
  559. }
  560. },
  561. set_date_options: function() {
  562. var me = this;
  563. var lang = frappe.boot.user.language;
  564. if(!$.fn.datepicker.language[lang]) {
  565. lang = 'en'
  566. }
  567. this.datepicker_options = {
  568. language: lang,
  569. autoClose: true,
  570. todayButton: new Date(),
  571. dateFormat: (frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd'),
  572. onSelect: function(dateStr) {
  573. me.$input.trigger('change');
  574. },
  575. onShow: function() {
  576. $('.datepicker--button:visible').text(__('Today'));
  577. },
  578. };
  579. },
  580. set_datepicker: function() {
  581. this.$input.datepicker(this.datepicker_options);
  582. this.datepicker = this.$input.data('datepicker');
  583. },
  584. set_t_for_today: function() {
  585. var me = this;
  586. this.$input.on("keydown", function(e) {
  587. if(e.which===84) { // 84 === t
  588. if(me.df.fieldtype=='Date') {
  589. me.set_value(frappe.datetime.str_to_user(
  590. frappe.datetime.nowdate()));
  591. } if(me.df.fieldtype=='Datetime') {
  592. me.set_value(frappe.datetime.str_to_user(
  593. frappe.datetime.now_datetime()));
  594. } if(me.df.fieldtype=='Time') {
  595. me.set_value(frappe.datetime.str_to_user(
  596. frappe.datetime.now_time()));
  597. }
  598. return false;
  599. }
  600. });
  601. },
  602. parse: function(value) {
  603. if(value) {
  604. return frappe.datetime.user_to_str(value);
  605. }
  606. },
  607. format_for_input: function(value) {
  608. if(value) {
  609. return frappe.datetime.str_to_user(value);
  610. }
  611. return "";
  612. },
  613. validate: function(value, callback) {
  614. if(value && !frappe.datetime.validate(value)) {
  615. frappe.msgprint(__("Date must be in format: {0}", [frappe.sys_defaults.date_format || "yyyy-mm-dd"]));
  616. callback("");
  617. return;
  618. }
  619. return callback(value);
  620. }
  621. });
  622. frappe.ui.form.ControlTime = frappe.ui.form.ControlData.extend({
  623. make_input: function() {
  624. var me = this;
  625. this._super();
  626. this.$input.datepicker({
  627. language: "en",
  628. timepicker: true,
  629. onlyTimepicker: true,
  630. timeFormat: "hh:ii:ss",
  631. onSelect: function(dateObj) {
  632. me.$input.trigger('change');
  633. },
  634. onShow: function() {
  635. $('.datepicker--button:visible').text(__('Now'));
  636. },
  637. todayButton: new Date()
  638. });
  639. this.datepicker = this.$input.data('datepicker');
  640. this.refresh();
  641. },
  642. set_input: function(value) {
  643. this._super(value);
  644. if(value
  645. && ((this.last_value && this.last_value !== this.value)
  646. || (!this.datepicker.selectedDates.length))) {
  647. this.datepicker.selectDate(moment(value, 'hh:mm:ss')._d);
  648. }
  649. },
  650. });
  651. frappe.ui.form.ControlDatetime = frappe.ui.form.ControlDate.extend({
  652. set_date_options: function() {
  653. this._super();
  654. this.datepicker_options.timepicker = true;
  655. this.datepicker_options.timeFormat = "hh:ii:ss";
  656. this.datepicker_options.onShow = function() {
  657. $('.datepicker--button:visible').text(__('Now'));
  658. };
  659. },
  660. parse: function(value) {
  661. if(value) {
  662. // parse and convert
  663. value = frappe.datetime.convert_to_system_tz(frappe.datetime.user_to_str(value));
  664. }
  665. return value;
  666. },
  667. format_for_input: function(value) {
  668. if(value) {
  669. // convert and format
  670. value = frappe.datetime.str_to_user(frappe.datetime.convert_to_user_tz(value));
  671. }
  672. return value || "";
  673. }
  674. });
  675. frappe.ui.form.ControlDateRange = frappe.ui.form.ControlData.extend({
  676. make_input: function() {
  677. var me = this;
  678. this._super();
  679. this.set_date_options();
  680. this.set_datepicker();
  681. this.refresh();
  682. },
  683. set_date_options: function() {
  684. var me = this;
  685. this.datepicker_options = {
  686. language: "en",
  687. range: true,
  688. autoClose: true,
  689. toggleSelected: false
  690. }
  691. this.datepicker_options.dateFormat =
  692. (frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd');
  693. this.datepicker_options.onSelect = function(dateObj) {
  694. me.set_value(dateObj);
  695. }
  696. },
  697. set_datepicker: function() {
  698. this.$input.datepicker(this.datepicker_options);
  699. this.datepicker = this.$input.data('datepicker');
  700. },
  701. set_input: function(value, value2) {
  702. this.last_value = this.value;
  703. if (value && value2) {
  704. this.value = [value, value2];
  705. } else {
  706. this.value = value
  707. }
  708. if (this.value) {
  709. this.$input && this.$input.val(this.format_for_input(this.value[0], this.value[1]));
  710. } else {
  711. this.$input && this.$input.val("")
  712. }
  713. this.set_disp_area();
  714. this.set_mandatory && this.set_mandatory(value);
  715. },
  716. parse: function(value) {
  717. if(value && (value.indexOf(',') !== -1 || value.indexOf('to') !== -1)) {
  718. var vals = value.split(/[( to )(,)]/)
  719. var from_date = moment(frappe.datetime.user_to_obj(vals[0])).format('YYYY-MM-DD');
  720. var to_date = moment(frappe.datetime.user_to_obj(vals[vals.length-1])).format('YYYY-MM-DD');
  721. return [from_date, to_date];
  722. }
  723. },
  724. format_for_input: function(value,value2) {
  725. if(value && value2) {
  726. value = frappe.datetime.str_to_user(value);
  727. value2 = frappe.datetime.str_to_user(value2);
  728. return value + " to " + value2
  729. }
  730. return "";
  731. },
  732. validate: function(value, callback) {
  733. return callback(value);
  734. }
  735. });
  736. frappe.ui.form.ControlText = frappe.ui.form.ControlData.extend({
  737. html_element: "textarea",
  738. horizontal: false,
  739. make_wrapper: function() {
  740. this._super();
  741. this.$wrapper.find(".like-disabled-input").addClass("for-description");
  742. },
  743. make_input: function() {
  744. this._super();
  745. this.$input.css({'height': '300px'})
  746. }
  747. });
  748. frappe.ui.form.ControlLongText = frappe.ui.form.ControlText;
  749. frappe.ui.form.ControlSmallText = frappe.ui.form.ControlText.extend({
  750. make_input: function() {
  751. this._super();
  752. this.$input.css({'height': '150px'})
  753. }
  754. });
  755. frappe.ui.form.ControlCheck = frappe.ui.form.ControlData.extend({
  756. input_type: "checkbox",
  757. make_wrapper: function() {
  758. this.$wrapper = $('<div class="form-group frappe-control">\
  759. <div class="checkbox">\
  760. <label>\
  761. <span class="input-area"></span>\
  762. <span class="disp-area" style="display:none; margin-left: -20px;"></span>\
  763. <span class="label-area small"></span>\
  764. </label>\
  765. <p class="help-box small text-muted"></p>\
  766. </div>\
  767. </div>').appendTo(this.parent)
  768. },
  769. set_input_areas: function() {
  770. this.label_area = this.label_span = this.$wrapper.find(".label-area").get(0);
  771. this.input_area = this.$wrapper.find(".input-area").get(0);
  772. this.disp_area = this.$wrapper.find(".disp-area").get(0);
  773. },
  774. make_input: function() {
  775. this._super();
  776. this.$input.removeClass("form-control");
  777. },
  778. parse: function(value) {
  779. return this.input.checked ? 1 : 0;
  780. },
  781. validate: function(value, callback) {
  782. return callback(cint(value));
  783. },
  784. set_input: function(value) {
  785. if(this.input) {
  786. this.input.checked = (value ? 1 : 0);
  787. }
  788. this.last_value = value;
  789. this.set_mandatory(value);
  790. this.set_disp_area();
  791. },
  792. get_value: function() {
  793. if (!this.$input) {
  794. return;
  795. }
  796. return this.$input.prop("checked") ? 1 : 0;
  797. },
  798. });
  799. frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
  800. make_input: function() {
  801. var me = this;
  802. this.$input = $('<button class="btn btn-default btn-xs">')
  803. .prependTo(me.input_area)
  804. .on("click", function() {
  805. me.onclick();
  806. });
  807. this.input = this.$input.get(0);
  808. this.set_input_attributes();
  809. this.has_input = true;
  810. this.toggle_label(false);
  811. },
  812. onclick: function() {
  813. if(this.frm && this.frm.doc) {
  814. if(this.frm.script_manager.get_handlers(this.df.fieldname, this.doctype, this.docname).length) {
  815. this.frm.script_manager.trigger(this.df.fieldname, this.doctype, this.docname);
  816. } else {
  817. this.frm.runscript(this.df.options, this);
  818. }
  819. }
  820. else if(this.df.click) {
  821. this.df.click();
  822. }
  823. },
  824. set_input_areas: function() {
  825. this._super();
  826. $(this.disp_area).removeClass().addClass("hide");
  827. },
  828. set_empty_description: function() {
  829. this.$wrapper.find(".help-box").empty().toggle(false);
  830. },
  831. set_label: function() {
  832. $(this.label_span).html("&nbsp;");
  833. this.$input && this.$input.html((this.df.icon ?
  834. ('<i class="'+this.df.icon+' fa-fw"></i> ') : "") + __(this.df.label));
  835. }
  836. });
  837. frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
  838. make_input: function() {
  839. var me = this;
  840. this.$input = $('<button class="btn btn-default btn-sm btn-attach">')
  841. .html(__("Attach"))
  842. .prependTo(me.input_area)
  843. .on("click", function() {
  844. me.onclick();
  845. });
  846. this.$value = $('<div style="margin-top: 5px;">\
  847. <div class="ellipsis" style="display: inline-block; width: 90%;">\
  848. <i class="fa fa-paper-clip"></i> \
  849. <a class="attached-file" target="_blank"></a>\
  850. </div>\
  851. <a class="close">&times;</a></div>')
  852. .prependTo(me.input_area)
  853. .toggle(false);
  854. this.input = this.$input.get(0);
  855. this.set_input_attributes();
  856. this.has_input = true;
  857. this.$value.find(".close").on("click", function() {
  858. me.clear_attachment();
  859. })
  860. },
  861. clear_attachment: function() {
  862. var me = this;
  863. if(this.frm) {
  864. me.frm.attachments.remove_attachment_by_filename(me.value, function() {
  865. me.parse_validate_and_set_in_model(null);
  866. me.refresh();
  867. me.frm.save();
  868. });
  869. } else {
  870. this.dataurl = null;
  871. this.fileobj = null;
  872. this.set_input(null);
  873. this.refresh();
  874. }
  875. },
  876. onclick: function() {
  877. var me = this;
  878. if(this.doc) {
  879. var doc = this.doc.parent && frappe.model.get_doc(this.doc.parenttype, this.doc.parent) || this.doc;
  880. if (doc.__islocal) {
  881. frappe.msgprint(__("Please save the document before uploading."));
  882. return;
  883. }
  884. }
  885. if(!this.dialog) {
  886. this.dialog = new frappe.ui.Dialog({
  887. title: __(this.df.label || __("Upload")),
  888. fields: [
  889. {fieldtype:"HTML", fieldname:"upload_area"},
  890. {fieldtype:"HTML", fieldname:"or_attach", options: __("Or")},
  891. {fieldtype:"Select", fieldname:"select", label:__("Select from existing attachments") },
  892. {fieldtype:"Button", fieldname:"clear",
  893. label:__("Clear Attachment"), click: function() {
  894. me.clear_attachment();
  895. me.dialog.hide();
  896. }
  897. },
  898. ]
  899. });
  900. }
  901. this.dialog.show();
  902. this.dialog.get_field("upload_area").$wrapper.empty();
  903. // select from existing attachments
  904. var attachments = this.frm && this.frm.attachments.get_attachments() || [];
  905. var select = this.dialog.get_field("select");
  906. if(attachments.length) {
  907. attachments = $.map(attachments, function(o) { return o.file_url; })
  908. select.df.options = [""].concat(attachments);
  909. select.toggle(true);
  910. this.dialog.get_field("or_attach").toggle(true);
  911. select.refresh();
  912. } else {
  913. this.dialog.get_field("or_attach").toggle(false);
  914. select.toggle(false);
  915. }
  916. select.$input.val("");
  917. // show button if attachment exists
  918. this.dialog.get_field('clear').$wrapper.toggle(this.get_model_value() ? true : false);
  919. this.set_upload_options();
  920. frappe.upload.make(this.upload_options);
  921. },
  922. set_upload_options: function() {
  923. var me = this;
  924. this.upload_options = {
  925. parent: this.dialog.get_field("upload_area").$wrapper,
  926. args: {},
  927. allow_multiple: 0,
  928. max_width: this.df.max_width,
  929. max_height: this.df.max_height,
  930. options: this.df.options,
  931. btn: this.dialog.set_primary_action(__("Upload")),
  932. on_no_attach: function() {
  933. // if no attachmemts,
  934. // check if something is selected
  935. var selected = me.dialog.get_field("select").get_value();
  936. if(selected) {
  937. me.parse_validate_and_set_in_model(selected);
  938. me.dialog.hide();
  939. me.frm.save();
  940. } else {
  941. frappe.msgprint(__("Please attach a file or set a URL"));
  942. }
  943. },
  944. callback: function(attachment, r) {
  945. me.on_upload_complete(attachment);
  946. me.dialog.hide();
  947. },
  948. onerror: function() {
  949. me.dialog.hide();
  950. }
  951. }
  952. if ("is_private" in this.df) {
  953. this.upload_options.is_private = this.df.is_private;
  954. }
  955. if(this.frm) {
  956. this.upload_options.args = {
  957. from_form: 1,
  958. doctype: this.frm.doctype,
  959. docname: this.frm.docname
  960. }
  961. } else {
  962. this.upload_options.on_attach = function(fileobj, dataurl) {
  963. me.dialog.hide();
  964. me.fileobj = fileobj;
  965. me.dataurl = dataurl;
  966. if(me.on_attach) {
  967. me.on_attach()
  968. }
  969. if(me.df.on_attach) {
  970. me.df.on_attach(fileobj, dataurl);
  971. }
  972. me.on_upload_complete();
  973. }
  974. }
  975. },
  976. set_input: function(value, dataurl) {
  977. this.value = value;
  978. if(this.value) {
  979. this.$input.toggle(false);
  980. if(this.value.indexOf(",")!==-1) {
  981. var parts = this.value.split(",");
  982. var filename = parts[0];
  983. var dataurl = parts[1];
  984. }
  985. this.$value.toggle(true).find(".attached-file")
  986. .html(filename || this.value)
  987. .attr("href", dataurl || this.value);
  988. } else {
  989. this.$input.toggle(true);
  990. this.$value.toggle(false);
  991. }
  992. },
  993. get_value: function() {
  994. if(this.frm) {
  995. return this.value;
  996. } else {
  997. return this.fileobj ? (this.fileobj.filename + "," + this.dataurl) : null;
  998. }
  999. },
  1000. on_upload_complete: function(attachment) {
  1001. if(this.frm) {
  1002. this.parse_validate_and_set_in_model(attachment.file_url);
  1003. this.refresh();
  1004. this.frm.attachments.update_attachment(attachment);
  1005. this.frm.save();
  1006. } else {
  1007. this.value = this.get_value();
  1008. this.refresh();
  1009. }
  1010. },
  1011. });
  1012. frappe.ui.form.ControlAttachImage = frappe.ui.form.ControlAttach.extend({
  1013. make: function() {
  1014. var me = this;
  1015. this._super();
  1016. this.img_wrapper = $('<div style="margin: 7px 0px;">\
  1017. <div class="missing-image attach-missing-image"><i class="octicon octicon-circle-slash"></i></div></div>')
  1018. .appendTo(this.wrapper);
  1019. this.img = $("<img class='img-responsive attach-image-display'>")
  1020. .appendTo(this.img_wrapper).toggle(false);
  1021. // propagate click to Attach button
  1022. this.img_wrapper.find(".missing-image").on("click", function() { me.$input.click(); });
  1023. this.img.on("click", function() { me.$input.click(); });
  1024. this.$wrapper.on("refresh", function() {
  1025. me.set_image();
  1026. if(me.get_status()=="Read") {
  1027. $(me.disp_area).toggle(false);
  1028. }
  1029. });
  1030. this.set_image();
  1031. },
  1032. set_image: function() {
  1033. if(this.get_value()) {
  1034. $(this.img_wrapper).find(".missing-image").toggle(false);
  1035. this.img.attr("src", this.dataurl ? this.dataurl : this.value).toggle(true);
  1036. } else {
  1037. $(this.img_wrapper).find(".missing-image").toggle(true);
  1038. this.img.toggle(false);
  1039. }
  1040. }
  1041. });
  1042. frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({
  1043. html_element: "select",
  1044. make_input: function() {
  1045. var me = this;
  1046. this._super();
  1047. this.set_options();
  1048. },
  1049. set_input: function(value) {
  1050. // refresh options first - (new ones??)
  1051. this.set_options(value || "");
  1052. var input_value = null;
  1053. if(this.$input) {
  1054. var input_value = this.$input.val();
  1055. }
  1056. // not a possible option, repair
  1057. if(this.doctype && this.docname) {
  1058. // model value is not an option,
  1059. // set the default option (displayed)
  1060. var model_value = frappe.model.get_value(this.doctype, this.docname, this.df.fieldname);
  1061. if(model_value == null && (input_value || "") != (model_value || "")) {
  1062. this.set_model_value(input_value);
  1063. } else {
  1064. this.last_value = value;
  1065. }
  1066. } else {
  1067. if(value !== input_value) {
  1068. this.set_value(input_value);
  1069. }
  1070. }
  1071. this._super(value);
  1072. },
  1073. set_options: function(value) {
  1074. var options = this.df.options || [];
  1075. if(typeof this.df.options==="string") {
  1076. options = this.df.options.split("\n");
  1077. }
  1078. // nothing changed
  1079. if(options.toString() === this.last_options) {
  1080. return;
  1081. }
  1082. this.last_options = options.toString();
  1083. if(this.$input) {
  1084. var selected = this.$input.find(":selected").val();
  1085. this.$input.empty().add_options(options || []);
  1086. if(value===undefined && selected) {
  1087. this.$input.val(selected);
  1088. }
  1089. }
  1090. },
  1091. get_file_attachment_list: function() {
  1092. if(!this.frm) return;
  1093. var fl = frappe.model.docinfo[this.frm.doctype][this.frm.docname];
  1094. if(fl && fl.attachments) {
  1095. this.set_description("");
  1096. var options = [""];
  1097. $.each(fl.attachments, function(i, f) {
  1098. options.push(f.file_url)
  1099. });
  1100. return options;
  1101. } else {
  1102. this.set_description(__("Please attach a file first."))
  1103. return [""];
  1104. }
  1105. }
  1106. });
  1107. // special features for link
  1108. // buttons
  1109. // autocomplete
  1110. // link validation
  1111. // custom queries
  1112. // add_fetches
  1113. frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
  1114. make_input: function() {
  1115. var me = this;
  1116. // line-height: 1 is for Mozilla 51, shows extra padding otherwise
  1117. $('<div class="link-field ui-front" style="position: relative; line-height: 1;">\
  1118. <input type="text" class="input-with-feedback form-control">\
  1119. <span class="link-btn">\
  1120. <a class="btn-open no-decoration" title="' + __("Open Link") + '">\
  1121. <i class="octicon octicon-arrow-right"></i></a>\
  1122. </span>\
  1123. </div>').prependTo(this.input_area);
  1124. this.$input_area = $(this.input_area);
  1125. this.$input = this.$input_area.find('input');
  1126. this.$link = this.$input_area.find('.link-btn');
  1127. this.$link_open = this.$link.find('.btn-open');
  1128. this.set_input_attributes();
  1129. this.$input.on("focus", function() {
  1130. setTimeout(function() {
  1131. if(me.$input.val() && me.get_options()) {
  1132. me.$link.toggle(true);
  1133. me.$link_open.attr('href', '#Form/' + me.get_options() + '/' + me.$input.val());
  1134. }
  1135. if(!me.$input.val()) {
  1136. me.$input.val("").trigger("input");
  1137. }
  1138. }, 500);
  1139. });
  1140. this.$input.on("blur", function() {
  1141. // if this disappears immediately, the user's click
  1142. // does not register, hence timeout
  1143. setTimeout(function() {
  1144. me.$link.toggle(false);
  1145. }, 500);
  1146. });
  1147. this.input = this.$input.get(0);
  1148. this.has_input = true;
  1149. this.translate_values = true;
  1150. var me = this;
  1151. this.setup_buttons();
  1152. this.setup_awesomeplete();
  1153. if(this.df.change) {
  1154. this.$input.on("change", function() {
  1155. me.df.change.apply(this);
  1156. });
  1157. }
  1158. },
  1159. get_options: function() {
  1160. return this.df.options;
  1161. },
  1162. setup_buttons: function() {
  1163. var me = this;
  1164. if(this.only_input && !this.with_link_btn) {
  1165. this.$input_area.find(".link-btn").remove();
  1166. }
  1167. },
  1168. open_advanced_search: function() {
  1169. var doctype = this.get_options();
  1170. if(!doctype) return;
  1171. new frappe.ui.form.LinkSelector({
  1172. doctype: doctype,
  1173. target: this,
  1174. txt: this.get_value()
  1175. });
  1176. return false;
  1177. },
  1178. new_doc: function() {
  1179. var doctype = this.get_options();
  1180. var me = this;
  1181. if(!doctype) return;
  1182. // set values to fill in the new document
  1183. if(this.df.get_route_options_for_new_doc) {
  1184. frappe.route_options = this.df.get_route_options_for_new_doc(this);
  1185. } else {
  1186. frappe.route_options = {};
  1187. }
  1188. // partially entered name field
  1189. frappe.route_options.name_field = this.get_value();
  1190. // reference to calling link
  1191. frappe._from_link = this;
  1192. frappe._from_link_scrollY = $(document).scrollTop();
  1193. var trimmed_doctype = doctype.replace(/ /g, '');
  1194. var controller_name = "QuickEntryForm";
  1195. if(frappe.ui.form[trimmed_doctype + "QuickEntryForm"]){
  1196. controller_name = trimmed_doctype + "QuickEntryForm";
  1197. }
  1198. new frappe.ui.form[controller_name](doctype, function(doc) {
  1199. if(me.frm) {
  1200. me.parse_validate_and_set_in_model(doc.name);
  1201. } else {
  1202. me.set_value(doc.name);
  1203. }
  1204. });
  1205. return false;
  1206. },
  1207. setup_awesomeplete: function() {
  1208. var me = this;
  1209. this.$input.cache = {};
  1210. this.awesomplete = new Awesomplete(me.input, {
  1211. minChars: 0,
  1212. maxItems: 99,
  1213. autoFirst: true,
  1214. list: [],
  1215. data: function (item, input) {
  1216. return {
  1217. label: item.label || item.value,
  1218. value: item.value
  1219. };
  1220. },
  1221. filter: function(item, input) {
  1222. return true;
  1223. },
  1224. item: function (item, input) {
  1225. var d = this.get_item(item.value);
  1226. if(!d.label) { d.label = d.value; }
  1227. var _label = (me.translate_values) ? __(d.label) : d.label;
  1228. var html = "<strong>" + _label + "</strong>";
  1229. if(d.description && d.value!==d.description) {
  1230. html += '<br><span class="small">' + __(d.description) + '</span>';
  1231. }
  1232. return $('<li></li>')
  1233. .data('item.autocomplete', d)
  1234. .prop('aria-selected', 'false')
  1235. .html('<a><p>' + html + '</p></a>')
  1236. .get(0);
  1237. },
  1238. sort: function(a, b) {
  1239. return 0;
  1240. }
  1241. });
  1242. this.$input.on("input", function(e) {
  1243. var doctype = me.get_options();
  1244. if(!doctype) return;
  1245. if (!me.$input.cache[doctype]) {
  1246. me.$input.cache[doctype] = {};
  1247. }
  1248. var term = e.target.value;
  1249. if (me.$input.cache[doctype][term]!=null) {
  1250. // immediately show from cache
  1251. me.awesomplete.list = me.$input.cache[doctype][term];
  1252. }
  1253. var args = {
  1254. 'txt': term,
  1255. 'doctype': doctype,
  1256. };
  1257. me.set_custom_query(args);
  1258. frappe.call({
  1259. type: "GET",
  1260. method:'frappe.desk.search.search_link',
  1261. no_spinner: true,
  1262. args: args,
  1263. callback: function(r) {
  1264. if(!me.$input.is(":focus")) {
  1265. return;
  1266. }
  1267. if(!me.df.only_select) {
  1268. if(frappe.model.can_create(doctype)
  1269. && me.df.fieldtype !== "Dynamic Link") {
  1270. // new item
  1271. r.results.push({
  1272. label: "<span class='text-primary link-option'>"
  1273. + "<i class='fa fa-plus' style='margin-right: 5px;'></i> "
  1274. + __("Create a new {0}", [__(me.df.options)])
  1275. + "</span>",
  1276. value: "create_new__link_option",
  1277. action: me.new_doc
  1278. })
  1279. }
  1280. // advanced search
  1281. r.results.push({
  1282. label: "<span class='text-primary link-option'>"
  1283. + "<i class='fa fa-search' style='margin-right: 5px;'></i> "
  1284. + __("Advanced Search")
  1285. + "</span>",
  1286. value: "advanced_search__link_option",
  1287. action: me.open_advanced_search
  1288. })
  1289. }
  1290. me.$input.cache[doctype][term] = r.results;
  1291. me.awesomplete.list = me.$input.cache[doctype][term];
  1292. }
  1293. });
  1294. });
  1295. this.$input.on("blur", function() {
  1296. if(me.selected) {
  1297. me.selected = false;
  1298. return;
  1299. }
  1300. var value = me.get_value();
  1301. if(value!==me.last_value) {
  1302. me.parse_validate_and_set_in_model(value);
  1303. }
  1304. });
  1305. this.$input.on("awesomplete-open", function(e) {
  1306. me.$wrapper.css({"z-index": 100});
  1307. me.$wrapper.find('ul').css({"z-index": 100});
  1308. me.autocomplete_open = true;
  1309. });
  1310. this.$input.on("awesomplete-close", function(e) {
  1311. me.$wrapper.css({"z-index": 1});
  1312. me.autocomplete_open = false;
  1313. });
  1314. this.$input.on("awesomplete-select", function(e) {
  1315. var o = e.originalEvent;
  1316. var item = me.awesomplete.get_item(o.text.value);
  1317. me.autocomplete_open = false;
  1318. // prevent selection on tab
  1319. var TABKEY = 9;
  1320. if(e.keyCode === TABKEY) {
  1321. e.preventDefault();
  1322. me.awesomplete.close();
  1323. return false;
  1324. }
  1325. if(item.action) {
  1326. item.value = "";
  1327. item.action.apply(me);
  1328. }
  1329. // if remember_last_selected is checked in the doctype against the field,
  1330. // then add this value
  1331. // to defaults so you do not need to set it again
  1332. // unless it is changed.
  1333. if(me.df.remember_last_selected_value) {
  1334. frappe.boot.user.last_selected_values[me.df.options] = item.value;
  1335. }
  1336. me.parse_validate_and_set_in_model(item.value);
  1337. });
  1338. this.$input.on("awesomplete-selectcomplete", function(e) {
  1339. var o = e.originalEvent;
  1340. if(o.text.value.indexOf("__link_option") !== -1) {
  1341. me.$input.val("");
  1342. }
  1343. });
  1344. },
  1345. set_custom_query: function(args) {
  1346. var set_nulls = function(obj) {
  1347. $.each(obj, function(key, value) {
  1348. if(value!==undefined) {
  1349. obj[key] = value;
  1350. }
  1351. });
  1352. return obj;
  1353. }
  1354. if(this.get_query || this.df.get_query) {
  1355. var get_query = this.get_query || this.df.get_query;
  1356. if($.isPlainObject(get_query)) {
  1357. var filters = null;
  1358. if(get_query.filters) {
  1359. // passed as {'filters': {'key':'value'}}
  1360. filters = get_query.filters;
  1361. } else if(get_query.query) {
  1362. // passed as {'query': 'path.to.method'}
  1363. args.query = get_query;
  1364. } else {
  1365. // dict is filters
  1366. filters = get_query;
  1367. }
  1368. if (filters) {
  1369. var filters = set_nulls(filters);
  1370. // extend args for custom functions
  1371. $.extend(args, filters);
  1372. // add "filters" for standard query (search.py)
  1373. args.filters = filters;
  1374. }
  1375. } else if(typeof(get_query)==="string") {
  1376. args.query = get_query;
  1377. } else {
  1378. // get_query by function
  1379. var q = (get_query)(this.frm && this.frm.doc || this.doc, this.doctype, this.docname);
  1380. if (typeof(q)==="string") {
  1381. // returns a string
  1382. args.query = q;
  1383. } else if($.isPlainObject(q)) {
  1384. // returns a plain object with filters
  1385. if(q.filters) {
  1386. set_nulls(q.filters);
  1387. }
  1388. // turn off value translation
  1389. if(q.translate_values !== undefined) {
  1390. this.translate_values = q.translate_values;
  1391. }
  1392. // extend args for custom functions
  1393. $.extend(args, q);
  1394. // add "filters" for standard query (search.py)
  1395. args.filters = q.filters;
  1396. }
  1397. }
  1398. }
  1399. if(this.df.filters) {
  1400. set_nulls(this.df.filters);
  1401. if(!args.filters) args.filters = {};
  1402. $.extend(args.filters, this.df.filters);
  1403. }
  1404. },
  1405. validate: function(value, callback) {
  1406. // validate the value just entered
  1407. var me = this;
  1408. if(this.df.options=="[Select]" || this.df.ignore_link_validation) {
  1409. callback(value);
  1410. return;
  1411. }
  1412. this.validate_link_and_fetch(this.df, this.get_options(),
  1413. this.docname, value, callback);
  1414. },
  1415. validate_link_and_fetch: function(df, doctype, docname, value, callback) {
  1416. var me = this;
  1417. if(value) {
  1418. var fetch = '';
  1419. if(this.frm && this.frm.fetch_dict[df.fieldname]) {
  1420. fetch = this.frm.fetch_dict[df.fieldname].columns.join(', ');
  1421. }
  1422. return frappe.call({
  1423. method:'frappe.desk.form.utils.validate_link',
  1424. type: "GET",
  1425. args: {
  1426. 'value': value,
  1427. 'options': doctype,
  1428. 'fetch': fetch
  1429. },
  1430. no_spinner: true,
  1431. callback: function(r) {
  1432. if(r.message=='Ok') {
  1433. if(r.fetch_values && docname) {
  1434. me.set_fetch_values(df, docname, r.fetch_values);
  1435. }
  1436. if(callback) callback(r.valid_value);
  1437. } else {
  1438. if(callback) callback("");
  1439. }
  1440. }
  1441. });
  1442. } else if(callback) {
  1443. callback(value);
  1444. }
  1445. },
  1446. set_fetch_values: function(df, docname, fetch_values) {
  1447. var fl = this.frm.fetch_dict[df.fieldname].fields;
  1448. for(var i=0; i < fl.length; i++) {
  1449. frappe.model.set_value(df.parent, docname, fl[i], fetch_values[i], df.fieldtype);
  1450. }
  1451. }
  1452. });
  1453. if(Awesomplete) {
  1454. Awesomplete.prototype.get_item = function(value) {
  1455. return this._list.find(function(item) {
  1456. return item.value === value;
  1457. });
  1458. }
  1459. }
  1460. frappe.ui.form.ControlDynamicLink = frappe.ui.form.ControlLink.extend({
  1461. get_options: function() {
  1462. if(this.df.get_options) {
  1463. return this.df.get_options();
  1464. }
  1465. if (this.docname==null && cur_dialog) {
  1466. //for dialog box
  1467. return cur_dialog.get_value(this.df.options)
  1468. }
  1469. if (cur_frm==null && cur_list){
  1470. //for list page
  1471. return cur_list.wrapper.find("input[data-fieldname*="+this.df.options+"]").val()
  1472. }
  1473. var options = frappe.model.get_value(this.df.parent, this.docname, this.df.options);
  1474. // if(!options) {
  1475. // frappe.msgprint(__("Please set {0} first",
  1476. // [frappe.meta.get_docfield(this.df.parent, this.df.options, this.docname).label]));
  1477. // }
  1478. return options;
  1479. },
  1480. });
  1481. frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
  1482. make_input: function() {
  1483. this._super();
  1484. $(this.input_area).find("textarea")
  1485. .allowTabs()
  1486. .css({"height":"400px", "font-family": "Monaco, \"Courier New\", monospace"});
  1487. }
  1488. });
  1489. frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
  1490. make_input: function() {
  1491. this.has_input = true;
  1492. this.make_editor();
  1493. this.hide_elements_on_mobile();
  1494. this.setup_drag_drop();
  1495. this.setup_image_dialog();
  1496. },
  1497. make_editor: function() {
  1498. var me = this;
  1499. this.editor = $("<div>").appendTo(this.input_area);
  1500. // Note: while updating summernote, please make sure all 'p' blocks
  1501. // in the summernote source code are replaced by 'div' blocks.
  1502. // by default summernote, adds <p> blocks for new paragraphs, which adds
  1503. // unexpected whitespaces, esp for email replies.
  1504. this.editor.summernote({
  1505. minHeight: 400,
  1506. toolbar: [
  1507. ['magic', ['style']],
  1508. ['style', ['bold', 'italic', 'underline', 'clear']],
  1509. ['fontsize', ['fontsize']],
  1510. ['color', ['color']],
  1511. ['para', ['ul', 'ol', 'paragraph', 'hr']],
  1512. //['height', ['height']],
  1513. ['media', ['link', 'picture', 'video', 'table']],
  1514. ['misc', ['fullscreen', 'codeview']]
  1515. ],
  1516. keyMap: {
  1517. pc: {
  1518. 'CTRL+ENTER': ''
  1519. },
  1520. mac: {
  1521. 'CMD+ENTER': ''
  1522. }
  1523. },
  1524. prettifyHtml: true,
  1525. dialogsInBody: true,
  1526. callbacks: {
  1527. onInit: function() {
  1528. // firefox hack that puts the caret in the wrong position
  1529. // when div is empty. To fix, seed with a <br>.
  1530. // See https://bugzilla.mozilla.org/show_bug.cgi?id=550434
  1531. // this function is executed only once
  1532. $(".note-editable[contenteditable='true']").one('focus', function() {
  1533. var $this = $(this);
  1534. $this.html($this.html() + '<br>');
  1535. });
  1536. },
  1537. onChange: function(value) {
  1538. me.parse_validate_and_set_in_model(value);
  1539. },
  1540. onKeydown: function(e) {
  1541. var key = frappe.ui.keys.get_key(e);
  1542. // prevent 'New DocType (Ctrl + B)' shortcut in editor
  1543. if(['ctrl+b', 'meta+b'].indexOf(key) !== -1) {
  1544. e.stopPropagation();
  1545. }
  1546. if(key.indexOf('escape') !== -1) {
  1547. if(me.note_editor.hasClass('fullscreen')) {
  1548. // exit fullscreen on escape key
  1549. me.note_editor
  1550. .find('.note-btn.btn-fullscreen')
  1551. .trigger('click');
  1552. }
  1553. }
  1554. },
  1555. },
  1556. icons: {
  1557. 'align': 'fa fa-align',
  1558. 'alignCenter': 'fa fa-align-center',
  1559. 'alignJustify': 'fa fa-align-justify',
  1560. 'alignLeft': 'fa fa-align-left',
  1561. 'alignRight': 'fa fa-align-right',
  1562. 'indent': 'fa fa-indent',
  1563. 'outdent': 'fa fa-outdent',
  1564. 'arrowsAlt': 'fa fa-arrows-alt',
  1565. 'bold': 'fa fa-bold',
  1566. 'caret': 'caret',
  1567. 'circle': 'fa fa-circle',
  1568. 'close': 'fa fa-close',
  1569. 'code': 'fa fa-code',
  1570. 'eraser': 'fa fa-eraser',
  1571. 'font': 'fa fa-font',
  1572. 'frame': 'fa fa-frame',
  1573. 'italic': 'fa fa-italic',
  1574. 'link': 'fa fa-link',
  1575. 'unlink': 'fa fa-chain-broken',
  1576. 'magic': 'fa fa-magic',
  1577. 'menuCheck': 'fa fa-check',
  1578. 'minus': 'fa fa-minus',
  1579. 'orderedlist': 'fa fa-list-ol',
  1580. 'pencil': 'fa fa-pencil',
  1581. 'picture': 'fa fa-image',
  1582. 'question': 'fa fa-question',
  1583. 'redo': 'fa fa-redo',
  1584. 'square': 'fa fa-square',
  1585. 'strikethrough': 'fa fa-strikethrough',
  1586. 'subscript': 'fa fa-subscript',
  1587. 'superscript': 'fa fa-superscript',
  1588. 'table': 'fa fa-table',
  1589. 'textHeight': 'fa fa-text-height',
  1590. 'trash': 'fa fa-trash',
  1591. 'underline': 'fa fa-underline',
  1592. 'undo': 'fa fa-undo',
  1593. 'unorderedlist': 'fa fa-list-ul',
  1594. 'video': 'fa fa-video-camera'
  1595. }
  1596. });
  1597. this.note_editor = $(this.input_area).find('.note-editor');
  1598. // to fix <p> on enter
  1599. this.set_input('<div><br></div>');
  1600. },
  1601. setup_drag_drop: function() {
  1602. var me = this;
  1603. this.note_editor.on('dragenter dragover', false)
  1604. .on('drop', function(e) {
  1605. var dataTransfer = e.originalEvent.dataTransfer;
  1606. if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
  1607. me.note_editor.focus();
  1608. var files = [].slice.call(dataTransfer.files);
  1609. files.forEach(file => {
  1610. me.get_image(file, (url) => {
  1611. me.editor.summernote('insertImage', url, file.name);
  1612. });
  1613. });
  1614. }
  1615. e.preventDefault();
  1616. e.stopPropagation();
  1617. });
  1618. },
  1619. get_image: function (fileobj, callback) {
  1620. var freader = new FileReader(),
  1621. me = this;
  1622. freader.onload = function() {
  1623. var dataurl = freader.result;
  1624. // add filename to dataurl
  1625. var parts = dataurl.split(",");
  1626. parts[0] += ";filename=" + fileobj.name;
  1627. dataurl = parts[0] + ',' + parts[1];
  1628. callback(dataurl);
  1629. }
  1630. freader.readAsDataURL(fileobj);
  1631. },
  1632. hide_elements_on_mobile: function() {
  1633. this.note_editor.find('.note-btn-underline,\
  1634. .note-btn-italic, .note-fontsize,\
  1635. .note-color, .note-height, .btn-codeview')
  1636. .addClass('hidden-xs');
  1637. if($('.toggle-sidebar').is(':visible')) {
  1638. // disable tooltips on mobile
  1639. this.note_editor.find('.note-btn')
  1640. .attr('data-original-title', '');
  1641. }
  1642. },
  1643. get_value: function() {
  1644. return this.editor? this.editor.summernote('code'): '';
  1645. },
  1646. set_input: function(value) {
  1647. if(value == null) value = "";
  1648. value = frappe.dom.remove_script_and_style(value);
  1649. if(value !== this.get_value()) {
  1650. this.editor.summernote('code', value);
  1651. }
  1652. this.last_value = value;
  1653. },
  1654. set_focus: function() {
  1655. return this.editor.summernote('focus');
  1656. },
  1657. set_upload_options: function() {
  1658. var me = this;
  1659. this.upload_options = {
  1660. parent: this.image_dialog.get_field("upload_area").$wrapper,
  1661. args: {},
  1662. max_width: this.df.max_width,
  1663. max_height: this.df.max_height,
  1664. options: "Image",
  1665. btn: this.image_dialog.set_primary_action(__("Insert")),
  1666. on_no_attach: function() {
  1667. // if no attachmemts,
  1668. // check if something is selected
  1669. var selected = me.image_dialog.get_field("select").get_value();
  1670. if(selected) {
  1671. me.editor.summernote('insertImage', selected);
  1672. me.image_dialog.hide();
  1673. } else {
  1674. frappe.msgprint(__("Please attach a file or set a URL"));
  1675. }
  1676. },
  1677. callback: function(attachment, r) {
  1678. me.editor.summernote('insertImage', attachment.file_url, attachment.file_name);
  1679. me.image_dialog.hide();
  1680. },
  1681. onerror: function() {
  1682. me.image_dialog.hide();
  1683. }
  1684. }
  1685. if ("is_private" in this.df) {
  1686. this.upload_options.is_private = this.df.is_private;
  1687. }
  1688. if(this.frm) {
  1689. this.upload_options.args = {
  1690. from_form: 1,
  1691. doctype: this.frm.doctype,
  1692. docname: this.frm.docname
  1693. }
  1694. } else {
  1695. this.upload_options.on_attach = function(fileobj, dataurl) {
  1696. me.editor.summernote('insertImage', dataurl);
  1697. me.image_dialog.hide();
  1698. frappe.hide_progress();
  1699. }
  1700. }
  1701. },
  1702. setup_image_dialog: function() {
  1703. this.note_editor.find('[data-original-title="Image"]').on('click', (e) => {
  1704. if(!this.image_dialog) {
  1705. this.image_dialog = new frappe.ui.Dialog({
  1706. title: __("Image"),
  1707. fields: [
  1708. {fieldtype:"HTML", fieldname:"upload_area"},
  1709. {fieldtype:"HTML", fieldname:"or_attach", options: __("Or")},
  1710. {fieldtype:"Select", fieldname:"select", label:__("Select from existing attachments") },
  1711. ]
  1712. });
  1713. }
  1714. this.image_dialog.show();
  1715. this.image_dialog.get_field("upload_area").$wrapper.empty();
  1716. // select from existing attachments
  1717. var attachments = this.frm && this.frm.attachments.get_attachments() || [];
  1718. var select = this.image_dialog.get_field("select");
  1719. if(attachments.length) {
  1720. attachments = $.map(attachments, function(o) { return o.file_url; })
  1721. select.df.options = [""].concat(attachments);
  1722. select.toggle(true);
  1723. this.image_dialog.get_field("or_attach").toggle(true);
  1724. select.refresh();
  1725. } else {
  1726. this.image_dialog.get_field("or_attach").toggle(false);
  1727. select.toggle(false);
  1728. }
  1729. select.$input.val("");
  1730. this.set_upload_options();
  1731. frappe.upload.make(this.upload_options);
  1732. });
  1733. }
  1734. });
  1735. frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({
  1736. make: function() {
  1737. this._super();
  1738. // add title if prev field is not column / section heading or html
  1739. this.grid = new frappe.ui.form.Grid({
  1740. frm: this.frm,
  1741. df: this.df,
  1742. perm: this.perm || (this.frm && this.frm.perm) || this.df.perm,
  1743. parent: this.wrapper
  1744. })
  1745. if(this.frm) {
  1746. this.frm.grids[this.frm.grids.length] = this;
  1747. }
  1748. // description
  1749. if(this.df.description) {
  1750. $('<p class="text-muted small">' + __(this.df.description) + '</p>')
  1751. .appendTo(this.wrapper);
  1752. }
  1753. var me = this;
  1754. this.$wrapper.on("refresh", function() {
  1755. me.grid.refresh();
  1756. return false;
  1757. });
  1758. },
  1759. get_parsed_value: function() {
  1760. if(this.grid) {
  1761. return this.grid.get_data();
  1762. }
  1763. }
  1764. });
  1765. frappe.ui.form.ControlSignature = frappe.ui.form.ControlData.extend({
  1766. saving: false,
  1767. loading: false,
  1768. make: function() {
  1769. var me = this;
  1770. this._super();
  1771. // make jSignature field
  1772. this.$pad = $('<div class="signature-field"></div>')
  1773. .appendTo(me.wrapper)
  1774. .jSignature({height:300, width: "100%", "lineWidth": 0.8})
  1775. .on('change', this.on_save_sign.bind(this));
  1776. this.img_wrapper = $(`<div class="signature-display">
  1777. <div class="missing-image attach-missing-image">
  1778. <i class="octicon octicon-circle-slash"></i>
  1779. </div></div>`)
  1780. .appendTo(this.wrapper);
  1781. this.img = $("<img class='img-responsive attach-image-display'>")
  1782. .appendTo(this.img_wrapper).toggle(false);
  1783. this.$btnWrapper = $(`<div class="signature-btn-row">
  1784. <a href="#" type="button" class="signature-reset btn btn-default">
  1785. <i class="glyphicon glyphicon-repeat"></i></a>`)
  1786. .appendTo(this.$pad)
  1787. .on("click", '.signature-reset', function() {
  1788. me.on_reset_sign();
  1789. return false;
  1790. });
  1791. // handle refresh by reloading the pad
  1792. this.$wrapper.on("refresh", this.on_refresh.bind(this));
  1793. },
  1794. on_refresh: function(e) {
  1795. // prevent to load the second time
  1796. this.$wrapper.find(".control-input").toggle(false);
  1797. this.set_editable(this.get_status()=="Write");
  1798. this.load_pad();
  1799. if(this.get_status()=="Read") {
  1800. $(this.disp_area).toggle(false);
  1801. }
  1802. },
  1803. set_image: function(value) {
  1804. if(value) {
  1805. $(this.img_wrapper).find(".missing-image").toggle(false);
  1806. this.img.attr("src", value).toggle(true);
  1807. } else {
  1808. $(this.img_wrapper).find(".missing-image").toggle(true);
  1809. this.img.toggle(false);
  1810. }
  1811. },
  1812. load_pad: function() {
  1813. // make sure not triggered during saving
  1814. if (this.saving) return;
  1815. // get value
  1816. var value = this.get_value();
  1817. // import data for pad
  1818. if (this.$pad) {
  1819. this.loading = true;
  1820. // reset in all cases
  1821. this.$pad.jSignature('reset');
  1822. if (value) {
  1823. // load the image to find out the size, because scaling will affect
  1824. // stroke width
  1825. try {
  1826. this.$pad.jSignature('setData', value);
  1827. this.set_image(value);
  1828. }
  1829. catch (e){
  1830. console.log("Cannot set data for signature", value, e);
  1831. }
  1832. }
  1833. this.loading = false;
  1834. }
  1835. },
  1836. set_editable: function(editable) {
  1837. this.$pad.toggle(editable);
  1838. this.img_wrapper.toggle(!editable);
  1839. this.$btnWrapper.toggle(editable);
  1840. if (editable) {
  1841. this.$btnWrapper.addClass('editing');
  1842. }
  1843. else {
  1844. this.$btnWrapper.removeClass('editing');
  1845. }
  1846. },
  1847. set_my_value: function(value) {
  1848. if (this.saving || this.loading) return;
  1849. this.saving = true;
  1850. this.set_value(value);
  1851. this.value = value;
  1852. this.saving = false;
  1853. },
  1854. get_value: function() {
  1855. return this.value? this.value: this.get_model_value();
  1856. },
  1857. // reset signature canvas
  1858. on_reset_sign: function() {
  1859. this.$pad.jSignature("reset");
  1860. this.set_my_value("");
  1861. },
  1862. // save signature value to model and display
  1863. on_save_sign: function() {
  1864. if (this.saving || this.loading) return;
  1865. var base64_img = this.$pad.jSignature("getData");
  1866. this.set_my_value(base64_img);
  1867. this.set_image(this.get_value());
  1868. }
  1869. });
  1870. frappe.ui.form.fieldtype_icons = {
  1871. "Date": "fa fa-calendar",
  1872. "Time": "fa fa-time",
  1873. "Datetime": "fa fa-time",
  1874. "Code": "fa fa-code",
  1875. "Select": "fa fa-flag"
  1876. };