@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,552 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Link Primitives - Semantic Link Rendering with Target Support
|
|
4
|
+
*
|
|
5
|
+
*
|
|
6
|
+
* Renders semantic HTML links with proper href, target, and security
|
|
7
|
+
* attributes for zDisplay link events.
|
|
8
|
+
*
|
|
9
|
+
* @module rendering/link_primitives
|
|
10
|
+
* @layer 1.0 (Event-aware primitive renderer)
|
|
11
|
+
* @pattern Factory + Event Handler Pattern
|
|
12
|
+
*
|
|
13
|
+
* Philosophy:
|
|
14
|
+
* - Semantic HTML (use <a> for navigation, not <button>)
|
|
15
|
+
* - Security-first (auto-add rel="noopener noreferrer" for _blank)
|
|
16
|
+
* - Target support (_blank, _self, _parent, _top, custom window.open())
|
|
17
|
+
* - Mode-aware (handles internal vs external vs anchor links)
|
|
18
|
+
*
|
|
19
|
+
* Link Types:
|
|
20
|
+
* - Internal Delta: $zAbout → Client-side routing
|
|
21
|
+
* - Internal zPath: @.UI.zUI.zAbout → Client-side routing
|
|
22
|
+
* - External: https://example.com → Native browser navigation
|
|
23
|
+
* - Anchor: #section → Smooth scroll to element
|
|
24
|
+
* - Placeholder: # → No navigation (styled text)
|
|
25
|
+
*
|
|
26
|
+
* Target Behavior:
|
|
27
|
+
* - _self: Navigate in current tab (default)
|
|
28
|
+
* - _blank: Open in new tab/window (auto-add security)
|
|
29
|
+
* - _parent: Navigate parent frame
|
|
30
|
+
* - _top: Navigate top-level frame
|
|
31
|
+
* - Custom window: Use window.open() with features
|
|
32
|
+
*
|
|
33
|
+
* Security:
|
|
34
|
+
* - External _blank links: Auto-add rel="noopener noreferrer"
|
|
35
|
+
* - Prevents window.opener exploitation (Tabnabbing attack)
|
|
36
|
+
* - User can override via explicit rel parameter
|
|
37
|
+
*
|
|
38
|
+
* Dependencies:
|
|
39
|
+
* - utils/dom_utils.js (createElement, setAttributes)
|
|
40
|
+
* - bifrost_client.js (for client-side navigation)
|
|
41
|
+
*
|
|
42
|
+
* Exports:
|
|
43
|
+
* - renderLink(linkData, container, client, logger) → HTMLAnchorElement
|
|
44
|
+
*
|
|
45
|
+
* Example:
|
|
46
|
+
* ```javascript
|
|
47
|
+
* import { renderLink } from './link_primitives.js';
|
|
48
|
+
*
|
|
49
|
+
* // Internal link
|
|
50
|
+
* renderLink({
|
|
51
|
+
* label: 'About',
|
|
52
|
+
* href: '$zAbout',
|
|
53
|
+
* target: '_self'
|
|
54
|
+
* }, container, bifrostClient);
|
|
55
|
+
*
|
|
56
|
+
* // External link (new tab)
|
|
57
|
+
* renderLink({
|
|
58
|
+
* label: 'GitHub',
|
|
59
|
+
* href: 'https://github.com',
|
|
60
|
+
* target: '_blank',
|
|
61
|
+
* _zClass: 'zBtn zBtn-primary'
|
|
62
|
+
* }, container, bifrostClient);
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
// ─────────────────────────────────────────────────────────────────
|
|
67
|
+
// Imports
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
// Layer 2: Utilities
|
|
71
|
+
import { createElement } from '../../../zSys/dom/dom_utils.js';
|
|
72
|
+
|
|
73
|
+
// Link type constants (must match backend ZLinkResolver.classify_href)
|
|
74
|
+
const LINK_TYPE_INTERNAL_DELTA = 'delta';
|
|
75
|
+
const LINK_TYPE_INTERNAL_ZPATH = 'zpath';
|
|
76
|
+
const LINK_TYPE_EXTERNAL = 'external';
|
|
77
|
+
const LINK_TYPE_ANCHOR = 'anchor';
|
|
78
|
+
const LINK_TYPE_PLACEHOLDER = 'placeholder';
|
|
79
|
+
|
|
80
|
+
// Named exports for shared use by text_renderer.js and other consumers
|
|
81
|
+
export { LINK_TYPE_INTERNAL_DELTA, LINK_TYPE_INTERNAL_ZPATH, LINK_TYPE_EXTERNAL, LINK_TYPE_ANCHOR, LINK_TYPE_PLACEHOLDER };
|
|
82
|
+
|
|
83
|
+
// Target constants
|
|
84
|
+
const TARGET_BLANK = '_blank';
|
|
85
|
+
const TARGET_SELF = '_self';
|
|
86
|
+
|
|
87
|
+
//
|
|
88
|
+
// Helper: Fallback Link Type Detection (Frontend Safety)
|
|
89
|
+
//
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Detect link type from href when backend doesn't provide it.
|
|
93
|
+
*
|
|
94
|
+
* This is a fallback mechanism to ensure robust link rendering even
|
|
95
|
+
* when the backend omits the link_type field. It mirrors the backend's
|
|
96
|
+
* detection logic in display_event_links.py.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} href - Link destination
|
|
99
|
+
* @returns {string} Detected link type constant
|
|
100
|
+
* @private
|
|
101
|
+
*/
|
|
102
|
+
export function detectLinkType(href) { return _detectLinkTypeFromHref(href); }
|
|
103
|
+
|
|
104
|
+
function _detectLinkTypeFromHref(href) {
|
|
105
|
+
if (!href || href === '#') {
|
|
106
|
+
return LINK_TYPE_PLACEHOLDER;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// External URLs (http, https, www)
|
|
110
|
+
if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('www.')) {
|
|
111
|
+
return LINK_TYPE_EXTERNAL;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Anchor links (#section)
|
|
115
|
+
if (href.startsWith('#') && href !== '#') {
|
|
116
|
+
return LINK_TYPE_ANCHOR;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Internal delta links ($zBlock)
|
|
120
|
+
if (href.startsWith('$') || href.includes('$')) {
|
|
121
|
+
return LINK_TYPE_INTERNAL_DELTA;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Internal zPath links (@.UI.zUI.zBlock)
|
|
125
|
+
if (href.startsWith('@')) {
|
|
126
|
+
return LINK_TYPE_INTERNAL_ZPATH;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Default: treat web routes (/path) as internal delta
|
|
130
|
+
if (href.startsWith('/')) {
|
|
131
|
+
return LINK_TYPE_INTERNAL_DELTA;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Fallback: placeholder
|
|
135
|
+
return LINK_TYPE_PLACEHOLDER;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
//
|
|
139
|
+
// Main Link Renderer
|
|
140
|
+
//
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Render a semantic link with mode-aware behavior and target support.
|
|
144
|
+
*
|
|
145
|
+
* Handles internal navigation (client-side routing), external links
|
|
146
|
+
* (native browser), anchor links (smooth scroll), and placeholder links
|
|
147
|
+
* (styled text only).
|
|
148
|
+
*
|
|
149
|
+
* Security: Auto-adds rel="noopener noreferrer" for external _blank links.
|
|
150
|
+
*
|
|
151
|
+
* @param {Object} linkData - Link configuration from backend
|
|
152
|
+
* @param {string} linkData.label - Link text to display
|
|
153
|
+
* @param {string} linkData.href - Link destination
|
|
154
|
+
* @param {string} linkData.target - Target behavior (_self, _blank, etc.)
|
|
155
|
+
* @param {string} linkData.link_type - Detected link type (delta, external, etc.)
|
|
156
|
+
* @param {string} [linkData.rel] - Link relationship (security)
|
|
157
|
+
* @param {string} [linkData._zClass] - CSS classes for styling
|
|
158
|
+
* @param {string} [linkData.color] - Color theme
|
|
159
|
+
* @param {Object} [linkData.window] - Window.open() features
|
|
160
|
+
* @param {HTMLElement} container - DOM element to append link to
|
|
161
|
+
* @param {Object} client - BifrostClient instance for navigation
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* // Internal navigation
|
|
165
|
+
* renderLink({
|
|
166
|
+
* label: 'About',
|
|
167
|
+
* href: '$zAbout',
|
|
168
|
+
* target: '_self',
|
|
169
|
+
* link_type: 'delta'
|
|
170
|
+
* }, containerDiv, bifrostClient);
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* // External link with new tab
|
|
174
|
+
* renderLink({
|
|
175
|
+
* label: 'Documentation',
|
|
176
|
+
* href: 'https://docs.example.com',
|
|
177
|
+
* target: '_blank',
|
|
178
|
+
* link_type: 'external',
|
|
179
|
+
* _zClass: 'zBtn zBtn-primary'
|
|
180
|
+
* }, containerDiv, bifrostClient);
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* // Anchor link (smooth scroll)
|
|
184
|
+
* renderLink({
|
|
185
|
+
* label: 'Features',
|
|
186
|
+
* href: '#features',
|
|
187
|
+
* target: '_self',
|
|
188
|
+
* link_type: 'anchor'
|
|
189
|
+
* }, containerDiv, bifrostClient);
|
|
190
|
+
*/
|
|
191
|
+
export function renderLink(linkData, container, client, logger = console) {
|
|
192
|
+
const {
|
|
193
|
+
label,
|
|
194
|
+
href,
|
|
195
|
+
target = TARGET_SELF,
|
|
196
|
+
link_type,
|
|
197
|
+
rel = '',
|
|
198
|
+
_zClass = '',
|
|
199
|
+
color = '',
|
|
200
|
+
window: windowFeatures = {},
|
|
201
|
+
disabled = false,
|
|
202
|
+
} = linkData;
|
|
203
|
+
|
|
204
|
+
let detectedLinkType = link_type;
|
|
205
|
+
if (!detectedLinkType || detectedLinkType === 'undefined') {
|
|
206
|
+
logger.warn('[LinkPrimitives] link_type missing, detecting from href:', href);
|
|
207
|
+
detectedLinkType = _detectLinkTypeFromHref(href);
|
|
208
|
+
logger.debug('[LinkPrimitives] Detected link type:', detectedLinkType, 'for href:', href);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Create semantic <a> element
|
|
212
|
+
const link = createElement('a');
|
|
213
|
+
link.textContent = label;
|
|
214
|
+
|
|
215
|
+
// TERMINAL-FIRST PATTERN: color is the source of truth
|
|
216
|
+
// Smart class inference based on context:
|
|
217
|
+
// - If _zClass has 'zBtn' → add zBtn-{color}
|
|
218
|
+
// - If plain link (no zBtn) → add zText-{color}
|
|
219
|
+
// - Group styling handled by orchestrator
|
|
220
|
+
|
|
221
|
+
let hasButtonClass = false;
|
|
222
|
+
if (_zClass) {
|
|
223
|
+
const classes = _zClass.split(' ').filter(c => c.trim());
|
|
224
|
+
if (classes.length > 0) {
|
|
225
|
+
link.classList.add(...classes);
|
|
226
|
+
hasButtonClass = classes.some(c => c === 'zBtn' || c.startsWith('zBtn-'));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (color) {
|
|
231
|
+
const colorLower = color.toLowerCase();
|
|
232
|
+
if (hasButtonClass) {
|
|
233
|
+
const colorClass = `zBtn-${colorLower}`;
|
|
234
|
+
if (!link.classList.contains(colorClass)) {
|
|
235
|
+
link.classList.add(colorClass);
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
const colorClass = `zText-${colorLower}`;
|
|
239
|
+
link.classList.add(colorClass);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// RBAC disabled — render non-clickable element (Python gate fired)
|
|
244
|
+
if (disabled) {
|
|
245
|
+
link.setAttribute('aria-disabled', 'true');
|
|
246
|
+
link.style.pointerEvents = 'none';
|
|
247
|
+
link.style.opacity = '0.4';
|
|
248
|
+
link.style.cursor = 'not-allowed';
|
|
249
|
+
logger.debug('[LinkPrimitives] Link disabled (RBAC denied by server):', href);
|
|
250
|
+
if (container) container.appendChild(link);
|
|
251
|
+
return link;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Handle different link types using DETECTED type (fallback-safe)
|
|
255
|
+
logger.debug('[LinkPrimitives] Setting up link type:', detectedLinkType, 'href:', href, 'hasClient:', !!client);
|
|
256
|
+
switch (detectedLinkType) {
|
|
257
|
+
case LINK_TYPE_INTERNAL_DELTA:
|
|
258
|
+
case LINK_TYPE_INTERNAL_ZPATH:
|
|
259
|
+
case 'internal_delta': // Backend sends with underscore prefix
|
|
260
|
+
case 'internal_zpath': // Backend sends with underscore prefix
|
|
261
|
+
_setupInternalLink(link, href, target, windowFeatures, client, logger);
|
|
262
|
+
break;
|
|
263
|
+
|
|
264
|
+
case LINK_TYPE_EXTERNAL:
|
|
265
|
+
_setupExternalLink(link, href, target, rel, windowFeatures, logger);
|
|
266
|
+
break;
|
|
267
|
+
|
|
268
|
+
case LINK_TYPE_ANCHOR:
|
|
269
|
+
_setupAnchorLink(link, href, logger);
|
|
270
|
+
break;
|
|
271
|
+
|
|
272
|
+
case LINK_TYPE_PLACEHOLDER:
|
|
273
|
+
_setupPlaceholderLink(link);
|
|
274
|
+
break;
|
|
275
|
+
|
|
276
|
+
default:
|
|
277
|
+
logger.warn('[LinkPrimitives] Unknown link type after detection:', detectedLinkType);
|
|
278
|
+
_setupPlaceholderLink(link);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Append to container if provided (legacy), otherwise return element
|
|
282
|
+
if (container) {
|
|
283
|
+
container.appendChild(link);
|
|
284
|
+
}
|
|
285
|
+
return link; // Return link element for direct use
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
//
|
|
289
|
+
// zPath to URL Conversion
|
|
290
|
+
//
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Convert zPath to URL path for client-side routing.
|
|
294
|
+
*
|
|
295
|
+
* Example conversions:
|
|
296
|
+
* - @.UI.zProducts.zTheme.zUI.zGrid.zGrid_Details → /zProducts/zTheme/zGrid
|
|
297
|
+
* - @.UI.zAbout.zAbout_Details → /zAbout
|
|
298
|
+
* - $zBlock → $zBlock (delta links pass through)
|
|
299
|
+
* - /regular/path → /regular/path (web paths pass through)
|
|
300
|
+
*
|
|
301
|
+
* @private
|
|
302
|
+
* @param {string} href - zPath or regular path
|
|
303
|
+
* @returns {string} URL path for navigation
|
|
304
|
+
*/
|
|
305
|
+
export function convertZPathToURL(href) { return _convertZPathToURL(href); }
|
|
306
|
+
|
|
307
|
+
function _convertZPathToURL(href) {
|
|
308
|
+
// Pass through delta links ($) and web paths (/)
|
|
309
|
+
if (!href.startsWith('@')) {
|
|
310
|
+
return href;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Parse zPath: @.UI.zProducts.zTheme.zUI.zGrid.zGrid_Details
|
|
314
|
+
// 1. Remove @.UI. prefix
|
|
315
|
+
// 2. Split remaining path by dots
|
|
316
|
+
// 3. Remove zUI (file prefix marker)
|
|
317
|
+
// 4. Remove final block name (ends with _Details or _Section)
|
|
318
|
+
// 5. Convert to /path/format
|
|
319
|
+
|
|
320
|
+
let path = href.replace(/^@\.UI\./, ''); // Remove @.UI.
|
|
321
|
+
const parts = path.split('.');
|
|
322
|
+
|
|
323
|
+
// Filter out zUI markers and block names (typically last segment with _)
|
|
324
|
+
const pathParts = parts.filter((part, index) => {
|
|
325
|
+
// Keep non-zUI parts
|
|
326
|
+
if (part === 'zUI') return false;
|
|
327
|
+
// Remove last segment if it looks like a block name (has underscore or ends in Details/Section)
|
|
328
|
+
if (index === parts.length - 1 && (part.includes('_') || part.endsWith('Details') || part.endsWith('Section'))) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
return true;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Convert to /path format
|
|
335
|
+
return '/' + pathParts.join('/');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
//
|
|
339
|
+
// Internal Link Setup (Client-Side Routing)
|
|
340
|
+
//
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Setup internal link for client-side routing.
|
|
344
|
+
*
|
|
345
|
+
* Prevents default browser navigation and uses BifrostClient.navigate()
|
|
346
|
+
* for SPA-style routing. Supports opening in new tab via window.open().
|
|
347
|
+
*
|
|
348
|
+
* @private
|
|
349
|
+
* @param {HTMLAnchorElement} link - Link element to configure
|
|
350
|
+
* @param {string} href - Internal path (delta or zPath)
|
|
351
|
+
* @param {string} target - Target behavior
|
|
352
|
+
* @param {Object} windowFeatures - Custom window features
|
|
353
|
+
* @param {Object} client - BifrostClient instance
|
|
354
|
+
* @param {Object} logger - Logger instance
|
|
355
|
+
*/
|
|
356
|
+
function _setupInternalLink(link, href, target, windowFeatures, client, logger) {
|
|
357
|
+
// Convert zPath to URL path if needed
|
|
358
|
+
const navigationPath = _convertZPathToURL(href);
|
|
359
|
+
|
|
360
|
+
logger.debug('[LinkPrimitives] _setupInternalLink called:', { href, navigationPath, hasClient: !!client, hasNavigateMethod: !!(client && client._navigateToRoute) });
|
|
361
|
+
|
|
362
|
+
// CRITICAL: Set href to the actual path for proper browser behavior
|
|
363
|
+
// This allows middle-click, right-click "Open in new tab", and accessibility
|
|
364
|
+
link.href = navigationPath;
|
|
365
|
+
|
|
366
|
+
// Internal link setup (silent)
|
|
367
|
+
|
|
368
|
+
link.addEventListener('click', (e) => {
|
|
369
|
+
e.preventDefault();
|
|
370
|
+
e.stopPropagation();
|
|
371
|
+
logger.debug('[LinkPrimitives] Link clicked:', navigationPath);
|
|
372
|
+
|
|
373
|
+
if (target === TARGET_BLANK) {
|
|
374
|
+
// Open in new tab/window using window.open()
|
|
375
|
+
const newWindow = _openInNewWindow(navigationPath, windowFeatures, client, logger);
|
|
376
|
+
if (newWindow) {
|
|
377
|
+
logger.debug(`[LinkPrimitives] Opened ${navigationPath} in new tab`);
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
// Navigate in current tab via client-side routing
|
|
381
|
+
if (client && typeof client._navigateToRoute === 'function') {
|
|
382
|
+
logger.debug('[LinkPrimitives] Calling client._navigateToRoute:', navigationPath);
|
|
383
|
+
client._navigateToRoute(navigationPath);
|
|
384
|
+
} else {
|
|
385
|
+
logger.error('[LinkPrimitives] [ERROR] BifrostClient._navigateToRoute() not available:', {
|
|
386
|
+
hasClient: !!client,
|
|
387
|
+
clientType: client ? client.constructor.name : 'none',
|
|
388
|
+
clientKeys: client ? Object.keys(client).filter(k => k.includes('nav')) : []
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
//
|
|
396
|
+
// External Link Setup (Native Browser Navigation)
|
|
397
|
+
//
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Setup external link with proper security and target attributes.
|
|
401
|
+
*
|
|
402
|
+
* Auto-adds rel="noopener noreferrer" for _blank to prevent window.opener
|
|
403
|
+
* exploitation (Tabnabbing attack).
|
|
404
|
+
*
|
|
405
|
+
* @private
|
|
406
|
+
* @param {HTMLAnchorElement} link - Link element to configure
|
|
407
|
+
* @param {string} href - External URL
|
|
408
|
+
* @param {string} target - Target behavior
|
|
409
|
+
* @param {string} rel - Link relationship
|
|
410
|
+
* @param {Object} windowFeatures - Custom window features
|
|
411
|
+
* @param {Object} logger - Logger instance
|
|
412
|
+
*/
|
|
413
|
+
function _setupExternalLink(link, href, target, rel, windowFeatures, logger) {
|
|
414
|
+
link.href = href;
|
|
415
|
+
link.target = target;
|
|
416
|
+
|
|
417
|
+
// External link setup (silent)
|
|
418
|
+
|
|
419
|
+
// Security: Auto-add rel="noopener noreferrer" for _blank
|
|
420
|
+
if (target === TARGET_BLANK && !rel) {
|
|
421
|
+
link.rel = 'noopener noreferrer';
|
|
422
|
+
} else if (rel) {
|
|
423
|
+
link.rel = rel;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Custom window features (if specified)
|
|
427
|
+
if (target === TARGET_BLANK && Object.keys(windowFeatures).length > 0) {
|
|
428
|
+
link.addEventListener('click', (e) => {
|
|
429
|
+
e.preventDefault();
|
|
430
|
+
logger.debug('[LinkPrimitives] External link clicked (custom window):', href);
|
|
431
|
+
_openInNewWindow(href, windowFeatures, null, logger);
|
|
432
|
+
});
|
|
433
|
+
} else {
|
|
434
|
+
// Add click log for standard external links too
|
|
435
|
+
link.addEventListener('click', (_e) => {
|
|
436
|
+
logger.debug('[LinkPrimitives] External link clicked (native):', href);
|
|
437
|
+
// No preventDefault - let browser handle normally
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
//
|
|
443
|
+
// Anchor Link Setup (Smooth Scroll)
|
|
444
|
+
//
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Setup anchor link for smooth scrolling to target element.
|
|
448
|
+
*
|
|
449
|
+
* Uses scrollIntoView with smooth behavior for better UX.
|
|
450
|
+
* Warns if target element not found.
|
|
451
|
+
*
|
|
452
|
+
* @private
|
|
453
|
+
* @param {HTMLAnchorElement} link - Link element to configure
|
|
454
|
+
* @param {string} href - Anchor hash (e.g., "#features")
|
|
455
|
+
* @param {Object} logger - Logger instance
|
|
456
|
+
*/
|
|
457
|
+
function _setupAnchorLink(link, href, logger) {
|
|
458
|
+
link.href = href;
|
|
459
|
+
link.addEventListener('click', (e) => {
|
|
460
|
+
e.preventDefault();
|
|
461
|
+
const targetElement = document.querySelector(href);
|
|
462
|
+
if (targetElement) {
|
|
463
|
+
targetElement.scrollIntoView({ behavior: 'smooth' });
|
|
464
|
+
} else {
|
|
465
|
+
logger.warn(`[LinkPrimitives] Anchor target not found: ${href}`);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
//
|
|
471
|
+
// Placeholder Link Setup (No Navigation)
|
|
472
|
+
//
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Setup placeholder link (no navigation action).
|
|
476
|
+
*
|
|
477
|
+
* Used for design/mock purposes or "coming soon" links.
|
|
478
|
+
* Prevents default click behavior.
|
|
479
|
+
*
|
|
480
|
+
* @private
|
|
481
|
+
* @param {HTMLAnchorElement} link - Link element to configure
|
|
482
|
+
*/
|
|
483
|
+
function _setupPlaceholderLink(link) {
|
|
484
|
+
link.href = '#';
|
|
485
|
+
|
|
486
|
+
// Placeholder link setup (silent)
|
|
487
|
+
|
|
488
|
+
link.addEventListener('click', (e) => {
|
|
489
|
+
e.preventDefault();
|
|
490
|
+
// No action - just styled text
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
//
|
|
495
|
+
// Window.open() Helper
|
|
496
|
+
//
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Open URL in new window with custom features.
|
|
500
|
+
*
|
|
501
|
+
* Centers the window on screen and applies custom width, height, and
|
|
502
|
+
* window features (menubar, toolbar, etc.).
|
|
503
|
+
*
|
|
504
|
+
* @private
|
|
505
|
+
* @param {string} url - URL to open
|
|
506
|
+
* @param {Object} features - Window features
|
|
507
|
+
* @param {number} [features.width=800] - Window width
|
|
508
|
+
* @param {number} [features.height=600] - Window height
|
|
509
|
+
* @param {string} [features.features] - Custom window.open() features string
|
|
510
|
+
* @param {Object} client - BifrostClient (for internal URLs)
|
|
511
|
+
* @param {Object} logger - Logger instance
|
|
512
|
+
* @returns {Window|null} New window reference or null if blocked
|
|
513
|
+
*/
|
|
514
|
+
function _openInNewWindow(url, features = {}, client = null, logger = console) {
|
|
515
|
+
const { width = 800, height = 600, features: customFeatures = '' } = features;
|
|
516
|
+
|
|
517
|
+
// Calculate center position
|
|
518
|
+
const left = (screen.width - width) / 2;
|
|
519
|
+
const top = (screen.height - height) / 2;
|
|
520
|
+
|
|
521
|
+
// Build features string
|
|
522
|
+
const featuresStr = customFeatures ||
|
|
523
|
+
`width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
|
|
524
|
+
|
|
525
|
+
// For internal URLs with client, construct full URL
|
|
526
|
+
let fullUrl = url;
|
|
527
|
+
if (client && (url.startsWith('$') || url.startsWith('@'))) {
|
|
528
|
+
// Convert zPath to URL path first
|
|
529
|
+
const navigationPath = _convertZPathToURL(url);
|
|
530
|
+
// Then construct full URL (same origin)
|
|
531
|
+
fullUrl = `${window.location.origin}${navigationPath}`;
|
|
532
|
+
logger.debug('[LinkPrimitives] Opening internal link in new window:', {
|
|
533
|
+
original: url,
|
|
534
|
+
converted: navigationPath,
|
|
535
|
+
fullUrl
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Open new window
|
|
540
|
+
const newWindow = window.open(fullUrl, '_blank', featuresStr);
|
|
541
|
+
|
|
542
|
+
if (newWindow) {
|
|
543
|
+
newWindow.focus();
|
|
544
|
+
return newWindow;
|
|
545
|
+
} else {
|
|
546
|
+
logger.error('[LinkPrimitives] Popup blocked or failed to open');
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export default { renderLink };
|
|
552
|
+
|