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