diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 362f4c79b3a..1acd7ee670d 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -72,7 +72,8 @@ 'Email', 'Name', 'Phone', - 'URL' + 'URL', + 'Barcode' ) default_fields = ( diff --git a/frappe/public/icons/timeless/symbol-defs.svg b/frappe/public/icons/timeless/symbol-defs.svg index 6108daa938a..0e8e24b7683 100644 --- a/frappe/public/icons/timeless/symbol-defs.svg +++ b/frappe/public/icons/timeless/symbol-defs.svg @@ -563,6 +563,10 @@ + + + diff --git a/frappe/public/js/desk.bundle.js b/frappe/public/js/desk.bundle.js index 66eb72cda07..c4ecf67c4f0 100644 --- a/frappe/public/js/desk.bundle.js +++ b/frappe/public/js/desk.bundle.js @@ -103,3 +103,4 @@ import "./frappe/ui/datatable.js"; import "./frappe/ui/driver.js"; import "./frappe/ui/plyr.js"; import "./frappe/barcode_scanner/index.js"; +import "./frappe/scanner"; diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index 977789fc1b8..e4a7dd6d593 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -67,6 +67,10 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp if (this.df.options == 'URL') { this.setup_url_field(); } + + if (this.df.options == 'Barcode') { + this.setup_barcode_field(); + } } setup_url_field() { @@ -113,6 +117,43 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp }); } + setup_barcode_field() { + this.$wrapper.find('.control-input').append( + ` + + ${frappe.utils.icon('scan', 'sm')} + + ` + ); + + this.$scan_btn = this.$wrapper.find('.link-btn'); + + this.$input.on("focus", () => { + setTimeout(() => { + this.$scan_btn.toggle(true); + }, 500); + }); + + const me = this; + this.$scan_btn.on('click', 'a', () => { + new frappe.ui.Scanner({ + dialog: true, + multiple: false, + on_scan(data) { + if (data && data.result && data.result.text) { + me.set_value(data.result.text); + } + } + }); + }); + + this.$input.on("blur", () => { + setTimeout(() => { + this.$scan_btn.toggle(false); + }, 500); + }); + } + bind_change_event() { const change_handler = e => { if (this.change) this.change(e); diff --git a/frappe/public/js/frappe/scanner/index.js b/frappe/public/js/frappe/scanner/index.js new file mode 100644 index 00000000000..0b9acf7f9e6 --- /dev/null +++ b/frappe/public/js/frappe/scanner/index.js @@ -0,0 +1,101 @@ +frappe.provide("frappe.ui"); + +frappe.ui.Scanner = class Scanner { + constructor(options) { + this.dialog = null; + this.handler = null; + this.options = options; + this.is_alive = false; + + if (!("multiple" in this.options)) { + this.options.multiple = false; + } + if (options.container) { + this.$scan_area = $(options.container); + this.scan_area_id = frappe.dom.set_unique_id(this.$scan_area); + } + if (options.dialog) { + this.dialog = this.make_dialog(); + this.dialog.show(); + } + } + + scan() { + this.load_lib().then(() => this.start_scan()); + } + + start_scan() { + if (!this.handler) { + this.handler = new Html5Qrcode(this.scan_area_id); // eslint-disable-line + } + this.handler + .start( + { facingMode: "environment" }, + { fps: 10, qrbox: 250 }, + (decodedText, decodedResult) => { + if (this.options.on_scan) { + try { + this.options.on_scan(decodedResult); + } catch (error) { + console.error(error); // eslint-disable-line + } + } + if (!this.options.multiple) { + this.stop_scan(); + this.hide_dialog(); + } + }, + errorMessage => { // eslint-disable-line + // parse error, ignore it. + } + ) + .catch(err => { + this.is_alive = false; + this.hide_dialog(); + console.error(err); // eslint-disable-line + }); + this.is_alive = true; + } + + stop_scan() { + if (this.handler && this.is_alive) { + this.handler.stop().then(() => { + this.is_alive = false; + this.$scan_area.empty(); + this.hide_dialog(); + }); + } + } + + make_dialog() { + let dialog = new frappe.ui.Dialog({ + title: __("Scan QRCode"), + fields: [ + { + fieldtype: "HTML", + fieldname: "scan_area" + } + ], + on_page_show: () => { + this.$scan_area = dialog.get_field("scan_area").$wrapper; + this.$scan_area.addClass("barcode-scanner"); + this.scan_area_id = frappe.dom.set_unique_id(this.$scan_area); + this.scan(); + }, + on_hide: () => { + this.stop_scan(); + } + }); + return dialog; + } + + hide_dialog() { + this.dialog && this.dialog.hide(); + } + + load_lib() { + return frappe.require( + "/assets/frappe/node_modules/html5-qrcode/dist/html5-qrcode.min.js" + ); + } +}; diff --git a/package.json b/package.json index c099b14adeb..5b9504e142f 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "frappe-gantt": "^0.5.0", "fuse.js": "^3.4.6", "highlight.js": "^10.4.1", + "html5-qrcode": "^2.0.11", "jquery": "3.5.0", "js-sha256": "^0.9.0", "jsbarcode": "^3.9.0", diff --git a/yarn.lock b/yarn.lock index 899e0955498..11a774780e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3183,6 +3183,11 @@ hsla-regex@^1.0.0: resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= +html5-qrcode@^2.0.11: + version "2.0.11" + resolved "https://registry.yarnpkg.com/html5-qrcode/-/html5-qrcode-2.0.11.tgz#2cc5f63e767be53dd6c6d56b6c4f180a12aa8075" + integrity sha512-cCrVOK2yJGPGSTjchqRhkBJIrxvojEwF/pDKLNxmTH1wiAsVc61ZnIqyApIVyNfn5dKRFax70Qpr7pZwbUNiUw== + http-cache-semantics@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"