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.
 
 
 
 
 
 

2040 lines
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. if(value
  555. && ((this.last_value && this.last_value !== value)
  556. || (!this.datepicker.selectedDates.length))) {
  557. this.datepicker.selectDate(frappe.datetime.str_to_obj(value));
  558. }
  559. },
  560. set_date_options: function() {
  561. var me = this;
  562. var lang = frappe.boot.user.language;
  563. if(!$.fn.datepicker.language[lang]) {
  564. lang = 'en'
  565. }
  566. this.datepicker_options = {
  567. language: lang,
  568. autoClose: true,
  569. todayButton: new Date(),
  570. dateFormat: (frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd'),
  571. onSelect: function(dateStr) {
  572. me.$input.trigger('change');
  573. },
  574. onShow: function() {
  575. $('.datepicker--button:visible').text(__('Today'));
  576. },
  577. };
  578. },
  579. set_datepicker: function() {
  580. this.$input.datepicker(this.datepicker_options);
  581. this.datepicker = this.$input.data('datepicker');
  582. },
  583. set_t_for_today: function() {
  584. var me = this;
  585. this.$input.on("keydown", function(e) {
  586. if(e.which===84) { // 84 === t
  587. if(me.df.fieldtype=='Date') {
  588. me.set_value(frappe.datetime.str_to_user(
  589. frappe.datetime.nowdate()));
  590. } if(me.df.fieldtype=='Datetime') {
  591. me.set_value(frappe.datetime.str_to_user(
  592. frappe.datetime.now_datetime()));
  593. } if(me.df.fieldtype=='Time') {
  594. me.set_value(frappe.datetime.str_to_user(
  595. frappe.datetime.now_time()));
  596. }
  597. return false;
  598. }
  599. });
  600. },
  601. parse: function(value) {
  602. if(value) {
  603. return frappe.datetime.user_to_str(value);
  604. }
  605. },
  606. format_for_input: function(value) {
  607. if(value) {
  608. return frappe.datetime.str_to_user(value);
  609. }
  610. return "";
  611. },
  612. validate: function(value, callback) {
  613. if(value && !frappe.datetime.validate(value)) {
  614. frappe.msgprint(__("Date must be in format: {0}", [frappe.sys_defaults.date_format || "yyyy-mm-dd"]));
  615. callback("");
  616. return;
  617. }
  618. return callback(value);
  619. }
  620. });
  621. frappe.ui.form.ControlTime = frappe.ui.form.ControlData.extend({
  622. make_input: function() {
  623. var me = this;
  624. this._super();
  625. this.$input.datepicker({
  626. language: "en",
  627. timepicker: true,
  628. onlyTimepicker: true,
  629. timeFormat: "hh:ii:ss",
  630. onSelect: function(dateObj) {
  631. me.$input.trigger('change');
  632. },
  633. onShow: function() {
  634. $('.datepicker--button:visible').text(__('Now'));
  635. },
  636. todayButton: new Date()
  637. });
  638. this.datepicker = this.$input.data('datepicker');
  639. this.refresh();
  640. },
  641. set_input: function(value) {
  642. this._super(value);
  643. if(value
  644. && ((this.last_value && this.last_value !== this.value)
  645. || (!this.datepicker.selectedDates.length))) {
  646. this.datepicker.selectDate(moment(value, 'hh:mm:ss')._d);
  647. }
  648. },
  649. });
  650. frappe.ui.form.ControlDatetime = frappe.ui.form.ControlDate.extend({
  651. set_date_options: function() {
  652. this._super();
  653. this.datepicker_options.timepicker = true;
  654. this.datepicker_options.timeFormat = "hh:ii:ss";
  655. this.datepicker_options.onShow = function() {
  656. $('.datepicker--button:visible').text(__('Now'));
  657. };
  658. },
  659. parse: function(value) {
  660. if(value) {
  661. // parse and convert
  662. value = frappe.datetime.convert_to_system_tz(frappe.datetime.user_to_str(value));
  663. }
  664. return value;
  665. },
  666. format_for_input: function(value) {
  667. if(value) {
  668. // convert and format
  669. value = frappe.datetime.str_to_user(frappe.datetime.convert_to_user_tz(value));
  670. }
  671. return value || "";
  672. }
  673. });
  674. frappe.ui.form.ControlDateRange = frappe.ui.form.ControlData.extend({
  675. make_input: function() {
  676. var me = this;
  677. this._super();
  678. this.set_date_options();
  679. this.set_datepicker();
  680. this.refresh();
  681. },
  682. set_date_options: function() {
  683. var me = this;
  684. this.datepicker_options = {
  685. language: "en",
  686. range: true,
  687. autoClose: true,
  688. toggleSelected: false
  689. }
  690. this.datepicker_options.dateFormat =
  691. (frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd');
  692. this.datepicker_options.onSelect = function(dateObj) {
  693. me.set_value(dateObj);
  694. }
  695. },
  696. set_datepicker: function() {
  697. this.$input.datepicker(this.datepicker_options);
  698. this.datepicker = this.$input.data('datepicker');
  699. },
  700. set_input: function(value, value2) {
  701. this.last_value = this.value;
  702. if (value && value2) {
  703. this.value = [value, value2];
  704. } else {
  705. this.value = value
  706. }
  707. if (this.value) {
  708. this.$input && this.$input.val(this.format_for_input(this.value[0], this.value[1]));
  709. } else {
  710. this.$input && this.$input.val("")
  711. }
  712. this.set_disp_area();
  713. this.set_mandatory && this.set_mandatory(value);
  714. },
  715. parse: function(value) {
  716. if(value && (value.indexOf(',') !== -1 || value.indexOf('to') !== -1)) {
  717. var vals = value.split(/[( to )(,)]/)
  718. var from_date = moment(frappe.datetime.user_to_obj(vals[0])).format('YYYY-MM-DD');
  719. var to_date = moment(frappe.datetime.user_to_obj(vals[vals.length-1])).format('YYYY-MM-DD');
  720. return [from_date, to_date];
  721. }
  722. },
  723. format_for_input: function(value,value2) {
  724. if(value && value2) {
  725. value = frappe.datetime.str_to_user(value);
  726. value2 = frappe.datetime.str_to_user(value2);
  727. return value + " to " + value2
  728. }
  729. return "";
  730. },
  731. validate: function(value, callback) {
  732. return callback(value);
  733. }
  734. });
  735. frappe.ui.form.ControlText = frappe.ui.form.ControlData.extend({
  736. html_element: "textarea",
  737. horizontal: false,
  738. make_wrapper: function() {
  739. this._super();
  740. this.$wrapper.find(".like-disabled-input").addClass("for-description");
  741. },
  742. make_input: function() {
  743. this._super();
  744. this.$input.css({'height': '300px'})
  745. }
  746. });
  747. frappe.ui.form.ControlLongText = frappe.ui.form.ControlText;
  748. frappe.ui.form.ControlSmallText = frappe.ui.form.ControlText.extend({
  749. make_input: function() {
  750. this._super();
  751. this.$input.css({'height': '150px'})
  752. }
  753. });
  754. frappe.ui.form.ControlCheck = frappe.ui.form.ControlData.extend({
  755. input_type: "checkbox",
  756. make_wrapper: function() {
  757. this.$wrapper = $('<div class="form-group frappe-control">\
  758. <div class="checkbox">\
  759. <label>\
  760. <span class="input-area"></span>\
  761. <span class="disp-area" style="display:none; margin-left: -20px;"></span>\
  762. <span class="label-area small"></span>\
  763. </label>\
  764. <p class="help-box small text-muted"></p>\
  765. </div>\
  766. </div>').appendTo(this.parent)
  767. },
  768. set_input_areas: function() {
  769. this.label_area = this.label_span = this.$wrapper.find(".label-area").get(0);
  770. this.input_area = this.$wrapper.find(".input-area").get(0);
  771. this.disp_area = this.$wrapper.find(".disp-area").get(0);
  772. },
  773. make_input: function() {
  774. this._super();
  775. this.$input.removeClass("form-control");
  776. },
  777. parse: function(value) {
  778. return this.input.checked ? 1 : 0;
  779. },
  780. validate: function(value, callback) {
  781. return callback(cint(value));
  782. },
  783. set_input: function(value) {
  784. if(this.input) {
  785. this.input.checked = (value ? 1 : 0);
  786. }
  787. this.last_value = value;
  788. this.set_mandatory(value);
  789. this.set_disp_area();
  790. },
  791. get_value: function() {
  792. if (!this.$input) {
  793. return;
  794. }
  795. return this.$input.prop("checked") ? 1 : 0;
  796. },
  797. });
  798. frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
  799. make_input: function() {
  800. var me = this;
  801. this.$input = $('<button class="btn btn-default btn-xs">')
  802. .prependTo(me.input_area)
  803. .on("click", function() {
  804. me.onclick();
  805. });
  806. this.input = this.$input.get(0);
  807. this.set_input_attributes();
  808. this.has_input = true;
  809. this.toggle_label(false);
  810. },
  811. onclick: function() {
  812. if(this.frm && this.frm.doc) {
  813. if(this.frm.script_manager.get_handlers(this.df.fieldname, this.doctype, this.docname).length) {
  814. this.frm.script_manager.trigger(this.df.fieldname, this.doctype, this.docname);
  815. } else {
  816. this.frm.runscript(this.df.options, this);
  817. }
  818. }
  819. else if(this.df.click) {
  820. this.df.click();
  821. }
  822. },
  823. set_input_areas: function() {
  824. this._super();
  825. $(this.disp_area).removeClass().addClass("hide");
  826. },
  827. set_empty_description: function() {
  828. this.$wrapper.find(".help-box").empty().toggle(false);
  829. },
  830. set_label: function() {
  831. $(this.label_span).html("&nbsp;");
  832. this.$input && this.$input.html((this.df.icon ?
  833. ('<i class="'+this.df.icon+' fa-fw"></i> ') : "") + __(this.df.label));
  834. }
  835. });
  836. frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
  837. make_input: function() {
  838. var me = this;
  839. this.$input = $('<button class="btn btn-default btn-sm btn-attach">')
  840. .html(__("Attach"))
  841. .prependTo(me.input_area)
  842. .on("click", function() {
  843. me.onclick();
  844. });
  845. this.$value = $('<div style="margin-top: 5px;">\
  846. <div class="ellipsis" style="display: inline-block; width: 90%;">\
  847. <i class="fa fa-paper-clip"></i> \
  848. <a class="attached-file" target="_blank"></a>\
  849. </div>\
  850. <a class="close">&times;</a></div>')
  851. .prependTo(me.input_area)
  852. .toggle(false);
  853. this.input = this.$input.get(0);
  854. this.set_input_attributes();
  855. this.has_input = true;
  856. this.$value.find(".close").on("click", function() {
  857. me.clear_attachment();
  858. })
  859. },
  860. clear_attachment: function() {
  861. var me = this;
  862. if(this.frm) {
  863. me.frm.attachments.remove_attachment_by_filename(me.value, function() {
  864. me.parse_validate_and_set_in_model(null);
  865. me.refresh();
  866. me.frm.save();
  867. });
  868. } else {
  869. this.dataurl = null;
  870. this.fileobj = null;
  871. this.set_input(null);
  872. this.refresh();
  873. }
  874. },
  875. onclick: function() {
  876. var me = this;
  877. if(this.doc) {
  878. var doc = this.doc.parent && frappe.model.get_doc(this.doc.parenttype, this.doc.parent) || this.doc;
  879. if (doc.__islocal) {
  880. frappe.msgprint(__("Please save the document before uploading."));
  881. return;
  882. }
  883. }
  884. if(!this.dialog) {
  885. this.dialog = new frappe.ui.Dialog({
  886. title: __(this.df.label || __("Upload")),
  887. fields: [
  888. {fieldtype:"HTML", fieldname:"upload_area"},
  889. {fieldtype:"HTML", fieldname:"or_attach", options: __("Or")},
  890. {fieldtype:"Select", fieldname:"select", label:__("Select from existing attachments") },
  891. {fieldtype:"Button", fieldname:"clear",
  892. label:__("Clear Attachment"), click: function() {
  893. me.clear_attachment();
  894. me.dialog.hide();
  895. }
  896. },
  897. ]
  898. });
  899. }
  900. this.dialog.show();
  901. this.dialog.get_field("upload_area").$wrapper.empty();
  902. // select from existing attachments
  903. var attachments = this.frm && this.frm.attachments.get_attachments() || [];
  904. var select = this.dialog.get_field("select");
  905. if(attachments.length) {
  906. attachments = $.map(attachments, function(o) { return o.file_url; })
  907. select.df.options = [""].concat(attachments);
  908. select.toggle(true);
  909. this.dialog.get_field("or_attach").toggle(true);
  910. select.refresh();
  911. } else {
  912. this.dialog.get_field("or_attach").toggle(false);
  913. select.toggle(false);
  914. }
  915. select.$input.val("");
  916. // show button if attachment exists
  917. this.dialog.get_field('clear').$wrapper.toggle(this.get_model_value() ? true : false);
  918. this.set_upload_options();
  919. frappe.upload.make(this.upload_options);
  920. },
  921. set_upload_options: function() {
  922. var me = this;
  923. this.upload_options = {
  924. parent: this.dialog.get_field("upload_area").$wrapper,
  925. args: {},
  926. allow_multiple: 0,
  927. max_width: this.df.max_width,
  928. max_height: this.df.max_height,
  929. options: this.df.options,
  930. btn: this.dialog.set_primary_action(__("Upload")),
  931. on_no_attach: function() {
  932. // if no attachmemts,
  933. // check if something is selected
  934. var selected = me.dialog.get_field("select").get_value();
  935. if(selected) {
  936. me.parse_validate_and_set_in_model(selected);
  937. me.dialog.hide();
  938. me.frm.save();
  939. } else {
  940. frappe.msgprint(__("Please attach a file or set a URL"));
  941. }
  942. },
  943. callback: function(attachment, r) {
  944. me.on_upload_complete(attachment);
  945. me.dialog.hide();
  946. },
  947. onerror: function() {
  948. me.dialog.hide();
  949. }
  950. }
  951. if ("is_private" in this.df) {
  952. this.upload_options.is_private = this.df.is_private;
  953. }
  954. if(this.frm) {
  955. this.upload_options.args = {
  956. from_form: 1,
  957. doctype: this.frm.doctype,
  958. docname: this.frm.docname
  959. }
  960. } else {
  961. this.upload_options.on_attach = function(fileobj, dataurl) {
  962. me.dialog.hide();
  963. me.fileobj = fileobj;
  964. me.dataurl = dataurl;
  965. if(me.on_attach) {
  966. me.on_attach()
  967. }
  968. if(me.df.on_attach) {
  969. me.df.on_attach(fileobj, dataurl);
  970. }
  971. me.on_upload_complete();
  972. }
  973. }
  974. },
  975. set_input: function(value, dataurl) {
  976. this.value = value;
  977. if(this.value) {
  978. this.$input.toggle(false);
  979. if(this.value.indexOf(",")!==-1) {
  980. var parts = this.value.split(",");
  981. var filename = parts[0];
  982. var dataurl = parts[1];
  983. }
  984. this.$value.toggle(true).find(".attached-file")
  985. .html(filename || this.value)
  986. .attr("href", dataurl || this.value);
  987. } else {
  988. this.$input.toggle(true);
  989. this.$value.toggle(false);
  990. }
  991. },
  992. get_value: function() {
  993. if(this.frm) {
  994. return this.value;
  995. } else {
  996. return this.fileobj ? (this.fileobj.filename + "," + this.dataurl) : null;
  997. }
  998. },
  999. on_upload_complete: function(attachment) {
  1000. if(this.frm) {
  1001. this.parse_validate_and_set_in_model(attachment.file_url);
  1002. this.refresh();
  1003. this.frm.attachments.update_attachment(attachment);
  1004. this.frm.save();
  1005. } else {
  1006. this.value = this.get_value();
  1007. this.refresh();
  1008. }
  1009. },
  1010. });
  1011. frappe.ui.form.ControlAttachImage = frappe.ui.form.ControlAttach.extend({
  1012. make: function() {
  1013. var me = this;
  1014. this._super();
  1015. this.img_wrapper = $('<div style="margin: 7px 0px;">\
  1016. <div class="missing-image attach-missing-image"><i class="octicon octicon-circle-slash"></i></div></div>')
  1017. .appendTo(this.wrapper);
  1018. this.img = $("<img class='img-responsive attach-image-display'>")
  1019. .appendTo(this.img_wrapper).toggle(false);
  1020. // propagate click to Attach button
  1021. this.img_wrapper.find(".missing-image").on("click", function() { me.$input.click(); });
  1022. this.img.on("click", function() { me.$input.click(); });
  1023. this.$wrapper.on("refresh", function() {
  1024. me.set_image();
  1025. if(me.get_status()=="Read") {
  1026. $(me.disp_area).toggle(false);
  1027. }
  1028. });
  1029. this.set_image();
  1030. },
  1031. set_image: function() {
  1032. if(this.get_value()) {
  1033. $(this.img_wrapper).find(".missing-image").toggle(false);
  1034. this.img.attr("src", this.dataurl ? this.dataurl : this.value).toggle(true);
  1035. } else {
  1036. $(this.img_wrapper).find(".missing-image").toggle(true);
  1037. this.img.toggle(false);
  1038. }
  1039. }
  1040. });
  1041. frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({
  1042. html_element: "select",
  1043. make_input: function() {
  1044. var me = this;
  1045. this._super();
  1046. this.set_options();
  1047. },
  1048. set_input: function(value) {
  1049. // refresh options first - (new ones??)
  1050. this.set_options(value || "");
  1051. var input_value = null;
  1052. if(this.$input) {
  1053. var input_value = this.$input.val();
  1054. }
  1055. // not a possible option, repair
  1056. if(this.doctype && this.docname) {
  1057. // model value is not an option,
  1058. // set the default option (displayed)
  1059. var model_value = frappe.model.get_value(this.doctype, this.docname, this.df.fieldname);
  1060. if(model_value == null && (input_value || "") != (model_value || "")) {
  1061. this.set_model_value(input_value);
  1062. } else {
  1063. this.last_value = value;
  1064. }
  1065. } else {
  1066. if(value !== input_value) {
  1067. this.set_value(input_value);
  1068. }
  1069. }
  1070. this._super(value);
  1071. },
  1072. set_options: function(value) {
  1073. var options = this.df.options || [];
  1074. if(typeof this.df.options==="string") {
  1075. options = this.df.options.split("\n");
  1076. }
  1077. // nothing changed
  1078. if(options.toString() === this.last_options) {
  1079. return;
  1080. }
  1081. this.last_options = options.toString();
  1082. if(this.$input) {
  1083. var selected = this.$input.find(":selected").val();
  1084. this.$input.empty().add_options(options || []);
  1085. if(value===undefined && selected) {
  1086. this.$input.val(selected);
  1087. }
  1088. }
  1089. },
  1090. get_file_attachment_list: function() {
  1091. if(!this.frm) return;
  1092. var fl = frappe.model.docinfo[this.frm.doctype][this.frm.docname];
  1093. if(fl && fl.attachments) {
  1094. this.set_description("");
  1095. var options = [""];
  1096. $.each(fl.attachments, function(i, f) {
  1097. options.push(f.file_url)
  1098. });
  1099. return options;
  1100. } else {
  1101. this.set_description(__("Please attach a file first."))
  1102. return [""];
  1103. }
  1104. }
  1105. });
  1106. // special features for link
  1107. // buttons
  1108. // autocomplete
  1109. // link validation
  1110. // custom queries
  1111. // add_fetches
  1112. frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
  1113. make_input: function() {
  1114. var me = this;
  1115. // line-height: 1 is for Mozilla 51, shows extra padding otherwise
  1116. $('<div class="link-field ui-front" style="position: relative; line-height: 1;">\
  1117. <input type="text" class="input-with-feedback form-control">\
  1118. <span class="link-btn">\
  1119. <a class="btn-open no-decoration" title="' + __("Open Link") + '">\
  1120. <i class="octicon octicon-arrow-right"></i></a>\
  1121. </span>\
  1122. </div>').prependTo(this.input_area);
  1123. this.$input_area = $(this.input_area);
  1124. this.$input = this.$input_area.find('input');
  1125. this.$link = this.$input_area.find('.link-btn');
  1126. this.$link_open = this.$link.find('.btn-open');
  1127. this.set_input_attributes();
  1128. this.$input.on("focus", function() {
  1129. setTimeout(function() {
  1130. if(me.$input.val() && me.get_options()) {
  1131. me.$link.toggle(true);
  1132. me.$link_open.attr('href', '#Form/' + me.get_options() + '/' + me.$input.val());
  1133. }
  1134. if(!me.$input.val()) {
  1135. me.$input.val("").trigger("input");
  1136. }
  1137. }, 500);
  1138. });
  1139. this.$input.on("blur", function() {
  1140. // if this disappears immediately, the user's click
  1141. // does not register, hence timeout
  1142. setTimeout(function() {
  1143. me.$link.toggle(false);
  1144. }, 500);
  1145. });
  1146. this.input = this.$input.get(0);
  1147. this.has_input = true;
  1148. this.translate_values = true;
  1149. var me = this;
  1150. this.setup_buttons();
  1151. this.setup_awesomeplete();
  1152. if(this.df.change) {
  1153. this.$input.on("change", function() {
  1154. me.df.change.apply(this);
  1155. });
  1156. }
  1157. },
  1158. get_options: function() {
  1159. return this.df.options;
  1160. },
  1161. setup_buttons: function() {
  1162. var me = this;
  1163. if(this.only_input && !this.with_link_btn) {
  1164. this.$input_area.find(".link-btn").remove();
  1165. }
  1166. },
  1167. open_advanced_search: function() {
  1168. var doctype = this.get_options();
  1169. if(!doctype) return;
  1170. new frappe.ui.form.LinkSelector({
  1171. doctype: doctype,
  1172. target: this,
  1173. txt: this.get_value()
  1174. });
  1175. return false;
  1176. },
  1177. new_doc: function() {
  1178. var doctype = this.get_options();
  1179. var me = this;
  1180. if(!doctype) return;
  1181. // set values to fill in the new document
  1182. if(this.df.get_route_options_for_new_doc) {
  1183. frappe.route_options = this.df.get_route_options_for_new_doc(this);
  1184. } else {
  1185. frappe.route_options = {};
  1186. }
  1187. // partially entered name field
  1188. frappe.route_options.name_field = this.get_value();
  1189. // reference to calling link
  1190. frappe._from_link = this;
  1191. frappe._from_link_scrollY = $(document).scrollTop();
  1192. var trimmed_doctype = doctype.replace(/ /g, '');
  1193. var controller_name = "QuickEntryForm";
  1194. if(frappe.ui.form[trimmed_doctype + "QuickEntryForm"]){
  1195. controller_name = trimmed_doctype + "QuickEntryForm";
  1196. }
  1197. new frappe.ui.form[controller_name](doctype, function(doc) {
  1198. if(me.frm) {
  1199. me.parse_validate_and_set_in_model(doc.name);
  1200. } else {
  1201. me.set_value(doc.name);
  1202. }
  1203. });
  1204. return false;
  1205. },
  1206. setup_awesomeplete: function() {
  1207. var me = this;
  1208. this.$input.cache = {};
  1209. this.awesomplete = new Awesomplete(me.input, {
  1210. minChars: 0,
  1211. maxItems: 99,
  1212. autoFirst: true,
  1213. list: [],
  1214. data: function (item, input) {
  1215. return {
  1216. label: item.label || item.value,
  1217. value: item.value
  1218. };
  1219. },
  1220. filter: function(item, input) {
  1221. return true;
  1222. },
  1223. item: function (item, input) {
  1224. var d = this.get_item(item.value);
  1225. if(!d.label) { d.label = d.value; }
  1226. var _label = (me.translate_values) ? __(d.label) : d.label;
  1227. var html = "<strong>" + _label + "</strong>";
  1228. if(d.description && d.value!==d.description) {
  1229. html += '<br><span class="small">' + __(d.description) + '</span>';
  1230. }
  1231. return $('<li></li>')
  1232. .data('item.autocomplete', d)
  1233. .prop('aria-selected', 'false')
  1234. .html('<a><p>' + html + '</p></a>')
  1235. .get(0);
  1236. },
  1237. sort: function(a, b) {
  1238. return 0;
  1239. }
  1240. });
  1241. this.$input.on("input", function(e) {
  1242. var doctype = me.get_options();
  1243. if(!doctype) return;
  1244. if (!me.$input.cache[doctype]) {
  1245. me.$input.cache[doctype] = {};
  1246. }
  1247. var term = e.target.value;
  1248. if (me.$input.cache[doctype][term]!=null) {
  1249. // immediately show from cache
  1250. me.awesomplete.list = me.$input.cache[doctype][term];
  1251. }
  1252. var args = {
  1253. 'txt': term,
  1254. 'doctype': doctype,
  1255. };
  1256. me.set_custom_query(args);
  1257. frappe.call({
  1258. type: "GET",
  1259. method:'frappe.desk.search.search_link',
  1260. no_spinner: true,
  1261. args: args,
  1262. callback: function(r) {
  1263. if(!me.$input.is(":focus")) {
  1264. return;
  1265. }
  1266. if(!me.df.only_select) {
  1267. if(frappe.model.can_create(doctype)
  1268. && me.df.fieldtype !== "Dynamic Link") {
  1269. // new item
  1270. r.results.push({
  1271. label: "<span class='text-primary link-option'>"
  1272. + "<i class='fa fa-plus' style='margin-right: 5px;'></i> "
  1273. + __("Create a new {0}", [__(me.df.options)])
  1274. + "</span>",
  1275. value: "create_new__link_option",
  1276. action: me.new_doc
  1277. })
  1278. }
  1279. // advanced search
  1280. r.results.push({
  1281. label: "<span class='text-primary link-option'>"
  1282. + "<i class='fa fa-search' style='margin-right: 5px;'></i> "
  1283. + __("Advanced Search")
  1284. + "</span>",
  1285. value: "advanced_search__link_option",
  1286. action: me.open_advanced_search
  1287. })
  1288. }
  1289. me.$input.cache[doctype][term] = r.results;
  1290. me.awesomplete.list = me.$input.cache[doctype][term];
  1291. }
  1292. });
  1293. });
  1294. this.$input.on("blur", function() {
  1295. if(me.selected) {
  1296. me.selected = false;
  1297. return;
  1298. }
  1299. var value = me.get_value();
  1300. if(value!==me.last_value) {
  1301. me.parse_validate_and_set_in_model(value);
  1302. }
  1303. });
  1304. this.$input.on("awesomplete-open", function(e) {
  1305. me.$wrapper.css({"z-index": 100});
  1306. me.$wrapper.find('ul').css({"z-index": 100});
  1307. me.autocomplete_open = true;
  1308. });
  1309. this.$input.on("awesomplete-close", function(e) {
  1310. me.$wrapper.css({"z-index": 1});
  1311. me.autocomplete_open = false;
  1312. });
  1313. this.$input.on("awesomplete-select", function(e) {
  1314. var o = e.originalEvent;
  1315. var item = me.awesomplete.get_item(o.text.value);
  1316. me.autocomplete_open = false;
  1317. // prevent selection on tab
  1318. var TABKEY = 9;
  1319. if(e.keyCode === TABKEY) {
  1320. e.preventDefault();
  1321. me.awesomplete.close();
  1322. return false;
  1323. }
  1324. if(item.action) {
  1325. item.value = "";
  1326. item.action.apply(me);
  1327. }
  1328. // if remember_last_selected is checked in the doctype against the field,
  1329. // then add this value
  1330. // to defaults so you do not need to set it again
  1331. // unless it is changed.
  1332. if(me.df.remember_last_selected_value) {
  1333. frappe.boot.user.last_selected_values[me.df.options] = item.value;
  1334. }
  1335. me.parse_validate_and_set_in_model(item.value);
  1336. });
  1337. this.$input.on("awesomplete-selectcomplete", function(e) {
  1338. var o = e.originalEvent;
  1339. if(o.text.value.indexOf("__link_option") !== -1) {
  1340. me.$input.val("");
  1341. }
  1342. });
  1343. },
  1344. set_custom_query: function(args) {
  1345. var set_nulls = function(obj) {
  1346. $.each(obj, function(key, value) {
  1347. if(value!==undefined) {
  1348. obj[key] = value;
  1349. }
  1350. });
  1351. return obj;
  1352. }
  1353. if(this.get_query || this.df.get_query) {
  1354. var get_query = this.get_query || this.df.get_query;
  1355. if($.isPlainObject(get_query)) {
  1356. var filters = null;
  1357. if(get_query.filters) {
  1358. // passed as {'filters': {'key':'value'}}
  1359. filters = get_query.filters;
  1360. } else if(get_query.query) {
  1361. // passed as {'query': 'path.to.method'}
  1362. args.query = get_query;
  1363. } else {
  1364. // dict is filters
  1365. filters = get_query;
  1366. }
  1367. if (filters) {
  1368. var filters = set_nulls(filters);
  1369. // extend args for custom functions
  1370. $.extend(args, filters);
  1371. // add "filters" for standard query (search.py)
  1372. args.filters = filters;
  1373. }
  1374. } else if(typeof(get_query)==="string") {
  1375. args.query = get_query;
  1376. } else {
  1377. // get_query by function
  1378. var q = (get_query)(this.frm && this.frm.doc || this.doc, this.doctype, this.docname);
  1379. if (typeof(q)==="string") {
  1380. // returns a string
  1381. args.query = q;
  1382. } else if($.isPlainObject(q)) {
  1383. // returns a plain object with filters
  1384. if(q.filters) {
  1385. set_nulls(q.filters);
  1386. }
  1387. // turn off value translation
  1388. if(q.translate_values !== undefined) {
  1389. this.translate_values = q.translate_values;
  1390. }
  1391. // extend args for custom functions
  1392. $.extend(args, q);
  1393. // add "filters" for standard query (search.py)
  1394. args.filters = q.filters;
  1395. }
  1396. }
  1397. }
  1398. if(this.df.filters) {
  1399. set_nulls(this.df.filters);
  1400. if(!args.filters) args.filters = {};
  1401. $.extend(args.filters, this.df.filters);
  1402. }
  1403. },
  1404. validate: function(value, callback) {
  1405. // validate the value just entered
  1406. var me = this;
  1407. if(this.df.options=="[Select]" || this.df.ignore_link_validation) {
  1408. callback(value);
  1409. return;
  1410. }
  1411. this.validate_link_and_fetch(this.df, this.get_options(),
  1412. this.docname, value, callback);
  1413. },
  1414. validate_link_and_fetch: function(df, doctype, docname, value, callback) {
  1415. var me = this;
  1416. if(value) {
  1417. var fetch = '';
  1418. if(this.frm && this.frm.fetch_dict[df.fieldname]) {
  1419. fetch = this.frm.fetch_dict[df.fieldname].columns.join(', ');
  1420. }
  1421. return frappe.call({
  1422. method:'frappe.desk.form.utils.validate_link',
  1423. type: "GET",
  1424. args: {
  1425. 'value': value,
  1426. 'options': doctype,
  1427. 'fetch': fetch
  1428. },
  1429. no_spinner: true,
  1430. callback: function(r) {
  1431. if(r.message=='Ok') {
  1432. if(r.fetch_values && docname) {
  1433. me.set_fetch_values(df, docname, r.fetch_values);
  1434. }
  1435. if(callback) callback(r.valid_value);
  1436. } else {
  1437. if(callback) callback("");
  1438. }
  1439. }
  1440. });
  1441. } else if(callback) {
  1442. callback(value);
  1443. }
  1444. },
  1445. set_fetch_values: function(df, docname, fetch_values) {
  1446. var fl = this.frm.fetch_dict[df.fieldname].fields;
  1447. for(var i=0; i < fl.length; i++) {
  1448. frappe.model.set_value(df.parent, docname, fl[i], fetch_values[i], df.fieldtype);
  1449. }
  1450. }
  1451. });
  1452. if(Awesomplete) {
  1453. Awesomplete.prototype.get_item = function(value) {
  1454. return this._list.find(function(item) {
  1455. return item.value === value;
  1456. });
  1457. }
  1458. }
  1459. frappe.ui.form.ControlDynamicLink = frappe.ui.form.ControlLink.extend({
  1460. get_options: function() {
  1461. if(this.df.get_options) {
  1462. return this.df.get_options();
  1463. }
  1464. if (this.docname==null && cur_dialog) {
  1465. //for dialog box
  1466. return cur_dialog.get_value(this.df.options)
  1467. }
  1468. if (cur_frm==null && cur_list){
  1469. //for list page
  1470. return cur_list.wrapper.find("input[data-fieldname*="+this.df.options+"]").val()
  1471. }
  1472. var options = frappe.model.get_value(this.df.parent, this.docname, this.df.options);
  1473. // if(!options) {
  1474. // frappe.msgprint(__("Please set {0} first",
  1475. // [frappe.meta.get_docfield(this.df.parent, this.df.options, this.docname).label]));
  1476. // }
  1477. return options;
  1478. },
  1479. });
  1480. frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
  1481. make_input: function() {
  1482. this._super();
  1483. $(this.input_area).find("textarea")
  1484. .allowTabs()
  1485. .css({"height":"400px", "font-family": "Monaco, \"Courier New\", monospace"});
  1486. }
  1487. });
  1488. frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
  1489. make_input: function() {
  1490. this.has_input = true;
  1491. this.make_editor();
  1492. this.hide_elements_on_mobile();
  1493. this.setup_drag_drop();
  1494. this.setup_image_dialog();
  1495. },
  1496. make_editor: function() {
  1497. var me = this;
  1498. this.editor = $("<div>").appendTo(this.input_area);
  1499. // Note: while updating summernote, please make sure all 'p' blocks
  1500. // in the summernote source code are replaced by 'div' blocks.
  1501. // by default summernote, adds <p> blocks for new paragraphs, which adds
  1502. // unexpected whitespaces, esp for email replies.
  1503. this.editor.summernote({
  1504. minHeight: 400,
  1505. toolbar: [
  1506. ['magic', ['style']],
  1507. ['style', ['bold', 'italic', 'underline', 'clear']],
  1508. ['fontsize', ['fontsize']],
  1509. ['color', ['color']],
  1510. ['para', ['ul', 'ol', 'paragraph', 'hr']],
  1511. //['height', ['height']],
  1512. ['media', ['link', 'picture', 'video', 'table']],
  1513. ['misc', ['fullscreen', 'codeview']]
  1514. ],
  1515. keyMap: {
  1516. pc: {
  1517. 'CTRL+ENTER': ''
  1518. },
  1519. mac: {
  1520. 'CMD+ENTER': ''
  1521. }
  1522. },
  1523. prettifyHtml: true,
  1524. dialogsInBody: true,
  1525. callbacks: {
  1526. onInit: function() {
  1527. // firefox hack that puts the caret in the wrong position
  1528. // when div is empty. To fix, seed with a <br>.
  1529. // See https://bugzilla.mozilla.org/show_bug.cgi?id=550434
  1530. // this function is executed only once
  1531. $(".note-editable[contenteditable='true']").one('focus', function() {
  1532. var $this = $(this);
  1533. $this.html($this.html() + '<br>');
  1534. });
  1535. },
  1536. onChange: function(value) {
  1537. me.parse_validate_and_set_in_model(value);
  1538. },
  1539. onKeydown: function(e) {
  1540. var key = frappe.ui.keys.get_key(e);
  1541. // prevent 'New DocType (Ctrl + B)' shortcut in editor
  1542. if(['ctrl+b', 'meta+b'].indexOf(key) !== -1) {
  1543. e.stopPropagation();
  1544. }
  1545. if(key.indexOf('escape') !== -1) {
  1546. if(me.note_editor.hasClass('fullscreen')) {
  1547. // exit fullscreen on escape key
  1548. me.note_editor
  1549. .find('.note-btn.btn-fullscreen')
  1550. .trigger('click');
  1551. }
  1552. }
  1553. },
  1554. },
  1555. icons: {
  1556. 'align': 'fa fa-align',
  1557. 'alignCenter': 'fa fa-align-center',
  1558. 'alignJustify': 'fa fa-align-justify',
  1559. 'alignLeft': 'fa fa-align-left',
  1560. 'alignRight': 'fa fa-align-right',
  1561. 'indent': 'fa fa-indent',
  1562. 'outdent': 'fa fa-outdent',
  1563. 'arrowsAlt': 'fa fa-arrows-alt',
  1564. 'bold': 'fa fa-bold',
  1565. 'caret': 'caret',
  1566. 'circle': 'fa fa-circle',
  1567. 'close': 'fa fa-close',
  1568. 'code': 'fa fa-code',
  1569. 'eraser': 'fa fa-eraser',
  1570. 'font': 'fa fa-font',
  1571. 'frame': 'fa fa-frame',
  1572. 'italic': 'fa fa-italic',
  1573. 'link': 'fa fa-link',
  1574. 'unlink': 'fa fa-chain-broken',
  1575. 'magic': 'fa fa-magic',
  1576. 'menuCheck': 'fa fa-check',
  1577. 'minus': 'fa fa-minus',
  1578. 'orderedlist': 'fa fa-list-ol',
  1579. 'pencil': 'fa fa-pencil',
  1580. 'picture': 'fa fa-image',
  1581. 'question': 'fa fa-question',
  1582. 'redo': 'fa fa-redo',
  1583. 'square': 'fa fa-square',
  1584. 'strikethrough': 'fa fa-strikethrough',
  1585. 'subscript': 'fa fa-subscript',
  1586. 'superscript': 'fa fa-superscript',
  1587. 'table': 'fa fa-table',
  1588. 'textHeight': 'fa fa-text-height',
  1589. 'trash': 'fa fa-trash',
  1590. 'underline': 'fa fa-underline',
  1591. 'undo': 'fa fa-undo',
  1592. 'unorderedlist': 'fa fa-list-ul',
  1593. 'video': 'fa fa-video-camera'
  1594. }
  1595. });
  1596. this.note_editor = $(this.input_area).find('.note-editor');
  1597. // to fix <p> on enter
  1598. this.set_input('<div><br></div>');
  1599. },
  1600. setup_drag_drop: function() {
  1601. var me = this;
  1602. this.note_editor.on('dragenter dragover', false)
  1603. .on('drop', function(e) {
  1604. var dataTransfer = e.originalEvent.dataTransfer;
  1605. if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
  1606. me.note_editor.focus();
  1607. var files = [].slice.call(dataTransfer.files);
  1608. files.forEach(file => {
  1609. me.get_image(file, (url) => {
  1610. me.editor.summernote('insertImage', url, file.name);
  1611. });
  1612. });
  1613. }
  1614. e.preventDefault();
  1615. e.stopPropagation();
  1616. });
  1617. },
  1618. get_image: function (fileobj, callback) {
  1619. var freader = new FileReader(),
  1620. me = this;
  1621. freader.onload = function() {
  1622. var dataurl = freader.result;
  1623. // add filename to dataurl
  1624. var parts = dataurl.split(",");
  1625. parts[0] += ";filename=" + fileobj.name;
  1626. dataurl = parts[0] + ',' + parts[1];
  1627. callback(dataurl);
  1628. }
  1629. freader.readAsDataURL(fileobj);
  1630. },
  1631. hide_elements_on_mobile: function() {
  1632. this.note_editor.find('.note-btn-underline,\
  1633. .note-btn-italic, .note-fontsize,\
  1634. .note-color, .note-height, .btn-codeview')
  1635. .addClass('hidden-xs');
  1636. if($('.toggle-sidebar').is(':visible')) {
  1637. // disable tooltips on mobile
  1638. this.note_editor.find('.note-btn')
  1639. .attr('data-original-title', '');
  1640. }
  1641. },
  1642. get_value: function() {
  1643. return this.editor? this.editor.summernote('code'): '';
  1644. },
  1645. set_input: function(value) {
  1646. if(value == null) value = "";
  1647. value = frappe.dom.remove_script_and_style(value);
  1648. if(value !== this.get_value()) {
  1649. this.editor.summernote('code', value);
  1650. }
  1651. this.last_value = value;
  1652. },
  1653. set_focus: function() {
  1654. return this.editor.summernote('focus');
  1655. },
  1656. set_upload_options: function() {
  1657. var me = this;
  1658. this.upload_options = {
  1659. parent: this.image_dialog.get_field("upload_area").$wrapper,
  1660. args: {},
  1661. max_width: this.df.max_width,
  1662. max_height: this.df.max_height,
  1663. options: "Image",
  1664. btn: this.image_dialog.set_primary_action(__("Insert")),
  1665. on_no_attach: function() {
  1666. // if no attachmemts,
  1667. // check if something is selected
  1668. var selected = me.image_dialog.get_field("select").get_value();
  1669. if(selected) {
  1670. me.editor.summernote('insertImage', selected);
  1671. me.image_dialog.hide();
  1672. } else {
  1673. frappe.msgprint(__("Please attach a file or set a URL"));
  1674. }
  1675. },
  1676. callback: function(attachment, r) {
  1677. me.editor.summernote('insertImage', attachment.file_url, attachment.file_name);
  1678. me.image_dialog.hide();
  1679. },
  1680. onerror: function() {
  1681. me.image_dialog.hide();
  1682. }
  1683. }
  1684. if ("is_private" in this.df) {
  1685. this.upload_options.is_private = this.df.is_private;
  1686. }
  1687. if(this.frm) {
  1688. this.upload_options.args = {
  1689. from_form: 1,
  1690. doctype: this.frm.doctype,
  1691. docname: this.frm.docname
  1692. }
  1693. } else {
  1694. this.upload_options.on_attach = function(fileobj, dataurl) {
  1695. me.editor.summernote('insertImage', dataurl);
  1696. me.image_dialog.hide();
  1697. frappe.hide_progress();
  1698. }
  1699. }
  1700. },
  1701. setup_image_dialog: function() {
  1702. this.note_editor.find('[data-original-title="Image"]').on('click', (e) => {
  1703. if(!this.image_dialog) {
  1704. this.image_dialog = new frappe.ui.Dialog({
  1705. title: __("Image"),
  1706. fields: [
  1707. {fieldtype:"HTML", fieldname:"upload_area"},
  1708. {fieldtype:"HTML", fieldname:"or_attach", options: __("Or")},
  1709. {fieldtype:"Select", fieldname:"select", label:__("Select from existing attachments") },
  1710. ]
  1711. });
  1712. }
  1713. this.image_dialog.show();
  1714. this.image_dialog.get_field("upload_area").$wrapper.empty();
  1715. // select from existing attachments
  1716. var attachments = this.frm && this.frm.attachments.get_attachments() || [];
  1717. var select = this.image_dialog.get_field("select");
  1718. if(attachments.length) {
  1719. attachments = $.map(attachments, function(o) { return o.file_url; })
  1720. select.df.options = [""].concat(attachments);
  1721. select.toggle(true);
  1722. this.image_dialog.get_field("or_attach").toggle(true);
  1723. select.refresh();
  1724. } else {
  1725. this.image_dialog.get_field("or_attach").toggle(false);
  1726. select.toggle(false);
  1727. }
  1728. select.$input.val("");
  1729. this.set_upload_options();
  1730. frappe.upload.make(this.upload_options);
  1731. });
  1732. }
  1733. });
  1734. frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({
  1735. make: function() {
  1736. this._super();
  1737. // add title if prev field is not column / section heading or html
  1738. this.grid = new frappe.ui.form.Grid({
  1739. frm: this.frm,
  1740. df: this.df,
  1741. perm: this.perm || (this.frm && this.frm.perm) || this.df.perm,
  1742. parent: this.wrapper
  1743. })
  1744. if(this.frm) {
  1745. this.frm.grids[this.frm.grids.length] = this;
  1746. }
  1747. // description
  1748. if(this.df.description) {
  1749. $('<p class="text-muted small">' + __(this.df.description) + '</p>')
  1750. .appendTo(this.wrapper);
  1751. }
  1752. var me = this;
  1753. this.$wrapper.on("refresh", function() {
  1754. me.grid.refresh();
  1755. return false;
  1756. });
  1757. },
  1758. get_parsed_value: function() {
  1759. if(this.grid) {
  1760. return this.grid.get_data();
  1761. }
  1762. }
  1763. });
  1764. frappe.ui.form.ControlSignature = frappe.ui.form.ControlData.extend({
  1765. saving: false,
  1766. loading: false,
  1767. make: function() {
  1768. var me = this;
  1769. this._super();
  1770. // make jSignature field
  1771. this.$pad = $('<div class="signature-field"></div>')
  1772. .appendTo(me.wrapper)
  1773. .jSignature({height:300, width: "100%", "lineWidth": 0.8})
  1774. .on('change', this.on_save_sign.bind(this));
  1775. this.img_wrapper = $(`<div class="signature-display">
  1776. <div class="missing-image attach-missing-image">
  1777. <i class="octicon octicon-circle-slash"></i>
  1778. </div></div>`)
  1779. .appendTo(this.wrapper);
  1780. this.img = $("<img class='img-responsive attach-image-display'>")
  1781. .appendTo(this.img_wrapper).toggle(false);
  1782. this.$btnWrapper = $(`<div class="signature-btn-row">
  1783. <a href="#" type="button" class="signature-reset btn btn-default">
  1784. <i class="glyphicon glyphicon-repeat"></i></a>`)
  1785. .appendTo(this.$pad)
  1786. .on("click", '.signature-reset', function() {
  1787. me.on_reset_sign();
  1788. return false;
  1789. });
  1790. // handle refresh by reloading the pad
  1791. this.$wrapper.on("refresh", this.on_refresh.bind(this));
  1792. },
  1793. on_refresh: function(e) {
  1794. // prevent to load the second time
  1795. this.$wrapper.find(".control-input").toggle(false);
  1796. this.set_editable(this.get_status()=="Write");
  1797. this.load_pad();
  1798. if(this.get_status()=="Read") {
  1799. $(this.disp_area).toggle(false);
  1800. }
  1801. },
  1802. set_image: function(value) {
  1803. if(value) {
  1804. $(this.img_wrapper).find(".missing-image").toggle(false);
  1805. this.img.attr("src", value).toggle(true);
  1806. } else {
  1807. $(this.img_wrapper).find(".missing-image").toggle(true);
  1808. this.img.toggle(false);
  1809. }
  1810. },
  1811. load_pad: function() {
  1812. // make sure not triggered during saving
  1813. if (this.saving) return;
  1814. // get value
  1815. var value = this.get_value();
  1816. // import data for pad
  1817. if (this.$pad) {
  1818. this.loading = true;
  1819. // reset in all cases
  1820. this.$pad.jSignature('reset');
  1821. if (value) {
  1822. // load the image to find out the size, because scaling will affect
  1823. // stroke width
  1824. try {
  1825. this.$pad.jSignature('setData', value);
  1826. this.set_image(value);
  1827. }
  1828. catch (e){
  1829. console.log("Cannot set data for signature", value, e);
  1830. }
  1831. }
  1832. this.loading = false;
  1833. }
  1834. },
  1835. set_editable: function(editable) {
  1836. this.$pad.toggle(editable);
  1837. this.img_wrapper.toggle(!editable);
  1838. this.$btnWrapper.toggle(editable);
  1839. if (editable) {
  1840. this.$btnWrapper.addClass('editing');
  1841. }
  1842. else {
  1843. this.$btnWrapper.removeClass('editing');
  1844. }
  1845. },
  1846. set_my_value: function(value) {
  1847. if (this.saving || this.loading) return;
  1848. this.saving = true;
  1849. this.set_value(value);
  1850. this.value = value;
  1851. this.saving = false;
  1852. },
  1853. get_value: function() {
  1854. return this.value? this.value: this.get_model_value();
  1855. },
  1856. // reset signature canvas
  1857. on_reset_sign: function() {
  1858. this.$pad.jSignature("reset");
  1859. this.set_my_value("");
  1860. },
  1861. // save signature value to model and display
  1862. on_save_sign: function() {
  1863. if (this.saving || this.loading) return;
  1864. var base64_img = this.$pad.jSignature("getData");
  1865. this.set_my_value(base64_img);
  1866. this.set_image(this.get_value());
  1867. }
  1868. });
  1869. frappe.ui.form.fieldtype_icons = {
  1870. "Date": "fa fa-calendar",
  1871. "Time": "fa fa-time",
  1872. "Datetime": "fa fa-time",
  1873. "Code": "fa fa-code",
  1874. "Select": "fa fa-flag"
  1875. };