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.
 
 
 
 
 
 

1298 lines
35 KiB

  1. // Copyright (c) 2013, Web Notes 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. .addClass("ui-front")
  28. .attr("data-fieldtype", this.df.fieldtype);
  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. },
  35. toggle: function(show) {
  36. this.$wrapper.toggleClass("hide-control", !!!show);
  37. },
  38. // returns "Read", "Write" or "None"
  39. // as strings based on permissions
  40. get_status: function(explain) {
  41. var status = frappe.perm.get_field_display_status(this.df,
  42. frappe.model.get_doc(this.doctype, this.docname), this.perm || (this.frm && this.frm.perm), explain);
  43. // hide if no value
  44. if (this.doctype && status==="Read"
  45. && is_null(frappe.model.get_value(this.doctype, this.docname, this.df.fieldname))
  46. && !in_list(["HTML"], this.df.fieldtype)) {
  47. if(explain) console.log("By Hide Read-only, null fields: None");
  48. status = "None";
  49. }
  50. return status;
  51. },
  52. refresh: function() {
  53. this.disp_status = this.get_status();
  54. this.$wrapper
  55. && this.$wrapper.toggleClass("hide-control", this.disp_status=="None")
  56. && this.$wrapper.trigger("refresh");
  57. },
  58. get_doc: function() {
  59. return this.doctype && this.docname
  60. && locals[this.doctype] && locals[this.doctype][this.docname] || {};
  61. },
  62. set_value: function(value) {
  63. this.parse_validate_and_set_in_model(value);
  64. },
  65. parse_validate_and_set_in_model: function(value) {
  66. var me = this;
  67. if(this.inside_change_event) return;
  68. this.inside_change_event = true;
  69. if(this.parse) value = this.parse(value);
  70. var set = function(value) {
  71. me.set_model_value(value);
  72. me.inside_change_event = false;
  73. me.set_mandatory && me.set_mandatory(value);
  74. }
  75. this.validate ? this.validate(value, set) : set(value);
  76. },
  77. get_parsed_value: function() {
  78. var me = this;
  79. return this.get_value ?
  80. (this.parse ? this.parse(this.get_value()) : this.get_value()) :
  81. undefined;
  82. },
  83. set_model_value: function(value) {
  84. if(frappe.model.set_value(this.doctype, this.docname, this.df.fieldname,
  85. value, this.df.fieldtype)) {
  86. this.last_value = value;
  87. }
  88. },
  89. });
  90. frappe.ui.form.ControlHTML = frappe.ui.form.Control.extend({
  91. make: function() {
  92. this._super();
  93. var me = this;
  94. this.disp_area = this.wrapper;
  95. this.$wrapper.on("refresh", function() {
  96. var content = me.get_content();
  97. if(content) me.$wrapper.html(content);
  98. return false;
  99. });
  100. },
  101. get_content: function() {
  102. return this.df.options || "";
  103. },
  104. html: function(html) {
  105. this.$wrapper.html(html || this.get_content());
  106. }
  107. });
  108. frappe.ui.form.ControlHeading = frappe.ui.form.ControlHTML.extend({
  109. get_content: function() {
  110. return "<h4>" + __(this.df.label) + "</h4>";
  111. }
  112. });
  113. frappe.ui.form.ControlImage = frappe.ui.form.Control.extend({
  114. make: function() {
  115. this._super();
  116. var me = this;
  117. this.$wrapper = $("<div></div>")
  118. .appendTo(this.parent)
  119. .css({"max-width": "600px", "margin": "0px"});
  120. this.$body = $("<div></div>").appendTo(this.$wrapper)
  121. .css({"margin-bottom": "10px", "max-width": "100%"})
  122. this.$wrapper.on("refresh", function() {
  123. var doc = null;
  124. me.$body.empty();
  125. if(me.docname) {
  126. var doc = frappe.model.get_doc(me.doctype, me.docname);
  127. }
  128. if(doc && me.df.options && doc[me.df.options]) {
  129. me.$img = $("<img src='"+doc[me.df.options]+"' style='max-width: 100%;'>")
  130. .appendTo(me.$body);
  131. } else {
  132. me.$buffer = $("<div class='missing-image'><i class='octicon octicon-circle-slash'></i></div>")
  133. .appendTo(me.$body)
  134. }
  135. return false;
  136. });
  137. $('<div class="clearfix"></div>').appendTo(this.$wrapper);
  138. }
  139. });
  140. frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
  141. // horizontal: true,
  142. make: function() {
  143. // parent element
  144. this._super();
  145. this.set_input_areas();
  146. // set description
  147. this.set_max_width();
  148. this.setup_update_on_refresh();
  149. },
  150. make_wrapper: function() {
  151. if(this.only_input) {
  152. this.$wrapper = $('<div class="form-group frappe-control">').appendTo(this.parent);
  153. } else {
  154. this.$wrapper = $('<div class="frappe-control">\
  155. <div class="form-group" style="margin: 0px">\
  156. <label class="control-label" style="padding-right: 0px;"></label>\
  157. <div class="control-input-wrapper">\
  158. <div class="control-input"></div>\
  159. <div class="control-value like-disabled-input" style="display: none;"></div>\
  160. <p class="help-box small text-muted hidden-xs"></p>\
  161. </div>\
  162. </div>\
  163. </div>').appendTo(this.parent);
  164. }
  165. },
  166. set_input_areas: function() {
  167. if(this.only_input) {
  168. this.input_area = this.wrapper;
  169. } else {
  170. this.label_area = this.label_span = this.$wrapper.find("label").get(0);
  171. this.input_area = this.$wrapper.find(".control-input").get(0);
  172. // keep a separate display area to rendered formatted values
  173. // like links, currencies, HTMLs etc.
  174. this.disp_area = this.$wrapper.find(".control-value").get(0);
  175. }
  176. },
  177. set_max_width: function() {
  178. if(this.horizontal) {
  179. this.$wrapper.css({"max-width": "600px"});
  180. }
  181. },
  182. // update input value, label, description
  183. // display (show/hide/read-only),
  184. // mandatory style on refresh
  185. setup_update_on_refresh: function() {
  186. var me = this;
  187. this.$wrapper.on("refresh", function() {
  188. if(me.disp_status != "None") {
  189. // refresh value
  190. if(me.doctype && me.docname) {
  191. me.value = frappe.model.get_value(me.doctype, me.docname, me.df.fieldname);
  192. }
  193. if(me.disp_status=="Write") {
  194. me.disp_area && $(me.disp_area).toggle(false);
  195. $(me.input_area).toggle(true);
  196. $(me.input_area).find("input").prop("disabled", false);
  197. if(!me.has_input) {
  198. me.make_input();
  199. if(me.df.on_make) {
  200. me.df.on_make(me);
  201. }
  202. };
  203. if(me.doctype && me.docname) {
  204. me.set_input(me.value);
  205. } else {
  206. me.set_input();
  207. }
  208. } else {
  209. $(me.input_area).toggle(false);
  210. $(me.input_area).find("input").prop("disabled", true);
  211. if (me.disp_area) {
  212. me.set_disp_area();
  213. $(me.disp_area).toggle(true);
  214. }
  215. }
  216. me.set_description();
  217. me.set_label();
  218. me.set_mandatory(me.value);
  219. }
  220. return false;
  221. });
  222. },
  223. set_disp_area: function() {
  224. this.disp_area && $(this.disp_area)
  225. .html(frappe.format(this.value, this.df, {no_icon:true, inline:true},
  226. this.doc || (this.frm && this.frm.doc)));
  227. },
  228. bind_change_event: function() {
  229. var me = this;
  230. this.$input && this.$input.on("change", this.change || function(e) {
  231. if(me.doctype && me.docname && me.get_value) {
  232. me.parse_validate_and_set_in_model(me.get_value());
  233. } else {
  234. // inline
  235. var value = me.get_value();
  236. var parsed = me.parse ? me.parse(value) : value;
  237. var set_input = function(before, after) {
  238. if(before !== after) {
  239. me.set_input(after);
  240. } else {
  241. me.set_mandatory && me.set_mandatory(before);
  242. }
  243. }
  244. if(me.validate) {
  245. me.validate(parsed, function(validated) {
  246. set_input(value, validated);
  247. });
  248. } else {
  249. set_input(value, parsed);
  250. }
  251. }
  252. });
  253. },
  254. bind_focusout: function() {
  255. // on touchscreen devices, scroll to top
  256. // so that static navbar and page head don't overlap the input
  257. if (frappe.dom.is_touchscreen()) {
  258. var me = this;
  259. this.$input && this.$input.on("focusout", function() {
  260. if (frappe.dom.is_touchscreen()) {
  261. frappe.ui.scroll(me.$wrapper);
  262. }
  263. });
  264. }
  265. },
  266. set_label: function(label) {
  267. if(label) this.df.label = label;
  268. if(this.only_input || this.df.label==this._label)
  269. return;
  270. // var icon = frappe.ui.form.fieldtype_icons[this.df.fieldtype];
  271. // if(this.df.fieldtype==="Link") {
  272. // icon = frappe.boot.doctype_icons[this.df.options];
  273. // } else if(this.df.link_doctype) {
  274. // icon = frappe.boot.doctype_icons[this.df.link_doctype];
  275. // }
  276. var icon = "";
  277. this.label_span.innerHTML = (icon ? '<i class="'+icon+'"></i> ' : "") +
  278. __(this.df.label) || "&nbsp;";
  279. this._label = this.df.label;
  280. },
  281. set_description: function() {
  282. if(this.only_input || this.df.description===this._description)
  283. return;
  284. if(this.df.description) {
  285. this.$wrapper.find(".help-box").html(__(this.df.description));
  286. } else {
  287. this.set_empty_description();
  288. }
  289. this._description = this.df.description;
  290. },
  291. set_empty_description: function() {
  292. this.$wrapper.find(".help-box").html("");
  293. },
  294. set_mandatory: function(value) {
  295. this.$wrapper.toggleClass("has-error", (this.df.reqd && is_null(value)) ? true : false);
  296. },
  297. });
  298. frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({
  299. html_element: "input",
  300. input_type: "text",
  301. make_input: function() {
  302. this.$input = $("<"+ this.html_element +">")
  303. .attr("type", this.input_type)
  304. .addClass("input-with-feedback form-control")
  305. .prependTo(this.input_area)
  306. this.set_input_attributes();
  307. this.input = this.$input.get(0);
  308. this.has_input = true;
  309. this.bind_change_event();
  310. this.bind_focusout();
  311. },
  312. set_input_attributes: function() {
  313. this.$input
  314. .attr("data-fieldtype", this.df.fieldtype)
  315. .attr("data-fieldname", this.df.fieldname)
  316. .attr("placeholder", this.df.placeholder || "")
  317. if(this.doctype) {
  318. this.$input.attr("data-doctype", this.doctype);
  319. }
  320. if(this.df.input_css) {
  321. this.$input.css(this.df.input_css);
  322. }
  323. if(this.df.input_class) {
  324. this.$input.addClass(this.df.input_class);
  325. }
  326. },
  327. set_input: function(val) {
  328. this.$input && this.$input.val(this.format_for_input(val));
  329. this.set_disp_area();
  330. this.last_value = val;
  331. this.set_mandatory && this.set_mandatory(val);
  332. },
  333. get_value: function() {
  334. return this.$input ? this.$input.val() : undefined;
  335. },
  336. format_for_input: function(val) {
  337. return val==null ? "" : val;
  338. },
  339. validate: function(v, callback) {
  340. if(this.df.options == 'Phone') {
  341. if(v+''=='') {
  342. callback("");
  343. return;
  344. }
  345. v1 = ''
  346. // phone may start with + and must only have numbers later, '-' and ' ' are stripped
  347. v = v.replace(/ /g, '').replace(/-/g, '').replace(/\(/g, '').replace(/\)/g, '');
  348. // allow initial +,0,00
  349. if(v && v.substr(0,1)=='+') {
  350. v1 = '+'; v = v.substr(1);
  351. }
  352. if(v && v.substr(0,2)=='00') {
  353. v1 += '00'; v = v.substr(2);
  354. }
  355. if(v && v.substr(0,1)=='0') {
  356. v1 += '0'; v = v.substr(1);
  357. }
  358. v1 += cint(v) + '';
  359. callback(v1);
  360. } else if(this.df.options == 'Email') {
  361. if(v+''=='') {
  362. callback("");
  363. return;
  364. }
  365. if(!validate_email(v)) {
  366. msgprint(__("Invalid Email: {0}", [v]));
  367. callback("");
  368. } else
  369. callback(v);
  370. } else {
  371. callback(v);
  372. }
  373. }
  374. });
  375. frappe.ui.form.ControlReadOnly = frappe.ui.form.ControlData.extend({
  376. get_status: function(explain) {
  377. var status = this._super(explain);
  378. if(status==="Write")
  379. status = "Read";
  380. return;
  381. },
  382. });
  383. frappe.ui.form.ControlPassword = frappe.ui.form.ControlData.extend({
  384. input_type: "password"
  385. });
  386. frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({
  387. make_input: function() {
  388. var me = this;
  389. this._super();
  390. this.$input
  391. .addClass("text-right")
  392. .on("focus", function() {
  393. setTimeout(function() {
  394. if(!document.activeElement) return;
  395. me.validate(document.activeElement.value, function(val) {
  396. document.activeElement.value = val;
  397. });
  398. document.activeElement.select()
  399. }, 100);
  400. return false;
  401. })
  402. },
  403. parse: function(value) {
  404. return cint(value, null);
  405. },
  406. validate: function(value, callback) {
  407. return callback(value);
  408. }
  409. });
  410. frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({
  411. parse: function(value) {
  412. return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision());
  413. },
  414. format_for_input: function(value) {
  415. var number_format;
  416. if (this.df.fieldtype==="Float" && this.df.options && this.df.options.trim()) {
  417. number_format = this.get_number_format();
  418. }
  419. var formatted_value = format_number(parseFloat(value), number_format, this.get_precision());
  420. return isNaN(parseFloat(value)) ? "" : formatted_value;
  421. },
  422. // even a float field can be formatted based on currency format instead of float format
  423. get_number_format: function() {
  424. var currency = frappe.meta.get_field_currency(this.df, this.get_doc());
  425. return get_number_format(currency);
  426. },
  427. get_precision: function() {
  428. // round based on field precision or float precision, else don't round
  429. return this.df.precision || cint(frappe.boot.sysdefaults.float_precision, null);
  430. }
  431. });
  432. frappe.ui.form.ControlCurrency = frappe.ui.form.ControlFloat.extend({
  433. format_for_input: function(value) {
  434. var formatted_value = format_number(parseFloat(value), this.get_number_format(), this.get_precision());
  435. return isNaN(parseFloat(value)) ? "" : formatted_value;
  436. },
  437. get_precision: function() {
  438. // always round based on field precision or currency's precision
  439. // this method is also called in this.parse()
  440. if (!this.df.precision) {
  441. this.df.precision = get_number_format_info(this.get_number_format()).precision;
  442. }
  443. return this.df.precision;
  444. }
  445. });
  446. frappe.ui.form.ControlPercent = frappe.ui.form.ControlFloat;
  447. frappe.ui.form.ControlDate = frappe.ui.form.ControlData.extend({
  448. datepicker_options: {
  449. altFormat:'yy-mm-dd',
  450. changeYear: true,
  451. yearRange: "-70Y:+10Y",
  452. },
  453. make_input: function() {
  454. this._super();
  455. this.set_datepicker();
  456. },
  457. set_datepicker: function() {
  458. this.datepicker_options.dateFormat =
  459. (frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd').replace("yyyy", "yy")
  460. this.$input.datepicker(this.datepicker_options);
  461. },
  462. parse: function(value) {
  463. return value ? dateutil.user_to_str(value) : value;
  464. },
  465. format_for_input: function(value) {
  466. return value ? dateutil.str_to_user(value) : "";
  467. },
  468. validate: function(value, callback) {
  469. if(!dateutil.validate(value)) {
  470. msgprint (__("Date must be in format: {0}", [sys_defaults.date_format || "yyyy-mm-dd"]));
  471. callback("");
  472. return;
  473. }
  474. return callback(value);
  475. }
  476. })
  477. import_timepicker = function() {
  478. frappe.require("assets/frappe/js/lib/jquery/jquery.ui.slider.min.js");
  479. frappe.require("assets/frappe/js/lib/jquery/jquery.ui.sliderAccess.js");
  480. frappe.require("assets/frappe/js/lib/jquery/jquery.ui.timepicker-addon.css");
  481. frappe.require("assets/frappe/js/lib/jquery/jquery.ui.timepicker-addon.js");
  482. }
  483. frappe.ui.form.ControlTime = frappe.ui.form.ControlData.extend({
  484. make_input: function() {
  485. import_timepicker();
  486. this._super();
  487. this.$input.timepicker({
  488. timeFormat: 'HH:mm:ss',
  489. });
  490. }
  491. });
  492. frappe.ui.form.ControlDatetime = frappe.ui.form.ControlDate.extend({
  493. set_datepicker: function() {
  494. this.datepicker_options.timeFormat = "HH:mm:ss";
  495. this.datepicker_options.dateFormat =
  496. (frappe.boot.sysdefaults.date_format || 'yy-mm-dd').replace('yyyy','yy');
  497. this.$input.datetimepicker(this.datepicker_options);
  498. },
  499. make_input: function() {
  500. import_timepicker();
  501. this._super();
  502. },
  503. });
  504. frappe.ui.form.ControlText = frappe.ui.form.ControlData.extend({
  505. html_element: "textarea",
  506. horizontal: false,
  507. make_wrapper: function() {
  508. this._super();
  509. this.$wrapper.find(".like-disabled-input").addClass("for-description");
  510. }
  511. });
  512. frappe.ui.form.ControlLongText = frappe.ui.form.ControlText;
  513. frappe.ui.form.ControlSmallText = frappe.ui.form.ControlText;
  514. frappe.ui.form.ControlCheck = frappe.ui.form.ControlData.extend({
  515. input_type: "checkbox",
  516. make_wrapper: function() {
  517. this.$wrapper = $('<div class="form-group frappe-control">\
  518. <div class="checkbox">\
  519. <label>\
  520. <span class="input-area"></span>\
  521. <span class="disp-area" style="display:none; margin-left: -20px;"></span>\
  522. <span class="label-area small"></span>\
  523. </label>\
  524. <p class="help-box small text-muted"></p>\
  525. </div>\
  526. </div>').appendTo(this.parent)
  527. },
  528. set_input_areas: function() {
  529. this.label_area = this.label_span = this.$wrapper.find(".label-area").get(0);
  530. this.input_area = this.$wrapper.find(".input-area").get(0);
  531. this.disp_area = this.$wrapper.find(".disp-area").get(0);
  532. },
  533. make_input: function() {
  534. this._super();
  535. this.$input.removeClass("form-control");
  536. },
  537. parse: function(value) {
  538. return this.input.checked ? 1 : 0;
  539. },
  540. validate: function(value, callback) {
  541. return callback(cint(value));
  542. },
  543. set_input: function(value) {
  544. this.input.checked = value ? 1 : 0;
  545. this.last_value = value;
  546. }
  547. });
  548. frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
  549. make_input: function() {
  550. var me = this;
  551. this.$input = $('<button class="btn btn-default btn-sm">')
  552. .prependTo(me.input_area)
  553. .on("click", function() {
  554. me.onclick();
  555. });
  556. this.input = this.$input.get(0);
  557. this.set_input_attributes();
  558. this.has_input = true;
  559. this.$wrapper.find(".control-label").addClass("hide");
  560. },
  561. onclick: function() {
  562. if(this.frm && this.frm.doc) {
  563. if(this.frm.script_manager.get_handlers(this.df.fieldname, this.doctype, this.docname).length) {
  564. this.frm.script_manager.trigger(this.df.fieldname, this.doctype, this.docname);
  565. } else {
  566. this.frm.runscript(this.df.options, this);
  567. }
  568. }
  569. else if(this.df.click) {
  570. this.df.click();
  571. }
  572. },
  573. set_input_areas: function() {
  574. this._super();
  575. $(this.disp_area).removeClass().addClass("hide");
  576. },
  577. set_empty_description: function() {
  578. this.$wrapper.find(".help-box").empty().toggle(false);
  579. },
  580. set_label: function() {
  581. $(this.label_span).html("&nbsp;");
  582. this.$input && this.$input.html((this.df.icon ?
  583. ('<i class="'+this.df.icon+' icon-fixed-width"></i> ') : "") + __(this.df.label));
  584. }
  585. });
  586. frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
  587. make_input: function() {
  588. var me = this;
  589. this.$input = $('<button class="btn btn-default btn-sm btn-attach">')
  590. .html(__("Attach"))
  591. .prependTo(me.input_area)
  592. .on("click", function() {
  593. me.onclick();
  594. });
  595. this.$value = $('<div style="margin-top: 5px;">\
  596. <div class="text-ellipsis" style="display: inline-block; width: 90%;">\
  597. <i class="icon-paper-clip"></i> \
  598. <a class="attached-file" target="_blank"></a>\
  599. </div>\
  600. <a class="close">&times;</a></div>')
  601. .prependTo(me.input_area)
  602. .toggle(false);
  603. this.input = this.$input.get(0);
  604. this.set_input_attributes();
  605. this.has_input = true;
  606. this.$value.find(".close").on("click", function() {
  607. if(me.frm) {
  608. me.frm.attachments.remove_attachment_by_filename(me.value, function() {
  609. me.parse_validate_and_set_in_model(null);
  610. me.refresh();
  611. });
  612. } else {
  613. me.dataurl = null;
  614. me.fileobj = null;
  615. me.set_input(null);
  616. me.refresh();
  617. }
  618. })
  619. },
  620. onclick: function() {
  621. if(this.doc && this.doc.__islocal) {
  622. frappe.msgprint(__("Please save the document before uploading."));
  623. return;
  624. }
  625. if(!this.dialog) {
  626. this.dialog = new frappe.ui.Dialog({
  627. title: __(this.df.label || __("Upload")),
  628. fields: [
  629. {fieldtype:"HTML", fieldname:"upload_area"},
  630. {fieldtype:"HTML", fieldname:"or_attach", options: __("Or")},
  631. {fieldtype:"Select", fieldname:"select", label:__("Select from existing attachments") },
  632. ]
  633. });
  634. }
  635. this.dialog.show();
  636. this.dialog.get_field("upload_area").$wrapper.empty();
  637. // select from existing attachments
  638. var attachments = this.frm && this.frm.attachments.get_attachments() || [];
  639. var select = this.dialog.get_field("select");
  640. if(attachments.length) {
  641. attachments = $.map(attachments, function(o) { return o.file_url; })
  642. select.df.options = [""].concat(attachments);
  643. select.toggle(true);
  644. this.dialog.get_field("or_attach").toggle(true);
  645. select.refresh();
  646. } else {
  647. this.dialog.get_field("or_attach").toggle(false);
  648. select.toggle(false);
  649. }
  650. select.$input.val("");
  651. this.set_upload_options();
  652. frappe.upload.make(this.upload_options);
  653. },
  654. set_upload_options: function() {
  655. var me = this;
  656. this.upload_options = {
  657. parent: this.dialog.get_field("upload_area").$wrapper,
  658. args: {},
  659. max_width: this.df.max_width,
  660. max_height: this.df.max_height,
  661. options: this.df.options,
  662. btn: this.dialog.set_primary_action(__("Upload")),
  663. on_no_attach: function() {
  664. // if no attachmemts,
  665. // check if something is selected
  666. var selected = me.dialog.get_field("select").get_value();
  667. if(selected) {
  668. me.parse_validate_and_set_in_model(selected);
  669. me.dialog.hide();
  670. } else {
  671. msgprint(__("Please attach a file or set a URL"));
  672. }
  673. },
  674. callback: function(attachment, r) {
  675. me.on_upload_complete(attachment);
  676. me.dialog.hide();
  677. },
  678. onerror: function() {
  679. me.dialog.hide();
  680. },
  681. }
  682. if(this.frm) {
  683. this.upload_options.args = {
  684. from_form: 1,
  685. doctype: this.frm.doctype,
  686. docname: this.frm.docname,
  687. }
  688. } else {
  689. this.upload_options.on_attach = function(fileobj, dataurl) {
  690. me.dialog.hide();
  691. me.fileobj = fileobj;
  692. me.dataurl = dataurl;
  693. if(me.on_attach) {
  694. me.on_attach()
  695. }
  696. if(me.df.on_attach) {
  697. me.df.on_attach(fileobj, dataurl);
  698. }
  699. me.on_upload_complete();
  700. }
  701. }
  702. },
  703. set_input: function(value, dataurl) {
  704. this.value = value;
  705. if(this.value) {
  706. this.$input.toggle(false);
  707. this.$value.toggle(true).find(".attached-file")
  708. .html(this.value)
  709. .attr("href", dataurl || this.value);
  710. } else {
  711. this.$input.toggle(true);
  712. this.$value.toggle(false);
  713. }
  714. },
  715. get_value: function() {
  716. if(this.frm) {
  717. return this.value;
  718. } else {
  719. return this.fileobj ? (this.fileobj.filename + "," + this.dataurl) : null;
  720. }
  721. },
  722. on_upload_complete: function(attachment) {
  723. if(this.frm) {
  724. this.parse_validate_and_set_in_model(attachment.file_url);
  725. this.refresh();
  726. this.frm.attachments.update_attachment(attachment);
  727. } else {
  728. this.set_input(this.fileobj.filename, this.dataurl);
  729. //this.refresh();
  730. }
  731. },
  732. });
  733. frappe.ui.form.ControlAttachImage = frappe.ui.form.ControlAttach.extend({
  734. make_input: function() {
  735. var me = this;
  736. this._super();
  737. this.img_wrapper = $('<div style="margin: 7px 0px;">\
  738. <div class="missing-image attach-missing-image"><i class="octicon octicon-circle-slash"></i></div></div>')
  739. .prependTo(this.input_area);
  740. this.img = $("<img class='img-responsive attach-image-display'>")
  741. .appendTo(this.img_wrapper).toggle(false);
  742. // propagate click to Attach button
  743. this.img_wrapper.find(".missing-image").on("click", function() { me.$input.click(); });
  744. this.img.on("click", function() { me.$input.click(); });
  745. this.$wrapper.on("refresh", function() {
  746. if(me.get_value()) {
  747. $(me.input_area).find(".missing-image").toggle(false);
  748. me.img.attr("src", me.dataurl ? me.dataurl : me.value).toggle(true);
  749. } else {
  750. $(me.input_area).find(".missing-image").toggle(true);
  751. me.img.toggle(false);
  752. }
  753. });
  754. },
  755. });
  756. frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({
  757. html_element: "select",
  758. make_input: function() {
  759. var me = this;
  760. this._super();
  761. this.set_options();
  762. },
  763. set_input: function(value) {
  764. // refresh options first - (new ones??)
  765. this.set_options(value || "");
  766. this._super(value);
  767. var input_value = this.$input.val();
  768. // not a possible option, repair
  769. if(this.doctype && this.docname) {
  770. // model value is not an option,
  771. // set the default option (displayed)
  772. var model_value = frappe.model.get_value(this.doctype, this.docname, this.df.fieldname);
  773. if(model_value == null && (input_value || "") != (model_value || "")) {
  774. this.set_model_value(input_value);
  775. } else {
  776. this.last_value = value;
  777. }
  778. } else {
  779. if(value !== input_value) {
  780. this.set_value(input_value);
  781. }
  782. }
  783. },
  784. set_options: function(value) {
  785. var options = this.df.options || [];
  786. if(typeof this.df.options==="string") {
  787. options = this.df.options.split("\n");
  788. }
  789. if(this.in_filter && options[0] != "") {
  790. options = add_lists([''], options);
  791. }
  792. // nothing changed
  793. if(options.toString() === this.last_options) {
  794. return;
  795. }
  796. this.last_options = options.toString();
  797. var selected = this.$input.find(":selected").val();
  798. this.$input.empty().add_options(options || []);
  799. if(value===undefined && selected) {
  800. this.$input.val(selected);
  801. }
  802. },
  803. get_file_attachment_list: function() {
  804. if(!this.frm) return;
  805. var fl = frappe.model.docinfo[this.frm.doctype][this.frm.docname];
  806. if(fl && fl.attachments) {
  807. this.set_description("");
  808. var options = [""];
  809. $.each(fl.attachments, function(i, f) {
  810. options.push(f.file_url)
  811. });
  812. return options;
  813. } else {
  814. this.set_description(__("Please attach a file first."))
  815. return [""];
  816. }
  817. }
  818. });
  819. // special features for link
  820. // buttons
  821. // autocomplete
  822. // link validation
  823. // custom queries
  824. // add_fetches
  825. frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
  826. make_input: function() {
  827. var me = this;
  828. $('<div class="link-field" style="position: relative;">\
  829. <input type="text" class="input-with-feedback form-control">\
  830. <span class="link-btn">\
  831. <a class="btn-open grey" title="' + __("Open Link") + '">\
  832. <i class="icon-link"></i></a>\
  833. </span>\
  834. </div>').prependTo(this.input_area);
  835. this.$input_area = $(this.input_area);
  836. this.$input = this.$input_area.find('input');
  837. this.$link = this.$input_area.find('.link-btn');
  838. this.set_input_attributes();
  839. this.$input.on("focus", function() {
  840. me.$link.toggle(true);
  841. setTimeout(function() {
  842. if(!me.$input.val()) {
  843. me.$input.autocomplete("search", "");
  844. }
  845. }, 500);
  846. });
  847. this.$input.on("blur", function() {
  848. setTimeout(function() { me.$link.toggle(false); }, 500);
  849. });
  850. this.input = this.$input.get(0);
  851. this.has_input = true;
  852. var me = this;
  853. this.setup_buttons();
  854. this.setup_autocomplete();
  855. },
  856. get_options: function() {
  857. return this.df.options;
  858. },
  859. setup_buttons: function() {
  860. var me = this;
  861. // open
  862. this.$input_area.find(".btn-open").on("click", function() {
  863. var value = me.get_value();
  864. if(value && me.get_options())
  865. frappe.set_route("Form", me.get_options(), value);
  866. });
  867. if(this.only_input) this.$input_area.find(".link-btn").remove();
  868. },
  869. open_advanced_search: function() {
  870. var doctype = this.get_options();
  871. if(!doctype) return;
  872. new frappe.ui.form.LinkSelector({
  873. doctype: doctype,
  874. target: this,
  875. txt: this.get_value()
  876. });
  877. },
  878. new_doc: function() {
  879. var doctype = this.get_options();
  880. if(!doctype) return;
  881. if(this.frm) {
  882. this.frm.new_doc(doctype, this, {"name_field": this.get_value()});
  883. } else {
  884. new_doc(doctype, {"name_field": this.get_value()});
  885. }
  886. },
  887. setup_autocomplete: function() {
  888. var me = this;
  889. this.$input.on("blur", function() {
  890. if(me.selected) {
  891. me.selected = false;
  892. return;
  893. }
  894. if(me.doctype && me.docname) {
  895. var value = me.get_value();
  896. if(value!==me.last_value) {
  897. me.parse_validate_and_set_in_model(value);
  898. }
  899. }
  900. });
  901. this.$input.cache = {};
  902. this.$input.autocomplete({
  903. minLength: 0,
  904. source: function(request, response) {
  905. var doctype = me.get_options();
  906. if(!doctype) return;
  907. if (!me.$input.cache[doctype]) {
  908. me.$input.cache[doctype] = {};
  909. }
  910. if (me.$input.cache[doctype][request.term]!=null) {
  911. // immediately show from cache
  912. response(me.$input.cache[doctype][request.term]);
  913. }
  914. var args = {
  915. 'txt': request.term,
  916. 'doctype': doctype,
  917. };
  918. me.set_custom_query(args);
  919. return frappe.call({
  920. type: "GET",
  921. method:'frappe.desk.search.search_link',
  922. no_spinner: true,
  923. args: args,
  924. callback: function(r) {
  925. if(!me.df.only_select) {
  926. if(frappe.model.can_create(doctype)
  927. && me.df.fieldtype !== "Dynamic Link") {
  928. // new item
  929. r.results.push({
  930. value: "<span class='text-primary link-option'>"
  931. + "<i class='icon-plus' style='margin-right: 5px;'></i> "
  932. + __("Create a new {0}", [__(me.df.options)])
  933. + "</span>",
  934. action: me.new_doc
  935. });
  936. };
  937. // advanced search
  938. r.results.push({
  939. value: "<span class='text-primary link-option'>"
  940. + "<i class='icon-search' style='margin-right: 5px;'></i> "
  941. + __("Advanced Search")
  942. + "</span>",
  943. action: me.open_advanced_search
  944. });
  945. }
  946. me.$input.cache[doctype][request.term] = r.results;
  947. response(r.results);
  948. },
  949. });
  950. },
  951. open: function(event, ui) {
  952. me.autocomplete_open = true;
  953. },
  954. close: function(event, ui) {
  955. me.autocomplete_open = false;
  956. },
  957. focus: function( event, ui ) {
  958. if(ui.item.action) {
  959. return false;
  960. }
  961. },
  962. select: function(event, ui) {
  963. me.autocomplete_open = false;
  964. if(ui.item.action) {
  965. ui.item.action.apply(me);
  966. return false;
  967. }
  968. if(me.frm && me.frm.doc) {
  969. me.selected = true;
  970. me.parse_validate_and_set_in_model(ui.item.value);
  971. } else {
  972. me.$input.val(ui.item.value);
  973. me.$input.trigger("change");
  974. }
  975. }
  976. }).data('ui-autocomplete')._renderItem = function(ul, d) {
  977. var html = "<strong>" + __(d.value) + "</strong>";
  978. if(d.description && d.value!==d.description) {
  979. html += '<br><span class="small">' + __(d.description) + '</span>';
  980. }
  981. return $('<li></li>')
  982. .data('item.autocomplete', d)
  983. .html('<a><p>' + html + '</p></a>')
  984. .appendTo(ul);
  985. };
  986. // remove accessibility span (for now)
  987. this.$wrapper.find(".ui-helper-hidden-accessible").remove();
  988. },
  989. set_custom_query: function(args) {
  990. var set_nulls = function(obj) {
  991. $.each(obj, function(key, value) {
  992. if(value!==undefined) {
  993. obj[key] = value || null;
  994. }
  995. });
  996. return obj;
  997. }
  998. if(this.get_query || this.df.get_query) {
  999. var get_query = this.get_query || this.df.get_query;
  1000. if($.isPlainObject(get_query)) {
  1001. $.extend(args, set_nulls(get_query));
  1002. } else if(typeof(get_query)==="string") {
  1003. args.query = get_query;
  1004. } else {
  1005. var q = (get_query)(this.frm && this.frm.doc, this.doctype, this.docname);
  1006. if (typeof(q)==="string") {
  1007. args.query = q;
  1008. } else if($.isPlainObject(q)) {
  1009. if(q.filters) {
  1010. set_nulls(q.filters);
  1011. }
  1012. $.extend(args, q);
  1013. }
  1014. }
  1015. }
  1016. if(this.df.filters) {
  1017. set_nulls(this.df.filters);
  1018. if(!args.filters) args.filters = {};
  1019. $.extend(args.filters, this.df.filters);
  1020. }
  1021. },
  1022. validate: function(value, callback) {
  1023. // validate the value just entered
  1024. var me = this;
  1025. if(this.df.options=="[Select]") {
  1026. callback(value);
  1027. return;
  1028. }
  1029. this.frm.script_manager.validate_link_and_fetch(this.df, this.get_options(),
  1030. this.docname, value, callback);
  1031. },
  1032. });
  1033. frappe.ui.form.ControlDynamicLink = frappe.ui.form.ControlLink.extend({
  1034. get_options: function() {
  1035. var options = frappe.model.get_value(this.df.parent, this.docname, this.df.options);
  1036. if(!options) {
  1037. msgprint(__("Please set {0} first",
  1038. [frappe.meta.get_docfield(this.df.parent, this.df.options, this.docname).label]));
  1039. }
  1040. return options;
  1041. },
  1042. });
  1043. frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
  1044. make_input: function() {
  1045. this._super();
  1046. $(this.input_area).find("textarea").css({"height":"400px", "font-family": "Monaco, \"Courier New\", monospace"});
  1047. }
  1048. });
  1049. frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
  1050. editor_name: "bsEditor",
  1051. horizontal: false,
  1052. make_input: function() {
  1053. $(this.input_area).css({"min-height":"360px"});
  1054. this.has_input = true;
  1055. this.make_rich_text_editor();
  1056. this.make_markdown_editor();
  1057. this.make_switcher();
  1058. },
  1059. make_rich_text_editor: function() {
  1060. var me = this;
  1061. this.editor_wrapper = $("<div>").appendTo(this.input_area);
  1062. this.editor = new (frappe.provide(this.editor_name))({
  1063. parent: this.editor_wrapper,
  1064. change: function(value) {
  1065. me.md_editor.val(value);
  1066. me.parse_validate_and_set_in_model(value);
  1067. },
  1068. field: this
  1069. });
  1070. this.editor.editor.keypress("ctrl+s meta+s", function() {
  1071. me.frm.save_or_update();
  1072. });
  1073. },
  1074. make_markdown_editor: function() {
  1075. var me = this;
  1076. this.md_editor_wrapper = $("<div class='hide'>")
  1077. .appendTo(this.input_area);
  1078. this.md_editor = $("<textarea class='form-control'>").css({
  1079. "height": "451px",
  1080. "font-family": "Monaco, \"Courier New\", monospace"
  1081. })
  1082. .appendTo(this.md_editor_wrapper)
  1083. .allowTabs()
  1084. .on("change", function() {
  1085. var value = $(this).val();
  1086. me.editor.set_input(value);
  1087. me.parse_validate_and_set_in_model(value);
  1088. });
  1089. $('<div class="text-muted small">Add &lt;!-- markdown --&gt; \
  1090. to always interpret as markdown</div>')
  1091. .appendTo(this.md_editor_wrapper);
  1092. },
  1093. make_switcher: function() {
  1094. var me = this;
  1095. this.current_editor = this.editor;
  1096. this.switcher = $('<p class="text-right small">\
  1097. <a href="#" class="switcher"></a></p>')
  1098. .appendTo(this.input_area)
  1099. .click(function() {
  1100. me.switch();
  1101. return false;
  1102. });
  1103. this.render_switcher();
  1104. },
  1105. switch: function() {
  1106. if(this.current_editor===this.editor) {
  1107. // switch to md
  1108. var value = this.editor.get_value();
  1109. this.editor_wrapper.addClass("hide");
  1110. this.md_editor_wrapper.removeClass("hide");
  1111. this.current_editor = this.md_editor;
  1112. this.add_type_marker("markdown");
  1113. } else {
  1114. // switch to html
  1115. var value = this.md_editor.val();
  1116. this.md_editor_wrapper.addClass("hide");
  1117. this.editor_wrapper.removeClass("hide");
  1118. this.current_editor = this.editor;
  1119. this.add_type_marker("html");
  1120. }
  1121. this.render_switcher();
  1122. },
  1123. add_type_marker: function(marker) {
  1124. var opp_marker = marker==="html" ? "markdown" : "html";
  1125. if(!this.value) this.value = "";
  1126. if(this.value.indexOf("<!-- " + opp_marker + " -->")!==-1) {
  1127. // replace opposite marker
  1128. this.set_value(this.value.split("<!-- " + opp_marker + " -->").join("<!-- " + marker + " -->"));
  1129. } else if(this.value.indexOf("<!-- " + marker + " -->")===-1) {
  1130. // add marker (marker missing)
  1131. this.set_value(this.value + "\n\n\n<!-- " + marker + " -->");
  1132. }
  1133. },
  1134. render_switcher: function() {
  1135. this.switcher.html(__("Edit as {0}", [this.current_editor == this.editor ?
  1136. __("Markdown") : __("Rich Text")]));
  1137. },
  1138. get_value: function() {
  1139. return this.current_editor === this.editor
  1140. ? this.editor.get_value()
  1141. : this.md_editor.val();
  1142. },
  1143. set_input: function(value) {
  1144. this._set_input(value);
  1145. // guess editor type
  1146. var is_markdown = false;
  1147. if(value) {
  1148. if(value.indexOf("<!-- markdown -->") !== -1) {
  1149. var is_markdown = true;
  1150. }
  1151. if((is_markdown && this.current_editor===this.editor)
  1152. || (!is_markdown && this.current_editor===this.md_editor)) {
  1153. this.switch();
  1154. }
  1155. }
  1156. },
  1157. _set_input: function(value) {
  1158. this.editor.set_input(value);
  1159. this.md_editor.val(value);
  1160. this.last_value = value;
  1161. }
  1162. });
  1163. frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({
  1164. make: function() {
  1165. this._super();
  1166. // add title if prev field is not column / section heading or html
  1167. var prev_fieldtype = "";
  1168. if(this.df.idx > 1) {
  1169. var prev_fieldtype = cur_frm.meta.fields[this.df.idx - 2].fieldtype;
  1170. }
  1171. if(frappe.model.layout_fields.indexOf(prev_fieldtype)===-1) {
  1172. $("<label>" + __(this.df.label) + "<label>").appendTo(this.wrapper);
  1173. }
  1174. this.grid = new frappe.ui.form.Grid({
  1175. frm: this.frm,
  1176. df: this.df,
  1177. perm: this.perm || this.frm.perm,
  1178. parent: this.wrapper
  1179. })
  1180. if(this.frm)
  1181. this.frm.grids[this.frm.grids.length] = this;
  1182. // description
  1183. if(this.df.description) {
  1184. $('<p class="text-muted small">' + __(this.df.description) + '</p>')
  1185. .appendTo(this.wrapper);
  1186. }
  1187. var me = this;
  1188. this.$wrapper.on("refresh", function() {
  1189. me.grid.refresh();
  1190. return false;
  1191. });
  1192. }
  1193. })
  1194. frappe.ui.form.fieldtype_icons = {
  1195. "Date": "icon-calendar",
  1196. "Time": "icon-time",
  1197. "Datetime": "icon-time",
  1198. "Code": "icon-code",
  1199. "Select": "icon-flag"
  1200. };