seitime-frappe/frappe/public/js/frappe/form/layout.js
2023-07-14 15:41:20 +05:30

787 lines
19 KiB
JavaScript

import Section from "./section.js";
import Tab from "./tab.js";
import Column from "./column.js";
frappe.ui.form.Layout = class Layout {
constructor(opts) {
this.views = {};
this.pages = [];
this.tabs = [];
this.sections = [];
this.page_breaks = [];
this.sections_dict = {};
this.fields_list = [];
this.fields_dict = {};
this.section_count = 0;
this.column_count = 0;
$.extend(this, opts);
}
make() {
if (!this.parent && this.body) {
this.parent = this.body;
}
this.wrapper = $('<div class="form-layout">').appendTo(this.parent);
this.message = $('<div class="form-message hidden"></div>').appendTo(this.wrapper);
this.page = $('<div class="form-page"></div>').appendTo(this.wrapper);
if (!this.fields) {
this.fields = this.get_doctype_fields();
}
if (this.is_tabbed_layout()) {
this.setup_tabbed_layout();
}
this.setup_tab_events();
this.frm && this.setup_tooltip_events();
this.render();
}
setup_tabbed_layout() {
$(`
<div class="form-tabs-list">
<ul class="nav form-tabs" id="form-tabs" role="tablist"></ul>
</div>
`).appendTo(this.page);
this.tab_link_container = this.page.find(".form-tabs");
this.tabs_content = $(`<div class="form-tab-content tab-content"></div>`).appendTo(
this.page
);
this.setup_events();
}
get_doctype_fields() {
let fields = [this.get_new_name_field()];
if (this.doctype_layout) {
fields = fields.concat(this.get_fields_from_layout());
} else {
fields = fields.concat(
frappe.meta.sort_docfields(frappe.meta.docfield_map[this.doctype])
);
}
return fields;
}
get_new_name_field() {
return {
parent: this.frm.doctype,
fieldtype: "Data",
fieldname: "__newname",
reqd: 1,
hidden: 1,
label: __("Name"),
get_status: function (field) {
if (
field.frm &&
field.frm.is_new() &&
field.frm.meta.autoname &&
["prompt", "name"].includes(field.frm.meta.autoname.toLowerCase())
) {
return "Write";
}
return "None";
},
};
}
get_fields_from_layout() {
const fields = [];
for (let f of this.doctype_layout.fields) {
const docfield = copy_dict(frappe.meta.docfield_map[this.doctype][f.fieldname]);
docfield.label = f.label;
fields.push(docfield);
}
return fields;
}
show_message(html, color) {
if (this.message_color) {
// remove previous color
this.message.removeClass(this.message_color);
}
this.message_color =
color && ["yellow", "blue", "red", "green", "orange"].includes(color) ? color : "blue";
if (html) {
if (html.substr(0, 1) !== "<") {
// wrap in a block
html = "<div>" + html + "</div>";
}
this.message.removeClass("hidden").addClass(this.message_color);
$(html).appendTo(this.message);
} else {
this.message.empty().addClass("hidden");
}
}
render(new_fields) {
let fields = new_fields || this.fields;
this.section = null;
this.column = null;
if (this.no_opening_section() && !this.is_tabbed_layout()) {
this.fields.unshift({ fieldtype: "Section Break" });
}
if (this.is_tabbed_layout()) {
// add a tab without `fieldname` to avoid conflicts
let default_tab = {
label: __("Details"),
fieldtype: "Tab Break",
fieldname: "__details",
};
let first_field_visible = this.fields.find((element) => element.hidden == false);
let first_tab =
first_field_visible?.fieldtype === "Tab Break" ? first_field_visible : null;
if (!first_tab) {
this.fields.splice(0, 0, default_tab);
} else {
// reshuffle __newname field to accomodate under 1st Tab Break
let newname_field = this.fields.find((df) => df.fieldname === "__newname");
if (newname_field && newname_field.get_status(this) === "Write") {
this.fields.splice(0, 1);
this.fields.splice(1, 0, newname_field);
}
}
}
fields.forEach((df) => {
switch (df.fieldtype) {
case "Fold":
this.make_page(df);
break;
case "Page Break":
this.make_page_break();
this.make_section(df);
break;
case "Section Break":
this.make_section(df);
break;
case "Column Break":
this.make_column(df);
break;
case "Tab Break":
this.make_tab(df);
break;
default:
this.make_field(df);
}
});
}
no_opening_section() {
return (
(this.fields[0] && this.fields[0].fieldtype != "Section Break") || !this.fields.length
);
}
no_opening_tab() {
return (this.fields[1] && this.fields[1].fieldtype != "Tab Break") || !this.fields.length;
}
is_tabbed_layout() {
return this.fields.find((f) => f.fieldtype === "Tab Break");
}
replace_field(fieldname, df, render) {
df.fieldname = fieldname; // change of fieldname is avoided
if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) {
const prev_fieldobj = this.fields_dict[fieldname];
const fieldobj = this.init_field(df, prev_fieldobj.parent, render);
prev_fieldobj.$wrapper.replaceWith(fieldobj.$wrapper);
const idx = this.fields_list.findIndex((e) => e == prev_fieldobj);
this.fields_list.splice(idx, 1, fieldobj);
this.fields_dict[fieldname] = fieldobj;
this.sections.forEach((section) => section.replace_field(fieldname, fieldobj));
prev_fieldobj.tab?.replace_field(fieldobj);
this.refresh_fields([df]);
}
}
make_field(df, colspan, render) {
!this.section && this.make_section();
!this.column && this.make_column();
const parent = this.column.form.get(0);
const fieldobj = this.init_field(df, parent, render);
this.fields_list.push(fieldobj);
this.fields_dict[df.fieldname] = fieldobj;
this.section.add_field(fieldobj);
this.column.add_field(fieldobj);
if (this.current_tab) {
this.current_tab.add_field(fieldobj);
}
}
init_field(df, parent, render = false) {
const fieldobj = frappe.ui.form.make_control({
df: df,
doctype: this.doctype,
parent: parent,
frm: this.frm,
render_input: render,
doc: this.doc,
layout: this,
});
fieldobj.layout = this;
return fieldobj;
}
make_page_break() {
this.page = $('<div class="form-page page-break"></div>').appendTo(this.wrapper);
}
make_page(df) {
let me = this;
let head = $(`
<div class="form-clickable-section text-center">
<a class="btn-fold h6 text-muted">
${__("Show more details")}
</a>
</div>
`).appendTo(this.wrapper);
this.page = $('<div class="form-page second-page hide"></div>').appendTo(this.wrapper);
this.fold_btn = head.find(".btn-fold").on("click", function () {
let page = $(this).parent().next();
if (page.hasClass("hide")) {
$(this).removeClass("btn-fold").html(__("Hide details"));
page.removeClass("hide");
frappe.utils.scroll_to($(this), true, 30);
me.folded = false;
} else {
$(this).addClass("btn-fold").html(__("Show more details"));
page.addClass("hide");
me.folded = true;
}
});
this.section = null;
this.folded = true;
}
unfold() {
this.fold_btn.trigger("click");
}
make_section(df = {}) {
this.section_count++;
if (!df.fieldname) {
df.fieldname = `__section_${this.section_count}`;
df.fieldtype = "Section Break";
}
this.section = new Section(
this.current_tab ? this.current_tab.wrapper : this.page,
df,
this.card_layout,
this
);
this.sections.push(this.section);
this.sections_dict[df.fieldname] = this.section;
// append to layout fields
if (df) {
this.fields_dict[df.fieldname] = this.section;
this.fields_list.push(this.section);
}
this.column = null;
}
make_column(df = {}) {
this.column_count++;
if (!df.fieldname) {
df.fieldname = `__column_${this.section_count}`;
df.fieldtype = "Column Break";
}
this.column = new Column(this.section, df);
if (df && df.fieldname) {
this.fields_list.push(this.column);
}
}
make_tab(df) {
this.section = null;
let tab = new Tab(this, df, this.frm, this.tab_link_container, this.tabs_content);
this.current_tab = tab;
this.make_section({ fieldtype: "Section Break" });
this.tabs.push(tab);
return tab;
}
refresh(doc) {
if (doc) this.doc = doc;
if (this.frm) {
this.wrapper.find(".empty-form-alert").remove();
}
// NOTE this might seem redundant at first, but it needs to be executed when frm.refresh_fields is called
this.attach_doc_and_docfields(true);
if (this.frm && this.frm.wrapper) {
$(this.frm.wrapper).trigger("refresh-fields");
}
// dependent fields
this.refresh_dependency();
// refresh sections
this.refresh_sections();
if (this.frm) {
// collapse sections
this.refresh_section_collapse();
}
if (document.activeElement) {
if (document.activeElement.tagName == "INPUT" && this.is_numeric_field_active()) {
document.activeElement.select();
}
}
}
is_numeric_field_active() {
const control = $(document.activeElement).closest(".frappe-control");
const fieldtype = (control.data() || {}).fieldtype;
return frappe.model.numeric_fieldtypes.includes(fieldtype);
}
refresh_sections() {
// hide invisible sections
this.wrapper.find(".form-section:not(.hide-control)").each(function () {
const section = $(this).removeClass("empty-section visible-section");
if (section.find(".frappe-control:not(.hide-control)").length) {
section.addClass("visible-section");
} else {
// nothing visible, hide the section
section.addClass("empty-section");
}
});
// refresh tabs
this.is_tabbed_layout() && this.refresh_tabs();
}
refresh_tabs() {
for (let tab of this.tabs) {
tab.refresh();
}
const visible_tabs = this.tabs.filter((tab) => !tab.hidden);
if (visible_tabs && visible_tabs.length == 1) {
visible_tabs[0].tab_link.toggleClass("hide show");
}
this.set_tab_as_active();
}
select_tab(label_or_fieldname) {
for (let tab of this.tabs) {
if (
tab.label.toLowerCase() === label_or_fieldname.toLowerCase() ||
tab.df.fieldname?.toLowerCase() === label_or_fieldname.toLowerCase()
) {
tab.set_active();
return;
}
}
}
set_tab_as_active() {
let frm_active_tab = this?.frm.get_active_tab?.();
if (frm_active_tab) {
frm_active_tab.set_active();
} else if (this.tabs.length) {
// set first tab as active when opening for first time, or new doc
let first_visible_tab = this.tabs.find((tab) => !tab.is_hidden());
first_visible_tab && first_visible_tab.set_active();
}
}
refresh_fields(fields) {
let fieldnames = fields.map((field) => {
if (field.fieldname) return field.fieldname;
});
this.fields_list.map((fieldobj) => {
if (fieldnames.includes(fieldobj.df.fieldname)) {
fieldobj.refresh();
if (fieldobj.df["default"]) {
fieldobj.set_input(fieldobj.df["default"]);
}
}
});
}
add_fields(fields) {
this.render(fields);
this.refresh_fields(fields);
}
refresh_section_collapse() {
if (!(this.sections && this.sections.length)) return;
for (let i = 0; i < this.sections.length; i++) {
let section = this.sections[i];
let df = section.df;
if (df && df.collapsible) {
let collapse = true;
if (df.collapsible_depends_on) {
collapse = !this.evaluate_depends_on_value(df.collapsible_depends_on);
}
if (collapse && section.has_missing_mandatory()) {
collapse = false;
}
section.collapse(collapse);
}
}
}
attach_doc_and_docfields(refresh) {
let me = this;
for (let i = 0, l = this.fields_list.length; i < l; i++) {
let fieldobj = this.fields_list[i];
if (me.doc) {
fieldobj.doc = me.doc;
fieldobj.doctype = me.doc.doctype;
fieldobj.docname = me.doc.name;
fieldobj.df =
frappe.meta.get_docfield(me.doc.doctype, fieldobj.df.fieldname, me.doc.name) ||
fieldobj.df;
}
refresh && fieldobj.df && fieldobj.refresh && fieldobj.refresh();
}
}
refresh_section_count() {
this.wrapper.find(".section-count-label:visible").each(function (i) {
$(this).html(i + 1);
});
}
setup_events() {
let last_scroll = 0;
let tabs_list = $(".form-tabs-list");
let tabs_content = this.tabs_content[0];
if (!tabs_list.length) return;
$(window).scroll(
frappe.utils.throttle(() => {
let current_scroll = document.documentElement.scrollTop;
if (current_scroll > 0 && last_scroll <= current_scroll) {
tabs_list.removeClass("form-tabs-sticky-down");
tabs_list.addClass("form-tabs-sticky-up");
} else {
tabs_list.removeClass("form-tabs-sticky-up");
tabs_list.addClass("form-tabs-sticky-down");
}
last_scroll = current_scroll;
}, 500)
);
this.tab_link_container.off("click").on("click", ".nav-link", (e) => {
e.preventDefault();
e.stopImmediatePropagation();
$(e.currentTarget).tab("show");
if (tabs_content.getBoundingClientRect().top < 100) {
tabs_content.scrollIntoView();
setTimeout(() => {
$(".page-head").css("top", "-15px");
$(".form-tabs-list").removeClass("form-tabs-sticky-down");
$(".form-tabs-list").addClass("form-tabs-sticky-up");
}, 3);
}
});
}
setup_tab_events() {
this.wrapper.on("keydown", (ev) => {
if (ev.which == 9) {
let current = $(ev.target);
let doctype = current.attr("data-doctype");
let fieldname = current.attr("data-fieldname");
if (doctype) {
return this.handle_tab(doctype, fieldname, ev.shiftKey);
}
}
});
}
setup_tooltip_events() {
$(document).on("keydown", (e) => {
if (e.altKey) {
this.wrapper.addClass("show-tooltip");
}
});
$(document).on("keyup", (e) => {
if (!e.altKey) {
this.wrapper.removeClass("show-tooltip");
}
});
this.frm.page &&
frappe.ui.keys.add_shortcut({
shortcut: "alt+hover",
page: this.frm.page,
description: __("Show Fieldname (click to copy on clipboard)"),
});
}
handle_tab(doctype, fieldname, shift) {
let grid_row = null,
prev = null,
fields = this.fields_list,
focused = false;
// in grid
if (doctype != this.doctype) {
grid_row = this.get_open_grid_row();
if (!grid_row || !grid_row.layout) {
return;
}
fields = grid_row.layout.fields_list;
}
for (let i = 0, len = fields.length; i < len; i++) {
if (fields[i].df.fieldname == fieldname) {
if (shift) {
if (prev) {
this.set_focus(prev);
} else {
$(this.primary_button).focus();
}
break;
}
if (i < len - 1) {
focused = this.focus_on_next_field(i, fields);
}
if (focused) {
break;
}
}
if (this.is_visible(fields[i])) prev = fields[i];
}
if (!focused) {
// last field in this group
if (grid_row) {
// in grid
if (grid_row.doc.idx == grid_row.grid.grid_rows.length) {
// last row, close it and find next field
grid_row.toggle_view(false, function () {
grid_row.grid.frm.layout.handle_tab(
grid_row.grid.df.parent,
grid_row.grid.df.fieldname
);
});
} else {
// next row
grid_row.grid.grid_rows[grid_row.doc.idx].toggle_view(true);
}
} else if (!shift) {
// End of tab navigation
$(this.primary_button).focus();
}
}
return false;
}
focus_on_next_field(start_idx, fields) {
// loop to find next eligible fields
for (let i = start_idx + 1, len = fields.length; i < len; i++) {
let field = fields[i];
if (this.is_visible(field)) {
if (field.df.fieldtype === "Table") {
// open table grid
if (!(field.grid.grid_rows && field.grid.grid_rows.length)) {
// empty grid, add a new row
field.grid.add_new_row();
}
// show grid row (if exists)
field.grid.grid_rows[0].show_form();
return true;
} else if (!in_list(frappe.model.no_value_type, field.df.fieldtype)) {
this.set_focus(field);
return true;
}
}
}
}
is_visible(field) {
return (
field.disp_status === "Write" && field.df && "hidden" in field.df && !field.df.hidden
);
}
set_focus(field) {
if (field.tab) {
field.tab.set_active();
}
// next is table, show the table
if (field.df.fieldtype == "Table") {
if (!field.grid.grid_rows.length) {
field.grid.add_new_row(1);
} else {
field.grid.grid_rows[0].toggle_view(true);
}
} else if (field.editor) {
field.editor.set_focus();
} else if (field.$input) {
field.$input.focus();
}
}
get_open_grid_row() {
return $(".grid-row-open").data("grid_row");
}
refresh_dependency() {
/**
Resolve "depends_on" and show / hide accordingly
build dependants' dictionary
*/
let has_dep = false;
const fields = this.fields_list.concat(this.tabs);
for (let fkey in fields) {
let f = fields[fkey];
if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) {
has_dep = true;
break;
}
}
if (!has_dep) return;
// show / hide based on values
for (let i = fields.length - 1; i >= 0; i--) {
let f = fields[i];
f.guardian_has_value = true;
if (f.df.depends_on) {
// evaluate guardian
f.guardian_has_value = this.evaluate_depends_on_value(f.df.depends_on);
// show / hide
if (f.guardian_has_value) {
if (f.df.hidden_due_to_dependency) {
f.df.hidden_due_to_dependency = false;
f.refresh();
}
} else {
if (!f.df.hidden_due_to_dependency) {
f.df.hidden_due_to_dependency = true;
f.refresh();
}
}
}
if (f.df.mandatory_depends_on) {
this.set_dependant_property(f.df.mandatory_depends_on, f.df.fieldname, "reqd");
}
if (f.df.read_only_depends_on) {
this.set_dependant_property(
f.df.read_only_depends_on,
f.df.fieldname,
"read_only"
);
}
}
this.refresh_section_count();
}
set_dependant_property(condition, fieldname, property) {
let set_property = this.evaluate_depends_on_value(condition);
let value = set_property ? 1 : 0;
let form_obj;
if (this.frm) {
form_obj = this.frm;
} else if (this.is_dialog || this.doctype === "Web Form") {
form_obj = this;
}
if (form_obj) {
if (this.doc && this.doc.parent && this.doc.parentfield) {
form_obj.setting_dependency = true;
form_obj.set_df_property(
this.doc.parentfield,
property,
value,
this.doc.parent,
fieldname,
this.doc.name
);
form_obj.setting_dependency = false;
// refresh child fields
this.fields_dict[fieldname] && this.fields_dict[fieldname].refresh();
} else {
form_obj.set_df_property(fieldname, property, value);
}
}
}
evaluate_depends_on_value(expression) {
let out = null;
let doc = this.doc;
if (!doc && this.get_values) {
doc = this.get_values(true);
}
if (!doc) {
return;
}
let parent = this.frm ? this.frm.doc : this.doc || null;
if (typeof expression === "boolean") {
out = expression;
} else if (typeof expression === "function") {
out = expression(doc);
} else if (expression.substr(0, 5) == "eval:") {
try {
out = frappe.utils.eval(expression.substr(5), { doc, parent });
if (parent && parent.istable && expression.includes("is_submittable")) {
out = true;
}
} catch (e) {
frappe.throw(__('Invalid "depends_on" expression'));
}
} else if (expression.substr(0, 3) == "fn:" && this.frm) {
out = this.frm.script_manager.trigger(
expression.substr(3),
this.doctype,
this.docname
);
} else {
var value = doc[expression];
if ($.isArray(value)) {
out = !!value.length;
} else {
out = !!value;
}
}
return out;
}
};