var scanner = (function() { var _callback = null; var _allow_raw = true; var _allowed_types = null; var _camera = null; var _modal = null; var _buffer = ''; var _last_key_ts = 0; var _max_key_gap = 0; var _HW_THRESHOLD = 50; function _on_paste(e) { if (!_callback) return; var el = document.activeElement; if (el && el.tagName === 'TEXTAREA') return; var text = ((e.originalEvent || e).clipboardData || window.clipboardData).getData('text'); if (!text) return; text = text.trim(); if (text.length < 3) return; e.preventDefault(); _resolve(text); } function _on_keydown(e) { var now = Date.now(); var gap = now - _last_key_ts; if (gap > 500) { _buffer = ''; _max_key_gap = 0; } else if (_buffer.length > 0) { _max_key_gap = Math.max(_max_key_gap, gap); } _last_key_ts = now; if (e.key === 'Enter' && _buffer.length >= 3) { var barcode = _buffer; var is_scanner_input = _max_key_gap <= _HW_THRESHOLD; _buffer = ''; _max_key_gap = 0; if (is_scanner_input) { e.preventDefault(); _remove_typed_barcode(barcode); _resolve(barcode); } return; } if (e.key.length === 1) { _buffer += e.key; } } function _remove_typed_barcode(barcode) { var el = document.activeElement; if (!el || !('value' in el)) return; if (el.tagName !== 'INPUT' && el.tagName !== 'TEXTAREA') return; var value = String(el.value || ''); if (!value.endsWith(barcode)) return; el.value = value.slice(0, value.length - barcode.length); $(el).trigger('input'); } function _resolve(barcode) { if (!barcode || !_callback) return; $.ajax({ url: server_url + 'ics/api/engine/barcode_lookup.php', type: 'POST', dataType: 'json', data: { json: JSON.stringify({ barcode: barcode, action: 'read' }) }, headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') }, success: function(res) { if (res && res.success) { if ((!_allow_raw && res.type === 'raw') || (_allowed_types && _allowed_types.indexOf(res.type) === -1)) { _show_error('This page only accepts generated barcode labels.'); return; } _callback(res.sku || '', res.product_name || '', res.uom || '', res); return; } _show_error('Barcode not found: ' + barcode); }, error: function(xhr) { var msg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : 'Scanner error. Please try again.'; _show_error(msg === 'Barcode not found.' ? 'Barcode not found: ' + barcode : msg); } }); } function _build_modal() { if (_modal) return; $('body').append(` `); _modal = new bootstrap.Modal(document.getElementById('scanner_modal')); document.getElementById('scanner_modal').addEventListener('hidden.bs.modal', _stop_camera); } function _start_camera() { if (_camera) return; if (typeof Html5Qrcode === 'undefined') { _show_error('Camera scanner library is not available.'); return; } _camera = new Html5Qrcode('scanner_qr_reader'); _camera.start( { facingMode: 'environment' }, { fps: 10, qrbox: { width: 250, height: 120 } }, function(decodedText) { _modal.hide(); _resolve(decodedText); }, function() {} ).catch(function(err) { _show_error('Camera error: ' + err); }); } function _stop_camera() { if (!_camera) return; _camera.stop().catch(function() {}).finally(function() { _camera = null; }); } function _show_error(msg) { var $err = $('#scanner_error'); if ($err.length) { $err.text(msg).removeClass('d-none'); setTimeout(function() { $err.addClass('d-none'); }, 3000); return; } if (typeof bootbox !== 'undefined') bootbox.alert(msg); } return { init: function(options) { options = options || {}; _callback = options.onScan || null; _allow_raw = options.allowRaw !== false; _allowed_types = Array.isArray(options.allowedTypes) ? options.allowedTypes : null; document.removeEventListener('keydown', _on_keydown); document.removeEventListener('paste', _on_paste); document.addEventListener('keydown', _on_keydown); document.addEventListener('paste', _on_paste); _build_modal(); }, open_camera: function() { if (!_modal) _build_modal(); _modal.show(); setTimeout(_start_camera, 400); }, destroy: function() { document.removeEventListener('keydown', _on_keydown); document.removeEventListener('paste', _on_paste); _stop_camera(); } }; })();