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