diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index f28ccc6fe0..6bdf44bd48 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -29,6 +29,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
2000
);
this.count_upper_bound = 1001;
+ this._element_factory = new ElementFactory(this.doctype);
}
has_permissions() {
@@ -876,7 +877,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
column_html = this.settings.formatters[fieldname](value, df, doc);
} else {
column_html = {
- Subject: this.get_subject_html(doc),
+ Subject: this.get_subject_element(doc).innerHTML,
Field: field_html(),
}[col.type];
}
@@ -937,10 +938,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
let comment_count = null;
if (this.list_view_settings && !this.list_view_settings.disable_comment_count) {
- comment_count = $(``);
- $(comment_count).append(`
+ comment_count = ``;
}
html += `
@@ -949,7 +950,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
${settings_button || assigned_to}
${modified}
- ${comment_count ? $(comment_count).prop("outerHTML") : ""}
+ ${comment_count || ""}
${comment_count ? 'ยท' : ""}
${this.get_like_html(doc)}
@@ -1007,70 +1008,60 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
get_seen_class(doc) {
- return JSON.parse(doc._seen || "[]").includes(frappe.session.user) ? "" : "bold";
+ const seen_by = doc._seen ? JSON.parse(doc._seen) : [];
+ return seen_by.includes(frappe.session.user) ? "" : "bold";
}
get_like_html(doc) {
- const liked_by = JSON.parse(doc._liked_by || "[]");
- const heart_class = liked_by.includes(frappe.session.user)
- ? "liked-by liked"
- : "not-liked";
+ const liked_by = doc._liked_by ? JSON.parse(doc._liked_by) : [];
+ const is_liked = liked_by.includes(frappe.session.user);
const title = liked_by.map((u) => frappe.user_info(u).fullname).join(", ");
const div = document.createElement("div");
- div.innerHTML = `
-
- ${frappe.utils.icon("es-solid-heart", "sm", "like-icon")}
-
- `;
-
- const like = div.querySelector(".like-action");
- like.setAttribute("data-liked-by", doc._liked_by || "[]");
- like.setAttribute("data-doctype", this.doctype);
- like.setAttribute("data-name", doc.name);
- like.setAttribute("title", title);
+ div.appendChild(
+ this._element_factory.get_like_element(doc.name, is_liked, liked_by, title)
+ );
return div.innerHTML;
}
- get_subject_html(doc) {
- let subject_field = this.columns[0].df;
+ get_subject_element(doc) {
+ const ef = this._element_factory;
+ const div = document.createElement("div");
+ const checkboxspan = ef.get_checkboxspan_element();
+
+ const ellipsisSpan = document.createElement("span");
+ const seen = this.get_seen_class(doc);
+ if (seen) {
+ ellipsisSpan.classList.add("level-item", seen, "ellipsis");
+ }
+
+ div.appendChild(checkboxspan).appendChild(ef.get_checkbox_element(doc.name));
+ div.appendChild(ellipsisSpan).appendChild(
+ ef.get_link_element(doc.name, this.get_form_link(doc), this.get_subject_text(doc))
+ );
+
+ return div;
+ }
+
+ get_subject_text(doc) {
+ const subject_field = this.columns[0].df;
let value = doc[subject_field.fieldname];
if (this.settings.formatters && this.settings.formatters[subject_field.fieldname]) {
let formatter = this.settings.formatters[subject_field.fieldname];
value = formatter(value, subject_field, doc);
}
+
if (!value) {
value = doc.name;
}
- const seen = this.get_seen_class(doc);
-
- const div = document.createElement("div");
- div.innerHTML = `
-
-
-
-
-
-
- `;
-
- const checkbox = div.querySelector(".list-row-checkbox");
- checkbox.dataset.doctype = this.doctype;
- checkbox.dataset.name = doc.name;
-
- const link = div.querySelector(".level-item a");
- link.dataset.doctype = this.doctype;
- link.dataset.name = doc.name;
- link.href = this.get_form_link(doc);
- // "Text Editor" and some other fieldtypes can have html tags in them so strip and show text.
- // If no text is found show "No Text Found in {Field Label}"
- let textValue = frappe.utils.html2text(value);
- link.title = textValue;
- link.textContent = textValue;
-
- return div.innerHTML;
+ if (frappe.model.html_fieldtypes.includes(subject_field.fieldtype)) {
+ // NOTE: this is very slow, so only do it for HTML fields
+ return frappe.utils.html2text(value);
+ } else {
+ return value;
+ }
}
get_indicator_html(doc, show_workflow_state) {
@@ -2146,3 +2137,81 @@ frappe.get_list_view = (doctype) => {
let route = `List/${doctype}/List`;
return frappe.views.list_view[route];
};
+
+class ElementFactory {
+ /* Pre-create templates for HTML Elements on initialization and provide them
+ via the get_xxx_element methods. */
+ constructor(doctype) {
+ this.templates = {
+ checkbox: this.create_checkbox_element(doctype),
+ checkboxspan: this.create_checkboxspan_element(),
+ link: this.create_link_element(doctype),
+ like: this.create_like_element(doctype),
+ };
+ }
+
+ create_checkbox_element(doctype) {
+ const checkbox = document.createElement("input");
+ checkbox.classList.add("list-row-checkbox");
+ checkbox.type = "checkbox";
+ checkbox.dataset.doctype = doctype;
+ return checkbox;
+ }
+
+ create_link_element(doctype) {
+ const link = document.createElement("a");
+ link.classList.add("ellipsis");
+ link.dataset.doctype = doctype;
+
+ return link;
+ }
+
+ create_checkboxspan_element() {
+ const checkboxspan = document.createElement("span");
+ checkboxspan.classList.add("level-item", "select-like");
+
+ return checkboxspan;
+ }
+
+ create_like_element(doctype) {
+ const like = document.createElement("span");
+ like.classList.add("like-action");
+ like.innerHTML = frappe.utils.icon("es-solid-heart", "sm", "like-icon");
+ like.dataset.doctype = doctype;
+
+ return like;
+ }
+
+ get_checkbox_element(name) {
+ const checkbox = this.templates.checkbox.cloneNode(true);
+ checkbox.dataset.name = name;
+ return checkbox;
+ }
+
+ get_checkboxspan_element() {
+ return this.templates.checkboxspan.cloneNode(true);
+ }
+
+ get_link_element(name, href, text) {
+ const link = this.templates.link.cloneNode(true);
+ link.dataset.name = name;
+ link.href = href;
+ link.title = text;
+ link.textContent = text;
+
+ return link;
+ }
+
+ get_like_element(name, liked, liked_by, title) {
+ const like = this.templates.like.cloneNode(true);
+ like.dataset.name = name;
+
+ const heart_classes = liked ? ["liked-by", "liked"] : ["not-liked"];
+ like.classList.add(...heart_classes);
+
+ like.setAttribute("data-liked-by", liked_by || "[]");
+ like.setAttribute("title", title);
+
+ return like;
+ }
+}
diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js
index 5f5d534813..60270cace0 100644
--- a/frappe/public/js/frappe/model/model.js
+++ b/frappe/public/js/frappe/model/model.js
@@ -108,6 +108,16 @@ $.extend(frappe.model, {
"docstatus",
],
+ html_fieldtypes: [
+ "Text Editor",
+ "Text",
+ "Small Text",
+ "Long Text",
+ "HTML Editor",
+ "Markdown Editor",
+ "Code",
+ ],
+
std_fields: [
{ fieldname: "name", fieldtype: "Link", label: __("ID") },
{ fieldname: "owner", fieldtype: "Link", label: __("Created By"), options: "User" },