@customviews-js/customviews 1.1.6 → 1.1.7

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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @customviews-js/customviews v1.1.6
2
+ * @customviews-js/customviews v1.1.7
3
3
  * (c) 2025 Chan Ger Teck
4
4
  * Released under the MIT License.
5
5
  */
@@ -269,102 +269,6 @@ class VisibilityManager {
269
269
  }
270
270
  }
271
271
 
272
- /** --- Icon utilities --- */
273
- function ensureFontAwesomeInjected() {
274
- const isFontAwesomeLoaded = Array.from(document.styleSheets).some(sheet => sheet.href && (sheet.href.includes('font-awesome') || sheet.href.includes('fontawesome')));
275
- if (isFontAwesomeLoaded)
276
- return;
277
- const link = document.createElement('link');
278
- link.rel = 'stylesheet';
279
- link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css';
280
- link.setAttribute('data-customviews-fontawesome', 'true');
281
- document.head.appendChild(link);
282
- }
283
- function replaceIconShortcodes(text) {
284
- // Matches :fa-*, :fas-*, :fab-* etc.
285
- return text.replace(/:(fa[b|s|r]?)-([\w-]+):/g, (_, style, icon) => {
286
- // style = fa, fas, fab, far, etc.
287
- // Default to "fa" if only "fa-" is given
288
- return `<i class="${style} fa-${icon}"></i>`;
289
- });
290
- }
291
- /** --- Basic renderers --- */
292
- function renderImage(el, asset) {
293
- if (!asset.src)
294
- return;
295
- el.innerHTML = '';
296
- const img = document.createElement('img');
297
- img.src = asset.src;
298
- img.alt = asset.alt || '';
299
- // Apply custom styling if provided
300
- if (asset.className) {
301
- img.className = asset.className;
302
- }
303
- if (asset.style) {
304
- img.setAttribute('style', asset.style);
305
- }
306
- // Default styles (can be overridden by asset.style)
307
- img.style.maxWidth = img.style.maxWidth || '100%';
308
- img.style.height = img.style.height || 'auto';
309
- img.style.display = img.style.display || 'block';
310
- el.appendChild(img);
311
- }
312
- function renderText(el, asset) {
313
- if (asset.content != null) {
314
- el.textContent = asset.content;
315
- }
316
- // Apply custom styling if provided
317
- if (asset.className) {
318
- el.className = asset.className;
319
- }
320
- if (asset.style) {
321
- el.setAttribute('style', asset.style);
322
- }
323
- }
324
- function renderHtml(el, asset) {
325
- if (asset.content != null) {
326
- el.innerHTML = asset.content;
327
- }
328
- // Apply custom styling if provided
329
- if (asset.className) {
330
- el.className = asset.className;
331
- }
332
- if (asset.style) {
333
- el.setAttribute('style', asset.style);
334
- }
335
- }
336
- /** --- Unified asset renderer --- */
337
- function detectAssetType(asset) {
338
- // If src exists, it's an image
339
- if (asset.src)
340
- return 'image';
341
- // If content contains HTML tags, it's HTML
342
- if (asset.content && /<[^>]+>/.test(asset.content)) {
343
- return 'html';
344
- }
345
- return 'text';
346
- }
347
- function renderAssetInto(el, assetId, assetsManager) {
348
- const asset = assetsManager.get(assetId);
349
- if (!asset)
350
- return;
351
- const type = asset.type || detectAssetType(asset);
352
- switch (type) {
353
- case 'image':
354
- renderImage(el, asset);
355
- break;
356
- case 'text':
357
- renderText(el, asset);
358
- break;
359
- case 'html':
360
- renderHtml(el, asset);
361
- break;
362
- default:
363
- el.innerHTML = asset.content || String(asset);
364
- console.warn('[CustomViews] Unknown asset type:', type);
365
- }
366
- }
367
-
368
272
  // Constants for selectors
369
273
  const TABGROUP_SELECTOR = 'cv-tabgroup';
370
274
  const TAB_SELECTOR = 'cv-tab';
@@ -451,37 +355,35 @@ class TabManager {
451
355
  tabEl.classList.remove('cv-visible');
452
356
  }
453
357
  }
358
+ /**
359
+ * Extract header and body content from header component syntax: <cv-tab-header> and <cv-tab-body>
360
+ * Returns null if using old attribute-based syntax.
361
+ *
362
+ * @param tabEl The <cv-tab> element to inspect
363
+ * @returns Object with extracted content, or null if new syntax not used
364
+ */
365
+ static extractTabContent(tabEl) {
366
+ // Look for direct children
367
+ let headerEl = tabEl.querySelector(':scope > cv-tab-header');
368
+ if (!headerEl) {
369
+ return null;
370
+ }
371
+ const headerHTML = headerEl.innerHTML.trim();
372
+ // Find body element
373
+ let bodyEl = tabEl.querySelector(':scope > cv-tab-body');
374
+ // Fallback: try finding both header and body
375
+ // without :scope (in case of DOM manipulation) by iterating through tabEl.children if needed
376
+ return {
377
+ headerHTML,
378
+ bodyEl
379
+ };
380
+ }
454
381
  /**
455
382
  * Build navigation for tab groups with nav="auto" (one-time setup)
456
383
  */
457
384
  static buildNavs(rootEl, cfgGroups, onTabClick, onTabDoubleClick) {
458
385
  // Find all cv-tabgroup elements with nav="auto" or no nav attribute
459
386
  const tabGroups = rootEl.querySelectorAll(NAV_AUTO_SELECTOR);
460
- // Check if any tab headers contain Font Awesome shortcodes
461
- // Inject Font Awesome CSS only if needed
462
- let hasFontAwesomeShortcodes = false;
463
- tabGroups.forEach((groupEl) => {
464
- const groupId = groupEl.getAttribute('id');
465
- if (!groupId)
466
- return;
467
- const tabElements = Array.from(groupEl.children).filter((child) => child.tagName.toLowerCase() === 'cv-tab');
468
- // Check for Font Awesome shortcodes in tab headers
469
- tabElements.forEach((tabEl) => {
470
- const rawTabId = tabEl.getAttribute('id');
471
- if (!rawTabId)
472
- return;
473
- const splitIds = this.splitTabIds(rawTabId);
474
- const tabId = splitIds[0] || rawTabId;
475
- const header = tabEl.getAttribute('header') || this.getTabLabel(tabId, groupId, cfgGroups) || tabId || '';
476
- if (/:fa-[\w-]+:/.test(header)) {
477
- hasFontAwesomeShortcodes = true;
478
- }
479
- });
480
- });
481
- // Inject Font Awesome only if shortcodes are found
482
- if (hasFontAwesomeShortcodes) {
483
- ensureFontAwesomeInjected();
484
- }
485
387
  tabGroups.forEach((groupEl) => {
486
388
  const groupId = groupEl.getAttribute('id');
487
389
  if (!groupId)
@@ -516,23 +418,30 @@ class TabManager {
516
418
  const splitIds = this.splitTabIds(rawTabId);
517
419
  // If multiple IDs, use the first as primary
518
420
  const tabId = splitIds[0] || rawTabId;
519
- // Get header for this tab (single header, not multiple)
520
- const headerAttr = tabEl.getAttribute('header') || '';
421
+ // Get header for this tab - prefer new syntax over old attribute syntax
422
+ const extractedHeaderAndBody = this.extractTabContent(tabEl);
521
423
  let header = '';
522
- if (headerAttr) {
523
- // Single header provided on the element
524
- header = headerAttr;
424
+ // use <cv-tab-header> content if available
425
+ if (extractedHeaderAndBody && extractedHeaderAndBody.headerHTML) {
426
+ header = extractedHeaderAndBody.headerHTML;
525
427
  }
526
428
  else {
527
- // Use config label or id as fallback
528
- header = this.getTabLabel(tabId, groupId, cfgGroups) || tabId || '';
429
+ // use header attribute if available
430
+ const headerAttr = tabEl.getAttribute('header') || '';
431
+ if (headerAttr) {
432
+ header = headerAttr;
433
+ }
434
+ else {
435
+ // Use config label or id as fallback
436
+ header = this.getTabLabel(tabId, groupId, cfgGroups) || tabId || '';
437
+ }
529
438
  }
530
439
  // Create a single nav link for this tab element
531
440
  const listItem = document.createElement('li');
532
441
  listItem.className = 'nav-item';
533
442
  const navLink = document.createElement('a');
534
443
  navLink.className = 'nav-link';
535
- navLink.innerHTML = replaceIconShortcodes(header);
444
+ navLink.innerHTML = header;
536
445
  navLink.href = '#';
537
446
  navLink.setAttribute('data-tab-id', tabId);
538
447
  navLink.setAttribute('data-raw-tab-id', rawTabId);
@@ -769,6 +678,83 @@ class AssetsManager {
769
678
  }
770
679
  }
771
680
 
681
+ /** --- Basic renderers --- */
682
+ function renderImage(el, asset) {
683
+ if (!asset.src)
684
+ return;
685
+ el.innerHTML = '';
686
+ const img = document.createElement('img');
687
+ img.src = asset.src;
688
+ img.alt = asset.alt || '';
689
+ // Apply custom styling if provided
690
+ if (asset.className) {
691
+ img.className = asset.className;
692
+ }
693
+ if (asset.style) {
694
+ img.setAttribute('style', asset.style);
695
+ }
696
+ // Default styles (can be overridden by asset.style)
697
+ img.style.maxWidth = img.style.maxWidth || '100%';
698
+ img.style.height = img.style.height || 'auto';
699
+ img.style.display = img.style.display || 'block';
700
+ el.appendChild(img);
701
+ }
702
+ function renderText(el, asset) {
703
+ if (asset.content != null) {
704
+ el.textContent = asset.content;
705
+ }
706
+ // Apply custom styling if provided
707
+ if (asset.className) {
708
+ el.className = asset.className;
709
+ }
710
+ if (asset.style) {
711
+ el.setAttribute('style', asset.style);
712
+ }
713
+ }
714
+ function renderHtml(el, asset) {
715
+ if (asset.content != null) {
716
+ el.innerHTML = asset.content;
717
+ }
718
+ // Apply custom styling if provided
719
+ if (asset.className) {
720
+ el.className = asset.className;
721
+ }
722
+ if (asset.style) {
723
+ el.setAttribute('style', asset.style);
724
+ }
725
+ }
726
+ /** --- Unified asset renderer --- */
727
+ function detectAssetType(asset) {
728
+ // If src exists, it's an image
729
+ if (asset.src)
730
+ return 'image';
731
+ // If content contains HTML tags, it's HTML
732
+ if (asset.content && /<[^>]+>/.test(asset.content)) {
733
+ return 'html';
734
+ }
735
+ return 'text';
736
+ }
737
+ function renderAssetInto(el, assetId, assetsManager) {
738
+ const asset = assetsManager.get(assetId);
739
+ if (!asset)
740
+ return;
741
+ const type = asset.type || detectAssetType(asset);
742
+ switch (type) {
743
+ case 'image':
744
+ renderImage(el, asset);
745
+ break;
746
+ case 'text':
747
+ renderText(el, asset);
748
+ break;
749
+ case 'html':
750
+ renderHtml(el, asset);
751
+ break;
752
+ default:
753
+ el.innerHTML = asset.content || String(asset);
754
+ console.warn('[CustomViews] Unknown asset type:', type);
755
+ }
756
+ }
757
+
772
758
  // Constants for selectors
773
759
  const TOGGLE_DATA_SELECTOR = "[data-cv-toggle], [data-customviews-toggle]";
774
760
  const TOGGLE_ELEMENT_SELECTOR = "cv-toggle";
@@ -880,16 +866,21 @@ const TAB_STYLES = `
880
866
  margin-bottom: 1rem;
881
867
  list-style: none;
882
868
  border-bottom: 1px solid #dee2e6;
869
+
870
+ align-items: stretch;
883
871
  }
884
872
 
885
873
  .cv-tabs-nav .nav-item {
886
874
  margin-bottom: -1px;
887
875
  list-style: none;
888
- display: inline-block;
876
+ display: flex; /* was inline-block → make flex to stretch height */
877
+ align-items: stretch; /* stretch link to full height */
889
878
  }
890
879
 
891
880
  .cv-tabs-nav .nav-link {
892
- display: block;
881
+ display: flex;
882
+ align-items: center;
883
+ justify-content: center;
893
884
  padding: 0.5rem 1rem;
894
885
  color: #495057;
895
886
  text-decoration: none;
@@ -899,6 +890,13 @@ const TAB_STYLES = `
899
890
  border-top-right-radius: 0.25rem;
900
891
  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
901
892
  cursor: pointer;
893
+ min-height: 2.5rem;
894
+ box-sizing: border-box;
895
+ }
896
+
897
+ .cv-tabs-nav .nav-link p {
898
+ margin: 0; /* remove default margins */
899
+ display: inline; /* or inline-block */
902
900
  }
903
901
 
904
902
  .cv-tabs-nav .nav-link:hover,
@@ -951,6 +949,16 @@ cv-tab {
951
949
  display: block;
952
950
  }
953
951
 
952
+ /* Hide cv-tab-header source element; content is extracted to nav link */
953
+ cv-tab-header {
954
+ display: none !important;
955
+ }
956
+
957
+ /* Allow cv-tab-body to flow naturally */
958
+ cv-tab-body {
959
+ display: block;
960
+ }
961
+
954
962
  /* Override visibility for tab panels - use display instead of collapse animation */
955
963
  cv-tab.cv-hidden {
956
964
  display: none !important;
@@ -1313,6 +1321,24 @@ class CVToggle extends HTMLElement {
1313
1321
  // Element is managed by Core
1314
1322
  }
1315
1323
  }
1324
+ /**
1325
+ * <cv-tab-header> element - represents tab header with rich HTML formatting
1326
+ * Content is extracted and used in the navigation link
1327
+ */
1328
+ class CVTabHeader extends HTMLElement {
1329
+ connectedCallback() {
1330
+ // Element is a semantic container; TabManager extracts its content
1331
+ }
1332
+ }
1333
+ /**
1334
+ * <cv-tab-body> element - represents tab body content
1335
+ * Semantic container for tab panel content
1336
+ */
1337
+ class CVTabBody extends HTMLElement {
1338
+ connectedCallback() {
1339
+ // Element is a semantic container; visibility managed by TabManager
1340
+ }
1341
+ }
1316
1342
  /**
1317
1343
  * Register custom elements
1318
1344
  */
@@ -1327,6 +1353,12 @@ function registerCustomElements() {
1327
1353
  if (!customElements.get('cv-toggle')) {
1328
1354
  customElements.define('cv-toggle', CVToggle);
1329
1355
  }
1356
+ if (!customElements.get('cv-tab-header')) {
1357
+ customElements.define('cv-tab-header', CVTabHeader);
1358
+ }
1359
+ if (!customElements.get('cv-tab-body')) {
1360
+ customElements.define('cv-tab-body', CVTabBody);
1361
+ }
1330
1362
  }
1331
1363
 
1332
1364
  /**
@@ -2547,28 +2579,6 @@ class CustomViewsWidget {
2547
2579
  // Get tab groups
2548
2580
  const tabGroups = this.core.getTabGroups();
2549
2581
  let tabGroupControlsHTML = '';
2550
- // Check if any tab group or tab labels contain Font Awesome shortcodes
2551
- let hasFontAwesomeShortcodes = false;
2552
- if (this.options.showTabGroups && tabGroups && tabGroups.length > 0) {
2553
- for (const group of tabGroups) {
2554
- if (group.label && /:fa-[\w-]+:/.test(group.label)) {
2555
- hasFontAwesomeShortcodes = true;
2556
- break;
2557
- }
2558
- for (const tab of group.tabs) {
2559
- if (tab.label && /:fa-[\w-]+:/.test(tab.label)) {
2560
- hasFontAwesomeShortcodes = true;
2561
- break;
2562
- }
2563
- }
2564
- if (hasFontAwesomeShortcodes)
2565
- break;
2566
- }
2567
- }
2568
- // Inject Font Awesome only if shortcodes are found
2569
- if (hasFontAwesomeShortcodes) {
2570
- ensureFontAwesomeInjected();
2571
- }
2572
2582
  if (this.options.showTabGroups && tabGroups && tabGroups.length > 0) {
2573
2583
  tabGroupControlsHTML = `
2574
2584
  <div class="cv-tabgroup-card cv-tabgroup-header">
@@ -2588,10 +2598,10 @@ class CustomViewsWidget {
2588
2598
  ${tabGroups.map(group => `
2589
2599
  <div class="cv-tabgroup-card cv-tabgroup-item">
2590
2600
  <label class="cv-tabgroup-label" for="tab-group-${group.id}">
2591
- ${replaceIconShortcodes(group.label || group.id)}
2601
+ ${group.label || group.id}
2592
2602
  </label>
2593
2603
  <select id="tab-group-${group.id}" class="cv-tabgroup-select" data-group-id="${group.id}">
2594
- ${group.tabs.map(tab => `<option value="${tab.id}">${replaceIconShortcodes(tab.label || tab.id)}</option>`).join('')}
2604
+ ${group.tabs.map(tab => `<option value="${tab.id}">${tab.label || tab.id}</option>`).join('')}
2595
2605
  </select>
2596
2606
  </div>
2597
2607
  `).join('')}
@@ -2969,22 +2979,13 @@ function initializeFromScript() {
2969
2979
  let scriptTag = document.currentScript;
2970
2980
  // Fallback if currentScript is not available (executed after page load)
2971
2981
  if (!scriptTag) {
2972
- // Try to find the script tag by looking for our script
2973
- const scripts = document.querySelectorAll('script[src*="@customviews-js"]');
2974
- if (scripts.length > 0) {
2975
- // Find the most specific match (to avoid matching other custom-views scripts)
2976
- for (let i = 0; i < scripts.length; i++) {
2977
- const script = scripts[i];
2978
- const src = script.getAttribute('src') || '';
2979
- // Look for .min.js or .js at the end, or the package root
2980
- if (src.match(/@customviews-js\/customviews(\.min)?\.js($|\?)/) || src.includes('@customviews-js/customviews')) {
2981
- scriptTag = script;
2982
- break;
2983
- }
2984
- }
2985
- // If no specific match found, use the first one
2986
- if (!scriptTag) {
2987
- scriptTag = scripts[0];
2982
+ // Match the actual CustomViews bundle files (e.g., custom-views.min.js, custom-views.js)
2983
+ for (const script of document.scripts) {
2984
+ const src = script.src || '';
2985
+ // Match filenames like: custom-views.min.js, custom-views.js, custom-views.esm.js
2986
+ if (/custom-views(?:\.min)?\.(?:esm\.)?js($|\?)/i.test(src)) {
2987
+ scriptTag = script;
2988
+ break;
2988
2989
  }
2989
2990
  }
2990
2991
  }