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
RapidContext
Access · Discovery · Insight
www.rapidcontext.com