1/* Mobile Inventory homepage demo. Synthetic data only; no API, storage or external dependencies. */ 2(function () { 3 'use strict'; 4 const homepageRoot = new URL('../../', document.currentScript.src); 5 const root = document.getElementById('try_it'); 6 if (!root || root.dataset.demoReady === 'true') return; 7 root.dataset.demoReady = 'true'; 8 const $ = (selector) => root.querySelector(selector); 9 const $$ = (selector) => Array.from(root.querySelectorAll(selector)); 10 const t = (key, fallback) => { 11 const value = 12 window.MIHello && typeof window.MIHello.t === 'function' 13 ? window.MIHello.t(key, fallback) 14 : fallback; 15 return typeof value === 'string' && value !== key ? value : fallback; 16 }; 17 const interpolate = (key, fallback, values) => 18 t(key, fallback).replace(/\{\{\s*(\w+)\s*\}\}/g, (match, name) => 19 Object.prototype.hasOwnProperty.call(values, name) ? values[name] : match 20 ); 21 const esc = (value) => 22 String(value).replace( 23 /[&<>"']/g, 24 (character) => 25 ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[character]) 26 ); 27 const number = (value) => 28 new Intl.NumberFormat(document.documentElement.lang || 'en', { 29 maximumFractionDigits: 3, 30 }).format(value); 31 const data = [ 32 { 33 id: 'box-cutter', 34 key: 'trans_Box cutter', 35 name: 'Box cutter', 36 barcode: '40093059279', 37 stock: [5, 2, 0, 0], 38 image: 'assets/hello/images/demo-box-cutter.webp', 39 thumbnail: 'assets/hello/images/demo-box-cutter-thumb.webp', 40 }, 41 { 42 id: 'pencils', 43 key: 'trans_Pencils', 44 name: 'Pencils', 45 barcode: '95774033876', 46 stock: [20, 10, 3, 0], 47 thumbnail: 'assets/hello/images/demo-pencils-thumb.webp', 48 }, 49 { 50 id: 'erasers', 51 key: 'trans_Erasers', 52 name: 'Erasers', 53 barcode: '83767928702', 54 stock: [15, 5, 3, 0], 55 thumbnail: 'assets/hello/images/demo-erasers-thumb.webp', 56 }, 57 { 58 id: 'ruler', 59 key: 'trans_Ruler', 60 name: 'Ruler', 61 barcode: '52295514445', 62 stock: [10, 5, 2, 0], 63 thumbnail: 'assets/hello/images/demo-ruler-thumb.webp', 64 }, 65 { 66 id: 'mouse', 67 key: 'trans_Mouse', 68 name: 'Mouse', 69 barcode: '05061954019', 70 stock: [30, 10, 8, 2], 71 image: 'assets/hello/images/demo-mouse.webp', 72 thumbnail: 'assets/hello/images/demo-mouse-thumb.webp', 73 }, 74 ]; 75 const locations = [ 76 { id: 'a', key: 'trans_Warehouse A', name: 'Warehouse A' }, 77 { id: 'b', key: 'trans_Warehouse B', name: 'Warehouse B' }, 78 { id: 'office', key: 'trans_Office A', name: 'Office A' }, 79 { id: 'client', key: 'trans_Client X', name: 'Client X' }, 80 ]; 81 // Thousandths keep repeated decimal additions and transfers exact. 82 let products = data.map((product) => ({ 83 ...product, 84 units: product.stock.map((quantity) => quantity * 1000), 85 })); 86 const state = { 87 selected: null, 88 type: 'IN', 89 sort: 'name', 90 direction: 1, 91 query: '', 92 view: 'all', 93 hidden: new Set(), 94 recent: null, 95 message: null, 96 activeOption: -1, 97 }; 98 let recentTimer; 99 const productName = (product) => t(product.key, product.name); 100 const locationName = (location) => t(location.key, location.name); 101 const imagePath = (product) => product.image || 'assets/hello/images/demo-' + product.id + '.jpg'; 102 function imageMarkup(product, large) { 103 // Use compact 3x thumbnails for the 50px table and print images. 104 const source = !large && product.thumbnail ? product.thumbnail : imagePath(product); 105 return ( 106 '<img src="' + 107 new URL(source, homepageRoot).href + 108 '" alt="' + 109 esc(productName(product)) + 110 '"' + 111 (large ? '' : ' width="50" height="50" loading="lazy"') + 112 '>' 113 ); 114 } 115 const units = (product) => 116 state.view === 'all' 117 ? product.units.reduce((sum, value) => sum + value, 0) 118 : product.units[locations.findIndex((location) => location.id === state.view)]; 119 const norm = (value) => 120 String(value) 121 .normalize('NFD') 122 .replace(/[\u0300-\u036f]/g, '') 123 .toLocaleLowerCase(); 124 const visibleProducts = () => 125 products 126 .filter((product) => state.view === 'all' || units(product) > 0) 127 .filter((product) => 128 norm(productName(product) + ' ' + product.name + ' ' + product.barcode).includes( 129 norm(state.query) 130 ) 131 ) 132 .sort((a, b) => { 133 let result; 134 if (state.sort === 'stock') result = units(a) - units(b); 135 else if (state.sort === 'barcode') result = a.barcode.localeCompare(b.barcode); 136 else 137 result = productName(a).localeCompare( 138 productName(b), 139 document.documentElement.lang || 'en', 140 { sensitivity: 'base' } 141 ); 142 return result * state.direction; 143 }); 144 const headers = () => ({ 145 name: t('trans_Product name', 'Product name'), 146 barcode: t('trans_barcode', 'Barcode'), 147 stock: 148 state.view === 'all' 149 ? t('trans_Total stock', 'Total stock')
150 : t('trans_stock', 'Stock') + 151 ': ' + 152 locationName(locations.find((location) => location.id === state.view)), 153 image: t('trans_Image', 'Image'), 154 }); 155 const labelDefaults = new Map(); 156 $$('[data-i18n]').forEach((node) => labelDefaults.set(node, node.textContent)); 157 function refreshLabels() { 158 labelDefaults.forEach((fallback, node) => { 159 if (node.isConnected) node.textContent = t(node.dataset.i18n, fallback); 160 }); 161 $('#mi-demo-product').placeholder = t('trans_select_product', 'Select product').replace( 162 /^\*\s*/, 163 '' 164 ); 165 $('.mi-demo-picker-toggle').setAttribute( 166 'aria-label', 167 t('mi_demo_show_products', 'Show products') 168 ); 169 $('.mi-demo-types').setAttribute( 170 'aria-label', 171 t('mi_demo_transaction_type', 'Transaction type') 172 ); 173 $('.mi-demo-table-scroll').setAttribute('aria-label', t('mi_demo_products', 'Demo products')); 174 $('.mi-demo-lightbox-close').setAttribute('aria-label', t('trans_close', 'Close')); 175 if (state.selected) 176 $('#mi-demo-product').value = productName( 177 products.find((product) => product.id === state.selected) 178 ); 179 updateType(); 180 renderTable(); 181 renderMessage(); 182 if (!$('#mi-demo-products').hidden) renderOptions(); 183 const openProduct = products.find( 184 (product) => product.id === $('#mi-demo-lightbox').dataset.product 185 ); 186 if (openProduct) { 187 $('#mi-demo-image-title').textContent = productName(openProduct); 188 $('#mi-demo-large-visual').innerHTML = imageMarkup(openProduct, true); 189 } 190 } 191 function renderTable() { 192 const rows = visibleProducts(); 193 $('#mi-demo-stock-title').textContent = headers().stock; 194 $('#mi-demo-table tbody').innerHTML = rows.length 195 ? rows 196 .map((product) => { 197 const recent = state.recent && state.recent.id === product.id; 198 return ( 199 '<tr' + 200 (recent ? ' class="mi-demo-updated" data-change="' + state.recent.type + '"' : '') + 201 '>' + 202 '<td data-column="name">' + 203 esc(productName(product)) + 204 '</td>' + 205 '<td data-column="barcode">' + 206 product.barcode + 207 '</td>' + 208 '<td data-column="stock"><strong class="mi-demo-stock-value">' + 209 esc(number(units(product) / 1000)) + 210 '</strong>' + 211 (recent 212 ? '<span class="mi-demo-sr"> ' + esc(t('mi_demo_updated', 'Updated')) + '</span>' 213 : '') + 214 '</td>' + 215 '<td data-column="image"><button type="button" class="mi-demo-thumb" data-image="' + 216 product.id + 217 '" aria-label="' + 218 esc( 219 interpolate('mi_demo_view_image', 'View {{product}} image', { 220 product: productName(product), 221 }) 222 ) + 223 '">' + 224 imageMarkup(product, false) + 225 '</button></td></tr>' 226 ); 227 }) 228 .join('') 229 : '<tr><td class="mi-demo-empty" colspan="' + 230 (4 - state.hidden.size) + 231 '">' + 232 esc(t('table_no_records', 'No matching records found')) + 233 '</td></tr>'; 234 $$('[data-column]').forEach((node) => { 235 node.hidden = state.hidden.has(node.dataset.column); 236 }); 237 $$('th[data-column]').forEach((node) => { 238 const active = node.dataset.column === state.sort; 239 if (active) 240 node.setAttribute('aria-sort', state.direction === 1 ? 'ascending' : 'descending'); 241 else node.removeAttribute('aria-sort'); 242 const arrow = node.querySelector('.mi-demo-sort'); 243 if (arrow) arrow.textContent = active ? (state.direction === 1 ? 'â' : 'â') : 'â'; 244 }); 245 $('#mi-demo-count').textContent = interpolate( 246 'mi_demo_result_count', 247 '{{shown}} of {{total}} products', 248 { shown: number(rows.length), total: number(products.length) } 249 ); 250 renderLocationStock(); 251 } 252 function renderLocationStock() { 253 const product = products.find((item) => item.id === state.selected); 254 const location = locations.find((item) => item.id === $('#mi-demo-location').value); 255 $('#mi-demo-location-stock').textContent = 256 product && location 257 ? interpolate('mi_demo_available', '{{location}}: {{quantity}} in stock', { 258 location: locationName(location), 259 quantity: number(product.units[locations.indexOf(location)] / 1000), 260 }) 261 : '';
262 [$('#mi-demo-location'), $('#mi-demo-destination')].forEach((select) => { 263 locations.forEach((location, index) => { 264 select.options[index + 1].textContent = 265 locationName(location) + 266 (product ? ' (' + number(product.units[index] / 1000) + ')' : ''); 267 }); 268 }); 269 } 270 function renderMessage() { 271 const node = $('#mi-demo-message'); 272 node.className = 'mi-demo-message' + (state.message ? ' is-' + state.message.kind : ''); 273 if (!state.message) { 274 node.textContent = ''; 275 return; 276 } 277 const message = state.message; 278 let values = { ...message.values }; 279 if (values.productId) { 280 values.product = productName(products.find((product) => product.id === values.productId)); 281 } 282 if (values.locationId) { 283 values.location = locationName( 284 locations.find((location) => location.id === values.locationId) 285 ); 286 } 287 if (values.toId) { 288 values.to = locationName(locations.find((location) => location.id === values.toId)); 289 } 290 if (typeof values.amount === 'number') values.quantity = number(values.amount); 291 const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 292 icon.setAttribute('class', 'mi-demo-message-icon'); 293 icon.setAttribute('viewBox', '0 0 24 24'); 294 icon.setAttribute('fill', 'none'); 295 icon.setAttribute('stroke', 'currentColor'); 296 icon.setAttribute('stroke-width', '1.8'); 297 icon.setAttribute('stroke-linecap', 'round'); 298 icon.setAttribute('stroke-linejoin', 'round'); 299 icon.setAttribute('aria-hidden', 'true'); 300 icon.setAttribute('focusable', 'false'); 301 const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); 302 circle.setAttribute('cx', '12'); 303 circle.setAttribute('cy', '12'); 304 circle.setAttribute('r', '9'); 305 const symbol = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 306 symbol.setAttribute('d', message.kind === 'success' ? 'm8 12 3 3 5-6' : 'M12 7v6m0 4h.01'); 307 icon.append(circle, symbol); 308 const text = document.createElement('span'); 309 text.textContent = interpolate(message.key, message.fallback, values); 310 node.replaceChildren(icon, text); 311 } 312 function message(kind, key, fallback, values, focus) { 313 state.message = { kind, key, fallback, values: values || {} }; 314 renderMessage(); 315 if (focus) focus.focus(); 316 } 317 function updateType() { 318 $$('.mi-demo-type').forEach((button) => { 319 const active = button.dataset.type === state.type; 320 button.classList.toggle('is-active', active); 321 button.setAttribute('aria-pressed', String(active)); 322 }); 323 const isMove = state.type === 'MOVE'; 324 $('#mi-demo-destination-field').hidden = !isMove; 325 $('#mi-demo-destination').required = isMove; 326 $('#mi-demo-location-label').textContent = t( 327 isMove ? 'trans_from_location' : 'trans_select_location', 328 isMove ? 'From location' : 'Select location' 329 ); 330 $('#mi-demo-location').options[0].textContent = $('#mi-demo-location-label').textContent; 331 renderLocationStock(); 332 } 333 function options() { 334 const value = state.selected ? '' : $('#mi-demo-product').value; 335 return products.filter((product) => 336 norm(productName(product) + ' ' + product.name + ' ' + product.barcode).includes(norm(value)) 337 ); 338 } 339 function renderOptions() { 340 const rows = options(); 341 const active = state.activeOption; 342 $('#mi-demo-products').innerHTML = rows.length 343 ? rows 344 .map( 345 (product, index) => 346 '<li role="option" id="mi-demo-option-' + 347 index + 348 '" data-product="' + 349 product.id + 350 '" aria-selected="' + 351 (index === active) + 352 '">' + 353 esc(productName(product)) + 354 '<small>' + 355 product.barcode + 356 '</small></li>' 357 ) 358 .join('') 359 : '<li role="option" aria-disabled="true">' + 360 esc(t('trans_product_not_found', 'Product not found')) + 361 '</li>'; 362 if (active >= 0 && rows[active]) 363 $('#mi-demo-product').setAttribute('aria-activedescendant', 'mi-demo-option-' + active); 364 else $('#mi-demo-product').removeAttribute('aria-activedescendant'); 365 } 366 function showOptions() { 367 state.activeOption = -1; 368 renderOptions(); 369 $('#mi-demo-products').hidden = false;
370 $('#mi-demo-product').setAttribute('aria-expanded', 'true'); 371 } 372 function hideOptions() { 373 $('#mi-demo-products').hidden = true; 374 $('#mi-demo-product').setAttribute('aria-expanded', 'false'); 375 $('#mi-demo-product').removeAttribute('aria-activedescendant'); 376 } 377 function choose(id) { 378 const product = products.find((item) => item.id === id); 379 if (!product) return; 380 state.selected = id; 381 $('#mi-demo-product').value = productName(product); 382 hideOptions(); 383 renderLocationStock(); 384 } 385 $('#mi-demo-product').addEventListener('focus', showOptions); 386 $('#mi-demo-product').addEventListener('input', () => { 387 state.selected = null; 388 showOptions(); 389 renderLocationStock(); 390 }); 391 $('.mi-demo-picker-toggle').addEventListener('click', () => { 392 if ($('#mi-demo-products').hidden) { 393 $('#mi-demo-product').focus(); 394 showOptions(); 395 } else hideOptions(); 396 }); 397 $('#mi-demo-products').addEventListener('mousedown', (event) => event.preventDefault()); 398 $('#mi-demo-products').addEventListener('click', (event) => { 399 const option = event.target.closest('[data-product]'); 400 if (option) choose(option.dataset.product); 401 }); 402 $('#mi-demo-product').addEventListener('keydown', (event) => { 403 if (event.key === 'Escape') { 404 hideOptions(); 405 event.preventDefault(); 406 return; 407 } 408 if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { 409 event.preventDefault(); 410 if ($('#mi-demo-products').hidden) showOptions(); 411 const rows = options(); 412 state.activeOption = rows.length 413 ? state.activeOption === -1 414 ? event.key === 'ArrowDown' 415 ? 0 416 : rows.length - 1 417 : (state.activeOption + (event.key === 'ArrowDown' ? 1 : -1) + rows.length) % rows.length 418 : -1; 419 renderOptions(); 420 const selected = $('#mi-demo-products [aria-selected=true]'); 421 if (selected) selected.scrollIntoView({ block: 'nearest' }); 422 return; 423 } 424 if (event.key === 'Enter' && !$('#mi-demo-products').hidden) { 425 event.preventDefault(); 426 const rows = options(); 427 const selected = rows[state.activeOption] || (rows.length === 1 ? rows[0] : null); 428 if (selected) choose(selected.id); 429 } 430 if (event.key === 'Tab') hideOptions(); 431 }); 432 document.addEventListener('click', (event) => { 433 if (!event.target.closest('.mi-demo-picker')) hideOptions(); 434 }); 435 $$('.mi-demo-type').forEach((button) => 436 button.addEventListener('click', () => { 437 state.type = button.dataset.type; 438 state.message = null; 439 renderMessage(); 440 updateType(); 441 }) 442 ); 443 const quantityInput = $('#mi-demo-quantity'); 444 const quantityButtons = $$('[data-quantity-step]'); 445 function quantityUnits() { 446 const value = Number(quantityInput.value); 447 return quantityInput.value.trim() && Number.isFinite(value) ? Math.round(value * 1000) : 0; 448 } 449 function updateQuantityControls() { 450 const value = quantityUnits(); 451 quantityButtons.forEach((button) => { 452 const next = value + Number(button.dataset.quantityStep) * 1000; 453 button.disabled = next < 1 || next > 1000000000; 454 }); 455 } 456 function stepQuantity(delta) { 457 const next = quantityUnits() + delta * 1000; 458 if (next < 1 || next > 1000000000) return; 459 quantityInput.value = String(next / 1000); 460 quantityInput.dispatchEvent(new Event('input', { bubbles: true })); 461 quantityInput.dispatchEvent(new Event('change', { bubbles: true })); 462 } 463 quantityButtons.forEach((button) => 464 button.addEventListener('click', () => stepQuantity(Number(button.dataset.quantityStep))) 465 ); 466 quantityInput.addEventListener('focus', () => quantityInput.select()); 467 quantityInput.addEventListener('input', updateQuantityControls); 468 quantityInput.addEventListener('keydown', (event) => { 469 if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { 470 event.preventDefault(); 471 stepQuantity(event.key === 'ArrowUp' ? 1 : -1); 472 } 473 }); 474 updateQuantityControls(); 475 $('#mi-demo-location').addEventListener('change', renderLocationStock); 476 $('#mi-demo-form').addEventListener('submit', (event) => { 477 event.preventDefault(); 478 hideOptions(); 479 const product = products.find((item) => item.id === state.selected); 480 const quantityInput = $('#mi-demo-quantity'); 481 const raw = quantityInput.value.trim(); 482 const quantity = Number(raw); 483 const scaled = Math.round(quantity * 1000); 484 if (!product) 485 return message( 486 'error', 487 'trans_msg_product_required', 488 'Please select a product.', 489 {}, 490 $('#mi-demo-product') 491 ); 492 if ( 493 !raw || 494 !Number.isFinite(quantity) || 495 scaled < 1 || 496 quantity > 1000000 || 497 quantity !== scaled / 1000 498 ) 499 return message( 500 'error', 501 'mi_demo_quantity_invalid', 502 'Enter a quantity above 0 and up to 1,000,000, with at most 3 decimal places.', 503 {}, 504 quantityInput 505 ); 506 const from = locations.findIndex((location) => location.id === $('#mi-demo-location').value); 507 const to = locations.findIndex((location) => location.id === $('#mi-demo-destination').value); 508 if (from < 0) 509 return message( 510 'error', 511 'mi_demo_location_required', 512 'Please select a location.', 513 {}, 514 $('#mi-demo-location') 515 ); 516 if (state.type === 'MOVE' && to < 0) 517 return message( 518 'error', 519 'mi_demo_destination_required', 520 'Please select a destination location.', 521 {}, 522 $('#mi-demo-destination') 523 ); 524 if (state.type === 'MOVE' && to === from) 525 return message( 526 'error', 527 'mi_demo_different_locations', 528 'Choose two different locations for a move.', 529 {}, 530 $('#mi-demo-destination') 531 ); 532 if (state.type !== 'IN' && product.units[from] < scaled) 533 return message( 534 'error', 535 'mi_demo_not_enough_stock', 536 'There is not enough stock at this location. Try a smaller quantity or another location.', 537 {}, 538 quantityInput 539 ); 540 if ( 541 state.type === 'IN' && 542 !Number.isSafeInteger(product.units.reduce((sum, value) => sum + value, 0) + scaled) 543 ) 544 return message( 545 'error', 546 'mi_demo_stock_limit', 547 'The demo stock limit has been reached. Reset the demo to continue.', 548 {} 549 ); 550 if (state.type === 'MOVE' && !Number.isSafeInteger(product.units[to] + scaled)) 551 return message( 552 'error', 553 'mi_demo_stock_limit', 554 'The demo stock limit has been reached. Reset the demo to continue.', 555 {} 556 ); 557 product.units[from] += state.type === 'IN' ? scaled : -scaled; 558 if (state.type === 'MOVE') product.units[to] += scaled; 559 const common = { productId: product.id, locationId: locations[from].id, amount: quantity }; 560 if (state.type === 'IN') 561 message( 562 'success', 563 'mi_demo_added', 564 'Added {{quantity}} {{product}} to {{location}}.', 565 common 566 ); 567 else if (state.type === 'OUT') 568 message( 569 'success', 570 'mi_demo_removed', 571 'Removed {{quantity}} {{product}} from {{location}}.', 572 common 573 ); 574 else 575 message( 576 'success', 577 'mi_demo_moved', 578 'Moved {{quantity}} {{product}} from {{location}} to {{to}}
578. Total stock is unchanged.', 579 { ...common, toId: locations[to].id } 580 ); 581 state.recent = { id: product.id, type: state.type }; 582 clearTimeout(recentTimer); 583 renderTable(); 584 recentTimer = setTimeout(() => { 585 state.recent = null; 586 $$('.mi-demo-updated').forEach((row) => { 587 row.classList.remove('mi-demo-updated'); 588 row.removeAttribute('data-change'); 589 const note = row.querySelector('.mi-demo-sr'); 590 if (note) note.remove(); 591 }); 592 }, 3000); 593 }); 594 $('#mi-demo-search').addEventListener('input', (event) => { 595 state.query = event.target.value; 596 renderTable(); 597 }); 598 $('#mi-demo-view').addEventListener('change', (event) => { 599 state.view = event.target.value; 600 renderTable(); 601 }); 602 $$('[data-sort]').forEach((button) => 603 button.addEventListener('click', () => { 604 state.direction = state.sort === button.dataset.sort ? -state.direction : 1; 605 state.sort = button.dataset.sort; 606 renderTable(); 607 }) 608 ); 609 $$('.mi-demo-columns input').forEach((input) => 610 input.addEventListener('change', () => { 611 if (!input.checked && state.hidden.size === 3) { 612 input.checked = true; 613 return; 614 } 615 if (input.checked) state.hidden.delete(input.value); 616 else state.hidden.add(input.value); 617 renderTable(); 618 }) 619 ); 620 $('#mi-demo-reset').addEventListener('click', () => { 621 clearTimeout(recentTimer); 622 products = data.map((product) => ({ 623 ...product, 624 units: product.stock.map((quantity) => quantity * 1000), 625 })); 626 Object.assign(state, { 627 selected: null, 628 type: 'IN', 629 sort: 'name', 630 direction: 1, 631 query: '', 632 view: 'all', 633 recent: null, 634 activeOption: -1, 635 }); 636 state.hidden.clear(); 637 $('#mi-demo-form').reset(); 638 updateQuantityControls(); 639 $('#mi-demo-product').value = ''; 640 $('#mi-demo-search').value = ''; 641 $('#mi-demo-view').value = 'all'; 642 $$('.mi-demo-columns input').forEach((input) => { 643 input.checked = true; 644 }); 645 $('#mi-demo-column-picker').open = false; 646 hideOptions(); 647 updateType(); 648 renderTable(); 649 message( 650 'success', 651 'mi_demo_reset_done', 652 'Demo reset. All sample quantities have been restored.' 653 ); 654 }); 655 $('#mi-demo-table').addEventListener('click', (event) => { 656 const button = event.target.closest('[data-image]'); 657 if (!button) return; 658 const product = products.find((item) => item.id === button.dataset.image); 659 if (!product) return; 660 const dialog = $('#mi-demo-lightbox'); 661 dialog.dataset.product = product.id; 662 $('#mi-demo-large-visual').innerHTML = imageMarkup(product, true); 663 $('#mi-demo-image-title').textContent = productName(product); 664 dialog.showModal(); 665 }); 666 $('.mi-demo-lightbox-close').addEventListener('click', () => $('#mi-demo-lightbox').close()); 667 $('#mi-demo-lightbox').addEventListener('close', () => { 668 const product = products.find((item) => item.id === $('#mi-demo-lightbox').dataset.product); 669 const trigger = product && $('[data-image="' + product.id + '"]'); 670 if (trigger) trigger.focus({ preventScroll: true }); 671 }); 672 $('#mi-demo-lightbox').addEventListener('click', (event) => { 673 if (event.target === $('#mi-demo-lightbox')) { 674 const bounds = event.target.getBoundingClientRect(); 675 if ( 676 event.clientX < bounds.left || 677 event.clientX > bounds.right || 678 event.clientY < bounds.top || 679 event.clientY > bounds.bottom 680 ) 681 event.target.close(); 682 } 683 }); 684 function exportRows() { 685 const columns = ['name', 'barcode', 'stock', 'image'].filter( 686 (column) => !state.hidden.has(column) 687 ); 688 const titles = headers(); 689 const rows = [columns.map((column) => titles[column])]; 690 visibleProducts().forEach((product) => 691 rows.push( 692 columns.map((column) => 693 column === 'name' 694 ? productName(product) 695 : column === 'stock' 696 ? units(product) / 1000 697 : column === 'barcode' 698 ? product.barcode 699 : productName(product) 700 ) 701 ) 702 ); 703 return rows; 704 } 705 function worksheet(rows) { 706 const letter = (index) => { 707 let text = ''; 708 for (let value = index + 1; value > 0; value = Math.floor((value - 1) / 26)) 709 text = String.fromCharCode(65 + ((value - 1) % 26)) + text; 710 return text; 711 }; 712 return ( 713 '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>' + 714 rows 715 .map(
716 (row, r) => 717 '<row r="' + 718 (r + 1) + 719 '">' + 720 row 721 .map((value, c) => 722 typeof value === 'number' 723 ? '<c r="' + letter(c) + (r + 1) + '"><v>' + value + '</v></c>' 724 : '<c r="' + 725 letter(c) + 726 (r + 1) + 727 '" t="inlineStr"><is><t xml:space="preserve">' + 728 esc(value) + 729 '</t></is></c>' 730 ) 731 .join('') + 732 '</row>' 733 ) 734 .join('') + 735 '</sheetData></worksheet>' 736 ); 737 } 738 // Standard uncompressed ZIP container. Each workbook entry receives its own CRC-32. 739 function zip(files) { 740 const encoder = new TextEncoder(); 741 const parts = []; 742 const directory = []; 743 let offset = 0; 744 const crc = (bytes) => { 745 let value = 0xffffffff; 746 for (const byte of bytes) { 747 value ^= byte; 748 for (let bit = 0; bit < 8; bit++) value = (value >>> 1) ^ (value & 1 ? 0xedb88320 : 0); 749 } 750 return (value ^ 0xffffffff) >>> 0; 751 }; 752 const header = (size) => new Uint8Array(size); 753 const set = (bytes, pos, value, size) => { 754 const view = new DataView(bytes.buffer); 755 if (size === 2) view.setUint16(pos, value, true); 756 else view.setUint32(pos, value, true); 757 }; 758 for (const [name, text] of Object.entries(files)) { 759 const nameBytes = encoder.encode(name); 760 const bytes = encoder.encode(text); 761 const checksum = crc(bytes); 762 const local = header(30); 763 set(local, 0, 0x04034b50, 4); 764 set(local, 4, 20, 2); 765 set(local, 6, 0x800, 2); 766 set(local, 12, 33, 2); 767 set(local, 14, checksum, 4); 768 set(local, 18, bytes.length, 4); 769 set(local, 22, bytes.length, 4); 770 set(local, 26, nameBytes.length, 2); 771 parts.push(local, nameBytes, bytes); 772 const central = header(46); 773 set(central, 0, 0x02014b50, 4); 774 set(central, 4, 20, 2); 775 set(central, 6, 20, 2); 776 set(central, 8, 0x800, 2); 777 set(central, 14, 33, 2); 778 set(central, 16, checksum, 4); 779 set(central, 20, bytes.length, 4); 780 set(central, 24, bytes.length, 4); 781 set(central, 28, nameBytes.length, 2); 782 set(central, 42, offset, 4); 783 directory.push(central, nameBytes); 784 offset += local.length + nameBytes.length + bytes.length; 785 } 786 const length = directory.reduce((sum, item) => sum + item.length, 0); 787 const end = header(22); 788 set(end, 0, 0x06054b50, 4); 789 set(end, 8, Object.keys(files).length, 2); 790 set(end, 10, Object.keys(files).length, 2); 791 set(end, 12, length, 4); 792 set(end, 16, offset, 4); 793 return new Blob([...parts, ...directory, end], { 794 type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 795 }); 796 } 797 function workbook() { 798 const stocks = [ 799 [ 800 t('trans_Product name', 'Product name'), 801 t('trans_barcode', 'Barcode'), 802 ...locations.map(locationName), 803 t('trans_Total stock', 'Total stock'), 804 ], 805 ...visibleProducts().map((product) => [ 806 productName(product), 807 product.barcode, 808 ...product.units.map((quantity) => quantity / 1000), 809 product.units.reduce((sum, value) => sum + value, 0) / 1000, 810 ]), 811 ]; 812 const files = { 813 '[Content_Types].xml': 814 '<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/worksheets/sheet2.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/></Types>', 815 '_rels/.rels': 816 '<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>', 817 'xl/workbook.xml': 818 '<?xml version="1.0" encoding="UTF-8"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="Products" sheetId="1" r:id="rId1"/><sheet name="Stock by location" sheetId="2" r:id="rId2"/></sheets></workbook>', 819 'xl/_rels/workbook.xml.rels': 820 '<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet2.xml"/></Relationships>', 821 'xl/worksheets/sheet1.xml': worksheet(exportRows()), 822 'xl/worksheets/sheet2.xml': worksheet(stocks), 823 }; 824 return zip(files); 825 } 826 $('#mi-demo-export').addEventListener('click', () => { 827 try { 828 const blob = workbook(); 829 const url = URL.createObjectURL(blob); 830 const link = document.createElement('a'); 831 link.href = url; 832 link.download = 'Mobile-Inventory-demo-' + new Date().toISOString().slice(0, 10) + '.xlsx'; 833 link.style.display = 'none'; 834 document.body.appendChild(link); 835 link.click(); 836 link.remove(); 837 setTimeout(() => URL.revokeObjectURL(url), 10000); 838 } catch (error) { 839 message('error', 'mi_demo_export_error', 'The file could not be created. Please try again.'); 840 } 841 }); 842 $('#mi-demo-print').addEventListener('click', () => { 843 const rows = exportRows(); 844 const columns = ['name', 'barcode', 'stock', 'image'].filter( 845 (column) => !state.hidden.has(column) 846 ); 847 const printProducts = visibleProducts(); 848 const frame = document.createElement('iframe'); 849 frame.className = 'mi-demo-print-frame'; 850 frame.title = t('print', 'Print'); 851 frame.style.cssText = 852 'position:fixed;width:1px;height:1px;inset:0;opacity:0;pointer-events:none;border:0'; 853 frame.onload = async () => { 854 const printWindow = frame.contentWindow; 855 if (!printWindow) return; 856 printWindow.addEventListener('afterprint', () => frame.remove(), { once: true }); 857 await Promise.allSettled([
858 ...Array.from(printWindow.document.images, (img) => img.decode()), 859 printWindow.document.fonts.ready, 860 ]); 861 await new Promise((resolve) => 862 printWindow.requestAnimationFrame(() => printWindow.requestAnimationFrame(resolve)) 863 ); 864 if (frame.isConnected) { 865 printWindow.focus(); 866 printWindow.print(); 867 } 868 }; 869 frame.srcdoc = 870 '<!doctype html><html lang="' + 871 esc(document.documentElement.lang || 'en') + 872 '" dir="' + 873 esc(document.documentElement.dir || 'ltr') + 874 '"><head><meta charset="utf-8"><title>Mobile Inventory ' + 875 esc(t('mi_demo_products', 'Demo products')) + 876 '</title><style>body{font:14px Arial,sans-serif;color:#222;padding:20px}h1{font-size:22px}table{border-collapse:collapse;width:100%}td,th{border:1px solid #ccc;padding:9px;text-align:start}th{background:#eee}tr{break-inside:avoid}img{display:block;width:50px;height:50px;object-fit:contain}</style></head><body><h1>Mobile Inventory: ' + 877 esc(t('mi_demo_products', 'Demo products')) + 878 '</h1><table><thead><tr>' + 879 rows[0].map((value) => '<th>' + esc(value) + '</th>').join('') + 880 '</tr></thead><tbody>' + 881 rows 882 .slice(1) 883 .map( 884 (row, index) => 885 '<tr>' + 886 row 887 .map( 888 (value, column) => 889 '<td>' + 890 (columns[column] === 'image' 891 ? imageMarkup(printProducts[index], false).replace(' loading="lazy"', '') 892 : esc(typeof value === 'number' ? number(value) : value)) + 893 '</td>' 894 ) 895 .join('') + 896 '</tr>' 897 ) 898 .join('') + 899 '</tbody></table></body></html>'; 900 document.body.appendChild(frame); 901 }); 902 const columnPicker = $('#mi-demo-column-picker'); 903 document.addEventListener('click', (event) => { 904 if (!columnPicker.contains(event.target)) columnPicker.open = false; 905 }); 906 columnPicker.addEventListener('keydown', (event) => { 907 if (event.key === 'Escape') { 908 columnPicker.open = false; 909 columnPicker.querySelector('summary').focus(); 910 } 911 }); 912 document.addEventListener('hello:languagechange', refreshLabels); 913 refreshLabels(); 914})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.