Source RapidContext_Widget_TableColumn.js

1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2024 Per Cederberg. All rights reserved.
4 *
5 * This program is free software: you can redistribute it and/or
6 * modify it under the terms of the BSD license.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 *
12 * See the RapidContext LICENSE for more details.
13 */
14
15// Namespace initialization
16if (typeof(RapidContext) == "undefined") {
17    RapidContext = {};
18}
19RapidContext.Widget = RapidContext.Widget || { Classes: {} };
20
21/**
22 * Creates a new data table column widget.
23 *
24 * @constructor
25 * @param {Object} attrs the widget and node attributes
26 * @param {string} attrs.title the column title
27 * @param {string} attrs.field the data property name
28 * @param {string} [attrs.type] the data property type, one of
29 *            "string", "number", "date", "time", "datetime",
30 *            "boolean" or "object" (defaults to "string")
31 * @param {string} [attrs.sort] the sort direction, one of "asc",
32 *            "desc", "none" (disabled) or null (unsorted)
33 * @param {number} [attrs.maxLength] the maximum data length,
34 *            overflow will be displayed as a tooltip
35 * @param {boolean} [attrs.key] the unique key value flag, only to be
36 *            set for a single column per table
37 * @param {string} [attrs.tooltip] the tooltip text to display on the
38 *            column header
39 * @param {string} [attrs.cellStyle] the CSS styles or class names to set on
40 *            the rendered cells
41 * @param {function} [attrs.renderer] the function that renders the converted
42 *            data value into a table cell, called as
43 *            `renderer(<td>, value, data)` with the DOM node, field value and
44 *            data object as arguments
45 *
46 * @return {Widget} the widget DOM node
47 *
48 * @class The table column widget class. Used to provide a sortable data table
49 *     column, using a `<th>` HTML element for the header (and rendering data
50 *     to `<td>` HTML elements).
51 * @extends RapidContext.Widget
52 *
53 * @example <caption>JavaScript</caption>
54 * let attrs1 = { field: "id", title: "Identifier", key: true, type: "number" };
55 * let attrs2 = { field: "name", title: "Name", maxLength: 50, sort: "asc" };
56 * let attrs3 = { field: "modified", title: "Last Modified", type: "datetime" };
57 * let col1 = RapidContext.Widget.TableColumn(attrs1);
58 * let col2 = RapidContext.Widget.TableColumn(attrs2);
59 * let col3 = RapidContext.Widget.TableColumn(attrs3);
60 * let exampleTable = RapidContext.Widget.Table({}, col1, col2, col3);
61 *
62 * @example <caption>User Interface XML</caption>
63 * <Table id="exampleTable" w="50%" h="100%">
64 *   <TableColumn field="id" title="Identifier" key="true" type="number" />
65 *   <TableColumn field="name" title="Name" maxLength="50" sort="asc" />
66 *   <TableColumn field="modified" title="Last Modified" type="datetime" />
67 * </Table>
68 */
69RapidContext.Widget.TableColumn = function (attrs) {
70    if (attrs.field == null) {
71        throw new Error("The 'field' attribute cannot be null for a TableColumn");
72    }
73    let o = document.createElement("th");
74    RapidContext.Widget._widgetMixin(o, RapidContext.Widget.TableColumn);
75    o.addClass("widgetTableColumn");
76    o.setAttrs(Object.assign({ title: attrs.field, type: "string", key: false }, attrs));
77    o.on("click", o._handleClick);
78    return o;
79};
80
81// Register widget class
82RapidContext.Widget.Classes.TableColumn = RapidContext.Widget.TableColumn;
83
84/**
85 * Returns the widget container DOM node.
86 *
87 * @return {Node} returns null, since child nodes are not supported
88 */
89RapidContext.Widget.TableColumn.prototype._containerNode = function () {
90    return null;
91};
92
93/**
94 * Updates the widget or HTML DOM node attributes. Note that some
95 * updates will not take effect until the parent table is cleared
96 * or data is reloaded.
97 *
98 * @param {Object} attrs the widget and node attributes to set
99 * @param {string} [attrs.title] the column title
100 * @param {string} [attrs.field] the data property name
101 * @param {string} [attrs.type] the data property type, one of
102 *            "string", "number", "date", "time", "datetime",
103 *            "boolean" or "object"
104 * @param {string} [attrs.sort] the sort direction, one of "asc",
105 *            "desc", "none" (disabled) or null (unsorted)
106 * @param {number} [attrs.maxLength] the maximum data length,
107 *            overflow will be displayed as a tooltip
108 * @param {boolean} [attrs.key] the unique key value flag, only to be
109 *            set for a single column per table
110 * @param {string} [attrs.tooltip] the tooltip text to display on the
111 *            column header
112 * @param {string} [attrs.cellStyle] the CSS styles or class names to set on
113 *            the rendered cells
114 * @param {function} [attrs.renderer] the function that renders the converted
115 *            data value into a table cell, called as
116 *            `renderer(<td>, value, data)` with the DOM node, field value and
117 *            data object as arguments
118 */
119RapidContext.Widget.TableColumn.prototype.setAttrs = function (attrs) {
120    attrs = Object.assign({}, attrs);
121    if ("title" in attrs) {
122        this.innerText = attrs.title;
123        delete attrs.title;
124    }
125    if ("sort" in attrs) {
126        this.classList.toggle("sortNone", attrs.sort == "none");
127        this.classList.toggle("sortDesc", attrs.sort == "desc");
128        this.classList.toggle("sortAsc", attrs.sort == "asc");
129    }
130    if ("maxLength" in attrs) {
131        attrs.maxLength = parseInt(attrs.maxLength, 10) || null;
132    }
133    if ("key" in attrs) {
134        attrs.key = RapidContext.Data.bool(attrs.key);
135    }
136    if ("tooltip" in attrs) {
137        attrs.title = attrs.tooltip;
138        delete attrs.tooltip;
139    }
140    if ("renderer" in attrs) {
141        let valid = typeof(attrs.renderer) === "function";
142        attrs.renderer = valid ? attrs.renderer : null;
143    }
144    this.__setAttrs(attrs);
145};
146
147/**
148 * Maps and converts the column field value from the source object.
149 * The data is converted depending on the column data type.
150 *
151 * @param src                the source object (containing the field)
152 *
153 * @return the mapped value
154 */
155RapidContext.Widget.TableColumn.prototype._map = function (src) {
156    let value = src[this.field];
157    if (value != null) {
158        switch (this.type) {
159        case "number":
160            if (value instanceof Number) {
161                value = value.valueOf();
162            } else if (typeof(value) != "number") {
163                value = parseFloat(value);
164            }
165            break;
166        case "date":
167            if (/^@\d+$/.test(value)) {
168                value = new Date(+value.substr(1));
169            }
170            if (value instanceof Date) {
171                value = MochiKit.DateTime.toISODate(value);
172            } else {
173                value = MochiKit.Text.truncate(value, 10);
174            }
175            break;
176        case "datetime":
177            if (/^@\d+$/.test(value)) {
178                value = new Date(+value.substr(1));
179            }
180            if (value instanceof Date) {
181                value = MochiKit.DateTime.toISOTimestamp(value);
182            } else {
183                value = MochiKit.Text.truncate(value, 19);
184            }
185            break;
186        case "time":
187            if (/^@\d+$/.test(value)) {
188                value = new Date(+value.substr(1));
189            }
190            if (value instanceof Date) {
191                value = MochiKit.DateTime.toISOTime(value);
192            } else {
193                value = String(value);
194                if (value.length > 8) {
195                    value = value.substring(value.length - 8);
196                }
197            }
198            break;
199        case "boolean":
200            if (typeof(value) !== "boolean") {
201                value = RapidContext.Data.bool(value);
202            }
203            break;
204        case "string":
205            if (Array.isArray(value) || RapidContext.Fn.isObject(value)) {
206                value = JSON.stringify(value);
207            } else {
208                value = String(value);
209            }
210            break;
211        }
212    }
213    return value;
214};
215
216/**
217 * Renders the column field value into a table cell.
218 *
219 * @param obj                the data object (containing the field)
220 *
221 * @return the table cell DOM node
222 */
223RapidContext.Widget.TableColumn.prototype._render = function (obj) {
224    let td = document.createElement("td");
225    if (typeof(this.cellStyle) === "string" && this.cellStyle.includes(":")) {
226        td.style = this.cellStyle;
227    } else if (typeof(this.cellStyle) === "string") {
228        td.className = this.cellStyle;
229    }
230    try {
231        this.renderer(td, obj[this.field], obj.$data);
232    } catch (e) {
233        td.append(e.toString());
234    }
235    if (this.maxLength && this.maxLength < td.innerText.length) {
236        td.title = td.innerText;
237        td.innerText = td.innerText.substring(0, this.maxLength) + "\u2026";
238    }
239    return td;
240};
241
242/**
243 * Default cell value renderer. Adds an HTML representation of the value to the
244 * specified table cell. The value provided has already been converted to the
245 * configured column `type`.
246 *
247 * @param {Element} td the HTML <td> element to render into
248 * @param {*} value the value to display
249 * @param {Object} data the object containing the raw row data
250 */
251RapidContext.Widget.TableColumn.prototype.renderer = function (td, value, data) {
252    if (typeof(value) == "boolean") {
253        let css = value ? "fa fa-check-square" : "fa fa-square-o";
254        td.append(RapidContext.Widget.Icon(css));
255    } else if (typeof(value) == "number") {
256        td.append(isNaN(value) ? "" : String(value));
257    } else if (value != null) {
258        td.append(String(value));
259    }
260};
261
262/**
263 * Handles click events on the column header.
264 */
265RapidContext.Widget.TableColumn.prototype._handleClick = function () {
266    let table = this.closest(".widget.widgetTable");
267    let dir = (this.sort == "asc") ? "desc" : "asc";
268    table && table.sortData(this.field, dir);
269};
270