Source RapidContext_Widget_TableColumn.js

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