@zolomedia/bifrost-client 1.7.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/L1_Foundation/L1_Foundation.js +13 -0
- package/L1_Foundation/bootstrap/bootstrap.js +11 -0
- package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
- package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
- package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
- package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
- package/L1_Foundation/bootstrap/module_registry.js +102 -0
- package/L1_Foundation/bootstrap/prism_loader.js +164 -0
- package/L1_Foundation/config/client_config.js +110 -0
- package/L1_Foundation/config/config.js +7 -0
- package/L1_Foundation/connection/connection.js +8 -0
- package/L1_Foundation/connection/websocket_connection.js +122 -0
- package/L1_Foundation/constants/bifrost_constants.js +284 -0
- package/L1_Foundation/constants/constants.js +7 -0
- package/L1_Foundation/logger/logger.js +10 -0
- package/L2_Handling/L2_Handling.js +15 -0
- package/L2_Handling/cache/cache.js +22 -0
- package/L2_Handling/cache/cache_constants.js +69 -0
- package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
- package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
- package/L2_Handling/cache/orchestration/orchestration.js +12 -0
- package/L2_Handling/cache/storage/session_manager.js +289 -0
- package/L2_Handling/cache/storage/storage.js +10 -0
- package/L2_Handling/cache/storage/storage_manager.js +590 -0
- package/L2_Handling/display/composite/composite.js +13 -0
- package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
- package/L2_Handling/display/composite/swiper_renderer.js +564 -0
- package/L2_Handling/display/composite/terminal_renderer.js +922 -0
- package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
- package/L2_Handling/display/display.js +30 -0
- package/L2_Handling/display/feedback/feedback.js +11 -0
- package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
- package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
- package/L2_Handling/display/inputs/button_renderer.js +634 -0
- package/L2_Handling/display/inputs/form_renderer.js +583 -0
- package/L2_Handling/display/inputs/input_renderer.js +658 -0
- package/L2_Handling/display/inputs/inputs.js +12 -0
- package/L2_Handling/display/navigation/menu_renderer.js +206 -0
- package/L2_Handling/display/navigation/navigation.js +11 -0
- package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
- package/L2_Handling/display/orchestration/orchestration.js +11 -0
- package/L2_Handling/display/orchestration/renderer.js +430 -0
- package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
- package/L2_Handling/display/outputs/alert_renderer.js +161 -0
- package/L2_Handling/display/outputs/audio_renderer.js +94 -0
- package/L2_Handling/display/outputs/card_renderer.js +229 -0
- package/L2_Handling/display/outputs/code_renderer.js +66 -0
- package/L2_Handling/display/outputs/dl_renderer.js +131 -0
- package/L2_Handling/display/outputs/header_renderer.js +162 -0
- package/L2_Handling/display/outputs/icon_renderer.js +107 -0
- package/L2_Handling/display/outputs/image_renderer.js +145 -0
- package/L2_Handling/display/outputs/list_renderer.js +190 -0
- package/L2_Handling/display/outputs/outputs.js +19 -0
- package/L2_Handling/display/outputs/table_renderer.js +765 -0
- package/L2_Handling/display/outputs/text_renderer.js +818 -0
- package/L2_Handling/display/outputs/typography_renderer.js +293 -0
- package/L2_Handling/display/outputs/video_renderer.js +116 -0
- package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
- package/L2_Handling/display/primitives/form_primitives.js +526 -0
- package/L2_Handling/display/primitives/generic_containers.js +109 -0
- package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
- package/L2_Handling/display/primitives/link_primitives.js +552 -0
- package/L2_Handling/display/primitives/lists_primitives.js +262 -0
- package/L2_Handling/display/primitives/media_primitives.js +383 -0
- package/L2_Handling/display/primitives/primitives.js +19 -0
- package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
- package/L2_Handling/display/primitives/table_primitives.js +528 -0
- package/L2_Handling/display/primitives/typography_primitives.js +175 -0
- package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
- package/L2_Handling/display/specialized/specialized.js +10 -0
- package/L2_Handling/hooks/hooks.js +9 -0
- package/L2_Handling/hooks/menu_integration.js +57 -0
- package/L2_Handling/hooks/widget_hook_manager.js +292 -0
- package/L2_Handling/message/message.js +8 -0
- package/L2_Handling/message/message_handler.js +701 -0
- package/L2_Handling/navigation/navigation.js +8 -0
- package/L2_Handling/navigation/navigation_manager.js +403 -0
- package/L2_Handling/zhooks/features/cache_live.js +287 -0
- package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
- package/L2_Handling/zhooks/zhooks_manager.js +65 -0
- package/L2_Handling/zvaf/zvaf.js +8 -0
- package/L2_Handling/zvaf/zvaf_manager.js +334 -0
- package/L3_Abstraction/L3_Abstraction.js +12 -0
- package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
- package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
- package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
- package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
- package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
- package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
- package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
- package/L3_Abstraction/renderer/renderer.js +1 -0
- package/L3_Abstraction/session/session.js +1 -0
- package/L4_Orchestration/L4_Orchestration.js +11 -0
- package/L4_Orchestration/client/client.js +1 -0
- package/L4_Orchestration/facade/facade.js +9 -0
- package/L4_Orchestration/facade/manager_registry.js +118 -0
- package/L4_Orchestration/facade/renderer_registry.js +274 -0
- package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
- package/L4_Orchestration/lifecycle/initializer.js +135 -0
- package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
- package/L4_Orchestration/rendering/facade.js +94 -0
- package/L4_Orchestration/rendering/rendering.js +7 -0
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/bifrost_client.js +204 -0
- package/bifrost_core.js +1686 -0
- package/docs/ARCHITECTURE.md +111 -0
- package/docs/PROTOCOL.md +106 -0
- package/docs/RENDERERS.md +101 -0
- package/docs/SECURITY.md +92 -0
- package/package.json +24 -0
- package/syntax/prism-zconfig.js +41 -0
- package/syntax/prism-zenv.js +69 -0
- package/syntax/prism-zolo-theme.css +288 -0
- package/syntax/prism-zolo.js +380 -0
- package/syntax/prism-zschema.js +38 -0
- package/syntax/prism-zspark.js +25 -0
- package/syntax/prism-zui.js +68 -0
- package/zSys/accessibility/accessibility.js +10 -0
- package/zSys/accessibility/emoji_accessibility.js +173 -0
- package/zSys/dom/block_utils.js +122 -0
- package/zSys/dom/container_utils.js +370 -0
- package/zSys/dom/dom.js +13 -0
- package/zSys/dom/dom_utils.js +328 -0
- package/zSys/dom/encoding_utils.js +117 -0
- package/zSys/dom/style_utils.js +71 -0
- package/zSys/errors/error_display.js +299 -0
- package/zSys/errors/errors.js +10 -0
- package/zSys/theme/color_utils.js +274 -0
- package/zSys/theme/dark_mode_utils.js +272 -0
- package/zSys/theme/size_utils.js +256 -0
- package/zSys/theme/spacing_utils.js +405 -0
- package/zSys/theme/theme.js +14 -0
- package/zSys/theme/zbase.css +1735 -0
- package/zSys/theme/zbase_inject.js +161 -0
- package/zSys/theme/ztheme_utils.js +305 -0
- package/zSys/validation/error_boundary.js +201 -0
- package/zSys/validation/validation.js +11 -0
- package/zSys/validation/validation_utils.js +238 -0
- package/zSys/zSys.js +14 -0
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Table Renderer - Data Tables with Pagination
|
|
4
|
+
*
|
|
5
|
+
*
|
|
6
|
+
* Renders zTable events from zCLI backend (AdvancedData subsystem).
|
|
7
|
+
* Supports semantic HTML tables with zTheme styling, pagination metadata,
|
|
8
|
+
* and both array and object row formats.
|
|
9
|
+
*
|
|
10
|
+
* @module rendering/table_renderer
|
|
11
|
+
* @layer 3
|
|
12
|
+
* @pattern Strategy (single event type)
|
|
13
|
+
*
|
|
14
|
+
* Philosophy:
|
|
15
|
+
* - "Terminal first" - tables are fundamental data display primitives
|
|
16
|
+
* - Pure rendering (no client-side pagination/sorting - that's backend's job)
|
|
17
|
+
* - Semantic HTML (table/thead/tbody/tr/th/td tags)
|
|
18
|
+
* - Backend sends already-paginated data (we just render it)
|
|
19
|
+
* - Uses Layer 2 utilities exclusively (no inline logic)
|
|
20
|
+
*
|
|
21
|
+
* Dependencies:
|
|
22
|
+
* - Layer 0: bifrost_constants.js
|
|
23
|
+
* - Layer 2: dom_utils.js
|
|
24
|
+
*
|
|
25
|
+
* Exports:
|
|
26
|
+
* - TableRenderer: Class for rendering zTable events
|
|
27
|
+
*
|
|
28
|
+
* Example:
|
|
29
|
+
* ```javascript
|
|
30
|
+
* import { TableRenderer } from './table_renderer.js';
|
|
31
|
+
*
|
|
32
|
+
* const renderer = new TableRenderer(logger);
|
|
33
|
+
* renderer.render({
|
|
34
|
+
* title: 'Users',
|
|
35
|
+
* columns: ['id', 'name', 'email'],
|
|
36
|
+
* rows: [
|
|
37
|
+
* [1, 'Alice', 'alice@example.com'],
|
|
38
|
+
* [2, 'Bob', 'bob@example.com']
|
|
39
|
+
* ]
|
|
40
|
+
* }, 'zVaF');
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────
|
|
45
|
+
// Imports
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
// Layer 2: Utilities
|
|
49
|
+
import { TYPOGRAPHY } from '../../../L1_Foundation/constants/bifrost_constants.js';
|
|
50
|
+
import { createElement, setAttributes } from '../../../zSys/dom/dom_utils.js';
|
|
51
|
+
import { withErrorBoundary } from '../../../zSys/validation/error_boundary.js';
|
|
52
|
+
|
|
53
|
+
// Layer 0: Primitives
|
|
54
|
+
import {
|
|
55
|
+
createTable,
|
|
56
|
+
createThead,
|
|
57
|
+
createTbody,
|
|
58
|
+
createTr,
|
|
59
|
+
createTh,
|
|
60
|
+
createTd
|
|
61
|
+
} from '../primitives/table_primitives.js';
|
|
62
|
+
import { createDiv, createSpan } from '../primitives/generic_containers.js';
|
|
63
|
+
import { createButton } from '../primitives/interactive_primitives.js';
|
|
64
|
+
import { createInput } from '../primitives/form_primitives.js';
|
|
65
|
+
import { getBackgroundClass, getTextColorClass } from '../../../zSys/theme/color_utils.js';
|
|
66
|
+
import { getPaddingClass, getMarginClass, getGapClass } from '../../../zSys/theme/spacing_utils.js';
|
|
67
|
+
import { TextRenderer } from '../outputs/text_renderer.js';
|
|
68
|
+
|
|
69
|
+
//
|
|
70
|
+
// Table Renderer Class
|
|
71
|
+
//
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* TableRenderer - Renders data tables with pagination metadata
|
|
75
|
+
*
|
|
76
|
+
* Handles the 'zTable' zDisplay event from AdvancedData subsystem.
|
|
77
|
+
* Creates semantic HTML tables (table/thead/tbody) with zTheme styling.
|
|
78
|
+
*
|
|
79
|
+
* Backend sends already-paginated data, so this renderer just displays it.
|
|
80
|
+
* No client-side pagination/sorting logic (that's backend's responsibility).
|
|
81
|
+
*/
|
|
82
|
+
export class TableRenderer {
|
|
83
|
+
/**
|
|
84
|
+
* Create a TableRenderer instance
|
|
85
|
+
* @param {Object} logger - Logger instance for debugging
|
|
86
|
+
*/
|
|
87
|
+
constructor(logger) {
|
|
88
|
+
this.logger = logger || console;
|
|
89
|
+
this.logger.debug('[TableRenderer] Initialized');
|
|
90
|
+
|
|
91
|
+
// Initialize TextRenderer for markdown parsing in cells (DRY - reuse zMD logic)
|
|
92
|
+
this.textRenderer = new TextRenderer(this.logger);
|
|
93
|
+
|
|
94
|
+
// Wrap render method with error boundary
|
|
95
|
+
const originalRender = this.render.bind(this);
|
|
96
|
+
this.render = withErrorBoundary(originalRender, {
|
|
97
|
+
component: 'TableRenderer',
|
|
98
|
+
logger: this.logger
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Render a zTable event
|
|
104
|
+
*
|
|
105
|
+
* @param {Object} data - Table event data
|
|
106
|
+
* @param {string} data.title - Table title (optional)
|
|
107
|
+
* @param {Array<string>} data.columns - Column names
|
|
108
|
+
* @param {Array<Array|Object>} data.rows - Table rows (arrays or objects)
|
|
109
|
+
* @param {number} [data.limit] - Pagination limit (metadata only, rows already sliced)
|
|
110
|
+
* @param {number} [data.offset=0] - Pagination offset (metadata only)
|
|
111
|
+
* @param {boolean} [data.show_header=true] - Whether to show column headers
|
|
112
|
+
* @param {number} [data.indent=0] - Indentation level
|
|
113
|
+
* @param {string} [data.class] - Custom CSS class (optional)
|
|
114
|
+
* @param {string} zone - Target DOM element ID
|
|
115
|
+
* @returns {HTMLElement|null} Created table container or null if failed
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* // Array rows
|
|
119
|
+
* renderer.render({
|
|
120
|
+
* title: 'Users',
|
|
121
|
+
* columns: ['id', 'name'],
|
|
122
|
+
* rows: [[1, 'Alice'], [2, 'Bob']]
|
|
123
|
+
* }, 'zVaF');
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* // Object rows (typical from SQL queries)
|
|
127
|
+
* renderer.render({
|
|
128
|
+
* title: 'Users (showing 1-10 of 127)',
|
|
129
|
+
* columns: ['id', 'username', 'email'],
|
|
130
|
+
* rows: [
|
|
131
|
+
* {id: 1, username: 'alice', email: 'alice@example.com'},
|
|
132
|
+
* {id: 2, username: 'bob', email: 'bob@example.com'}
|
|
133
|
+
* ],
|
|
134
|
+
* limit: 10,
|
|
135
|
+
* offset: 0
|
|
136
|
+
* }, 'zVaF');
|
|
137
|
+
*/
|
|
138
|
+
render(data, zone) {
|
|
139
|
+
const {
|
|
140
|
+
title,
|
|
141
|
+
caption,
|
|
142
|
+
columns = [],
|
|
143
|
+
rows: allRows = [], // Backend sends ALL rows (we slice them)
|
|
144
|
+
limit,
|
|
145
|
+
offset = 0,
|
|
146
|
+
show_header = true,
|
|
147
|
+
zPages = false, // Enable navigation controls (First/Prev/Next/Last)
|
|
148
|
+
indent = 0,
|
|
149
|
+
class: classAttr,
|
|
150
|
+
_zClass, // Support both 'class' and '_zClass' from .zolo files
|
|
151
|
+
_zColumn, // Column-level classes: { colName: 'class1 class2' }
|
|
152
|
+
_zRows, // Row-pattern classes: { odd, even, first, last }
|
|
153
|
+
_tableInstanceId, // Unique DOM target ID for in-place navigation replacement
|
|
154
|
+
} = data;
|
|
155
|
+
|
|
156
|
+
// Use _zClass if provided, fallback to class attribute
|
|
157
|
+
const customClass = _zClass || classAttr;
|
|
158
|
+
|
|
159
|
+
// Get target container (optional for orchestrator pattern)
|
|
160
|
+
let container = null;
|
|
161
|
+
if (zone) {
|
|
162
|
+
container = document.getElementById(zone);
|
|
163
|
+
if (!container) {
|
|
164
|
+
this.logger.error(`[TableRenderer] [ERROR] Zone not found: ${zone}`);
|
|
165
|
+
// Continue anyway - return element for orchestrator to append
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Validate columns
|
|
170
|
+
if (columns.length === 0) {
|
|
171
|
+
this.logger.warn('[TableRenderer] [WARN] No columns provided');
|
|
172
|
+
// Still render empty table (semantic HTML)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
//
|
|
176
|
+
// CLIENT-SIDE PAGINATION: Slice rows based on limit/offset
|
|
177
|
+
//
|
|
178
|
+
let rows = allRows;
|
|
179
|
+
let hasMore = false;
|
|
180
|
+
let moreCount = 0;
|
|
181
|
+
|
|
182
|
+
if (limit !== null && limit !== undefined && limit > 0) {
|
|
183
|
+
// Slice rows: from offset to offset+limit
|
|
184
|
+
rows = allRows.slice(offset, offset + limit);
|
|
185
|
+
hasMore = (offset + limit) < allRows.length;
|
|
186
|
+
moreCount = allRows.length - (offset + limit);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Create outer container for title + table + footer
|
|
190
|
+
const wrapper = createElement('div', ['zTable-container']);
|
|
191
|
+
// _zClass/_zStyle are applied to the inner <table> below (zTable + modifiers),
|
|
192
|
+
// so the orchestrator's central metadata pass must skip this wrapper frame.
|
|
193
|
+
wrapper.__zMetaScoped = true;
|
|
194
|
+
|
|
195
|
+
// Mark interactive tables with a unique instance ID so navigation re-renders replace in-place.
|
|
196
|
+
// _tableInstanceId is preferred (survives navigation round-trips); generate one on first render.
|
|
197
|
+
const instanceId = _tableInstanceId || `${title || 'table'}_${Math.random().toString(36).substr(2, 9)}`;
|
|
198
|
+
if (zPages && limit && limit > 0) {
|
|
199
|
+
wrapper.setAttribute('data-table-id', instanceId);
|
|
200
|
+
wrapper.setAttribute('data-interactive', 'true');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Apply indent to wrapper (if specified)
|
|
204
|
+
const wrapperAttributes = {};
|
|
205
|
+
if (indent > 0) {
|
|
206
|
+
wrapperAttributes.style = `margin-left: ${indent}rem;`;
|
|
207
|
+
}
|
|
208
|
+
if (Object.keys(wrapperAttributes).length > 0) {
|
|
209
|
+
setAttributes(wrapper, wrapperAttributes);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Render title with pagination info (if provided)
|
|
213
|
+
if (title) {
|
|
214
|
+
const titleElement = this._renderTitle(title, rows.length, allRows.length, limit, offset);
|
|
215
|
+
wrapper.appendChild(titleElement);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Render caption as a description line UNDER the title (not a full-width row
|
|
219
|
+
// inside the table). Reads like a subtitle: what the table shows / source / date.
|
|
220
|
+
if (caption) {
|
|
221
|
+
const captionElement = createElement('p', ['zMuted', 'zMb-3']);
|
|
222
|
+
captionElement.style.fontSize = '0.875rem';
|
|
223
|
+
captionElement.textContent = this._decodeUnicodeEscapes(caption);
|
|
224
|
+
wrapper.appendChild(captionElement);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Create responsive table wrapper (zTheme class)
|
|
228
|
+
const tableWrapper = createElement('div', ['zTable-responsive']);
|
|
229
|
+
|
|
230
|
+
// zTable styling is BUILT IN — the event is always themed, no opt-in needed.
|
|
231
|
+
// Any _zClass / class from the .zolo is appended on top for extra modifiers
|
|
232
|
+
// (deduped so an explicit `_zClass: zTable` never doubles up).
|
|
233
|
+
const tableClasses = ['zTable'];
|
|
234
|
+
if (customClass) {
|
|
235
|
+
for (const c of String(customClass).split(/\s+/)) {
|
|
236
|
+
if (c && c !== 'zTable') tableClasses.push(c);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Create table element (using Layer 0 primitive)
|
|
241
|
+
const table = createTable({ class: tableClasses.length > 0 ? tableClasses.join(' ') : undefined });
|
|
242
|
+
|
|
243
|
+
// Render table head (if show_header is true)
|
|
244
|
+
if (show_header && columns.length > 0) {
|
|
245
|
+
const thead = this._renderTableHead(columns, _zColumn);
|
|
246
|
+
table.appendChild(thead);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Render table body
|
|
250
|
+
if (rows.length > 0) {
|
|
251
|
+
const tbody = this._renderTableBody(columns, rows, _zColumn, _zRows, offset, allRows.length);
|
|
252
|
+
table.appendChild(tbody);
|
|
253
|
+
} else {
|
|
254
|
+
// Empty table body (semantic HTML)
|
|
255
|
+
const tbody = createTbody();
|
|
256
|
+
table.appendChild(tbody);
|
|
257
|
+
this.logger.warn('[TableRenderer] [WARN] No rows to display');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Append table to wrapper
|
|
261
|
+
tableWrapper.appendChild(table);
|
|
262
|
+
wrapper.appendChild(tableWrapper);
|
|
263
|
+
|
|
264
|
+
//
|
|
265
|
+
// PAGINATION FOOTER: Interactive navigation OR simple "... N more rows"
|
|
266
|
+
//
|
|
267
|
+
if (zPages && limit && limit > 0) {
|
|
268
|
+
// Interactive mode: Render navigation buttons (First/Prev/Next/Last/Jump)
|
|
269
|
+
this._renderNavigationControls(wrapper, {
|
|
270
|
+
title,
|
|
271
|
+
_tableInstanceId: instanceId,
|
|
272
|
+
columns,
|
|
273
|
+
rows: allRows,
|
|
274
|
+
limit,
|
|
275
|
+
offset,
|
|
276
|
+
totalRows: allRows.length,
|
|
277
|
+
zPages: true,
|
|
278
|
+
_zClass,
|
|
279
|
+
_zColumn,
|
|
280
|
+
_zRows,
|
|
281
|
+
});
|
|
282
|
+
} else if (hasMore && moreCount > 0) {
|
|
283
|
+
// Simple truncation: Show "... N more rows" footer
|
|
284
|
+
const footer = this._renderMoreRowsFooter(moreCount);
|
|
285
|
+
wrapper.appendChild(footer);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Append wrapper to container (if zone was provided - legacy behavior)
|
|
289
|
+
// If no zone, just return element (orchestrator pattern)
|
|
290
|
+
if (container) {
|
|
291
|
+
container.appendChild(wrapper);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Log success
|
|
295
|
+
const paginationInfo = limit ? ` (showing ${rows.length} of ${allRows.length} total)` : '';
|
|
296
|
+
this.logger.log(`[TableRenderer] Rendered table (${columns.length} cols, ${rows.length} rows${paginationInfo}, indent: ${indent})`);
|
|
297
|
+
|
|
298
|
+
return wrapper;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Render table title with optional pagination info
|
|
303
|
+
* @private
|
|
304
|
+
* @param {string} title - Table title
|
|
305
|
+
* @param {number} displayedRowCount - Number of rows actually displayed (after pagination)
|
|
306
|
+
* @param {number} totalRowCount - Total number of rows (before pagination)
|
|
307
|
+
* @param {number} limit - Pagination limit
|
|
308
|
+
* @param {number} offset - Pagination offset
|
|
309
|
+
* @returns {HTMLElement} Title element (h4)
|
|
310
|
+
*/
|
|
311
|
+
_renderTitle(title, displayedRowCount, totalRowCount, limit, offset) {
|
|
312
|
+
const titleElement = createElement('h4');
|
|
313
|
+
|
|
314
|
+
// Show pagination range in title if limited
|
|
315
|
+
if (limit !== null && limit !== undefined && limit > 0 && totalRowCount > 0) {
|
|
316
|
+
const showingStart = offset + 1;
|
|
317
|
+
const showingEnd = Math.min(offset + displayedRowCount, totalRowCount);
|
|
318
|
+
const decodedTitle = this._decodeUnicodeEscapes(title);
|
|
319
|
+
titleElement.textContent = `${decodedTitle} (showing ${showingStart}-${showingEnd} of ${totalRowCount})`;
|
|
320
|
+
} else {
|
|
321
|
+
titleElement.textContent = this._decodeUnicodeEscapes(title);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Apply zTheme styling
|
|
325
|
+
setAttributes(titleElement, {
|
|
326
|
+
class: 'zMb-3 zText-dark',
|
|
327
|
+
style: `font-weight: ${TYPOGRAPHY.FONT_WEIGHTS.MEDIUM};`
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return titleElement;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Render table head (column headers)
|
|
335
|
+
* @private
|
|
336
|
+
* @param {Array<string>} columns - Column names
|
|
337
|
+
* @returns {HTMLElement} thead element
|
|
338
|
+
*/
|
|
339
|
+
_renderTableHead(columns, _zColumn) {
|
|
340
|
+
const thead = createThead();
|
|
341
|
+
const headerRow = createTr();
|
|
342
|
+
|
|
343
|
+
columns.forEach(column => {
|
|
344
|
+
const th = createTh();
|
|
345
|
+
// Decode Unicode escapes in column names
|
|
346
|
+
th.textContent = this._decodeUnicodeEscapes(column); // XSS safe
|
|
347
|
+
const colClass = _zColumn?.[column];
|
|
348
|
+
if (colClass) th.className = colClass;
|
|
349
|
+
headerRow.appendChild(th);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
thead.appendChild(headerRow);
|
|
353
|
+
return thead;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Render table body (data rows)
|
|
358
|
+
* @private
|
|
359
|
+
* @param {Array<string>} columns - Column names (for object row mapping)
|
|
360
|
+
* @param {Array<Array|Object>} rows - Table rows
|
|
361
|
+
* @returns {HTMLElement} tbody element
|
|
362
|
+
*/
|
|
363
|
+
_renderTableBody(columns, rows, _zColumn, _zRows, offset = 0, totalRows = null) {
|
|
364
|
+
const tbody = createTbody();
|
|
365
|
+
const cellTracker = [];
|
|
366
|
+
const datasetLastIndex = totalRows !== null ? totalRows - 1 : null;
|
|
367
|
+
|
|
368
|
+
rows.forEach((row, rowIndex) => {
|
|
369
|
+
const tr = createTr();
|
|
370
|
+
|
|
371
|
+
// Apply _zRows pattern classes to <tr>
|
|
372
|
+
// first/last are dataset-absolute (not page-relative), odd/even use absolute index for
|
|
373
|
+
// consistent alternation across page boundaries.
|
|
374
|
+
if (_zRows) {
|
|
375
|
+
const trClasses = [];
|
|
376
|
+
const absoluteIndex = offset + rowIndex;
|
|
377
|
+
const isFirst = absoluteIndex === 0;
|
|
378
|
+
const isLast = datasetLastIndex !== null ? absoluteIndex === datasetLastIndex : false;
|
|
379
|
+
if (_zRows.first && isFirst) trClasses.push(_zRows.first);
|
|
380
|
+
if (_zRows.last && isLast) trClasses.push(_zRows.last);
|
|
381
|
+
if (!isFirst && !isLast) {
|
|
382
|
+
if (_zRows.odd && absoluteIndex % 2 === 0) trClasses.push(_zRows.odd);
|
|
383
|
+
if (_zRows.even && absoluteIndex % 2 === 1) trClasses.push(_zRows.even);
|
|
384
|
+
}
|
|
385
|
+
if (trClasses.length > 0) tr.className = trClasses.join(' ');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Handle both array and object rows (zData sends objects from SQL queries)
|
|
389
|
+
if (Array.isArray(row)) {
|
|
390
|
+
// Array row: [val1, val2, val3]
|
|
391
|
+
row.forEach((value, colIndex) => {
|
|
392
|
+
const colClass = _zColumn?.[columns[colIndex]] || null;
|
|
393
|
+
const cellContent = this._formatCellValue(value);
|
|
394
|
+
|
|
395
|
+
if (cellContent === '^^' && rowIndex > 0 && cellTracker[rowIndex - 1]?.[colIndex]) {
|
|
396
|
+
const prevCell = cellTracker[rowIndex - 1][colIndex];
|
|
397
|
+
const currentRowspan = parseInt(prevCell.getAttribute('rowspan') || '1');
|
|
398
|
+
prevCell.setAttribute('rowspan', currentRowspan + 1);
|
|
399
|
+
cellTracker[rowIndex] = cellTracker[rowIndex] || [];
|
|
400
|
+
cellTracker[rowIndex][colIndex] = prevCell;
|
|
401
|
+
} else {
|
|
402
|
+
const td = createTd();
|
|
403
|
+
if (colClass) td.className = colClass;
|
|
404
|
+
td.innerHTML = this._parseCellMarkdown(cellContent);
|
|
405
|
+
tr.appendChild(td);
|
|
406
|
+
cellTracker[rowIndex] = cellTracker[rowIndex] || [];
|
|
407
|
+
cellTracker[rowIndex][colIndex] = td;
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
} else {
|
|
411
|
+
// Object row: {col1: val1, col2: val2, ...}
|
|
412
|
+
// Supports cell descriptor: {col: {val: value, _zClass: 'className'}}
|
|
413
|
+
columns.forEach((column, colIndex) => {
|
|
414
|
+
const raw = row[column];
|
|
415
|
+
|
|
416
|
+
// Cell descriptor: { val: ..., _zClass: '...' } — object rows only
|
|
417
|
+
const isCellDescriptor = raw !== null
|
|
418
|
+
&& typeof raw === 'object'
|
|
419
|
+
&& !Array.isArray(raw)
|
|
420
|
+
&& 'val' in raw;
|
|
421
|
+
|
|
422
|
+
const cellValue = isCellDescriptor ? raw.val : raw;
|
|
423
|
+
const cellClass = isCellDescriptor ? (raw._zClass || null) : null;
|
|
424
|
+
|
|
425
|
+
// Column class is the BASE (alignment, tint…); a cell descriptor
|
|
426
|
+
// _zClass LAYERS on top so both apply (e.g. zText-end + zText-success).
|
|
427
|
+
// On a genuine conflict the later zTheme rule wins — not the cell.
|
|
428
|
+
const colClass = _zColumn?.[column] || null;
|
|
429
|
+
const combinedClass = [colClass, cellClass].filter(Boolean).join(' ') || null;
|
|
430
|
+
|
|
431
|
+
const cellContent = this._formatCellValue(cellValue);
|
|
432
|
+
|
|
433
|
+
if (cellContent === '^^' && rowIndex > 0 && cellTracker[rowIndex - 1]?.[colIndex]) {
|
|
434
|
+
const prevCell = cellTracker[rowIndex - 1][colIndex];
|
|
435
|
+
const currentRowspan = parseInt(prevCell.getAttribute('rowspan') || '1');
|
|
436
|
+
prevCell.setAttribute('rowspan', currentRowspan + 1);
|
|
437
|
+
cellTracker[rowIndex] = cellTracker[rowIndex] || [];
|
|
438
|
+
cellTracker[rowIndex][colIndex] = prevCell;
|
|
439
|
+
} else {
|
|
440
|
+
const td = createTd();
|
|
441
|
+
if (combinedClass) td.className = combinedClass;
|
|
442
|
+
td.innerHTML = this._parseCellMarkdown(cellContent);
|
|
443
|
+
tr.appendChild(td);
|
|
444
|
+
cellTracker[rowIndex] = cellTracker[rowIndex] || [];
|
|
445
|
+
cellTracker[rowIndex][colIndex] = td;
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
tbody.appendChild(tr);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return tbody;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Render "... N more rows" footer (shown when table is truncated)
|
|
458
|
+
* @private
|
|
459
|
+
* @param {number} moreCount - Number of additional rows not displayed
|
|
460
|
+
* @returns {HTMLElement} Footer element (p)
|
|
461
|
+
*/
|
|
462
|
+
_renderMoreRowsFooter(moreCount) {
|
|
463
|
+
const footer = createElement('p', ['zText-info', 'zMt-2', 'zMs-3']);
|
|
464
|
+
footer.style.fontStyle = 'italic';
|
|
465
|
+
footer.style.fontSize = '0.875rem';
|
|
466
|
+
footer.textContent = `... ${moreCount} more rows`;
|
|
467
|
+
|
|
468
|
+
return footer;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Render interactive navigation controls for paginated tables
|
|
473
|
+
* Creates First/Previous/Next/Last buttons + Jump to page input
|
|
474
|
+
* Buttons send 'table_navigate' events back to server (Terminal first!)
|
|
475
|
+
*
|
|
476
|
+
* STYLIZED COMPOSITION: Using Layer 0 primitives + Layer 2 utilities
|
|
477
|
+
*
|
|
478
|
+
* @private
|
|
479
|
+
* @param {HTMLElement} container - Container to append controls to
|
|
480
|
+
* @param {Object} tableState - Table state (limit, offset, totalRows, etc.)
|
|
481
|
+
*/
|
|
482
|
+
_renderNavigationControls(container, tableState) {
|
|
483
|
+
const { limit, offset, totalRows } = tableState;
|
|
484
|
+
|
|
485
|
+
// Calculate pagination metadata
|
|
486
|
+
const totalPages = Math.ceil(totalRows / limit);
|
|
487
|
+
const currentPage = Math.floor(offset / limit) + 1;
|
|
488
|
+
const canGoPrev = currentPage > 1;
|
|
489
|
+
const canGoNext = currentPage < totalPages;
|
|
490
|
+
|
|
491
|
+
//
|
|
492
|
+
// MODERN 2-ROW PAGINATION NAVIGATION (Primitives + Utilities)
|
|
493
|
+
// Row 1: Page Info (centered, full width)
|
|
494
|
+
// Row 2: Navigation Buttons (flexed, centered)
|
|
495
|
+
//
|
|
496
|
+
|
|
497
|
+
// Full-width wrapper (primitive + utilities)
|
|
498
|
+
const navWrapper = createDiv();
|
|
499
|
+
navWrapper.classList.add(
|
|
500
|
+
getMarginClass('top', 3),
|
|
501
|
+
getPaddingClass('all', 3),
|
|
502
|
+
getBackgroundClass('white'),
|
|
503
|
+
'zBorder',
|
|
504
|
+
'zRounded',
|
|
505
|
+
'zShadow-sm'
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
// ROW 1: Page Info Container (centered with proper zTheme classes)
|
|
509
|
+
const pageInfoRow = createDiv();
|
|
510
|
+
pageInfoRow.classList.add(
|
|
511
|
+
'zD-flex',
|
|
512
|
+
'zFlex-center', // Correct zTheme centering class
|
|
513
|
+
'zFlex-items-center', // Vertical alignment
|
|
514
|
+
getMarginClass('bottom', 3)
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
// Page info text (primitive + utilities)
|
|
518
|
+
const pageInfo = createSpan();
|
|
519
|
+
pageInfo.classList.add(getTextColorClass('muted'));
|
|
520
|
+
pageInfo.style.fontSize = '0.875rem';
|
|
521
|
+
pageInfo.style.fontWeight = TYPOGRAPHY.FONT_WEIGHTS.MEDIUM;
|
|
522
|
+
pageInfo.innerHTML = `<span class="zText-dark">Page ${currentPage}</span> of <span class="zText-dark">${totalPages}</span> <span class="zText-muted">(${totalRows} total rows)</span>`;
|
|
523
|
+
|
|
524
|
+
pageInfoRow.appendChild(pageInfo);
|
|
525
|
+
navWrapper.appendChild(pageInfoRow);
|
|
526
|
+
|
|
527
|
+
// ROW 2: Navigation Controls Container (centered with proper zTheme classes)
|
|
528
|
+
const navControlsRow = createDiv();
|
|
529
|
+
navControlsRow.classList.add(
|
|
530
|
+
'zD-flex',
|
|
531
|
+
'zFlex-center', // Correct zTheme centering class
|
|
532
|
+
'zFlex-items-center', // Vertical alignment
|
|
533
|
+
'zFlex-wrap', // Wrap on small screens
|
|
534
|
+
getGapClass(3)
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
//
|
|
538
|
+
// NAVIGATION BUTTONS (primitives + utilities)
|
|
539
|
+
//
|
|
540
|
+
const buttonGroup = createDiv();
|
|
541
|
+
buttonGroup.classList.add('zBtn-group', 'zBtn-group-sm');
|
|
542
|
+
|
|
543
|
+
// Helper to create navigation button (using primitives!)
|
|
544
|
+
const createNavButton = (label, command, enabled) => {
|
|
545
|
+
const btn = createButton('button');
|
|
546
|
+
btn.classList.add('zBtn', 'zBtn-sm');
|
|
547
|
+
|
|
548
|
+
if (enabled) {
|
|
549
|
+
btn.classList.add('zBtn-outline-primary');
|
|
550
|
+
btn.onclick = () => {
|
|
551
|
+
this.logger.log(`[TableRenderer] Navigation: ${command}`);
|
|
552
|
+
this._handleTableNavigation(command, tableState);
|
|
553
|
+
};
|
|
554
|
+
} else {
|
|
555
|
+
btn.classList.add('zBtn-outline-secondary');
|
|
556
|
+
btn.disabled = true;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
btn.innerHTML = label; // Support icons
|
|
560
|
+
return btn;
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
// Navigation buttons (First/Previous/Next/Last) - Using Bootstrap Icons
|
|
564
|
+
buttonGroup.appendChild(createNavButton('<i class="bi bi-skip-start-fill"></i> First', 'f', canGoPrev));
|
|
565
|
+
buttonGroup.appendChild(createNavButton('<i class="bi bi-chevron-left"></i> Prev', 'p', canGoPrev));
|
|
566
|
+
buttonGroup.appendChild(createNavButton('Next <i class="bi bi-chevron-right"></i>', 'n', canGoNext));
|
|
567
|
+
buttonGroup.appendChild(createNavButton('Last <i class="bi bi-skip-end-fill"></i>', 'l', canGoNext));
|
|
568
|
+
|
|
569
|
+
navControlsRow.appendChild(buttonGroup);
|
|
570
|
+
|
|
571
|
+
//
|
|
572
|
+
// JUMP TO PAGE (primitives + utilities)
|
|
573
|
+
//
|
|
574
|
+
const jumpContainer = createDiv();
|
|
575
|
+
jumpContainer.classList.add(
|
|
576
|
+
'zD-flex',
|
|
577
|
+
'zAlign-items-center',
|
|
578
|
+
getGapClass(2)
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
const jumpLabel = createSpan();
|
|
582
|
+
jumpLabel.classList.add(getTextColorClass('muted'));
|
|
583
|
+
jumpLabel.textContent = 'Jump to:';
|
|
584
|
+
jumpContainer.appendChild(jumpLabel);
|
|
585
|
+
|
|
586
|
+
const jumpInput = createInput('number');
|
|
587
|
+
jumpInput.classList.add('zInput', 'zInput-sm');
|
|
588
|
+
jumpInput.setAttribute('min', '1');
|
|
589
|
+
jumpInput.setAttribute('max', totalPages.toString());
|
|
590
|
+
jumpInput.setAttribute('placeholder', '#');
|
|
591
|
+
jumpInput.style.width = '60px';
|
|
592
|
+
jumpInput.style.textAlign = 'center';
|
|
593
|
+
|
|
594
|
+
const jumpBtn = createButton('button');
|
|
595
|
+
jumpBtn.classList.add('zBtn', 'zBtn-sm', 'zBtn-primary');
|
|
596
|
+
jumpBtn.textContent = 'Go';
|
|
597
|
+
jumpBtn.onclick = () => {
|
|
598
|
+
const pageNum = parseInt(jumpInput.value);
|
|
599
|
+
if (pageNum >= 1 && pageNum <= totalPages) {
|
|
600
|
+
this.logger.log(`[TableRenderer] Jumping to page: ${pageNum}`);
|
|
601
|
+
this._handleTableNavigation(pageNum.toString(), tableState);
|
|
602
|
+
jumpInput.value = '';
|
|
603
|
+
} else {
|
|
604
|
+
this.logger.warn(`[TableRenderer] [WARN] Invalid page number: ${pageNum} (must be 1-${totalPages})`);
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// Enter key on jump input
|
|
609
|
+
jumpInput.onkeypress = (e) => {
|
|
610
|
+
if (e.key === 'Enter') {
|
|
611
|
+
jumpBtn.click();
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
jumpContainer.appendChild(jumpInput);
|
|
616
|
+
jumpContainer.appendChild(jumpBtn);
|
|
617
|
+
navControlsRow.appendChild(jumpContainer);
|
|
618
|
+
|
|
619
|
+
// Append row 2 to wrapper
|
|
620
|
+
navWrapper.appendChild(navControlsRow);
|
|
621
|
+
|
|
622
|
+
// Append complete navigation to container
|
|
623
|
+
container.appendChild(navWrapper);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Handle table navigation (send command to server)
|
|
628
|
+
* In "Terminal first" philosophy, navigation updates happen server-side
|
|
629
|
+
* @private
|
|
630
|
+
* @param {string} command - Navigation command (first/prev/next/last/jump:N)
|
|
631
|
+
* @param {Object} tableState - Table state
|
|
632
|
+
*/
|
|
633
|
+
_handleTableNavigation(command, tableState) {
|
|
634
|
+
this.logger.log(`[TableRenderer] Navigation: ${command}`);
|
|
635
|
+
if (this.client && this.client.connection) {
|
|
636
|
+
// Fire-and-forget via raw WebSocket — no _requestId, no timeout
|
|
637
|
+
this.client.connection.send(JSON.stringify({
|
|
638
|
+
event: 'table_navigate',
|
|
639
|
+
data: { command, ...tableState }
|
|
640
|
+
}));
|
|
641
|
+
} else {
|
|
642
|
+
this.logger.warn('[TableRenderer] No client reference — cannot send table_navigate');
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Format cell value for display
|
|
648
|
+
* Handles null, undefined, objects, arrays, dates, numbers, strings
|
|
649
|
+
* @private
|
|
650
|
+
* @param {*} value - Cell value
|
|
651
|
+
* @returns {string} Formatted value
|
|
652
|
+
*/
|
|
653
|
+
_formatCellValue(value) {
|
|
654
|
+
// Handle null/undefined
|
|
655
|
+
if (value === null || value === undefined) {
|
|
656
|
+
return '—'; // Em dash for empty values
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Handle dates (ISO strings or Date objects)
|
|
660
|
+
if (value instanceof Date) {
|
|
661
|
+
return value.toLocaleDateString();
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Handle date-like strings (ISO 8601 format)
|
|
665
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value)) {
|
|
666
|
+
try {
|
|
667
|
+
const date = new Date(value);
|
|
668
|
+
if (!isNaN(date.getTime())) {
|
|
669
|
+
return date.toLocaleDateString();
|
|
670
|
+
}
|
|
671
|
+
} catch (e) {
|
|
672
|
+
// Fall through to default string handling
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Handle numbers
|
|
677
|
+
if (typeof value === 'number') {
|
|
678
|
+
// Format large numbers with commas
|
|
679
|
+
if (Math.abs(value) >= 1000) {
|
|
680
|
+
return value.toLocaleString();
|
|
681
|
+
}
|
|
682
|
+
return value.toString();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Handle booleans
|
|
686
|
+
if (typeof value === 'boolean') {
|
|
687
|
+
return value ? '[ok]' : '';
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Handle objects/arrays (JSON stringify with truncation)
|
|
691
|
+
if (typeof value === 'object') {
|
|
692
|
+
const json = JSON.stringify(value);
|
|
693
|
+
if (json.length > 50) {
|
|
694
|
+
return `${json.substring(0, 47) }...`;
|
|
695
|
+
}
|
|
696
|
+
return json;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Handle strings (decode Unicode escapes)
|
|
700
|
+
const str = String(value);
|
|
701
|
+
|
|
702
|
+
// Decode Unicode escapes (\UXXXX or U+XXXX format)
|
|
703
|
+
const decoded = this._decodeUnicodeEscapes(str);
|
|
704
|
+
|
|
705
|
+
// No truncation - let CSS handle overflow with text wrapping or ellipsis
|
|
706
|
+
// This ensures markdown links and formatted text aren't broken mid-parse
|
|
707
|
+
return decoded;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Decode Unicode escape sequences to actual characters
|
|
712
|
+
* Supports: \uXXXX (standard) and \UXXXXXXXX (extended) formats
|
|
713
|
+
*
|
|
714
|
+
* Note: Basic escape sequences (\n, \t, etc.) are handled by JSON.parse()
|
|
715
|
+
* automatically when receiving data from backend. We only need to decode
|
|
716
|
+
* custom Unicode formats that JSON doesn't handle.
|
|
717
|
+
*
|
|
718
|
+
* @param {string} text - Text containing Unicode escapes
|
|
719
|
+
* @returns {string} - Decoded text
|
|
720
|
+
* @private
|
|
721
|
+
*/
|
|
722
|
+
_decodeUnicodeEscapes(text) {
|
|
723
|
+
if (!text || typeof text !== 'string') return text;
|
|
724
|
+
|
|
725
|
+
// Replace \uXXXX format (standard 4-digit Unicode escape)
|
|
726
|
+
text = text.replace(/\\u([0-9A-Fa-f]{4})/g, (match, hexCode) => {
|
|
727
|
+
return String.fromCodePoint(parseInt(hexCode, 16));
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Replace \UXXXXXXXX format (extended 4-8 digit for supplementary characters & emojis)
|
|
731
|
+
text = text.replace(/\\U([0-9A-Fa-f]{4,8})/g, (match, hexCode) => {
|
|
732
|
+
return String.fromCodePoint(parseInt(hexCode, 16));
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
return text;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Parse markdown and HTML in table cells
|
|
740
|
+
* Reuses TextRenderer._parseMarkdown() logic (DRY - same as zMD)
|
|
741
|
+
*
|
|
742
|
+
* Supports:
|
|
743
|
+
* - `code` -> <code>code</code>
|
|
744
|
+
* - **bold** -> <strong>bold</strong>
|
|
745
|
+
* - *italic* -> <em>italic</em>
|
|
746
|
+
* - HTML tags pass through (e.g., <h1>text</h1>)
|
|
747
|
+
*
|
|
748
|
+
* @param {string} text - Cell content with potential markdown or HTML
|
|
749
|
+
* @returns {string} - HTML string with markdown parsed and HTML preserved
|
|
750
|
+
* @private
|
|
751
|
+
*/
|
|
752
|
+
_parseCellMarkdown(text) {
|
|
753
|
+
if (!text || typeof text !== 'string') return text;
|
|
754
|
+
|
|
755
|
+
// Reuse TextRenderer's markdown parser (DRY principle)
|
|
756
|
+
// This handles: `code`, **bold**, *italic*, [links](url), etc.
|
|
757
|
+
return this.textRenderer._parseMarkdown(text);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
//
|
|
762
|
+
// Default Export
|
|
763
|
+
//
|
|
764
|
+
export default TableRenderer;
|
|
765
|
+
|