Last active
June 2, 2026 17:23
-
-
Save vgmoose/2bd9ef6ebd9e414561a3e386adbdbbb0 to your computer and use it in GitHub Desktop.
Embeddable standalone HTML widget for XLSX files as an html5 table, using SheetJS. See very bottom of page to specify target xlsx URL and example data.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!-- | |
| EXCEL EMBED WIDGET (CRM Drop-in Snippet) | |
| HOW TO USE: | |
| 1. Copy the ENTIRE content of this snippet. | |
| 2. Paste it into a "Custom HTML" module or "Code Embed" block in HubSpot, Webflow, Shopify, or other CMS. | |
| 3. Replace the `src` attribute value in the <excel-embed> tag at the bottom with your own public Excel file URL. | |
| License: https://creativecommons.org/publicdomain/zero/1.0/deed.en | |
| --> | |
| <!-- 1. Load SheetJS library via cdnjs --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> | |
| <!-- 2. The Script defining the custom web component --> | |
| <script> | |
| (function () { | |
| // This component assumes SheetJS (window.XLSX) is loaded globally on the page. | |
| const template = document.createElement('template'); | |
| template.innerHTML = ` | |
| <style> | |
| :host { | |
| display: block; | |
| width: var(--excel-embed-width, 100%); | |
| height: var(--excel-embed-height, 400px); | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| font-size: 13px; | |
| box-sizing: border-box; | |
| } | |
| :host(:not([theme="dark"])) { | |
| --bg-color: #ffffff; | |
| --text-color: #333333; | |
| --header-bg: #f8f9fa; | |
| --header-text: #5c5c5c; | |
| --header-border: #d1d5db; | |
| --grid-color: #e5e7eb; | |
| --excel-green: #107c41; | |
| --excel-green-light: #e2f0d9; | |
| --excel-green-hover: #0c5c30; | |
| --selection-bg: rgba(16, 124, 65, 0.06); | |
| --selection-border: #107c41; | |
| --search-match-bg: #fef08a; | |
| --search-match-active-bg: #f97316; | |
| --tab-bg: #f3f4f6; | |
| --tab-border: #e5e7eb; | |
| --tab-active-bg: #ffffff; | |
| --tab-text: #4b5563; | |
| --tab-active-text: #107c41; | |
| --border-color: #d1d5db; | |
| --toolbar-bg: #f3f4f6; | |
| --shadow-color: rgba(0, 0, 0, 0.05); | |
| --scroll-thumb: #cbd5e1; | |
| --scroll-thumb-hover: #94a3b8; | |
| } | |
| :host([theme="dark"]) { | |
| --bg-color: #1e1e1e; | |
| --text-color: #e0e0e0; | |
| --header-bg: #2d2d2d; | |
| --header-text: #aaaaaa; | |
| --header-border: #404040; | |
| --grid-color: #333333; | |
| --excel-green: #107c41; | |
| --excel-green-light: #1b3d2b; | |
| --excel-green-hover: #169e53; | |
| --selection-bg: rgba(16, 124, 65, 0.2); | |
| --selection-border: #169e53; | |
| --search-match-bg: #854d0e; | |
| --search-match-active-bg: #c2410c; | |
| --tab-bg: #252526; | |
| --tab-border: #2d2d2d; | |
| --tab-active-bg: #1e1e1e; | |
| --tab-text: #9ca3af; | |
| --tab-active-text: #169e53; | |
| --border-color: #404040; | |
| --toolbar-bg: #252526; | |
| --shadow-color: rgba(0, 0, 0, 0.2); | |
| --scroll-thumb: #4b5563; | |
| --scroll-thumb-hover: #6b7280; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :host([theme="auto"]), :host(:not([theme="light"]):not([theme="dark"])) { | |
| --bg-color: #1e1e1e; | |
| --text-color: #e0e0e0; | |
| --header-bg: #2d2d2d; | |
| --header-text: #aaaaaa; | |
| --header-border: #404040; | |
| --grid-color: #333333; | |
| --excel-green: #107c41; | |
| --excel-green-light: #1b3d2b; | |
| --excel-green-hover: #169e53; | |
| --selection-bg: rgba(16, 124, 65, 0.2); | |
| --selection-border: #169e53; | |
| --search-match-bg: #854d0e; | |
| --search-match-active-bg: #c2410c; | |
| --tab-bg: #252526; | |
| --tab-border: #2d2d2d; | |
| --tab-active-bg: #1e1e1e; | |
| --tab-text: #9ca3af; | |
| --tab-active-text: #169e53; | |
| --border-color: #404040; | |
| --toolbar-bg: #252526; | |
| --shadow-color: rgba(0, 0, 0, 0.2); | |
| --scroll-thumb: #4b5563; | |
| --scroll-thumb-hover: #6b7280; | |
| } | |
| } | |
| .container { | |
| display: flex; | |
| flex-direction: column; | |
| width: 100%; | |
| height: 100%; | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| box-shadow: 0 4px 6px -1px var(--shadow-color), 0 2px 4px -1px var(--shadow-color); | |
| background: var(--bg-color); | |
| color: var(--text-color); | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .toolbar { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 6px 12px; | |
| background: var(--toolbar-bg); | |
| border-bottom: 1px solid var(--border-color); | |
| gap: 12px; | |
| flex-shrink: 0; | |
| user-select: none; | |
| } | |
| .toolbar-title { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-weight: 600; | |
| color: var(--excel-green); | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .toolbar-title .file-icon { | |
| font-size: 16px; | |
| } | |
| .toolbar-actions { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .search-box { | |
| display: flex; | |
| align-items: center; | |
| background: var(--bg-color); | |
| border: 1px solid var(--border-color); | |
| border-radius: 4px; | |
| padding: 2px 6px; | |
| height: 24px; | |
| box-sizing: border-box; | |
| } | |
| .search-input { | |
| border: none; | |
| background: transparent; | |
| color: var(--text-color); | |
| font-size: 12px; | |
| outline: none; | |
| width: 120px; | |
| transition: width 0.2s; | |
| } | |
| .search-input:focus { | |
| width: 180px; | |
| } | |
| .search-count { | |
| font-size: 11px; | |
| color: var(--header-text); | |
| margin: 0 6px; | |
| white-space: nowrap; | |
| } | |
| .search-btn { | |
| background: transparent; | |
| border: none; | |
| cursor: pointer; | |
| padding: 0 4px; | |
| color: var(--header-text); | |
| font-weight: bold; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 2px; | |
| height: 18px; | |
| width: 18px; | |
| } | |
| .search-btn:hover { | |
| background: var(--tab-bg); | |
| color: var(--text-color); | |
| } | |
| .btn { | |
| background: var(--bg-color); | |
| border: 1px solid var(--border-color); | |
| color: var(--text-color); | |
| font-size: 11px; | |
| font-weight: 500; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| transition: background 0.15s, border-color 0.15s; | |
| height: 24px; | |
| box-sizing: border-box; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .btn:hover { | |
| background: var(--tab-bg); | |
| border-color: var(--header-border); | |
| } | |
| .btn-primary { | |
| background: var(--excel-green); | |
| color: #ffffff; | |
| border-color: var(--excel-green-hover); | |
| } | |
| .btn-primary:hover { | |
| background: var(--excel-green-hover); | |
| color: #ffffff; | |
| border-color: var(--excel-green-hover); | |
| } | |
| .formula-bar { | |
| display: flex; | |
| align-items: center; | |
| padding: 4px 8px; | |
| border-bottom: 1px solid var(--border-color); | |
| background: var(--bg-color); | |
| gap: 8px; | |
| flex-shrink: 0; | |
| } | |
| .active-cell-address { | |
| min-width: 45px; | |
| font-weight: 600; | |
| text-align: center; | |
| color: var(--excel-green); | |
| border: 1px solid var(--border-color); | |
| border-radius: 3px; | |
| padding: 2px 4px; | |
| background: var(--header-bg); | |
| font-size: 12px; | |
| user-select: none; | |
| } | |
| .formula-fx { | |
| color: var(--header-text); | |
| font-style: italic; | |
| font-weight: bold; | |
| font-size: 13px; | |
| user-select: none; | |
| } | |
| .active-cell-value { | |
| flex: 1; | |
| border: 1px solid var(--border-color); | |
| border-radius: 3px; | |
| padding: 2px 8px; | |
| height: 20px; | |
| line-height: 20px; | |
| font-family: monospace; | |
| font-size: 12px; | |
| background: var(--bg-color); | |
| color: var(--text-color); | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .grid-viewport { | |
| flex: 1; | |
| overflow: auto; | |
| position: relative; | |
| background: var(--bg-color); | |
| } | |
| .excel-table { | |
| border-collapse: separate; | |
| border-spacing: 0; | |
| table-layout: fixed; | |
| width: max-content; | |
| background: var(--bg-color); | |
| } | |
| .excel-table th, .excel-table td { | |
| border-right: 1px solid var(--grid-color); | |
| border-bottom: 1px solid var(--grid-color); | |
| box-sizing: border-box; | |
| } | |
| .excel-table thead th { | |
| position: sticky; | |
| top: 0; | |
| background: var(--header-bg); | |
| color: var(--header-text); | |
| z-index: 2; | |
| font-weight: 500; | |
| font-size: 11px; | |
| height: 24px; | |
| border-bottom: 2px solid var(--header-border); | |
| border-right: 1px solid var(--grid-color); | |
| user-select: none; | |
| } | |
| .excel-table tbody th.row-header { | |
| position: sticky; | |
| left: 0; | |
| background: var(--header-bg); | |
| color: var(--header-text); | |
| z-index: 2; | |
| font-weight: 500; | |
| font-size: 10px; | |
| width: 40px; | |
| text-align: center; | |
| border-right: 2px solid var(--header-border); | |
| border-bottom: 1px solid var(--grid-color); | |
| user-select: none; | |
| } | |
| .excel-table thead th.corner-header { | |
| position: sticky; | |
| top: 0; | |
| left: 0; | |
| z-index: 3; | |
| width: 40px; | |
| background: var(--header-bg); | |
| border-right: 2px solid var(--header-border); | |
| border-bottom: 2px solid var(--header-border); | |
| } | |
| .excel-cell { | |
| height: 22px; | |
| padding: 2px 6px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| font-size: 12px; | |
| color: var(--text-color); | |
| cursor: cell; | |
| user-select: text; | |
| font-weight: normal; | |
| } | |
| .excel-cell.align-left { text-align: left; } | |
| .excel-cell.align-right { text-align: right; } | |
| .excel-cell.align-center { text-align: center; } | |
| .excel-cell:hover { | |
| background-color: var(--selection-bg); | |
| } | |
| .excel-cell.selected { | |
| box-shadow: inset 0 0 0 2px var(--selection-border); | |
| background-color: var(--selection-bg); | |
| } | |
| .col-header.highlighted, .row-header.highlighted { | |
| background: var(--excel-green-light) !important; | |
| color: var(--excel-green) !important; | |
| font-weight: bold !important; | |
| } | |
| .excel-cell.search-match { | |
| background-color: var(--search-match-bg) !important; | |
| } | |
| .excel-cell.search-match-active { | |
| background-color: var(--search-match-active-bg) !important; | |
| color: #ffffff !important; | |
| } | |
| .tabs-bar { | |
| display: flex; | |
| background: var(--tab-bg); | |
| border-top: 1px solid var(--border-color); | |
| padding: 0 8px; | |
| height: 32px; | |
| align-items: flex-end; | |
| overflow: hidden; | |
| flex-shrink: 0; | |
| user-select: none; | |
| } | |
| .tabs-scroll-container { | |
| display: flex; | |
| overflow-x: auto; | |
| overflow-y: hidden; | |
| scrollbar-width: none; | |
| height: 100%; | |
| align-items: flex-end; | |
| } | |
| .tabs-scroll-container::-webkit-scrollbar { | |
| display: none; | |
| } | |
| .sheet-tab { | |
| padding: 5px 16px; | |
| font-size: 12px; | |
| background: var(--tab-bg); | |
| color: var(--tab-text); | |
| border-top-left-radius: 4px; | |
| border-top-right-radius: 4px; | |
| border: 1px solid var(--tab-border); | |
| border-bottom: none; | |
| margin-right: 2px; | |
| cursor: pointer; | |
| white-space: nowrap; | |
| transition: background 0.15s, color 0.15s; | |
| height: 18px; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .sheet-tab:hover { | |
| background: var(--bg-color); | |
| color: var(--text-color); | |
| } | |
| .sheet-tab.active { | |
| background: var(--bg-color); | |
| color: var(--tab-active-text); | |
| font-weight: 600; | |
| border-top: 3px solid var(--excel-green); | |
| margin-top: -2px; | |
| height: 20px; | |
| z-index: 1; | |
| } | |
| .status-bar { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 4px 12px; | |
| font-size: 11px; | |
| background: var(--header-bg); | |
| border-top: 1px solid var(--border-color); | |
| color: var(--header-text); | |
| flex-shrink: 0; | |
| user-select: none; | |
| } | |
| .loader-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: var(--bg-color); | |
| display: flex; | |
| flex-direction: column; | |
| z-index: 10; | |
| opacity: 1; | |
| transition: opacity 0.3s ease; | |
| } | |
| .skeleton-header { | |
| height: 36px; | |
| background: var(--toolbar-bg); | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .skeleton-grid { | |
| flex: 1; | |
| padding: 12px; | |
| display: grid; | |
| grid-template-columns: repeat(10, 100px); | |
| grid-template-rows: repeat(15, 24px); | |
| gap: 2px; | |
| overflow: hidden; | |
| } | |
| .skeleton-cell { | |
| background: var(--header-bg); | |
| border-radius: 2px; | |
| animation: shimmer 1.5s infinite linear; | |
| background: linear-gradient( | |
| 90deg, | |
| var(--header-bg) 25%, | |
| var(--border-color) 50%, | |
| var(--header-bg) 75% | |
| ); | |
| background-size: 200% 100%; | |
| } | |
| @keyframes shimmer { | |
| 0% { background-position: -200% 0; } | |
| 100% { background-position: 200% 0; } | |
| } | |
| .overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: var(--bg-color); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 11; | |
| padding: 24px; | |
| box-sizing: border-box; | |
| text-align: center; | |
| } | |
| .overlay.hidden { | |
| display: none !important; | |
| } | |
| .error-title { | |
| color: #dc2626; | |
| font-weight: bold; | |
| font-size: 16px; | |
| margin-bottom: 8px; | |
| } | |
| .error-desc { | |
| font-size: 13px; | |
| color: var(--header-text); | |
| max-width: 400px; | |
| margin-bottom: 16px; | |
| } | |
| .file-input { | |
| display: none; | |
| } | |
| .grid-viewport::-webkit-scrollbar { | |
| width: 12px; | |
| height: 12px; | |
| } | |
| .grid-viewport::-webkit-scrollbar-track { | |
| background: var(--bg-color); | |
| border: 1px solid var(--grid-color); | |
| } | |
| .grid-viewport::-webkit-scrollbar-thumb { | |
| background: var(--scroll-thumb); | |
| border: 2px solid var(--bg-color); | |
| border-radius: 6px; | |
| } | |
| .grid-viewport::-webkit-scrollbar-thumb:hover { | |
| background: var(--scroll-thumb-hover); | |
| } | |
| </style> | |
| <div class="container"> | |
| <div class="toolbar"> | |
| <div class="toolbar-title"> | |
| <span class="file-icon">📊</span> | |
| <span class="file-name">loading...</span> | |
| </div> | |
| <div class="toolbar-actions"> | |
| <div class="search-box"> | |
| <input type="text" class="search-input" placeholder="Find in sheet..." disabled /> | |
| <span class="search-count"></span> | |
| <button class="search-btn search-prev" title="Previous match" disabled>↑</button> | |
| <button class="search-btn search-next" title="Next match" disabled>↓</button> | |
| </div> | |
| <button class="btn btn-export" id="export-csv-btn" disabled>CSV</button> | |
| <button class="btn btn-export" id="export-json-btn" disabled>JSON</button> | |
| </div> | |
| </div> | |
| <div class="formula-bar"> | |
| <div class="active-cell-address">-</div> | |
| <div class="formula-fx">fx</div> | |
| <div class="active-cell-value">Select a cell to view content...</div> | |
| </div> | |
| <div class="grid-viewport"> | |
| <table class="excel-table" id="excel-table-el"></table> | |
| </div> | |
| <div class="tabs-bar"> | |
| <div class="tabs-scroll-container" id="tabs-container"></div> | |
| </div> | |
| <div class="status-bar"> | |
| <div class="status-message">Loading libraries...</div> | |
| <div class="status-stats" id="status-stats">Rows: 0 | Columns: 0</div> | |
| </div> | |
| <div class="loader-overlay" id="loading-overlay"> | |
| <div class="skeleton-header"></div> | |
| <div class="skeleton-grid" id="skeleton-grid-el"></div> | |
| </div> | |
| <div class="overlay hidden" id="error-overlay"> | |
| <div class="error-title">Unable to Load Spreadsheet</div> | |
| <div class="error-desc" id="error-description"> | |
| Failed to fetch from the specified URL. | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| class ExcelEmbed extends HTMLElement { | |
| constructor() { | |
| super(); | |
| this.attachShadow({ mode: 'open' }); | |
| this.shadowRoot.appendChild(template.content.cloneNode(true)); | |
| this.containerEl = this.shadowRoot.querySelector('.container'); | |
| this.fileNameEl = this.shadowRoot.querySelector('.file-name'); | |
| this.tableEl = this.shadowRoot.querySelector('#excel-table-el'); | |
| this.tabsContainerEl = this.shadowRoot.querySelector('#tabs-container'); | |
| this.statusMessageEl = this.shadowRoot.querySelector('.status-message'); | |
| this.statusStatsEl = this.shadowRoot.querySelector('#status-stats'); | |
| this.loadingOverlayEl = this.shadowRoot.querySelector('#loading-overlay'); | |
| this.errorOverlayEl = this.shadowRoot.querySelector('#error-overlay'); | |
| this.errorDescEl = this.shadowRoot.querySelector('#error-description'); | |
| this.filePickerEl = this.shadowRoot.querySelector('#fallback-file-picker'); | |
| this.activeAddressEl = this.shadowRoot.querySelector('.active-cell-address'); | |
| this.activeValueEl = this.shadowRoot.querySelector('.active-cell-value'); | |
| this.searchInputEl = this.shadowRoot.querySelector('.search-input'); | |
| this.searchCountEl = this.shadowRoot.querySelector('.search-count'); | |
| this.searchPrevEl = this.shadowRoot.querySelector('.search-prev'); | |
| this.searchNextEl = this.shadowRoot.querySelector('.search-next'); | |
| this.exportCsvEl = this.shadowRoot.querySelector('#export-csv-btn'); | |
| this.exportJsonEl = this.shadowRoot.querySelector('#export-json-btn'); | |
| this.workbook = null; | |
| this.currentSheetName = ''; | |
| this.sheetNames = []; | |
| this.selectedCell = null; | |
| this.searchResults = []; | |
| this.currentSearchIndex = -1; | |
| this.handleFileSelect = this.handleFileSelect.bind(this); | |
| this.handleSearch = this.handleSearch.bind(this); | |
| this.navigateSearch = this.navigateSearch.bind(this); | |
| this.exportCSV = this.exportCSV.bind(this); | |
| this.exportJSON = this.exportJSON.bind(this); | |
| } | |
| static get observedAttributes() { | |
| return ['src', 'width', 'height', 'theme']; | |
| } | |
| attributeChangedCallback(name, oldValue, newValue) { | |
| if (oldValue === newValue) return; | |
| if (name === 'width') { | |
| this.style.setProperty('--excel-embed-width', newValue || '100%'); | |
| } else if (name === 'height') { | |
| this.style.setProperty('--excel-embed-height', newValue || '400px'); | |
| } else if (name === 'src' && this.isConnected) { | |
| this.loadWorkbookFromUrl(newValue); | |
| } | |
| } | |
| connectedCallback() { | |
| this.style.setProperty('--excel-embed-width', this.getAttribute('width') || '100%'); | |
| this.style.setProperty('--excel-embed-height', this.getAttribute('height') || '400px'); | |
| const skeletonGrid = this.shadowRoot.querySelector('#skeleton-grid-el'); | |
| skeletonGrid.innerHTML = Array.from({ length: 150 }) | |
| .map(() => '<div class="skeleton-cell"></div>') | |
| .join(''); | |
| this.filePickerEl.addEventListener('change', this.handleFileSelect); | |
| this.searchInputEl.addEventListener('input', this.handleSearch); | |
| this.searchPrevEl.addEventListener('click', () => this.navigateSearch(-1)); | |
| this.searchNextEl.addEventListener('click', () => this.navigateSearch(1)); | |
| this.exportCsvEl.addEventListener('click', this.exportCSV); | |
| this.exportJsonEl.addEventListener('click', this.exportJSON); | |
| // Check for XLSX library | |
| if (window.XLSX) { | |
| this.initWidget(); | |
| } else { | |
| this.statusMessageEl.textContent = 'Waiting for SheetJS...'; | |
| let checkCount = 0; | |
| const interval = setInterval(() => { | |
| checkCount++; | |
| if (window.XLSX) { | |
| clearInterval(interval); | |
| this.initWidget(); | |
| } else if (checkCount > 50) { // 5 seconds timeout | |
| clearInterval(interval); | |
| this.showError( | |
| 'SheetJS Library Missing', | |
| 'Please ensure the SheetJS script tag is included on the page:<br>' + | |
| '<code><script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script></code>' | |
| ); | |
| } | |
| }, 100); | |
| } | |
| } | |
| initWidget() { | |
| this.statusMessageEl.textContent = 'Ready'; | |
| const src = this.getAttribute('src'); | |
| if (src) { | |
| this.loadWorkbookFromUrl(src); | |
| } else { | |
| this.showError('No spreadsheet source provided.', 'Please specify a URL via the "src" attribute or choose a file below.'); | |
| } | |
| } | |
| disconnectedCallback() { | |
| this.filePickerEl.removeEventListener('change', this.handleFileSelect); | |
| this.searchInputEl.removeEventListener('input', this.handleSearch); | |
| this.exportCsvEl.removeEventListener('click', this.exportCSV); | |
| this.exportJsonEl.removeEventListener('click', this.exportJSON); | |
| } | |
| showLoader(visible) { | |
| if (visible) { | |
| this.loadingOverlayEl.style.opacity = '1'; | |
| this.loadingOverlayEl.style.display = 'flex'; | |
| } else { | |
| this.loadingOverlayEl.style.opacity = '0'; | |
| setTimeout(() => { | |
| this.loadingOverlayEl.style.display = 'none'; | |
| }, 300); | |
| } | |
| } | |
| showError(title, description) { | |
| this.showLoader(false); | |
| this.errorOverlayEl.classList.remove('hidden'); | |
| this.errorOverlayEl.querySelector('.error-title').textContent = title; | |
| this.errorDescEl.innerHTML = description; | |
| this.statusMessageEl.textContent = 'Error occurred'; | |
| } | |
| hideError() { | |
| this.errorOverlayEl.classList.add('hidden'); | |
| } | |
| async loadWorkbookFromUrl(url) { | |
| if (!url) return; | |
| this.hideError(); | |
| this.showLoader(true); | |
| // Check for inline Data URL (Base64) | |
| if (url.startsWith('data:')) { | |
| const commaIdx = url.indexOf(','); | |
| const meta = url.substring(0, commaIdx); | |
| const dataStr = url.substring(commaIdx + 1); | |
| const isBase64 = meta.includes('base64'); | |
| this.fileNameEl.textContent = 'Embedded Spreadsheet'; | |
| this.statusMessageEl.textContent = 'Parsing embedded data...'; | |
| try { | |
| const workbook = window.XLSX.read(dataStr, { | |
| type: isBase64 ? 'base64' : 'string', | |
| cellStyles: true, | |
| cellFormulas: true, | |
| cellDates: true, | |
| cellNF: true | |
| }); | |
| this.workbook = workbook; | |
| this.sheetNames = workbook.SheetNames; | |
| this.renderSheetTabs(); | |
| this.selectSheet(this.sheetNames[0]); | |
| this.showLoader(false); | |
| this.statusMessageEl.textContent = 'Spreadsheet loaded successfully'; | |
| this.searchInputEl.disabled = false; | |
| this.exportCsvEl.disabled = false; | |
| this.exportJsonEl.disabled = false; | |
| } catch (err) { | |
| this.showError('Format / Parse Error', `SheetJS was unable to parse the data URL: ${err.message}`); | |
| } | |
| return; | |
| } | |
| const filename = url.substring(url.lastIndexOf('/') + 1) || 'Spreadsheet'; | |
| this.fileNameEl.textContent = decodeURIComponent(filename); | |
| this.statusMessageEl.textContent = `Downloading ${filename}...`; | |
| try { | |
| const response = await fetch(url); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const arrayBuffer = await response.arrayBuffer(); | |
| this.parseArrayBuffer(arrayBuffer); | |
| } catch (err) { | |
| console.error('Fetch Error:', err); | |
| this.showError( | |
| 'Failed to Fetch Spreadsheet', | |
| `Could not download file from <code>${url}</code>.<br><br>` | |
| ); | |
| } | |
| } | |
| handleFileSelect(e) { | |
| if (e.target.files.length > 0) { | |
| this.parseLocalFile(e.target.files[0]); | |
| } | |
| } | |
| parseLocalFile(file) { | |
| this.hideError(); | |
| this.showLoader(true); | |
| this.fileNameEl.textContent = file.name; | |
| this.statusMessageEl.textContent = `Reading ${file.name}...`; | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| this.parseArrayBuffer(e.target.result); | |
| }; | |
| reader.onerror = () => { | |
| this.showError('Read Error', `Could not read local file ${file.name}.`); | |
| }; | |
| reader.readAsArrayBuffer(file); | |
| } | |
| parseArrayBuffer(buffer) { | |
| try { | |
| this.statusMessageEl.textContent = 'Parsing sheet data...'; | |
| const data = new Uint8Array(buffer); | |
| const workbook = window.XLSX.read(data, { | |
| type: 'array', | |
| cellStyles: true, | |
| cellFormulas: true, | |
| cellDates: true, | |
| cellNF: true | |
| }); | |
| this.workbook = workbook; | |
| this.sheetNames = workbook.SheetNames; | |
| if (this.sheetNames.length === 0) { | |
| throw new Error('No sheets found inside workbook.'); | |
| } | |
| this.renderSheetTabs(); | |
| this.selectSheet(this.sheetNames[0]); | |
| this.showLoader(false); | |
| this.statusMessageEl.textContent = 'Spreadsheet loaded successfully'; | |
| this.searchInputEl.disabled = false; | |
| this.exportCsvEl.disabled = false; | |
| this.exportJsonEl.disabled = false; | |
| } catch (err) { | |
| console.error('Parse Error:', err); | |
| this.showError('Format / Parse Error', `SheetJS was unable to parse the file. Error: ${err.message}`); | |
| } | |
| } | |
| renderSheetTabs() { | |
| this.tabsContainerEl.innerHTML = ''; | |
| this.sheetNames.forEach(sheetName => { | |
| const tab = document.createElement('div'); | |
| tab.className = 'sheet-tab'; | |
| tab.textContent = sheetName; | |
| tab.setAttribute('data-sheet', sheetName); | |
| tab.addEventListener('click', () => this.selectSheet(sheetName)); | |
| this.tabsContainerEl.appendChild(tab); | |
| }); | |
| } | |
| selectSheet(sheetName) { | |
| this.currentSheetName = sheetName; | |
| this.tabsContainerEl.querySelectorAll('.sheet-tab').forEach(tab => { | |
| if (tab.getAttribute('data-sheet') === sheetName) { | |
| tab.classList.add('active'); | |
| tab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); | |
| } else { | |
| tab.classList.remove('active'); | |
| } | |
| }); | |
| this.selectedCell = null; | |
| this.activeAddressEl.textContent = '-'; | |
| this.activeValueEl.textContent = 'Select a cell to view content...'; | |
| this.searchInputEl.value = ''; | |
| this.searchCountEl.textContent = ''; | |
| this.searchResults = []; | |
| this.currentSearchIndex = -1; | |
| this.searchPrevEl.disabled = true; | |
| this.searchNextEl.disabled = true; | |
| this.renderTable(sheetName); | |
| } | |
| getColLabel(colIdx) { | |
| let label = ''; | |
| let temp = colIdx; | |
| while (temp >= 0) { | |
| label = String.fromCharCode((temp % 26) + 65) + label; | |
| temp = Math.floor(temp / 26) - 1; | |
| } | |
| return label; | |
| } | |
| renderTable(sheetName) { | |
| const worksheet = this.workbook.Sheets[sheetName]; | |
| this.tableEl.innerHTML = ''; | |
| if (!worksheet || !worksheet['!ref']) { | |
| this.tableEl.innerHTML = `<tr><td style="padding: 20px; text-align: center; color: var(--header-text);">Empty Worksheet</td></tr>`; | |
| this.statusStatsEl.textContent = 'Rows: 0 | Columns: 0'; | |
| return; | |
| } | |
| const range = window.XLSX.utils.decode_range(worksheet['!ref']); | |
| const totalRows = range.e.r + 1; | |
| const totalCols = range.e.c + 1; | |
| this.statusStatsEl.textContent = `Rows: ${totalRows} | Columns: ${totalCols}`; | |
| const thead = document.createElement('thead'); | |
| const headerRow = document.createElement('tr'); | |
| const cornerHeader = document.createElement('th'); | |
| cornerHeader.className = 'corner-header'; | |
| headerRow.appendChild(cornerHeader); | |
| for (let c = 0; c < totalCols; c++) { | |
| const colHeader = document.createElement('th'); | |
| const colLabel = this.getColLabel(c); | |
| colHeader.className = 'col-header'; | |
| colHeader.setAttribute('data-col', colLabel); | |
| colHeader.textContent = colLabel; | |
| headerRow.appendChild(colHeader); | |
| } | |
| thead.appendChild(headerRow); | |
| this.tableEl.appendChild(thead); | |
| const tbody = document.createElement('tbody'); | |
| for (let r = 0; r < totalRows; r++) { | |
| const row = document.createElement('tr'); | |
| const rowHeader = document.createElement('th'); | |
| rowHeader.className = 'row-header'; | |
| rowHeader.setAttribute('data-row', r + 1); | |
| rowHeader.textContent = r + 1; | |
| row.appendChild(rowHeader); | |
| for (let c = 0; c < totalCols; c++) { | |
| const colLabel = this.getColLabel(c); | |
| const cellRef = `${colLabel}${r + 1}`; | |
| const cellObj = worksheet[cellRef]; | |
| const td = document.createElement('td'); | |
| td.className = 'excel-cell'; | |
| td.setAttribute('data-cell', cellRef); | |
| if (cellObj) { | |
| const formattedVal = cellObj.w !== undefined ? cellObj.w : (cellObj.v !== undefined ? cellObj.v : ''); | |
| td.textContent = formattedVal; | |
| if (cellObj.f) { | |
| td.setAttribute('data-formula', `=${cellObj.f}`); | |
| } | |
| td.setAttribute('data-value', cellObj.v !== undefined ? cellObj.v : ''); | |
| if (cellObj.t === 'n') { | |
| td.classList.add('align-right'); | |
| } else if (cellObj.t === 'b' || cellObj.t === 'd' || cellObj.t === 'e') { | |
| td.classList.add('align-center'); | |
| } else { | |
| td.classList.add('align-left'); | |
| } | |
| } else { | |
| td.textContent = ''; | |
| td.classList.add('align-left'); | |
| } | |
| td.addEventListener('click', (e) => this.selectCellElement(td, cellRef, cellObj)); | |
| row.appendChild(td); | |
| } | |
| tbody.appendChild(row); | |
| } | |
| this.tableEl.appendChild(tbody); | |
| if (worksheet['!cols']) { | |
| const colgroups = document.createElement('colgroup'); | |
| for (let c = 0; c < totalCols; c++) { | |
| const colEl = document.createElement('col'); | |
| if (worksheet['!cols'][c]) { | |
| const width = worksheet['!cols'][c].wpx ? `${worksheet['!cols'][c].wpx}px` : | |
| (worksheet['!cols'][c].wch ? `${worksheet['!cols'][c].wch * 8}px` : '100px'); | |
| colEl.style.width = width; | |
| } else { | |
| colEl.style.width = '100px'; | |
| } | |
| colgroups.appendChild(colEl); | |
| } | |
| this.tableEl.insertBefore(colgroups, thead); | |
| } | |
| } | |
| selectCellElement(tdEl, cellRef, cellObj) { | |
| if (this.selectedCell) { | |
| this.selectedCell.classList.remove('selected'); | |
| } | |
| this.tableEl.querySelectorAll('.highlighted').forEach(el => el.classList.remove('highlighted')); | |
| this.selectedCell = tdEl; | |
| tdEl.classList.add('selected'); | |
| const colLabel = cellRef.replace(/[0-9]/g, ''); | |
| const rowNum = cellRef.replace(/[A-Z]/g, ''); | |
| const colHeader = this.tableEl.querySelector(`.col-header[data-col="${colLabel}"]`); | |
| if (colHeader) colHeader.classList.add('highlighted'); | |
| const rowHeader = this.tableEl.querySelector(`.row-header[data-row="${rowNum}"]`); | |
| if (rowHeader) rowHeader.classList.add('highlighted'); | |
| this.activeAddressEl.textContent = cellRef; | |
| if (cellObj) { | |
| if (cellObj.f) { | |
| this.activeValueEl.textContent = `=${cellObj.f} (Result: ${cellObj.w !== undefined ? cellObj.w : cellObj.v})`; | |
| } else { | |
| this.activeValueEl.textContent = cellObj.v !== undefined ? cellObj.v : ''; | |
| } | |
| } else { | |
| this.activeValueEl.textContent = ''; | |
| } | |
| } | |
| handleSearch() { | |
| const query = this.searchInputEl.value.toLowerCase().trim(); | |
| this.tableEl.querySelectorAll('.search-match, .search-match-active').forEach(el => { | |
| el.classList.remove('search-match', 'search-match-active'); | |
| }); | |
| if (!query) { | |
| this.searchCountEl.textContent = ''; | |
| this.searchResults = []; | |
| this.currentSearchIndex = -1; | |
| this.searchPrevEl.disabled = true; | |
| this.searchNextEl.disabled = true; | |
| return; | |
| } | |
| this.searchResults = []; | |
| const cells = this.tableEl.querySelectorAll('.excel-cell'); | |
| cells.forEach(cell => { | |
| const text = cell.textContent.toLowerCase(); | |
| const formula = (cell.getAttribute('data-formula') || '').toLowerCase(); | |
| const rawVal = (cell.getAttribute('data-value') || '').toLowerCase(); | |
| if (text.includes(query) || formula.includes(query) || rawVal.includes(query)) { | |
| this.searchResults.push(cell); | |
| cell.classList.add('search-match'); | |
| } | |
| }); | |
| if (this.searchResults.length > 0) { | |
| this.currentSearchIndex = 0; | |
| this.highlightActiveSearchMatch(); | |
| this.searchPrevEl.disabled = false; | |
| this.searchNextEl.disabled = false; | |
| } else { | |
| this.currentSearchIndex = -1; | |
| this.searchCountEl.textContent = '0 of 0'; | |
| this.searchPrevEl.disabled = true; | |
| this.searchNextEl.disabled = true; | |
| } | |
| } | |
| highlightActiveSearchMatch() { | |
| this.tableEl.querySelectorAll('.search-match-active').forEach(el => { | |
| el.classList.remove('search-match-active'); | |
| }); | |
| const activeCell = this.searchResults[this.currentSearchIndex]; | |
| if (activeCell) { | |
| activeCell.classList.add('search-match-active'); | |
| activeCell.scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'nearest', | |
| inline: 'nearest' | |
| }); | |
| const cellRef = activeCell.getAttribute('data-cell'); | |
| const worksheet = this.workbook.Sheets[this.currentSheetName]; | |
| const cellObj = worksheet[cellRef]; | |
| this.selectCellElement(activeCell, cellRef, cellObj); | |
| this.searchCountEl.textContent = `${this.currentSearchIndex + 1} of ${this.searchResults.length}`; | |
| } | |
| } | |
| navigateSearch(dir) { | |
| if (this.searchResults.length === 0) return; | |
| this.currentSearchIndex += dir; | |
| if (this.currentSearchIndex >= this.searchResults.length) { | |
| this.currentSearchIndex = 0; | |
| } else if (this.currentSearchIndex < 0) { | |
| this.currentSearchIndex = this.searchResults.length - 1; | |
| } | |
| this.highlightActiveSearchMatch(); | |
| } | |
| getCurrentWorksheet() { | |
| return this.workbook ? this.workbook.Sheets[this.currentSheetName] : null; | |
| } | |
| exportCSV() { | |
| const ws = this.getCurrentWorksheet(); | |
| if (!ws) return; | |
| try { | |
| const csv = window.XLSX.utils.sheet_to_csv(ws); | |
| const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `${this.currentSheetName || 'export'}.csv`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| } catch (err) { | |
| alert(`Failed to export CSV: ${err.message}`); | |
| } | |
| } | |
| exportJSON() { | |
| const ws = this.getCurrentWorksheet(); | |
| if (!ws) return; | |
| try { | |
| const json = window.XLSX.utils.sheet_to_json(ws, { header: 1 }); | |
| const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json;charset=utf-8;' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `${this.currentSheetName || 'export'}.json`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| } catch (err) { | |
| alert(`Failed to export JSON: ${err.message}`); | |
| } | |
| } | |
| } | |
| customElements.define('excel-embed', ExcelEmbed); | |
| })(); | |
| </script> | |
| <!-- 2. The custom tag instances you can place anywhere in your page layout --> | |
| <excel-embed src="./z_sample.xlsx" width="100%" height="450px" theme="auto"> | |
| </excel-embed> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
