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