From e744aa8dc768d8038c8c93a6f8b12db2df028b0a Mon Sep 17 00:00:00 2001 From: Katharina Bogad Date: Wed, 23 Jun 2021 19:50:13 +0200 Subject: [PATCH] scoping: refactor components, GrocyProxy. --- js/components/BasePicker.js | 99 ++++++ js/components/barcodescanner.js | 349 ++++++++++----------- js/components/batterycard.js | 41 ++- js/components/calendarcard.js | 89 +++--- js/components/chorecard.js | 42 +-- js/components/datetimepicker.js | 257 ++++++++-------- js/components/datetimepicker2.js | 312 +------------------ js/components/locationpicker.js | 80 +---- js/components/numberpicker.js | 225 +++++++++----- js/components/productamountpicker.js | 148 +++++---- js/components/productcard.js | 161 +++++----- js/components/productpicker.js | 385 ++++++++++++------------ js/components/recipepicker.js | 77 +---- js/components/shoppinglocationpicker.js | 77 +---- js/components/userfieldsform.js | 71 +++-- js/components/userpicker.js | 110 +++---- js/grocy.js | 16 +- js/helpers/extensions.js | 3 - js/lib/proxy.js | 35 ++- package.json | 3 +- yarn.lock | 3 +- 21 files changed, 1189 insertions(+), 1394 deletions(-) create mode 100644 js/components/BasePicker.js diff --git a/js/components/BasePicker.js b/js/components/BasePicker.js new file mode 100644 index 00000000..63661b17 --- /dev/null +++ b/js/components/BasePicker.js @@ -0,0 +1,99 @@ +class BasePicker +{ + + constructor(Grocy, basename, scopeSelector = null) + { + this.Grocy = Grocy; + + this.scopeSelector = scopeSelector; + this.scope = scopeSelector != null ? $(scope) : $(document); + this.$ = scopeSelector != null ? $(scope).find : $; + + this.picker = null; + this.input_element; + + this.basename = basename; + } + + prefill() + { + if (this.picker == null) + { + console.error("Cannot prefill " + this.basename + ", picker not set."); + return; + } + + var doFokus = false; + var prefillByName = this.picker.parent().data('prefill-by-name').toString(); + if (typeof prefillByName !== "undefined") + { + var possibleOptionElement = $(this.basename + " option:contains(\"" + prefillByName + "\")").first(); + + if (possibleOptionElement.length > 0) + { + doFokus = true; + this.picker.val(possibleOptionElement.val()); + } + } + + var prefillById = this.picker.parent().data('prefill-by-id').toString(); + if (typeof prefillById !== "undefined") + { + doFokus = true; + this.picker.val(prefillById); + } + + if (doFokus) + { + this.picker.data('combobox').refresh(); + this.picker.trigger('change'); + + this.$(this.picker.parent().data('next-input-selector').toString()).focus(); + } + } + + initCombobox(selector) + { + this.$(selector).combobox({ + appendId: '_text_input', + bsVersion: '4', + clearIfNoMatch: false + }); + } + + GetPicker() + { + return this.picker; + } + + GetInputElement() + { + return this.input_element; + } + + GetValue() + { + return this.picker.val(); + } + + SetValue(value) + { + this.input_element.val(value); + this.input_element.trigger('change'); + } + + SetId(value) + { + this.picker.val(value); + this.picker.data('combobox').refresh(); + this.input_element.trigger('change'); + } + + Clear() + { + this.SetValue(''); + this.SetId(null); + } +} + +export default BasePicker; \ No newline at end of file diff --git a/js/components/barcodescanner.js b/js/components/barcodescanner.js index 0e90875d..9adc91ec 100644 --- a/js/components/barcodescanner.js +++ b/js/components/barcodescanner.js @@ -1,11 +1,159 @@ import Quagga from '@ericblade/quagga2/dist/quagga'; -function barcodescanner(Grocy) +class barcodescanner { - Grocy.Components.BarcodeScanner = {}; + constructor(Grocy, scopeSelector = null) + { + this.Grocy = Grocy; - Grocy.Components.BarcodeScanner.LiveVideoSizeAdjusted = false; - Grocy.Components.BarcodeScanner.CheckCapabilities = async function() + this.scopeSelector = scopeSelector; + this.scope = scopeSelector != null ? $(scope) : $(document); + this.$ = scopeSelector != null ? $(scope).find : $; + + // init component + this.LiveVideoSizeAdjusted = false; + + var self = this; + + Quagga.onDetected(function(result) + { + $.each(result.codeResult.decodedCodes, function(id, error) + { + if (error.error != undefined) + { + self.DecodedCodesCount++; + self.DecodedCodesErrorCount += parseFloat(error.error); + } + }); + + if (self.DecodedCodesErrorCount / self.DecodedCodesCount < 0.15) + { + self.StopScanning(); + $(document).trigger("Grocy.BarcodeScanned", [result.codeResult.code, self.CurrentTarget]); + } + }); + + Quagga.onProcessed(function(result) + { + var drawingCtx = Quagga.canvas.ctx.overlay; + var drawingCanvas = Quagga.canvas.dom.overlay; + + if (result) + { + if (result.boxes) + { + drawingCtx.clearRect(0, 0, parseInt(drawingCanvas.getAttribute("width")), parseInt(drawingCanvas.getAttribute("height"))); + result.boxes.filter(function(box) + { + return box !== result.box; + }).forEach(function(box) + { + Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, drawingCtx, { color: "yellow", lineWidth: 4 }); + }); + } + + if (result.box) + { + Quagga.ImageDebug.drawPath(result.box, { x: 0, y: 1 }, drawingCtx, { color: "green", lineWidth: 4 }); + } + + if (result.codeResult && result.codeResult.code) + { + Quagga.ImageDebug.drawPath(result.line, { x: 'x', y: 'y' }, drawingCtx, { color: "red", lineWidth: 4 }); + } + } + }); + + this.scope.on("click", "#barcodescanner-start-button", async function(e) + { + e.preventDefault(); + var inputElement = $(e.currentTarget).prev(); + if (inputElement.hasAttr("disabled")) + { + // Do nothing and disable the barcode scanner start button + $(e.currentTarget).addClass("disabled"); + return; + } + + self.CurrentTarget = inputElement.attr("data-target"); + + var dialog = bootbox.dialog({ + message: '
', + title: Grocy.translate('Scan a barcode'), + onEscape: function() + { + self.StopScanning(); + }, + size: 'big', + backdrop: true, + closeButton: true, + buttons: { + torch: { + label: '', + className: 'btn-warning responsive-button torch', + callback: function() + { + self.TorchOn(Quagga.CameraAccess.getActiveTrack()); + return false; + } + }, + cancel: { + label: Grocy.translate('Cancel'), + className: 'btn-secondary responsive-button', + callback: function() + { + self.StopScanning(); + } + } + } + }); + + // Add camera select to existing dialog + dialog.find('.bootbox-body').append('
'); + var cameraSelect = document.querySelector('.cameraSelect'); + + if (cameraSelect != null) + { + var cameras = await Quagga.CameraAccess.enumerateVideoDevices(); + cameras.forEach(camera => + { + var option = document.createElement("option"); + option.text = camera.label ? camera.label : camera.deviceId; // Use camera label if it exists, else show device id + option.value = camera.deviceId; + cameraSelect.appendChild(option); + }); + + // Set initial value to preferred camera if one exists - and if not, start out empty + cameraSelect.value = window.localStorage.getItem('cameraId'); + + cameraSelect.onchange = function() + { + window.localStorage.setItem('cameraId', cameraSelect.value); + Quagga.stop(); + self.StartScanning(); + }; + } + + self.StartScanning(); + }); + + setTimeout(function() + { + this.$scope(".barcodescanner-input:visible").each(function() + { + if ($(this).hasAttr("disabled")) + { + $(this).after(''); + } + else + { + $(this).after(''); + } + }); + }, 50); + } + + async CheckCapabilities() { var track = Quagga.CameraAccess.getActiveTrack(); var capabilities = {}; @@ -16,61 +164,64 @@ function barcodescanner(Grocy) // If there is more than 1 camera, show the camera selection var cameras = await Quagga.CameraAccess.enumerateVideoDevices(); - var cameraSelect = document.querySelector('.cameraSelect-wrapper'); - if (cameraSelect) + var cameraSelect = this.$('.cameraSelect-wrapper'); + if (cameraSelect.length) { - cameraSelect.style.display = cameras.length > 1 ? 'inline-block' : 'none'; + cameraSelect[0].style.display = cameras.length > 1 ? 'inline-block' : 'none'; } // Check if the camera is capable to turn on a torch. var canTorch = typeof capabilities.torch === 'boolean' && capabilities.torch // Remove the torch button, if either the device can not torch or AutoTorchOn is set. - var node = document.querySelector('.torch'); - if (node) + var node = this.$('.torch'); + if (node.length) { - node.style.display = canTorch && !Grocy.FeatureFlags.GROCY_FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA ? 'inline-block' : 'none'; + node[0].style.display = canTorch && !this.Grocy.FeatureFlags.GROCY_FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA ? 'inline-block' : 'none'; } // If AutoTorchOn is set, turn on the torch. - if (canTorch && Grocy.FeatureFlags.GROCY_FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA) + if (canTorch && this.Grocy.FeatureFlags.GROCY_FEATURE_FLAG_AUTO_TORCH_ON_WITH_CAMERA) { - Grocy.Components.BarcodeScanner.TorchOn(track); + this.TorchOn(track); } // Reduce the height of the video, if it's higher than then the viewport - if (!Grocy.Components.BarcodeScanner.LiveVideoSizeAdjusted) + if (!this.LiveVideoSizeAdjusted) { - var bc = document.getElementById('barcodescanner-container'); - if (bc) + var bc = this.$('barcodescanner-container'); + if (bc.length) { + bc = bc[0] var bcAspectRatio = bc.offsetWidth / bc.offsetHeight; var settings = track.getSettings(); if (bcAspectRatio > settings.aspectRatio) { - var v = document.querySelector('#barcodescanner-livestream video') - if (v) + var v = this.$('#barcodescanner-livestream video') + if (v.length) { - var c = document.querySelector('#barcodescanner-livestream canvas') + v = v[0]; + var c = this.$('#barcodescanner-livestream canvas')[0] var newWidth = v.clientWidth / bcAspectRatio * settings.aspectRatio + 'px'; v.style.width = newWidth; c.style.width = newWidth; } } - Grocy.Components.BarcodeScanner.LiveVideoSizeAdjusted = true; + this.LiveVideoSizeAdjusted = true; } } } - Grocy.Components.BarcodeScanner.StartScanning = function() + StartScanning() { - Grocy.Components.BarcodeScanner.DecodedCodesCount = 0; - Grocy.Components.BarcodeScanner.DecodedCodesErrorCount = 0; + this.DecodedCodesCount = 0; + this.DecodedCodesErrorCount = 0; + var self = this; Quagga.init({ inputStream: { name: "Live", type: "LiveStream", - target: document.querySelector("#barcodescanner-livestream"), + target: this.$("#barcodescanner-livestream")[0], constraints: { facingMode: "environment", ...(window.localStorage.getItem('cameraId') && { deviceId: window.localStorage.getItem('cameraId') }) // If preferred cameraId is set, request to use that specific camera @@ -124,8 +275,8 @@ function barcodescanner(Grocy) console.error(error); if (error) { - Grocy.FrontendHelpers.ShowGenericError("Error while initializing the barcode scanning library", error.message); - toastr.info(Grocy.translate("Camera access is only possible when supported and allowed by your browser and when grocy is served via a secure (https://) connection")); + self.Grocy.FrontendHelpers.ShowGenericError("Error while initializing the barcode scanning library", error.message); + toastr.info(self.Grocy.translate("Camera access is only possible when supported and allowed by your browser and when grocy is served via a secure (https://) connection")); window.localStorage.removeItem("cameraId"); setTimeout(function() { @@ -134,23 +285,23 @@ function barcodescanner(Grocy) return; } - Grocy.Components.BarcodeScanner.CheckCapabilities(); + this.CheckCapabilities(); Quagga.start(); }); } - Grocy.Components.BarcodeScanner.StopScanning = function() + StopScanning() { Quagga.stop(); - Grocy.Components.BarcodeScanner.DecodedCodesCount = 0; - Grocy.Components.BarcodeScanner.DecodedCodesErrorCount = 0; + this.DecodedCodesCount = 0; + this.DecodedCodesErrorCount = 0; bootbox.hideAll(); } - Grocy.Components.BarcodeScanner.TorchOn = function(track) + TorchOn(track) { if (track) { @@ -163,144 +314,6 @@ function barcodescanner(Grocy) }); } } - - Quagga.onDetected(function(result) - { - $.each(result.codeResult.decodedCodes, function(id, error) - { - if (error.error != undefined) - { - Grocy.Components.BarcodeScanner.DecodedCodesCount++; - Grocy.Components.BarcodeScanner.DecodedCodesErrorCount += parseFloat(error.error); - } - }); - - if (Grocy.Components.BarcodeScanner.DecodedCodesErrorCount / Grocy.Components.BarcodeScanner.DecodedCodesCount < 0.15) - { - Grocy.Components.BarcodeScanner.StopScanning(); - $(document).trigger("Grocy.BarcodeScanned", [result.codeResult.code, Grocy.Components.BarcodeScanner.CurrentTarget]); - } - }); - - Quagga.onProcessed(function(result) - { - var drawingCtx = Quagga.canvas.ctx.overlay; - var drawingCanvas = Quagga.canvas.dom.overlay; - - if (result) - { - if (result.boxes) - { - drawingCtx.clearRect(0, 0, parseInt(drawingCanvas.getAttribute("width")), parseInt(drawingCanvas.getAttribute("height"))); - result.boxes.filter(function(box) - { - return box !== result.box; - }).forEach(function(box) - { - Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, drawingCtx, { color: "yellow", lineWidth: 4 }); - }); - } - - if (result.box) - { - Quagga.ImageDebug.drawPath(result.box, { x: 0, y: 1 }, drawingCtx, { color: "green", lineWidth: 4 }); - } - - if (result.codeResult && result.codeResult.code) - { - Quagga.ImageDebug.drawPath(result.line, { x: 'x', y: 'y' }, drawingCtx, { color: "red", lineWidth: 4 }); - } - } - }); - - $(document).on("click", "#barcodescanner-start-button", async function(e) - { - e.preventDefault(); - var inputElement = $(e.currentTarget).prev(); - if (inputElement.hasAttr("disabled")) - { - // Do nothing and disable the barcode scanner start button - $(e.currentTarget).addClass("disabled"); - return; - } - - Grocy.Components.BarcodeScanner.CurrentTarget = inputElement.attr("data-target"); - - var dialog = bootbox.dialog({ - message: '
', - title: Grocy.translate('Scan a barcode'), - onEscape: function() - { - Grocy.Components.BarcodeScanner.StopScanning(); - }, - size: 'big', - backdrop: true, - closeButton: true, - buttons: { - torch: { - label: '', - className: 'btn-warning responsive-button torch', - callback: function() - { - Grocy.Components.BarcodeScanner.TorchOn(Quagga.CameraAccess.getActiveTrack()); - return false; - } - }, - cancel: { - label: Grocy.translate('Cancel'), - className: 'btn-secondary responsive-button', - callback: function() - { - Grocy.Components.BarcodeScanner.StopScanning(); - } - } - } - }); - - // Add camera select to existing dialog - dialog.find('.bootbox-body').append('
'); - var cameraSelect = document.querySelector('.cameraSelect'); - - if (cameraSelect != null) - { - var cameras = await Quagga.CameraAccess.enumerateVideoDevices(); - cameras.forEach(camera => - { - var option = document.createElement("option"); - option.text = camera.label ? camera.label : camera.deviceId; // Use camera label if it exists, else show device id - option.value = camera.deviceId; - cameraSelect.appendChild(option); - }); - - // Set initial value to preferred camera if one exists - and if not, start out empty - cameraSelect.value = window.localStorage.getItem('cameraId'); - - cameraSelect.onchange = function() - { - window.localStorage.setItem('cameraId', cameraSelect.value); - Quagga.stop(); - Grocy.Components.BarcodeScanner.StartScanning(); - }; - } - - Grocy.Components.BarcodeScanner.StartScanning(); - }); - - setTimeout(function() - { - $(".barcodescanner-input:visible").each(function() - { - if ($(this).hasAttr("disabled")) - { - $(this).after(''); - } - else - { - $(this).after(''); - } - }); - }, 50); - } export { barcodescanner } \ No newline at end of file diff --git a/js/components/batterycard.js b/js/components/batterycard.js index f2159d19..0c264b62 100644 --- a/js/components/batterycard.js +++ b/js/components/batterycard.js @@ -1,28 +1,37 @@ import { EmptyElementWhenMatches } from '../helpers/extensions' import { RefreshContextualTimeago } from '../configs/timeago' -function batterycard(Grocy) +class batterycard { - - Grocy.Components.BatteryCard = {}; - - Grocy.Components.BatteryCard.Refresh = function(batteryId) + constructor(Grocy, scopeSelector = null) { - Grocy.Api.Get('batteries/' + batteryId, + this.Grocy = Grocy; + + this.scopeSelector = scopeSelector; + this.scope = scopeSelector != null ? $(scope) : $(document); + this.$ = scopeSelector != null ? $(scope).find : $; + } + + Refresh(batteryId) + { + var self = this; + this.Grocy.Api.Get('batteries/' + batteryId, function(batteryDetails) { - $('#batterycard-battery-name').text(batteryDetails.battery.name); - $('#batterycard-battery-used_in').text(batteryDetails.battery.used_in); - $('#batterycard-battery-last-charged').text((batteryDetails.last_charged || Grocy.translate('never'))); - $('#batterycard-battery-last-charged-timeago').attr("datetime", batteryDetails.last_charged || ''); - $('#batterycard-battery-charge-cycles-count').text((batteryDetails.charge_cycles_count || '0')); + self.$('#batterycard-battery-name').text(batteryDetails.battery.name); + self.$('#batterycard-battery-used_in').text(batteryDetails.battery.used_in); + self.$('#batterycard-battery-last-charged').text((batteryDetails.last_charged || self.Grocy.translate('never'))); + self.$('#batterycard-battery-last-charged-timeago').attr("datetime", batteryDetails.last_charged || ''); + self.$('#batterycard-battery-charge-cycles-count').text((batteryDetails.charge_cycles_count || '0')); - $('#batterycard-battery-edit-button').attr("href", Grocy.FormatUrl("/battery/" + batteryDetails.battery.id.toString())); - $('#batterycard-battery-journal-button').attr("href", Grocy.FormatUrl("/batteriesjournal?embedded&battery=" + batteryDetails.battery.id.toString())); - $('#batterycard-battery-edit-button').removeClass("disabled"); - $('#batterycard-battery-journal-button').removeClass("disabled"); + self.$('#batterycard-battery-edit-button').attr("href", self.Grocy.FormatUrl("/battery/" + batteryDetails.battery.id.toString())); + self.$('#batterycard-battery-journal-button').attr("href", self.Grocy.FormatUrl("/batteriesjournal?embedded&battery=" + batteryDetails.battery.id.toString())); + self.$('#batterycard-battery-edit-button').removeClass("disabled"); + self.$('#batterycard-battery-journal-button').removeClass("disabled"); - EmptyElementWhenMatches('#batterycard-battery-last-charged-timeago', Grocy.translate('timeago_nan')); + EmptyElementWhenMatches(self.$('#batterycard-battery-last-charged-timeago'), self.Grocy.translate('timeago_nan')); + + // ToDo: Unscoped RefreshContextualTimeago(".batterycard"); }, function(xhr) diff --git a/js/components/calendarcard.js b/js/components/calendarcard.js index 11a16131..c7444e81 100644 --- a/js/components/calendarcard.js +++ b/js/components/calendarcard.js @@ -1,45 +1,54 @@ -function calendarcard(Grocy) +class calendarcard { - $('#calendar').datetimepicker( - { - format: 'L', - buttons: { - showToday: true, - showClose: false - }, - calendarWeeks: true, - locale: moment.locale(), - icons: { - time: 'far fa-clock', - date: 'far fa-calendar', - up: 'fas fa-arrow-up', - down: 'fas fa-arrow-down', - previous: 'fas fa-chevron-left', - next: 'fas fa-chevron-right', - today: 'fas fa-calendar-check', - clear: 'far fa-trash-alt', - close: 'far fa-times-circle' - }, - keepOpen: true, - inline: true, - keyBinds: { - up: function(widget) { }, - down: function(widget) { }, - 'control up': function(widget) { }, - 'control down': function(widget) { }, - left: function(widget) { }, - right: function(widget) { }, - pageUp: function(widget) { }, - pageDown: function(widget) { }, - enter: function(widget) { }, - escape: function(widget) { }, - 'control space': function(widget) { }, - t: function(widget) { }, - 'delete': function(widget) { } - } - }); + constructor(Grocy, scopeSelector = null) + { + this.Grocy = Grocy; - $('#calendar').datetimepicker('show'); + this.scopeSelector = scopeSelector; + this.scope = scopeSelector != null ? $(scope) : $(document); + this.$ = scopeSelector != null ? $(scope).find : $; + + this.$('#calendar').datetimepicker( + { + format: 'L', + buttons: { + showToday: true, + showClose: false + }, + calendarWeeks: true, + locale: moment.locale(), + icons: { + time: 'far fa-clock', + date: 'far fa-calendar', + up: 'fas fa-arrow-up', + down: 'fas fa-arrow-down', + previous: 'fas fa-chevron-left', + next: 'fas fa-chevron-right', + today: 'fas fa-calendar-check', + clear: 'far fa-trash-alt', + close: 'far fa-times-circle' + }, + keepOpen: true, + inline: true, + keyBinds: { + up: function(widget) { }, + down: function(widget) { }, + 'control up': function(widget) { }, + 'control down': function(widget) { }, + left: function(widget) { }, + right: function(widget) { }, + pageUp: function(widget) { }, + pageDown: function(widget) { }, + enter: function(widget) { }, + escape: function(widget) { }, + 'control space': function(widget) { }, + t: function(widget) { }, + 'delete': function(widget) { } + } + }); + + this.$('#calendar').datetimepicker('show'); + } } export { calendarcard } \ No newline at end of file diff --git a/js/components/chorecard.js b/js/components/chorecard.js index 05595a12..abe929a0 100644 --- a/js/components/chorecard.js +++ b/js/components/chorecard.js @@ -1,36 +1,44 @@ import { EmptyElementWhenMatches } from '../helpers/extensions' import { RefreshContextualTimeago } from '../configs/timeago' -function chorecard(Grocy) +class chorecard { - Grocy.Components.ChoreCard = {}; - - Grocy.Components.ChoreCard.Refresh = function(choreId) + constructor(Grocy, scopeSelector = null) { - Grocy.Api.Get('chores/' + choreId, + this.Grocy = Grocy; + + this.scopeSelector = scopeSelector; + this.scope = scopeSelector != null ? $(scope) : $(document); + this.$ = scopeSelector != null ? $(scope).find : $; + } + + Refresh(choreId) + { + var self = this; + this.Grocy.Api.Get('chores/' + choreId, function(choreDetails) { - $('#chorecard-chore-name').text(choreDetails.chore.name); - $('#chorecard-chore-last-tracked').text((choreDetails.last_tracked || Grocy.translate('never'))); - $('#chorecard-chore-last-tracked-timeago').attr("datetime", choreDetails.last_tracked || ''); - $('#chorecard-chore-tracked-count').text((choreDetails.tracked_count || '0')); - $('#chorecard-chore-last-done-by').text((choreDetails.last_done_by.display_name || Grocy.translate('Unknown'))); + self.$('#chorecard-chore-name').text(choreDetails.chore.name); + self.$('#chorecard-chore-last-tracked').text((choreDetails.last_tracked || self.Grocy.translate('never'))); + self.$('#chorecard-chore-last-tracked-timeago').attr("datetime", choreDetails.last_tracked || ''); + self.$('#chorecard-chore-tracked-count').text((choreDetails.tracked_count || '0')); + self.$('#chorecard-chore-last-done-by').text((choreDetails.last_done_by.display_name || self.Grocy.translate('Unknown'))); - $('#chorecard-chore-edit-button').attr("href", Grocy.FormatUrl("/chore/" + choreDetails.chore.id.toString())); - $('#chorecard-chore-journal-button').attr("href", Grocy.FormatUrl("/choresjournal?embedded&chore=" + choreDetails.chore.id.toString())); - $('#chorecard-chore-edit-button').removeClass("disabled"); - $('#chorecard-chore-journal-button').removeClass("disabled"); + self.$('#chorecard-chore-edit-button').attr("href", self.Grocy.FormatUrl("/chore/" + choreDetails.chore.id.toString())); + self.$('#chorecard-chore-journal-button').attr("href", self.Grocy.FormatUrl("/choresjournal?embedded&chore=" + choreDetails.chore.id.toString())); + self.$('#chorecard-chore-edit-button').removeClass("disabled"); + self.$('#chorecard-chore-journal-button').removeClass("disabled"); if (choreDetails.chore.track_date_only == 1) { - $("#chorecard-chore-last-tracked-timeago").addClass("timeago-date-only"); + self.$("#chorecard-chore-last-tracked-timeago").addClass("timeago-date-only"); } else { - $("#chorecard-chore-last-tracked-timeago").removeClass("timeago-date-only"); + self.$("#chorecard-chore-last-tracked-timeago").removeClass("timeago-date-only"); } - EmptyElementWhenMatches('#chorecard-chore-last-tracked-timeago', Grocy.translate('timeago_nan')); + EmptyElementWhenMatches(self.$('#chorecard-chore-last-tracked-timeago'), self.Grocy.translate('timeago_nan')); RefreshContextualTimeago(".chorecard"); }, function(xhr) diff --git a/js/components/datetimepicker.js b/js/components/datetimepicker.js index 06b3caec..73310226 100644 --- a/js/components/datetimepicker.js +++ b/js/components/datetimepicker.js @@ -1,100 +1,119 @@ import { EmptyElementWhenMatches } from '../helpers/extensions' import { RefreshContextualTimeago } from '../configs/timeago' -function datetimepicker(Grocy) +class datetimepicker { - Grocy.Components.DateTimePicker = {}; - - Grocy.Components.DateTimePicker.GetInputElement = function() + constructor(Grocy, scopeSelector = null, basename = "datetimepicker") { - return $('.datetimepicker').find('input').not(".form-check-input"); + this.Grocy = Grocy; + + this.scopeSelector = scopeSelector; + this.scope = scopeSelector != null ? $(scope) : $(document); + this.$ = scopeSelector != null ? $(scope).find : $; + + this.basename = basename; + + var inputElement = this.GetInputElement(); + var self = this; + + + this.startDate = null; + if (inputElement.data('init-with-now') === true) + { + this.startDate = moment().format(inputElement.data('format')); + } + if (inputElement.data('init-value').length > 0) + { + this.startDate = moment(inputElement.data('init-value')).format(inputElement.data('format')); + } + + this.limitDate = moment('2999-12-31 23:59:59'); + if (inputElement.data('limit-end-to-now') === true) + { + this.limitDate = moment(); + } + + // set some event handlers + inputElement.on('keyup', (e) => self.keyupHandler(this, e)); + inputElement.on('input', (e) => self.inputHandler(this, e)); + + this.$('.' + this.basename).on('update.datetimepicker', () => self.stateTrigger()); + this.$('.' + this.basename).on('hide.datetimepicker', () => self.stateTrigger()); + + this.$("#" + this.basename + "-shortcut").on("click", () => self.handleShortcut(this)); + + this.Init() } - Grocy.Components.DateTimePicker.GetValue = function() + GetInputElement() { - return Grocy.Components.DateTimePicker.GetInputElement().val(); + return this.$('.' + this.basename).find('input').not(".form-check-input"); } - Grocy.Components.DateTimePicker.SetValue = function(value) + GetValue() + { + return this.GetInputElement().val(); + } + + SetValue(value, triggerEvents = true) { // "Click" the shortcut checkbox when the desired value is // not the shortcut value and it is currently set - var shortcutValue = $("#datetimepicker-shortcut").data("datetimepicker-shortcut-value"); - if (value != shortcutValue && $("#datetimepicker-shortcut").is(":checked")) + var shortcutValue = this.$("#" + this.basename + "-shortcut").data(this.basename + "-shortcut-value"); + if (value != shortcutValue && this.$("#" + this.basename + "-shortcut").is(":checked")) { - $("#datetimepicker-shortcut").click(); - } - Grocy.Components.DateTimePicker.GetInputElement().val(value); - Grocy.Components.DateTimePicker.GetInputElement().trigger('change'); - - Grocy.Components.DateTimePicker.GetInputElement().keyup(); - } - - Grocy.Components.DateTimePicker.Clear = function() - { - $(".datetimepicker").datetimepicker("destroy"); - Grocy.Components.DateTimePicker.Init(); - - Grocy.Components.DateTimePicker.GetInputElement().val(""); - - // "Click" the shortcut checkbox when the desired value is - // not the shortcut value and it is currently set - var value = ""; - var shortcutValue = $("#datetimepicker-shortcut").data("datetimepicker-shortcut-value"); - if (value != shortcutValue && $("#datetimepicker-shortcut").is(":checked")) - { - $("#datetimepicker-shortcut").click(); + this.$("#" + this.basename + "-shortcut").click(); } - $('#datetimepicker-timeago').text(''); + var inputElement = this.GetInputElement(); + inputElement.val(value); + if (triggerEvents) + { + inputElement.trigger('change'); + + inputElement.keyup(); + } } - Grocy.Components.DateTimePicker.ChangeFormat = function(format) + Clear() { - $(".datetimepicker").datetimepicker("destroy"); - Grocy.Components.DateTimePicker.GetInputElement().data("format", format); - Grocy.Components.DateTimePicker.Init(); + this.$("." + this.basename).datetimepicker("destroy"); + this.Init(); + this.SetValue("", false); + + this.$('#' + this.basename + '-timeago').text(''); + } + + ChangeFormat(format) + { + this.$("." + this.basename).datetimepicker("destroy"); + var elem = this.GetInputElement(); + elem.data("format", format); + this.Init(); if (format == "YYYY-MM-DD") { - Grocy.Components.DateTimePicker.GetInputElement().addClass("date-only-datetimepicker"); + elem.addClass("date-only-datetimepicker"); } else { - Grocy.Components.DateTimePicker.GetInputElement().removeClass("date-only-datetimepicker"); + elem.removeClass("date-only-datetimepicker"); } } - var startDate = null; - var inputElement = Grocy.Components.DateTimePicker.GetInputElement(); - if (inputElement.data('init-with-now') === true) + Init() { - startDate = moment().format(inputElement.data('format')); - } - if (inputElement.data('init-value').length > 0) - { - startDate = moment(inputElement.data('init-value')).format(Grocy.Components.DateTimePicker.GetInputElement().data('format')); - } - - var limitDate = moment('2999-12-31 23:59:59'); - if (Grocy.Components.DateTimePicker.GetInputElement().data('limit-end-to-now') === true) - { - limitDate = moment(); - } - - Grocy.Components.DateTimePicker.Init = function() - { - $('.datetimepicker').datetimepicker( + this.$('.' + this.basename).datetimepicker( { - format: Grocy.Components.DateTimePicker.GetInputElement().data('format'), + format: this.GetInputElement().data('format'), buttons: { showToday: true, showClose: true }, - calendarWeeks: Grocy.CalendarShowWeekNumbers, - maxDate: limitDate, + calendarWeeks: this.Grocy.CalendarShowWeekNumbers, + maxDate: this.limitDate, locale: moment.locale(), - defaultDate: startDate, + defaultDate: this.startDate, useCurrent: false, icons: { time: 'far fa-clock', @@ -125,28 +144,30 @@ function datetimepicker(Grocy) } }); } - Grocy.Components.DateTimePicker.Init(); - Grocy.Components.DateTimePicker.GetInputElement().on('keyup', function(e) + + keyupHandler(_this, e) { - $('.datetimepicker').datetimepicker('hide'); + this.$('.' + this.basename).datetimepicker('hide'); - var value = Grocy.Components.DateTimePicker.GetValue(); + var inputElement = this.GetInputElement(); + + var value = this.GetValue(); var now = new Date(); var centuryStart = Number.parseInt(now.getFullYear().toString().substring(0, 2) + '00'); var centuryEnd = Number.parseInt(now.getFullYear().toString().substring(0, 2) + '99'); - var format = Grocy.Components.DateTimePicker.GetInputElement().data('format'); - var nextInputElement = $(Grocy.Components.DateTimePicker.GetInputElement().data('next-input-selector')); + var format = inputElement.data('format'); + var nextInputElement = this.$(inputElement.data('next-input-selector')); //If input is empty and any arrow key is pressed, set date to today if (value.length === 0 && (e.keyCode === 38 || e.keyCode === 40 || e.keyCode === 37 || e.keyCode === 39)) { - Grocy.Components.DateTimePicker.SetValue(moment(new Date(), format, true).format(format)); + this.SetValue(moment(new Date(), format, true).format(format)); nextInputElement.focus(); } else if (value === 'x' || value === 'X') { - Grocy.Components.DateTimePicker.SetValue(moment('2999-12-31 23:59:59').format(format)); + this.SetValue(moment('2999-12-31 23:59:59').format(format)); nextInputElement.focus(); } else if (value.length === 4 && !(Number.parseInt(value) > centuryStart && Number.parseInt(value) < centuryEnd)) @@ -156,18 +177,18 @@ function datetimepicker(Grocy) { date.add(1, "year"); } - Grocy.Components.DateTimePicker.SetValue(date.format(format)); + this.SetValue(date.format(format)); nextInputElement.focus(); } else if (value.length === 8 && $.isNumeric(value)) { - Grocy.Components.DateTimePicker.SetValue(value.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3')); + this.SetValue(value.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3')); nextInputElement.focus(); } else if (value.length === 7 && $.isNumeric(value.substring(0, 6)) && (value.substring(6, 7).toLowerCase() === "e" || value.substring(6, 7).toLowerCase() === "+")) { var endOfMonth = moment(value.substring(0, 4) + "-" + value.substring(4, 6) + "-01").endOf("month"); - Grocy.Components.DateTimePicker.SetValue(endOfMonth.format(format)); + this.SetValue(endOfMonth.format(format)); nextInputElement.focus(); } else @@ -221,23 +242,23 @@ function datetimepicker(Grocy) } //Custom validation - value = Grocy.Components.DateTimePicker.GetValue(); + value = this.GetValue(); dateObj = moment(value, format, true); - var element = Grocy.Components.DateTimePicker.GetInputElement()[0]; + var element = inputElement[0]; if (!dateObj.isValid()) { - if ($(element).hasAttr("required")) + if (inputElement.hasAttr("required")) { element.setCustomValidity("error"); } } else { - if (Grocy.Components.DateTimePicker.GetInputElement().data('limit-end-to-now') === true && dateObj.isAfter(moment())) + if (inputElement.data('limit-end-to-now') === true && dateObj.isAfter(moment())) { element.setCustomValidity("error"); } - else if (Grocy.Components.DateTimePicker.GetInputElement().data('limit-start-to-now') === true && dateObj.isBefore(moment())) + else if (inputElement.data('limit-start-to-now') === true && dateObj.isBefore(moment())) { element.setCustomValidity("error"); } @@ -246,72 +267,68 @@ function datetimepicker(Grocy) element.setCustomValidity(""); } - var earlierThanLimit = Grocy.Components.DateTimePicker.GetInputElement().data("earlier-than-limit"); + var earlierThanLimit = inputElement.data("earlier-than-limit"); if (!earlierThanLimit.isEmpty()) { if (moment(value).isBefore(moment(earlierThanLimit))) { - $("#datetimepicker-earlier-than-info").removeClass("d-none"); + this.$("#" + this.basename + "-earlier-than-info").removeClass("d-none"); } else { - $("#datetimepicker-earlier-than-info").addClass("d-none"); + this.$("#" + this.basename + "-earlier-than-info").addClass("d-none"); } } } // "Click" the shortcut checkbox when the shortcut value was // entered manually and it is currently not set - var shortcutValue = $("#datetimepicker-shortcut").data("datetimepicker-shortcut-value"); - if (value == shortcutValue && !$("#datetimepicker-shortcut").is(":checked")) + var shortcutValue = this.$("#" + this.basename + "-shortcut").data(this.basename + "-shortcut-value"); + if (value == shortcutValue && !this.$("#" + this.basename + "-shortcut").is(":checked")) { - $("#datetimepicker-shortcut").click(); + this.$("#" + this.basename + "-shortcut").click(); } - }); + } - Grocy.Components.DateTimePicker.GetInputElement().on('input', function(e) + inputHandler(_this, e) { - $('#datetimepicker-timeago').attr("datetime", Grocy.Components.DateTimePicker.GetValue()); - EmptyElementWhenMatches('#datetimepicker-timeago', Grocy.translate('timeago_nan')); - RefreshContextualTimeago("#datetimepicker-wrapper"); - }); + this.$('#' + this.basename + '-timeago').attr("datetime", this.GetValue()); + EmptyElementWhenMatches(this.$('#' + this.basename + '-timeago'), this.Grocy.translate('timeago_nan')); - $('.datetimepicker').on('update.datetimepicker', function(e) - { - Grocy.Components.DateTimePicker.GetInputElement().trigger('input'); - Grocy.Components.DateTimePicker.GetInputElement().trigger('change'); - Grocy.Components.DateTimePicker.GetInputElement().trigger('keypress'); - Grocy.Components.DateTimePicker.GetInputElement().trigger('keyup'); - }); + // TODO: scoping + RefreshContextualTimeago("#" + this.basename + "-wrapper"); + } - $('.datetimepicker').on('hide.datetimepicker', function(e) + stateTrigger() { - Grocy.Components.DateTimePicker.GetInputElement().trigger('input'); - Grocy.Components.DateTimePicker.GetInputElement().trigger('change'); - Grocy.Components.DateTimePicker.GetInputElement().trigger('keypress'); - Grocy.Components.DateTimePicker.GetInputElement().trigger('keyup'); - }); + var linputElement = this.GetInputElement(); + linputElement.trigger('input') + .trigger('change') + .trigger('keypress') + .trigger('keyup'); + } - $("#datetimepicker-shortcut").on("click", function() + handleShortcut(_this) { - if (this.checked) { - var value = $("#datetimepicker-shortcut").data("datetimepicker-shortcut-value"); - Grocy.Components.DateTimePicker.SetValue(value); - Grocy.Components.DateTimePicker.GetInputElement().attr("readonly", ""); - $(Grocy.Components.DateTimePicker.GetInputElement().data('next-input-selector')).focus(); - } - else - { - Grocy.Components.DateTimePicker.SetValue(""); - Grocy.Components.DateTimePicker.GetInputElement().removeAttr("readonly"); - Grocy.Components.DateTimePicker.GetInputElement().focus(); - } + var linputElement = this.GetInputElement(); + if (_this.checked) + { + var value = this.$("#" + this.basename + "-shortcut").data(this.basename + "-shortcut-value"); + this.SetValue(value); + this.GetInputElement().attr("readonly", ""); + this.$(linputElement.data('next-input-selector')).focus(); + } + else + { + this.SetValue(""); + linputElement.removeAttr("readonly"); + linputElement.focus(); + } - Grocy.Components.DateTimePicker.GetInputElement().trigger('input'); - Grocy.Components.DateTimePicker.GetInputElement().trigger('change'); - Grocy.Components.DateTimePicker.GetInputElement().trigger('keypress'); - }); + this.stateTrigger(); + } + } } export { datetimepicker } \ No newline at end of file diff --git a/js/components/datetimepicker2.js b/js/components/datetimepicker2.js index 5b781354..454be5c1 100644 --- a/js/components/datetimepicker2.js +++ b/js/components/datetimepicker2.js @@ -1,317 +1,13 @@ import { EmptyElementWhenMatches } from '../helpers/extensions' import { RefreshContextualTimeago } from '../configs/timeago' +import { datetimepicker } from './datetimepicker' -function datetimepicker2(Grocy) +class datetimepicker2 extends datetimepicker { - Grocy.Components.DateTimePicker2 = {}; - - Grocy.Components.DateTimePicker2.GetInputElement = function() + constructor(Grocy, scopeSelector = null) { - return $('.datetimepicker2').find('input').not(".form-check-input"); + super(Grocy, scopeSelector, "datetimepicker2"); } - - Grocy.Components.DateTimePicker2.GetValue = function() - { - return Grocy.Components.DateTimePicker2.GetInputElement().val(); - } - - Grocy.Components.DateTimePicker2.SetValue = function(value) - { - // "Click" the shortcut checkbox when the desired value is - // not the shortcut value and it is currently set - var shortcutValue = $("#datetimepicker2-shortcut").data("datetimepicker2-shortcut-value"); - if (value != shortcutValue && $("#datetimepicker2-shortcut").is(":checked")) - { - $("#datetimepicker2-shortcut").click(); - } - - Grocy.Components.DateTimePicker2.GetInputElement().val(value); - Grocy.Components.DateTimePicker2.GetInputElement().trigger('change'); - - Grocy.Components.DateTimePicker2.GetInputElement().keyup(); - } - - Grocy.Components.DateTimePicker2.Clear = function() - { - $(".datetimepicker2").datetimepicker("destroy"); - Grocy.Components.DateTimePicker2.Init(); - - Grocy.Components.DateTimePicker2.GetInputElement().val(""); - - // "Click" the shortcut checkbox when the desired value is - // not the shortcut value and it is currently set - var value = ""; - var shortcutValue = $("#datetimepicker2-shortcut").data("datetimepicker2-shortcut-value"); - if (value != shortcutValue && $("#datetimepicker2-shortcut").is(":checked")) - { - $("#datetimepicker2-shortcut").click(); - } - - $('#datetimepicker2-timeago').text(''); - } - - Grocy.Components.DateTimePicker2.ChangeFormat = function(format) - { - $(".datetimepicker2").datetimepicker("destroy"); - Grocy.Components.DateTimePicker2.GetInputElement().data("format", format); - Grocy.Components.DateTimePicker2.Init(); - - if (format == "YYYY-MM-DD") - { - Grocy.Components.DateTimePicker2.GetInputElement().addClass("date-only-datetimepicker"); - } - else - { - Grocy.Components.DateTimePicker2.GetInputElement().removeClass("date-only-datetimepicker"); - } - } - - var startDate = null; - if (Grocy.Components.DateTimePicker2.GetInputElement().data('init-with-now') === true) - { - startDate = moment().format(Grocy.Components.DateTimePicker2.GetInputElement().data('format')); - } - if (Grocy.Components.DateTimePicker2.GetInputElement().data('init-value').length > 0) - { - startDate = moment(Grocy.Components.DateTimePicker2.GetInputElement().data('init-value')).format(Grocy.Components.DateTimePicker2.GetInputElement().data('format')); - } - - var limitDate = moment('2999-12-31 23:59:59'); - if (Grocy.Components.DateTimePicker2.GetInputElement().data('limit-end-to-now') === true) - { - limitDate = moment(); - } - - Grocy.Components.DateTimePicker2.Init = function() - { - $('.datetimepicker2').datetimepicker( - { - format: Grocy.Components.DateTimePicker2.GetInputElement().data('format'), - buttons: { - showToday: true, - showClose: true - }, - calendarWeeks: Grocy.CalendarShowWeekNumbers, - maxDate: limitDate, - locale: moment.locale(), - defaultDate: startDate, - useCurrent: false, - icons: { - time: 'far fa-clock', - date: 'far fa-calendar', - up: 'fas fa-arrow-up', - down: 'fas fa-arrow-down', - previous: 'fas fa-chevron-left', - next: 'fas fa-chevron-right', - today: 'fas fa-calendar-check', - clear: 'far fa-trash-alt', - close: 'far fa-times-circle' - }, - sideBySide: true, - keyBinds: { - up: function(widget) { }, - down: function(widget) { }, - 'control up': function(widget) { }, - 'control down': function(widget) { }, - left: function(widget) { }, - right: function(widget) { }, - pageUp: function(widget) { }, - pageDown: function(widget) { }, - enter: function(widget) { }, - escape: function(widget) { }, - 'control space': function(widget) { }, - t: function(widget) { }, - 'delete': function(widget) { } - } - }); - } - Grocy.Components.DateTimePicker2.Init(); - - Grocy.Components.DateTimePicker2.GetInputElement().on('keyup', function(e) - { - $('.datetimepicker2').datetimepicker('hide'); - - var value = Grocy.Components.DateTimePicker2.GetValue(); - var now = new Date(); - var centuryStart = Number.parseInt(now.getFullYear().toString().substring(0, 2) + '00'); - var centuryEnd = Number.parseInt(now.getFullYear().toString().substring(0, 2) + '99'); - var format = Grocy.Components.DateTimePicker2.GetInputElement().data('format'); - var nextInputElement = $(Grocy.Components.DateTimePicker2.GetInputElement().data('next-input-selector')); - - //If input is empty and any arrow key is pressed, set date to today - if (value.length === 0 && (e.keyCode === 38 || e.keyCode === 40 || e.keyCode === 37 || e.keyCode === 39)) - { - Grocy.Components.DateTimePicker2.SetValue(moment(new Date(), format, true).format(format)); - nextInputElement.focus(); - } - else if (value === 'x' || value === 'X') - { - Grocy.Components.DateTimePicker2.SetValue(moment('2999-12-31 23:59:59').format(format)); - nextInputElement.focus(); - } - else if (value.length === 4 && !(Number.parseInt(value) > centuryStart && Number.parseInt(value) < centuryEnd)) - { - var date = moment((new Date()).getFullYear().toString() + value); - if (date.isBefore(moment())) - { - date.add(1, "year"); - } - Grocy.Components.DateTimePicker2.SetValue(date.format(format)); - nextInputElement.focus(); - } - else if (value.length === 8 && $.isNumeric(value)) - { - Grocy.Components.DateTimePicker2.SetValue(value.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3')); - nextInputElement.focus(); - } - else if (value.length === 7 && $.isNumeric(value.substring(0, 6)) && (value.substring(6, 7).toLowerCase() === "e" || value.substring(6, 7).toLowerCase() === "+")) - { - var endOfMonth = moment(value.substring(0, 4) + "-" + value.substring(4, 6) + "-01").endOf("month"); - Grocy.Components.DateTimePicker2.SetValue(endOfMonth.format(format)); - nextInputElement.focus(); - } - else - { - var dateObj = moment(value, format, true); - if (dateObj.isValid()) - { - if (e.shiftKey) - { - // WITH shift modifier key - - if (e.keyCode === 38) // Up - { - Grocy.Components.DateTimePicker2.SetValue(dateObj.add(-1, 'months').format(format)); - } - else if (e.keyCode === 40) // Down - { - Grocy.Components.DateTimePicker2.SetValue(dateObj.add(1, 'months').format(format)); - } - else if (e.keyCode === 37) // Left - { - Grocy.Components.DateTimePicker2.SetValue(dateObj.add(-1, 'years').format(format)); - } - else if (e.keyCode === 39) // Right - { - Grocy.Components.DateTimePicker2.SetValue(dateObj.add(1, 'years').format(format)); - } - } - else - { - // WITHOUT shift modifier key - - if (e.keyCode === 38) // Up - { - Grocy.Components.DateTimePicker2.SetValue(dateObj.add(-1, 'days').format(format)); - } - else if (e.keyCode === 40) // Down - { - Grocy.Components.DateTimePicker2.SetValue(dateObj.add(1, 'days').format(format)); - } - else if (e.keyCode === 37) // Left - { - Grocy.Components.DateTimePicker2.SetValue(dateObj.add(-1, 'weeks').format(format)); - } - else if (e.keyCode === 39) // Right - { - Grocy.Components.DateTimePicker2.SetValue(dateObj.add(1, 'weeks').format(format)); - } - } - } - } - - //Custom validation - value = Grocy.Components.DateTimePicker2.GetValue(); - dateObj = moment(value, format, true); - var element = Grocy.Components.DateTimePicker2.GetInputElement()[0]; - if (!dateObj.isValid()) - { - if ($(element).hasAttr("required")) - { - element.setCustomValidity("error"); - } - } - else - { - if (Grocy.Components.DateTimePicker2.GetInputElement().data('limit-end-to-now') === true && dateObj.isAfter(moment())) - { - element.setCustomValidity("error"); - } - else if (Grocy.Components.DateTimePicker2.GetInputElement().data('limit-start-to-now') === true && dateObj.isBefore(moment())) - { - element.setCustomValidity("error"); - } - else - { - element.setCustomValidity(""); - } - - var earlierThanLimit = Grocy.Components.DateTimePicker2.GetInputElement().data("earlier-than-limit"); - if (!earlierThanLimit.isEmpty()) - { - if (moment(value).isBefore(moment(earlierThanLimit))) - { - $("#datetimepicker-earlier-than-info").removeClass("d-none"); - } - else - { - $("#datetimepicker-earlier-than-info").addClass("d-none"); - } - } - } - - // "Click" the shortcut checkbox when the shortcut value was - // entered manually and it is currently not set - var shortcutValue = $("#datetimepicker2-shortcut").data("datetimepicker2-shortcut-value"); - if (value == shortcutValue && !$("#datetimepicker2-shortcut").is(":checked")) - { - $("#datetimepicker2-shortcut").click(); - } - }); - - Grocy.Components.DateTimePicker2.GetInputElement().on('input', function(e) - { - $('#datetimepicker2-timeago').attr("datetime", Grocy.Components.DateTimePicker2.GetValue()); - EmptyElementWhenMatches('#datetimepicker2-timeago', Grocy.translate('timeago_nan')); - RefreshContextualTimeago("#datetimepicker2-wrapper"); - }); - - $('.datetimepicker2').on('update.datetimepicker', function(e) - { - Grocy.Components.DateTimePicker2.GetInputElement().trigger('input'); - Grocy.Components.DateTimePicker2.GetInputElement().trigger('change'); - Grocy.Components.DateTimePicker2.GetInputElement().trigger('keypress'); - Grocy.Components.DateTimePicker2.GetInputElement().trigger('keyup'); - }); - - $('.datetimepicker2').on('hide.datetimepicker', function(e) - { - Grocy.Components.DateTimePicker2.GetInputElement().trigger('input'); - Grocy.Components.DateTimePicker2.GetInputElement().trigger('change'); - Grocy.Components.DateTimePicker2.GetInputElement().trigger('keypress'); - Grocy.Components.DateTimePicker2.GetInputElement().trigger('keyup'); - }); - - $("#datetimepicker2-shortcut").on("click", function() - { - if (this.checked) - { - var value = $("#datetimepicker2-shortcut").data("datetimepicker2-shortcut-value"); - Grocy.Components.DateTimePicker2.SetValue(value); - Grocy.Components.DateTimePicker2.GetInputElement().attr("readonly", ""); - $(Grocy.Components.DateTimePicker2.GetInputElement().data('next-input-selector')).focus(); - } - else - { - Grocy.Components.DateTimePicker2.SetValue(""); - Grocy.Components.DateTimePicker2.GetInputElement().removeAttr("readonly"); - Grocy.Components.DateTimePicker2.GetInputElement().focus(); - } - - Grocy.Components.DateTimePicker2.GetInputElement().trigger('input'); - Grocy.Components.DateTimePicker2.GetInputElement().trigger('change'); - Grocy.Components.DateTimePicker2.GetInputElement().trigger('keypress'); - }); } export { datetimepicker2 } \ No newline at end of file diff --git a/js/components/locationpicker.js b/js/components/locationpicker.js index 66e0b912..48053200 100644 --- a/js/components/locationpicker.js +++ b/js/components/locationpicker.js @@ -1,79 +1,17 @@ -function locationpicker(Grocy) +import BasePicker from "./BasePicker"; + +class locationpicker extends BasePicker { - - Grocy.Components.LocationPicker = {}; - - Grocy.Components.LocationPicker.GetPicker = function() + constructor(Grocy, scopeSelector = null) { - return $('#location_id'); - } + super(Grocy, "#location_id", scopeSelector); - Grocy.Components.LocationPicker.GetInputElement = function() - { - return $('#location_id_text_input'); - } + this.picker = this.$(this.basename); + this.inputElement = this.$('#location_id_text_input'); - Grocy.Components.LocationPicker.GetValue = function() - { - return $('#location_id').val(); - } + this.initCombobox('.location-combobox'); - Grocy.Components.LocationPicker.SetValue = function(value) - { - Grocy.Components.LocationPicker.GetInputElement().val(value); - Grocy.Components.LocationPicker.GetInputElement().trigger('change'); - } - - Grocy.Components.LocationPicker.SetId = function(value) - { - Grocy.Components.LocationPicker.GetPicker().val(value); - Grocy.Components.LocationPicker.GetPicker().data('combobox').refresh(); - Grocy.Components.LocationPicker.GetInputElement().trigger('change'); - } - - Grocy.Components.LocationPicker.Clear = function() - { - Grocy.Components.LocationPicker.SetValue(''); - Grocy.Components.LocationPicker.SetId(null); - } - - $('.location-combobox').combobox({ - appendId: '_text_input', - bsVersion: '4', - clearIfNoMatch: true - }); - - // these names seem a bit long, but as they live in global space - // and this is a component, they need to be unique. - var locationpicker_doFocus = false; - var this_location_picker = Grocy.Components.LocationPicker.GetPicker(); - - var prefillByName = this_location_picker.parent().data('prefill-by-name').toString(); - if (typeof prefillByName !== "undefined") - { - var possibleOptionElement = $("#location_id option:contains(\"" + prefillByName + "\")").first(); - - if (possibleOptionElement.length > 0) - { - locationpicker_doFocus = true; - this_location_picker.val(possibleOptionElement.val()); - } - } - - var prefillById = this_location_picker.parent().data('prefill-by-id').toString(); - if (typeof prefillById !== "undefined") - { - locationpicker_doFocus = true; - this_location_picker.val(prefillById); - } - - if (locationpicker_doFocus) - { - this_location_picker.data('combobox').refresh(); - this_location_picker.trigger('change'); - - $(this_location_picker.parent().data('next-input-selector').toString()) - .focus(); + this.prefill(); } } diff --git a/js/components/numberpicker.js b/js/components/numberpicker.js index f60d2cfd..98da2a69 100644 --- a/js/components/numberpicker.js +++ b/js/components/numberpicker.js @@ -1,99 +1,164 @@ -function numberpicker(Grocy) +class numberpicker { - $(".numberpicker-down-button").unbind('click').on("click", function() + constructor(Grocy, scopeSelector = null) { - var inputElement = $(this).parent().parent().find('input[type="number"]'); - inputElement.val(parseFloat(inputElement.val() || 1) - 1); - inputElement.trigger('keyup'); - inputElement.trigger('change'); - }); + this.Grocy = Grocy; - $(".numberpicker-up-button").unbind('click').on("click", function() - { - var inputElement = $(this).parent().parent().find('input[type="number"]'); - inputElement.val(parseFloat(inputElement.val() || 0) + 1); - inputElement.trigger('keyup'); - inputElement.trigger('change'); - }); + this.scopeSelector = scopeSelector; + this.scope = scopeSelector != null ? $(scope) : $(document); + this.$ = scopeSelector != null ? $(scope).find : $; + var self = this; - $(".numberpicker").on("keyup", function() - { - if ($(this).attr("data-not-equal") && !$(this).attr("data-not-equal").toString().isEmpty() && $(this).attr("data-not-equal") == $(this).val()) - { - $(this)[0].setCustomValidity("error"); - } - else - { - $(this)[0].setCustomValidity(""); - } - }); + this.$(".numberpicker-down-button").unbind('click').on("click", () => self.valueDownHandler(this)); + this.$(".numberpicker-up-button").unbind('click').on("click", () => self.valueUpHandler(this)); - $(".numberpicker").each(function() - { - new MutationObserver(function(mutations) + this.$(".numberpicker").on("keyup", function() { - mutations.forEach(function(mutation) + $this = $(this) + if ($this.attr("data-not-equal") && !$this.attr("data-not-equal").toString().isEmpty() && $this.attr("data-not-equal") == $this.val()) { - if (mutation.type == "attributes" && (mutation.attributeName == "min" || mutation.attributeName == "max" || mutation.attributeName == "data-not-equal" || mutation.attributeName == "data-initialised")) + $this[0].setCustomValidity("error"); + } + else + { + $this[0].setCustomValidity(""); + } + }); + + this.$(".numberpicker").on("keydown", function(e) + { + if (e.key == "ArrowUp") + { + e.preventDefault(); + $(this).parent().find(".numberpicker-up-button").click(); + } + else if (e.key == "ArrowDown") + { + e.preventDefault(); + $(this).parent().find(".numberpicker-down-button").click(); + } + }); + + var observer = new MutationObserver((mutations) => self.handleObservedChange(mutations)); + + this.$(".numberpicker").each(() => observer.observe(this, { + attributes: true + })); + + this.$(".numberpicker").attr("data-initialised", "true"); // Dummy change to trigger MutationObserver above once + } + + modifyValueHandler(_this, newValue) + { + var inputElement = this.$(_this).parent().parent().find('input[type="number"]'); + + if (newValue instanceof Function) + newValue = newValue(_this, inputElement); + + inputElement.val(newValue); + + inputElement.trigger('keyup').trigger('change'); + } + + valueUpHandler(_this) + { + this.modifyValueHandler(_this, (_this, inputElement) => { return parseFloat(inputElement.val() || 0) + 1; }); + } + + valueDownHandler(_this) + { + this.modifyValueHandler(_this, (_this, inputElement) => { return parseFloat(inputElement.val() || 1) - 1; }); + } + + handleObservedChange(mutation) + { + if (mutation.type == "attributes" && + (mutation.attributeName == "min" || + mutation.attributeName == "max" || + mutation.attributeName == "data-not-equal" || + mutation.attributeName == "data-initialised" + ) + ) + { + var element = $(mutation.target); + var min = element.attr("min"); + var decimals = element.attr("data-decimals"); + + var max = ""; + if (element.hasAttr("max")) + { + max = element.attr("max"); + } + + if (element.hasAttr("data-not-equal")) + { + var notEqual = element.attr("data-not-equal"); + + if (notEqual != "NaN") { - var element = $(mutation.target); - var min = element.attr("min"); - var decimals = element.attr("data-decimals"); - - var max = ""; - if (element.hasAttr("max")) - { - max = element.attr("max"); - } - - if (element.hasAttr("data-not-equal")) - { - var notEqual = element.attr("data-not-equal"); - - if (notEqual != "NaN") - { - if (max.isEmpty()) - { - element.parent().find(".invalid-feedback").text(Grocy.translate("This cannot be lower than %1$s or equal %2$s and needs to be a valid number with max. %3$s decimal places", parseFloat(min).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: decimals }), parseFloat(notEqual).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: decimals }), decimals)); - } - else - { - element.parent().find(".invalid-feedback").text(Grocy.translate("This must be between %1$s and %2$s, cannot equal %3$s and needs to be a valid number with max. %4$s decimal places", parseFloat(min).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: decimals }), parseFloat(max).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: decimals }), parseFloat(notEqual).toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }), decimals)); - } - - return; - } - } - if (max.isEmpty()) { - element.parent().find(".invalid-feedback").text(Grocy.translate("This cannot be lower than %1$s and needs to be a valid number with max. %2$s decimal places", parseFloat(min).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: decimals }), decimals)); + element.parent().find(".invalid-feedback").text( + this.Grocy.translate("This cannot be lower than %1$s or equal %2$s and needs to be a valid number with max. %3$s decimal places", + parseFloat(min).toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: decimals + }), + parseFloat(notEqual).toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: decimals + }), decimals) + ); } else { - element.parent().find(".invalid-feedback").text(Grocy.translate("This must between %1$s and %2$s and needs to be a valid number with max. %3$s decimal places", parseFloat(min).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: decimals }), parseFloat(max).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: decimals }), decimals)); + element.parent().find(".invalid-feedback").text( + this.Grocy.translate("This must be between %1$s and %2$s, cannot equal %3$s and needs to be a valid number with max. %4$s decimal places", + parseFloat(min).toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: decimals + }), + parseFloat(max).toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: decimals + }), + parseFloat(notEqual).toLocaleString(undefined, { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals + }), decimals) + ); } - } - }); - }).observe(this, { - attributes: true - }); - }); - $(".numberpicker").attr("data-initialised", "true"); // Dummy change to trigger MutationObserver above once - $(".numberpicker").on("keydown", function(e) - { - if (e.key == "ArrowUp") - { - e.preventDefault(); - $(this).parent().find(".numberpicker-up-button").click(); + return; + } + } + + if (max.isEmpty()) + { + element.parent().find(".invalid-feedback").text( + Grocy.translate("This cannot be lower than %1$s and needs to be a valid number with max. %2$s decimal places", + parseFloat(min).toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: decimals + }), decimals) + ); + } + else + { + element.parent().find(".invalid-feedback").text( + Grocy.translate("This must between %1$s and %2$s and needs to be a valid number with max. %3$s decimal places", + parseFloat(min).toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: decimals + }), + parseFloat(max).toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: decimals + }), decimals) + ); + } } - else if (e.key == "ArrowDown") - { - e.preventDefault(); - $(this).parent().find(".numberpicker-down-button").click(); - } - }); + } } export { numberpicker } \ No newline at end of file diff --git a/js/components/productamountpicker.js b/js/components/productamountpicker.js index d0131939..d40e6e58 100644 --- a/js/components/productamountpicker.js +++ b/js/components/productamountpicker.js @@ -1,22 +1,37 @@ -function productamountpicker(Grocy) +class productamountpicker { - Grocy.Use("numberpicker"); - - Grocy.Components.ProductAmountPicker = {}; - Grocy.Components.ProductAmountPicker.AllowAnyQuEnabled = false; - - Grocy.Components.ProductAmountPicker.Reload = function(productId, destinationQuId, forceInitialDisplayQu = false) + constructor(Grocy, scopeSelector = null) { - var conversionsForProduct = Grocy.QuantityUnitConversionsResolved.filter(elem => elem.product_id == productId); + this.Grocy = Grocy; - if (!Grocy.Components.ProductAmountPicker.AllowAnyQuEnabled) + this.scopeSelector = scopeSelector; + this.scope = scopeSelector != null ? $(scope) : $(document); + this.$ = scopeSelector != null ? $(scope).find : $; + + Grocy.Use("numberpicker"); + this.AllowAnyQuEnabled = false; + + this.qu_id = this.$("#qu_id"); + this.display_amount = this.$("#display_amount"); + + var self = this; + + this.$(".input-group-productamountpicker").on("change", () => self.onChangeHandler(this)); + this.$("#display_amount").on("keyup", () => self.$(".input-group-productamountpicker").trigger("change")); + } + + Reload(productId, destinationQuId, forceInitialDisplayQu = false) + { + var conversionsForProduct = this.Grocy.QuantityUnitConversionsResolved.filter(elem => elem.product_id == productId); + + if (!this.AllowAnyQuEnabled) { - var qu = Grocy.QuantityUnits.find(elem => elem.id == destinationQuId); - $("#qu_id").find("option").remove().end(); - $("#qu_id").attr("data-destination-qu-name", qu.name); - $("#qu_id").attr("data-destination-qu-name-plural", qu.name_plural); + var qu = thisGrocy.QuantityUnits.find(elem => elem.id == destinationQuId); + this.qu_id.find("option").remove().end(); + this.qu_id.attr("data-destination-qu-name", qu.name); + this.qu_id.attr("data-destination-qu-name-plural", qu.name_plural); - conversionsForProduct.forEach(conversion => + for (let conversion of conversionsForProduct) { var factor = parseFloat(conversion.factor); if (conversion.to_qu_id == destinationQuId) @@ -24,100 +39,115 @@ function productamountpicker(Grocy) factor = 1; } - if (!$('#qu_id option[value="' + conversion.to_qu_id + '"]').length) // Don't add the destination QU multiple times + if (!this.$('#qu_id option[value="' + conversion.to_qu_id + '"]').length) // Don't add the destination QU multiple times { - $("#qu_id").append(''); + this.qu_id.append(''); } - }); + }; } - if (!Grocy.Components.ProductAmountPicker.InitialValueSet || forceInitialDisplayQu) + if (!this.InitialValueSet || forceInitialDisplayQu) { - $("#qu_id").val($("#qu_id").attr("data-initial-qu-id")); + this.qu_id.val(this.qu_id.attr("data-initial-qu-id")); } - if (!Grocy.Components.ProductAmountPicker.InitialValueSet) + if (!this.InitialValueSet) { - var convertedAmount = $("#display_amount").val() * $("#qu_id option:selected").attr("data-qu-factor"); - $("#display_amount").val(convertedAmount); + var convertedAmount = this.display_amount.val() * this.$("#qu_id option:selected").attr("data-qu-factor"); + this.display_amount.val(convertedAmount); - Grocy.Components.ProductAmountPicker.InitialValueSet = true; + this.InitialValueSet = true; } if (conversionsForProduct.length === 1 && !forceInitialDisplayQu) { - $("#qu_id").val($("#qu_id option:first").val()); + this.qu_id.val(this.$("#qu_id option:first").val()); } - if ($('#qu_id option').length == 1) + if (this.$('#qu_id option').length == 1) { - $("#qu_id").attr("disabled", ""); + this.qu_id.attr("disabled", ""); } else { - $("#qu_id").removeAttr("disabled"); + this.qu_id.removeAttr("disabled"); } - $(".input-group-productamountpicker").trigger("change"); + this.$(".input-group-productamountpicker").trigger("change"); + + } - Grocy.Components.ProductAmountPicker.SetQuantityUnit = function(quId) + SetQuantityUnit(quId) { - $("#qu_id").val(quId); + this.qu_id.val(quId); } - Grocy.Components.ProductAmountPicker.AllowAnyQu = function(keepInitialQu = false) + AllowAnyQu(keepInitialQu = false) { - Grocy.Components.ProductAmountPicker.AllowAnyQuEnabled = true; + this.AllowAnyQuEnabled = true; - $("#qu_id").find("option").remove().end(); - Grocy.QuantityUnits.forEach(qu => + this.qu_id.find("option").remove().end(); + for (let qu of this.Grocy.QuantityUnits) { - $("#qu_id").append(''); - }); + this.qu_id.append(''); + } if (keepInitialQu) { - Grocy.Components.ProductAmountPicker.SetQuantityUnit($("#qu_id").attr("data-initial-qu-id")); + this.SetQuantityUnit(this.qu_id.attr("data-initial-qu-id")); } - $("#qu_id").removeAttr("disabled"); + this.qu_id.removeAttr("disabled"); - $(".input-group-productamountpicker").trigger("change"); + this.$(".input-group-productamountpicker").trigger("change"); } - Grocy.Components.ProductAmountPicker.Reset = function() + Reset() { - $("#qu_id").find("option").remove(); - $("#qu-conversion-info").addClass("d-none"); - $("#qu-display_amount-info").val(""); + this.qu_id.find("option").remove(); + this.$("#qu-conversion-info").addClass("d-none"); + this.$("#qu-display_amount-info").val(""); } - $(".input-group-productamountpicker").on("change", function() + onChangeHandler(_this) { - var selectedQuName = $("#qu_id option:selected").text(); - var quFactor = $("#qu_id option:selected").attr("data-qu-factor"); - var amount = $("#display_amount").val(); + var selectedQuName = this.$("#qu_id option:selected").text(); + var quFactor = this.$("#qu_id option:selected").attr("data-qu-factor"); + var amount = this.display_amount.val(); var destinationAmount = amount / quFactor; - var destinationQuName = Grocy.translaten(destinationAmount, $("#qu_id").attr("data-destination-qu-name"), $("#qu_id").attr("data-destination-qu-name-plural")) + var destinationQuName = this.Grocy.translaten(destinationAmount, + this.qu_id.attr("data-destination-qu-name"), + this.qu_id.attr("data-destination-qu-name-plural") + ); - if ($("#qu_id").attr("data-destination-qu-name") == selectedQuName || Grocy.Components.ProductAmountPicker.AllowAnyQuEnabled || amount.toString().isEmpty() || selectedQuName.toString().isEmpty()) + var conversionInfo = this.$("#qu-conversion-info"); + + if (this.qu_id.attr("data-destination-qu-name") == selectedQuName || + this.AllowAnyQuEnabled || + amount.toString().isEmpty() || + selectedQuName.toString().isEmpty()) { - $("#qu-conversion-info").addClass("d-none"); + conversionInfo.addClass("d-none"); } else { - $("#qu-conversion-info").removeClass("d-none"); - $("#qu-conversion-info").text(Grocy.translate("This equals %1$s %2$s", destinationAmount.toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }), destinationQuName)); + conversionInfo.removeClass("d-none"); + conversionInfo.text(this.Grocy.translate("This equals %1$s %2$s", + destinationAmount.toLocaleString({ + minimumFractionDigits: 0, + maximumFractionDigits: this.Grocy.UserSettings.stock_decimal_places_amounts + }), + destinationQuName) + ); } - $("#amount").val(destinationAmount.toFixed(Grocy.UserSettings.stock_decimal_places_amounts).replace(/0*$/g, '')); - }); - - $("#display_amount").on("keyup", function() - { - $(".input-group-productamountpicker").trigger("change"); - }); + $("#amount").val( + destinationAmount + .toFixed(this.Grocy.UserSettings.stock_decimal_places_amounts) + .replace(/0*$/g, '') + ); + } } export { productamountpicker } \ No newline at end of file diff --git a/js/components/productcard.js b/js/components/productcard.js index 0d1e6711..e8f2bd69 100644 --- a/js/components/productcard.js +++ b/js/components/productcard.js @@ -2,123 +2,146 @@ import Chart from 'chart.js'; import { EmptyElementWhenMatches } from '../helpers/extensions' import { RefreshContextualTimeago } from '../configs/timeago' -function productcard(Grocy) +class productcard { - - Grocy.Components.ProductCard = {}; - - Grocy.Components.ProductCard.Refresh = function(productId) + constructor(Grocy, scopeSelector = null) { - Grocy.Api.Get('stock/products/' + productId, + this.Grocy = Grocy; + + this.scopeSelector = scopeSelector; + this.scope = scopeSelector != null ? $(scope) : $(document); + this.$ = scopeSelector != null ? $(scope).find : $; + this.PriceHistoryChart = null; + var self = this; + + this.$("#productcard-product-description").on("shown.bs.collapse", function() + { + self.$(".expandable-text") + .find("a[data-toggle='collapse']") + .text(self.Grocy.translate("Show less")); + }) + + this.$("#productcard-product-description").on("hidden.bs.collapse", function() + { + self.$(".expandable-text") + .find("a[data-toggle='collapse']") + .text(self.Grocy.translate("Show more")); + }) + } + + Refresh(productId) + { + var self = this; + this.Grocy.Api.Get('stock/products/' + productId, function(productDetails) { var stockAmount = productDetails.stock_amount || '0'; var stockValue = productDetails.stock_value || '0'; var stockAmountOpened = productDetails.stock_amount_opened || '0'; - $('#productcard-product-name').text(productDetails.product.name); - $('#productcard-product-description').html(productDetails.product.description); - $('#productcard-product-stock-amount').text(stockAmount); - $('#productcard-product-stock-qu-name').text(Grocy.translaten(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)); - $('#productcard-product-stock-value').text(stockValue + ' ' + Grocy.Currency); - $('#productcard-product-last-purchased').text((productDetails.last_purchased || '2999-12-31').substring(0, 10)); - $('#productcard-product-last-purchased-timeago').attr("datetime", productDetails.last_purchased || '2999-12-31'); - $('#productcard-product-last-used').text((productDetails.last_used || '2999-12-31').substring(0, 10)); - $('#productcard-product-last-used-timeago').attr("datetime", productDetails.last_used || '2999-12-31'); + self.$('#productcard-product-name').text(productDetails.product.name); + self.$('#productcard-product-description').html(productDetails.product.description); + self.$('#productcard-product-stock-amount').text(stockAmount); + self.$('#productcard-product-stock-qu-name').text(self.Grocy.translaten(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)); + self.$('#productcard-product-stock-value').text(stockValue + ' ' + self.Grocy.Currency); + self.$('#productcard-product-last-purchased').text((productDetails.last_purchased || '2999-12-31').substring(0, 10)); + self.$('#productcard-product-last-purchased-timeago').attr("datetime", productDetails.last_purchased || '2999-12-31'); + self.$('#productcard-product-last-used').text((productDetails.last_used || '2999-12-31').substring(0, 10)); + self.$('#productcard-product-last-used-timeago').attr("datetime", productDetails.last_used || '2999-12-31'); if (productDetails.location != null) { - $('#productcard-product-location').text(productDetails.location.name); + self.$('#productcard-product-location').text(productDetails.location.name); } - $('#productcard-product-spoil-rate').text((parseFloat(productDetails.spoil_rate_percent) / 100).toLocaleString(undefined, { style: "percent" })); + self.$('#productcard-product-spoil-rate').text((parseFloat(productDetails.spoil_rate_percent) / 100).toLocaleString(undefined, { style: "percent" })); if (productDetails.is_aggregated_amount == 1) { - $('#productcard-product-stock-amount-aggregated').text(productDetails.stock_amount_aggregated); - $('#productcard-product-stock-qu-name-aggregated').text(Grocy.translaten(productDetails.stock_amount_aggregated, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)); + self.$('#productcard-product-stock-amount-aggregated').text(productDetails.stock_amount_aggregated); + self.$('#productcard-product-stock-qu-name-aggregated').text(self.Grocy.translaten(productDetails.stock_amount_aggregated, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)); if (productDetails.stock_amount_opened_aggregated > 0) { - $('#productcard-product-stock-opened-amount-aggregated').text(Grocy.translate('%s opened', productDetails.stock_amount_opened_aggregated)); + self.$('#productcard-product-stock-opened-amount-aggregated').text(self.Grocy.translate('%s opened', productDetails.stock_amount_opened_aggregated)); } else { - $('#productcard-product-stock-opened-amount-aggregated').text(""); + self.$('#productcard-product-stock-opened-amount-aggregated').text(""); } - $("#productcard-aggregated-amounts").removeClass("d-none"); + self.$("#productcard-aggregated-amounts").removeClass("d-none"); } else { - $("#productcard-aggregated-amounts").addClass("d-none"); + self.$("#productcard-aggregated-amounts").addClass("d-none"); } if (productDetails.product.description != null && !productDetails.product.description.isEmpty()) { - $("#productcard-product-description-wrapper").removeClass("d-none"); + self.$("#productcard-product-description-wrapper").removeClass("d-none"); } else { - $("#productcard-product-description-wrapper").addClass("d-none"); + self.$("#productcard-product-description-wrapper").addClass("d-none"); } if (productDetails.average_shelf_life_days == -1) { - $('#productcard-product-average-shelf-life').text(Grocy.translate("Unknown")); + self.$('#productcard-product-average-shelf-life').text(self.Grocy.translate("Unknown")); } else if (parseInt(productDetails.average_shelf_life_days) > 73000) // > 200 years aka forever { - $('#productcard-product-average-shelf-life').text(Grocy.translate("Unlimited")); + self.$('#productcard-product-average-shelf-life').text(self.Grocy.translate("Unlimited")); } else { - $('#productcard-product-average-shelf-life').text(moment.duration(productDetails.average_shelf_life_days, "days").humanize()); + self.$('#productcard-product-average-shelf-life').text(moment.duration(productDetails.average_shelf_life_days, "days").humanize()); } if (stockAmountOpened > 0) { - $('#productcard-product-stock-opened-amount').text(Grocy.translate('%s opened', stockAmountOpened)); + self.$('#productcard-product-stock-opened-amount').text(self.Grocy.translate('%s opened', stockAmountOpened)); } else { - $('#productcard-product-stock-opened-amount').text(""); + self.$('#productcard-product-stock-opened-amount').text(""); } - $('#productcard-product-edit-button').attr("href", Grocy.FormatUrl("/product/" + productDetails.product.id.toString() + '?' + 'returnto=' + encodeURIComponent(Grocy.CurrentUrlRelative))); - $('#productcard-product-journal-button').attr("href", Grocy.FormatUrl("/stockjournal?embedded&product=" + productDetails.product.id.toString())); - $('#productcard-product-stock-button').attr("href", Grocy.FormatUrl("/stockentries?embedded&product=" + productDetails.product.id.toString())); - $('#productcard-product-stock-button').removeClass("disabled"); - $('#productcard-product-edit-button').removeClass("disabled"); - $('#productcard-product-journal-button').removeClass("disabled"); + self.$('#productcard-product-edit-button').attr("href", self.Grocy.FormatUrl("/product/" + productDetails.product.id.toString() + '?' + 'returnto=' + encodeURIComponent(self.Grocy.CurrentUrlRelative))); + self.$('#productcard-product-journal-button').attr("href", self.Grocy.FormatUrl("/stockjournal?embedded&product=" + productDetails.product.id.toString())); + self.$('#productcard-product-stock-button').attr("href", self.Grocy.FormatUrl("/stockentries?embedded&product=" + productDetails.product.id.toString())); + self.$('#productcard-product-stock-button').removeClass("disabled"); + self.$('#productcard-product-edit-button').removeClass("disabled"); + self.$('#productcard-product-journal-button').removeClass("disabled"); if (productDetails.last_price !== null) { - $('#productcard-product-last-price').text(Number.parseFloat(productDetails.last_price).toLocaleString() + ' ' + Grocy.Currency + ' per ' + productDetails.quantity_unit_stock.name); + self.$('#productcard-product-last-price').text(Number.parseFloat(productDetails.last_price).toLocaleString() + ' ' + self.Grocy.Currency + ' per ' + productDetails.quantity_unit_stock.name); } else { - $('#productcard-product-last-price').text(Grocy.translate('Unknown')); + self.$('#productcard-product-last-price').text(self.Grocy.translate('Unknown')); } if (productDetails.avg_price !== null) { - $('#productcard-product-average-price').text(Number.parseFloat(productDetails.avg_price).toLocaleString() + ' ' + Grocy.Currency + ' per ' + productDetails.quantity_unit_stock.name); + self.$('#productcard-product-average-price').text(Number.parseFloat(productDetails.avg_price).toLocaleString() + ' ' + self.Grocy.Currency + ' per ' + productDetails.quantity_unit_stock.name); } else { - $('#productcard-product-average-price').text(Grocy.translate('Unknown')); + self.$('#productcard-product-average-price').text(self.Grocy.translate('Unknown')); } if (productDetails.product.picture_file_name !== null && !productDetails.product.picture_file_name.isEmpty()) { - $("#productcard-product-picture").removeClass("d-none"); - $("#productcard-product-picture").attr("src", Grocy.FormatUrl('/api/files/productpictures/' + btoa(productDetails.product.picture_file_name) + '?force_serve_as=picture&best_fit_width=400')); + self.$("#productcard-product-picture").removeClass("d-none"); + self.$("#productcard-product-picture").attr("src", Grocy.FormatUrl('/api/files/productpictures/' + btoa(productDetails.product.picture_file_name) + '?force_serve_as=picture&best_fit_width=400')); } else { - $("#productcard-product-picture").addClass("d-none"); + self.$("#productcard-product-picture").addClass("d-none"); } - EmptyElementWhenMatches('#productcard-product-last-purchased-timeago', Grocy.translate('timeago_nan')); - EmptyElementWhenMatches('#productcard-product-last-used-timeago', Grocy.translate('timeago_nan')); + EmptyElementWhenMatches(self.$('#productcard-product-last-purchased-timeago'), self.Grocy.translate('timeago_nan')); + EmptyElementWhenMatches(self.$('#productcard-product-last-used-timeago'), self.Grocy.translate('timeago_nan')); RefreshContextualTimeago(".productcard"); }, function(xhr) @@ -127,22 +150,22 @@ function productcard(Grocy) } ); - if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) + if (this.Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) { - Grocy.Api.Get('stock/products/' + productId + '/price-history', + this.Grocy.Api.Get('stock/products/' + productId + '/price-history', function(priceHistoryDataPoints) { if (priceHistoryDataPoints.length > 0) { - $("#productcard-product-price-history-chart").removeClass("d-none"); - $("#productcard-no-price-data-hint").addClass("d-none"); + self.$("#productcard-product-price-history-chart").removeClass("d-none"); + self.$("#productcard-no-price-data-hint").addClass("d-none"); - Grocy.Components.ProductCard.ReInitPriceHistoryChart(); + self.ReInitPriceHistoryChart(); var datasets = {}; - var chart = Grocy.Components.ProductCard.PriceHistoryChart.data; - priceHistoryDataPoints.forEach((dataPoint) => + var chart = self.PriceHistoryChart.data; + for (let dataPoint of priceHistoryDataPoints) { - var key = Grocy.translate("Unknown store"); + let key = Grocy.translate("Unknown store"); if (dataPoint.shopping_location) { key = dataPoint.shopping_location.name @@ -154,9 +177,9 @@ function productcard(Grocy) } chart.labels.push(moment(dataPoint.date).toDate()); datasets[key].push(dataPoint.price); + } - }); - Object.keys(datasets).forEach((key) => + for (let key of Object.keys(datasets)) { chart.datasets.push({ data: datasets[key], @@ -164,13 +187,13 @@ function productcard(Grocy) borderColor: "HSL(" + (129 * chart.datasets.length) + ",100%,50%)", label: key, }); - }); - Grocy.Components.ProductCard.PriceHistoryChart.update(); + } + self.PriceHistoryChart.update(); } else { - $("#productcard-product-price-history-chart").addClass("d-none"); - $("#productcard-no-price-data-hint").removeClass("d-none"); + self.$("#productcard-product-price-history-chart").addClass("d-none"); + self.$("#productcard-no-price-data-hint").removeClass("d-none"); } }, function(xhr) @@ -179,17 +202,17 @@ function productcard(Grocy) } ); } - }; + } - Grocy.Components.ProductCard.ReInitPriceHistoryChart = function() + ReInitPriceHistoryChart() { - if (typeof Grocy.Components.ProductCard.PriceHistoryChart !== "undefined") + if (this.PriceHistoryChart !== null) { - Grocy.Components.ProductCard.PriceHistoryChart.destroy(); + this.PriceHistoryChart.destroy(); } var format = 'YYYY-MM-DD'; - Grocy.Components.ProductCard.PriceHistoryChart = new Chart(document.getElementById("productcard-product-price-history-chart"), { + this.PriceHistoryChart = new Chart(this.$("#productcard-product-price-history-chart")[0], { type: "line", data: { labels: [ //Date objects @@ -230,16 +253,6 @@ function productcard(Grocy) } }); } - - $("#productcard-product-description").on("shown.bs.collapse", function() - { - $(".expandable-text").find("a[data-toggle='collapse']").text(Grocy.translate("Show less")); - }) - - $("#productcard-product-description").on("hidden.bs.collapse", function() - { - $(".expandable-text").find("a[data-toggle='collapse']").text(Grocy.translate("Show more")); - }) } export { productcard } \ No newline at end of file diff --git a/js/components/productpicker.js b/js/components/productpicker.js index 1e818335..e659d18d 100644 --- a/js/components/productpicker.js +++ b/js/components/productpicker.js @@ -1,159 +1,171 @@ -function productpicker(Grocy) +import BasePicker from "./BasePicker"; + +class productpicker extends BasePicker { - Grocy.Use('barcodescanner'); - Grocy.Components.ProductPicker = {}; - Grocy.Components.ProductPicker.GetPicker = function() + constructor(Grocy, scopeSelector = null) { - return $('#product_id'); + super(Grocy, "#product_id", scopeSelector) + + this.Grocy.Use('barcodescanner'); + + this.picker = this.$('#product_id'); + this.inputElement = this.$('#product_id_text_input'); + + var self = this; + + this.embedded = ""; + if (this.Grocy.GetUriParam("embedded") !== undefined) + { + this.embedded = "embedded"; + } + + this.initCombobox('.product-combobox'); + + this.prefill(); + + if (this.Grocy.GetUriParam("flow") === "InplaceAddBarcodeToExistingProduct") + { + this.$('#InplaceAddBarcodeToExistingProduct').text(this.Grocy.GetUriParam("barcode")); + this.$('#flow-info-InplaceAddBarcodeToExistingProduct').removeClass('d-none'); + this.$('#barcode-lookup-disabled-hint').removeClass('d-none'); + this.$('#barcode-lookup-hint').addClass('d-none'); + } + + this.PopupOpen = false; + + this.$('#product_id_text_input').on('blur', (e) => self.onBlurHandler(this, e)); + this.scope.on("Grocy.BarcodeScanned", (e, barcode, target) => self.onBarcodeScannedHandler(e, barcode, target)); + + $(document).on("shown.bs.modal", function(e) + { + self.$(".modal-footer").addClass("d-block").addClass("d-sm-flex"); + self.$(".modal-footer").find("button").addClass("mt-2").addClass("mt-sm-0"); + }); + + // Make that ENTER behaves the same like TAB (trigger blur to start workflows, + // but only when the dropdown is not opened) + this.$('#product_id_text_input').keydown(function(event) + { + if (event.keyCode === 13) // Enter + { + if (this.picker.hasClass("combobox-menu-visible")) + { + return; + } + + self.$("#product_id_text_input").trigger("blur"); + } + }); } - Grocy.Components.ProductPicker.GetInputElement = function() + prefill() { - return $('#product_id_text_input'); + var doFocus = false; + + var prefillProduct = this.Grocy.GetUriParam('product-name'); + var prefillProduct2 = this.picker.parent().data('prefill-by-name').toString(); + if (!prefillProduct2.isEmpty()) + { + prefillProduct = prefillProduct2; + } + if (typeof prefillProduct !== "undefined") + { + var possibleOptionElement = this.$("#product_id option[data-additional-searchdata*=\"" + prefillProduct + "\"]").first(); + if (possibleOptionElement.length === 0) + { + possibleOptionElement = this.$("#product_id option:contains(\"" + prefillProduct + "\")").first(); + } + + if (possibleOptionElement.length > 0) + { + doFocus = true; + this.picker.val(possibleOptionElement.val()); + } + } + + var prefillProductId = this.Grocy.GetUriParam("product"); + var prefillProductId2 = this.picker.parent().data('prefill-by-id').toString(); + if (!prefillProductId2.isEmpty()) + { + prefillProductId = prefillProductId2; + } + if (typeof prefillProductId !== "undefined") + { + this.picker.val(prefillProductId); + doFocus = true; + } + + if (doFocus) + { + this.picker.data('combobox').refresh(); + this.picker.trigger('change'); + + this.$(this.picker.parent().data('next-input-selector').toString()) + .focus(); + } } - Grocy.Components.ProductPicker.GetValue = function() + InProductAddWorkflow() { - return $('#product_id').val(); + return this.Grocy.GetUriParam('flow') == "InplaceNewProductWithName"; } - Grocy.Components.ProductPicker.SetValue = function(value) + InProductModifyWorkflow() { - Grocy.Components.ProductPicker.GetInputElement().val(value); - Grocy.Components.ProductPicker.GetInputElement().trigger('change'); + return this.Grocy.GetUriParam('flow') == "InplaceAddBarcodeToExistingProduct"; } - Grocy.Components.ProductPicker.SetId = function(value) + InAnyFlow() { - Grocy.Components.ProductPicker.GetPicker().val(value); - Grocy.Components.ProductPicker.GetPicker().data('combobox').refresh(); - Grocy.Components.ProductPicker.GetInputElement().trigger('change'); + return this.InProductAddWorkflow() || this.InProductModifyWorkflow(); } - Grocy.Components.ProductPicker.Clear = function() + FinishFlow() { - Grocy.Components.ProductPicker.SetValue(''); - Grocy.Components.ProductPicker.SetId(null); + this.Grocy.RemoveUriParam("flow"); + this.Grocy.RemoveUriParam("barcode"); + this.Grocy.RemoveUriParam("product-name"); } - Grocy.Components.ProductPicker.InProductAddWorkflow = function() + ShowCustomError(text) { - return Grocy.GetUriParam('flow') == "InplaceNewProductWithName"; - } - - Grocy.Components.ProductPicker.InProductModifyWorkflow = function() - { - return Grocy.GetUriParam('flow') == "InplaceAddBarcodeToExistingProduct"; - } - - Grocy.Components.ProductPicker.InAnyFlow = function() - { - return Grocy.Components.ProductPicker.InProductAddWorkflow() || Grocy.Components.ProductPicker.InProductModifyWorkflow(); - } - - Grocy.Components.ProductPicker.FinishFlow = function() - { - Grocy.RemoveUriParam("flow"); - Grocy.RemoveUriParam("barcode"); - Grocy.RemoveUriParam("product-name"); - } - - Grocy.Components.ProductPicker.ShowCustomError = function(text) - { - var element = $("#custom-productpicker-error"); + var element = this.$("#custom-productpicker-error"); element.text(text); element.removeClass("d-none"); } - Grocy.Components.ProductPicker.HideCustomError = function() + HideCustomError() { - $("#custom-productpicker-error").addClass("d-none"); + this.$("#custom-productpicker-error").addClass("d-none"); } - Grocy.Components.ProductPicker.Disable = function() + Disable() { - Grocy.Components.ProductPicker.GetInputElement().attr("disabled", ""); - $("#barcodescanner-start-button").attr("disabled", ""); - $("#barcodescanner-start-button").addClass("disabled"); + this.inputElement.attr("disabled", ""); + this.$("#barcodescanner-start-button").attr("disabled", ""); + this.$("#barcodescanner-start-button").addClass("disabled"); } - Grocy.Components.ProductPicker.Enable = function() + Enable() { - Grocy.Components.ProductPicker.GetInputElement().removeAttr("disabled"); - $("#barcodescanner-start-button").removeAttr("disabled"); - $("#barcodescanner-start-button").removeClass("disabled"); + this.inputElement.removeAttr("disabled"); + this.$("#barcodescanner-start-button").removeAttr("disabled"); + this.$("#barcodescanner-start-button").removeClass("disabled"); } - $('.product-combobox').combobox({ - appendId: '_text_input', - bsVersion: '4', - clearIfNoMatch: false - }); - - var this_product_picker = Grocy.Components.ProductPicker.GetPicker(); - var productpicker_doFocus = false; - - var prefillProduct = Grocy.GetUriParam('product-name'); - var prefillProduct2 = this_product_picker.parent().data('prefill-by-name').toString(); - if (!prefillProduct2.isEmpty()) + onBlurHandler(_this, e) { - prefillProduct = prefillProduct2; - } - if (typeof prefillProduct !== "undefined") - { - var possibleOptionElement = $("#product_id option[data-additional-searchdata*=\"" + prefillProduct + "\"]").first(); - if (possibleOptionElement.length === 0) - { - possibleOptionElement = $("#product_id option:contains(\"" + prefillProduct + "\")").first(); - } + var self = this; - if (possibleOptionElement.length > 0) - { - productpicker_doFocus = true; - this_product_picker.val(possibleOptionElement.val()); - } - } - - var prefillProductId = Grocy.GetUriParam("product"); - var prefillProductId2 = this_product_picker.parent().data('prefill-by-id').toString(); - if (!prefillProductId2.isEmpty()) - { - prefillProductId = prefillProductId2; - } - if (typeof prefillProductId !== "undefined") - { - this_product_picker.val(prefillProductId); - productpicker_doFocus = true; - } - - if (productpicker_doFocus) - { - this_product_picker.data('combobox').refresh(); - this_product_picker.trigger('change'); - - $(this_product_picker.parent().data('next-input-selector').toString()) - .focus(); - } - - if (Grocy.GetUriParam("flow") === "InplaceAddBarcodeToExistingProduct") - { - $('#InplaceAddBarcodeToExistingProduct').text(Grocy.GetUriParam("barcode")); - $('#flow-info-InplaceAddBarcodeToExistingProduct').removeClass('d-none'); - $('#barcode-lookup-disabled-hint').removeClass('d-none'); - $('#barcode-lookup-hint').addClass('d-none'); - } - - Grocy.Components.ProductPicker.PopupOpen = false; - $('#product_id_text_input').on('blur', function(e) - { - if (Grocy.Components.ProductPicker.GetPicker().hasClass("combobox-menu-visible")) + if (this.picker.hasClass("combobox-menu-visible")) { return; } - $('#product_id').attr("barcode", "null"); - var input = $('#product_id_text_input').val().toString(); + this.picker.attr("barcode", "null"); + + var input = this.$('#product_id_text_input').val().toString(); var possibleOptionElement = []; // did we enter a grocycode? @@ -163,62 +175,66 @@ function productpicker(Grocy) if (gc[1] == "p") { // find product id - possibleOptionElement = $("#product_id option[value=\"" + gc[2] + "\"]").first(); - $("#product_id").data("grocycode", true); + possibleOptionElement = this.$("#product_id option[value=\"" + gc[2] + "\"]").first(); + this.picker.data("grocycode", true); } } else // process barcode as usual { - possibleOptionElement = $("#product_id option[data-additional-searchdata*=\"" + input + ",\"]").first(); + possibleOptionElement = this.$("#product_id option[data-additional-searchdata*=\"" + input + ",\"]").first(); } - if (Grocy.GetUriParam('flow') === undefined && input.length > 0 && possibleOptionElement.length > 0) + if (this.Grocy.GetUriParam('flow') === undefined && input.length > 0 && possibleOptionElement.length > 0) { - $('#product_id').val(possibleOptionElement.val()); - $('#product_id').attr("barcode", input); - $('#product_id').data('combobox').refresh(); - $('#product_id').trigger('change'); + this.picker.val(possibleOptionElement.val()); + this.picker.attr("barcode", input); + this.picker.data('combobox').refresh(); + this.picker.trigger('change'); } else { - if (Grocy.Components.ProductPicker.PopupOpen === true) + if (this.PopupOpen === true) { return; } - var optionElement = $("#product_id option:contains(\"" + input + "\")").first(); - if (input.length > 0 && optionElement.length === 0 && Grocy.GetUriParam('flow') === undefined && Grocy.Components.ProductPicker.GetPicker().parent().data('disallow-all-product-workflows').toString() === "false") + var optionElement = this.$("#product_id option:contains(\"" + input + "\")").first(); + if (input.length > 0 && + optionElement.length === 0 && + this.Grocy.GetUriParam('flow') === undefined && + this.picker.parent().data('disallow-all-product-workflows').toString() === "false") { var addProductWorkflowsAdditionalCssClasses = ""; - if (Grocy.Components.ProductPicker.GetPicker().parent().data('disallow-add-product-workflows').toString() === "true") + if (this.picker.parent().data('disallow-add-product-workflows').toString() === "true") { addProductWorkflowsAdditionalCssClasses = "d-none"; } - var embedded = ""; - if (Grocy.GetUriParam("embedded") !== undefined) - { - embedded = "embedded"; - } - var buttons = { cancel: { - label: Grocy.translate('Cancel'), + label: this.Grocy.translate('Cancel'), className: 'btn-secondary responsive-button', callback: function() { - Grocy.Components.ProductPicker.PopupOpen = false; - Grocy.Components.ProductPicker.SetValue(''); + self.PopupOpen = false; + self.SetValue(''); } }, addnewproduct: { - label: 'P ' + Grocy.translate('Add as new product'), + label: 'P ' + this.Grocy.translate('Add as new product'), className: 'btn-success add-new-product-dialog-button responsive-button ' + addProductWorkflowsAdditionalCssClasses, callback: function() { - Grocy.Components.ProductPicker.PopupOpen = false; - window.location.href = Grocy.FormatUrl('/product/new?flow=InplaceNewProductWithName&name=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(Grocy.CurrentUrlRelative + "?flow=InplaceNewProductWithName&" + embedded) + "&" + embedded); + self.PopupOpen = false; + window.location.href = self.Grocy.FormatUrl( + '/product/new?flow=InplaceNewProductWithName' + + '&name=' + encodeURIComponent(input) + + '&returnto=' + encodeURIComponent( + Grocy.CurrentUrlRelative + + "?flow=InplaceNewProductWithName" + + "&" + self.embedded) + + "&" + self.embedded); } }, addbarcode: { @@ -226,8 +242,12 @@ function productpicker(Grocy) className: 'btn-info add-new-barcode-dialog-button responsive-button', callback: function() { - Grocy.Components.ProductPicker.PopupOpen = false; - window.location.href = Grocy.FormatUrl(Grocy.CurrentUrlRelative + '?flow=InplaceAddBarcodeToExistingProduct&barcode=' + encodeURIComponent(input)); + self.PopupOpen = false; + window.location.href = Grocy.FormatUrl( + self.Grocy.CurrentUrlRelative + + '?flow=InplaceAddBarcodeToExistingProduct' + + '&barcode=' + encodeURIComponent(input) + ); } }, addnewproductwithbarcode: { @@ -235,8 +255,18 @@ function productpicker(Grocy) className: 'btn-warning add-new-product-with-barcode-dialog-button responsive-button ' + addProductWorkflowsAdditionalCssClasses, callback: function() { - Grocy.Components.ProductPicker.PopupOpen = false; - window.location.href = Grocy.FormatUrl('/product/new?flow=InplaceNewProductWithBarcode&barcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(Grocy.CurrentUrlRelative + "?flow=InplaceAddBarcodeToExistingProduct&barcode=" + input + "&" + embedded) + "&" + embedded); + self.PopupOpen = false; + window.location.href = Grocy.FormatUrl( + '/product/new' + + '?flow=InplaceNewProductWithBarcode' + + '&barcode=' + encodeURIComponent(input) + + '&returnto=' + encodeURIComponent( + self.Grocy.CurrentUrlRelative + + "?flow=InplaceAddBarcodeToExistingProduct" + + "&barcode=" + input + + "&" + self.embedded + ) + + "&" + self.embedded); } } }; @@ -248,21 +278,21 @@ function productpicker(Grocy) className: 'btn-primary responsive-button retry-camera-scanning-button', callback: function() { - Grocy.Components.ProductPicker.PopupOpen = false; - Grocy.Components.ProductPicker.SetValue(''); - $("#barcodescanner-start-button").click(); + self.PopupOpen = false; + self.SetValue(''); + self.$("#barcodescanner-start-button").click(); } }; } - Grocy.Components.ProductPicker.PopupOpen = true; + this.PopupOpen = true; bootbox.dialog({ - message: Grocy.translate('"%s" could not be resolved to a product, how do you want to proceed?', input), - title: Grocy.translate('Create or assign product'), + message: this.Grocy.translate('"%s" could not be resolved to a product, how do you want to proceed?', input), + title: this.Grocy.translate('Create or assign product'), onEscape: function() { - Grocy.Components.ProductPicker.PopupOpen = false; - Grocy.Components.ProductPicker.SetValue(''); + self.PopupOpen = false; + self.SetValue(''); }, size: 'large', backdrop: true, @@ -272,66 +302,49 @@ function productpicker(Grocy) { if (e.key === 'B' || e.key === 'b') { - $('.add-new-barcode-dialog-button').not(".d-none").click(); + self.$('.add-new-barcode-dialog-button').not(".d-none").click(); } if (e.key === 'p' || e.key === 'P') { - $('.add-new-product-dialog-button').not(".d-none").click(); + self.$('.add-new-product-dialog-button').not(".d-none").click(); } if (e.key === 'a' || e.key === 'A') { - $('.add-new-product-with-barcode-dialog-button').not(".d-none").click(); + self.$('.add-new-product-with-barcode-dialog-button').not(".d-none").click(); } if (e.key === 'c' || e.key === 'C') { - $('.retry-camera-scanning-button').not(".d-none").click(); + self.$('.retry-camera-scanning-button').not(".d-none").click(); } }); } } - }); + } - $(document).on("Grocy.BarcodeScanned", function(e, barcode, target) + onBarcodeScannedHandler(e, barcode, target) { + var self = this; if (!(target == "@productpicker" || target == "undefined" || target == undefined)) // Default target { return; } // Don't know why the blur event does not fire immediately ... this works... - Grocy.Components.ProductPicker.GetInputElement().focusout(); - Grocy.Components.ProductPicker.GetInputElement().focus(); - Grocy.Components.ProductPicker.GetInputElement().blur(); + this.inputElement + .focusout() + .focus() + .blur(); - Grocy.Components.ProductPicker.GetInputElement().val(barcode); + this.inputElement.val(barcode); setTimeout(function() { - Grocy.Components.ProductPicker.GetInputElement().focusout(); - Grocy.Components.ProductPicker.GetInputElement().focus(); - Grocy.Components.ProductPicker.GetInputElement().blur(); + self.inputElement + .focusout() + .focus() + .blur(); }, 200); - }); - - $(document).on("shown.bs.modal", function(e) - { - $(".modal-footer").addClass("d-block").addClass("d-sm-flex"); - $(".modal-footer").find("button").addClass("mt-2").addClass("mt-sm-0"); - }) - - // Make that ENTER behaves the same like TAB (trigger blur to start workflows, but only when the dropdown is not opened) - $('#product_id_text_input').keydown(function(event) - { - if (event.keyCode === 13) // Enter - { - if (Grocy.Components.ProductPicker.GetPicker().hasClass("combobox-menu-visible")) - { - return; - } - - $("#product_id_text_input").trigger("blur"); - } - }); + } } export { productpicker } \ No newline at end of file diff --git a/js/components/recipepicker.js b/js/components/recipepicker.js index 082559f2..bcae1104 100644 --- a/js/components/recipepicker.js +++ b/js/components/recipepicker.js @@ -1,76 +1,17 @@ -function recipepicker(Grocy) +import BasePicker from './BasePicker' + +class recipepicker extends BasePicker { - Grocy.Components.RecipePicker = {}; - - Grocy.Components.RecipePicker.GetPicker = function() + constructor(Grocy, scopeSelector = null) { - return $('#recipe_id'); - } + super(Grocy, "#recipe_id", scopeSelector); - Grocy.Components.RecipePicker.GetInputElement = function() - { - return $('#recipe_id_text_input'); - } + this.picker = this.$(this.basename); + this.input_element = this.$(this.basename + '_text_input'); - Grocy.Components.RecipePicker.GetValue = function() - { - return $('#recipe_id').val(); - } - - Grocy.Components.RecipePicker.SetValue = function(value) - { - Grocy.Components.RecipePicker.GetInputElement().val(value); - Grocy.Components.RecipePicker.GetInputElement().trigger('change'); - } - - Grocy.Components.RecipePicker.SetId = function(value) - { - Grocy.Components.RecipePicker.GetPicker().val(value); - Grocy.Components.RecipePicker.GetPicker().data('combobox').refresh(); - Grocy.Components.RecipePicker.GetInputElement().trigger('change'); - } - - Grocy.Components.RecipePicker.Clear = function() - { - Grocy.Components.RecipePicker.SetValue(''); - Grocy.Components.RecipePicker.SetId(null); - } - - $('.recipe-combobox').combobox({ - appendId: '_text_input', - bsVersion: '4', - clearIfNoMatch: true - }); - - var this_recipe_picker = Grocy.Components.RecipePicker.GetPicker(); - var recipe_picker_doFocus = false; - - var prefillByName = this_recipe_picker.parent().data('prefill-by-name').toString(); - if (typeof prefillByName !== "undefined") - { - var possibleOptionElement = $("#recipe_id option:contains(\"" + prefillByName + "\")").first(); - - if (possibleOptionElement.length > 0) - { - recipe_picker_doFocus = true; - this_recipe_picker.val(possibleOptionElement.val()); - } - } - - var prefillById = this_recipe_picker.parent().data('prefill-by-id').toString(); - if (typeof prefillById !== "undefined") - { - recipe_picker_doFocus = true; - this_recipe_picker.val(prefillById); - } - - if (recipe_picker_doFocus) - { - this_recipe_picker.data('combobox').refresh(); - this_recipe_picker.trigger('change'); - - $(this_recipe_picker.parent().data('next-input-selector').toString()).focus(); + this.initCombobox('.recipe-combobox'); + this.prefill(); } } diff --git a/js/components/shoppinglocationpicker.js b/js/components/shoppinglocationpicker.js index 0e4768c2..77441ca4 100644 --- a/js/components/shoppinglocationpicker.js +++ b/js/components/shoppinglocationpicker.js @@ -1,76 +1,17 @@ -function shoppinglocationpicker(Grocy) +import BasePicker from "./BasePicker"; + +class shoppinglocationpicker extends BasePicker { - Grocy.Components.ShoppingLocationPicker = {}; - Grocy.Components.ShoppingLocationPicker.GetPicker = function() + constructor(Grocy, scopeSelector = null) { - return $('#shopping_location_id'); - } + super(Grocy, "#shopping_location_id", scopeSelector); - Grocy.Components.ShoppingLocationPicker.GetInputElement = function() - { - return $('#shopping_location_id_text_input'); - } + this.picker = this.$(this.basename); + this.input_element = this.$(this.basename + '_text_input'); - Grocy.Components.ShoppingLocationPicker.GetValue = function() - { - return $('#shopping_location_id').val(); - } - - Grocy.Components.ShoppingLocationPicker.SetValue = function(value) - { - Grocy.Components.ShoppingLocationPicker.GetInputElement().val(value); - Grocy.Components.ShoppingLocationPicker.GetInputElement().trigger('change'); - } - - Grocy.Components.ShoppingLocationPicker.SetId = function(value) - { - Grocy.Components.ShoppingLocationPicker.GetPicker().val(value); - Grocy.Components.ShoppingLocationPicker.GetPicker().data('combobox').refresh(); - Grocy.Components.ShoppingLocationPicker.GetInputElement().trigger('change'); - } - - Grocy.Components.ShoppingLocationPicker.Clear = function() - { - Grocy.Components.ShoppingLocationPicker.SetValue(''); - Grocy.Components.ShoppingLocationPicker.SetId(null); - } - - $('.shopping-location-combobox').combobox({ - appendId: '_text_input', - bsVersion: '4', - clearIfNoMatch: true - }); - - var shoppinglocationpicker_doFocus = false; - var this_shoppinglocation_picker = $('#shopping_location_id'); - - var prefillByName = this_shoppinglocation_picker.parent().data('prefill-by-name').toString(); - if (typeof prefillByName !== "undefined") - { - var possibleOptionElement = $("#shopping_location_id option:contains(\"" + prefillByName + "\")").first(); - - if (possibleOptionElement.length > 0) - { - this_shoppinglocation_picker.val(possibleOptionElement.val()); - shoppinglocationpicker_doFocus = true; - } - } - - var prefillById = this_shoppinglocation_picker.parent().data('prefill-by-id').toString(); - if (typeof prefillById !== "undefined") - { - this_shoppinglocation_picker.val(prefillById); - shoppinglocationpicker_doFocus = true; - } - - if (shoppinglocationpicker_doFocus) - { - this_shoppinglocation_picker.data('combobox').refresh(); - this_shoppinglocation_picker.trigger('change'); - - $(this_shoppinglocation_picker.parent().data('next-input-selector').toString()) - .focus(); + this.initCombobox('.recipe-combobox'); + this.prefill(); } } diff --git a/js/components/userfieldsform.js b/js/components/userfieldsform.js index c0171129..3e025111 100644 --- a/js/components/userfieldsform.js +++ b/js/components/userfieldsform.js @@ -1,17 +1,28 @@ import { RandomString } from '../helpers/extensions'; import { LoadImagesLazy } from '../configs/lazy' +import uuid from 'uuid'; +import { datetimepicker } from "./datetimepicker"; -function userfieldsform(Grocy) +class userfieldsform { - if (document.querySelector('.datetimepicker-input') !== null) + constructor(Grocy, scopeSelector = null) { - Grocy.Use("datetimepicker"); - } - Grocy.Components.UserfieldsForm = {}; + this.Grocy = Grocy; - Grocy.Components.UserfieldsForm.Save = function(success, error) + this.scopeSelector = scopeSelector; + this.scope = scopeSelector != null ? $(scope) : $(document); + this.$ = scopeSelector != null ? $(scope).find : $; + + this.$(".userfield-link").keyup(); + + // We need to further scope some of the embedded components. + // Store those instances here. + this.components = [] + } + + Save(success, error) { - if (!$("#userfields-form").length) + if (!this.$("#userfields-form").length) { if (success) { @@ -22,8 +33,9 @@ function userfieldsform(Grocy) } var jsonData = {}; + var self = this; - $("#userfields-form .userfield-input").not("div").each(function() + this.$("#userfields-form .userfield-input").not("div").each(function() { var input = $(this); var fieldName = input.attr("data-userfield-name"); @@ -42,10 +54,10 @@ function userfieldsform(Grocy) var oldFile = input.data('old-file') if (oldFile) { - Grocy.Api.Delete('files/userfiles/' + oldFile, null, null, + self.Grocy.Api.Delete('files/userfiles/' + oldFile, null, null, function(xhr) { - Grocy.FrontendHelpers.ShowGenericError('Could not delete file', xhr); + self.Grocy.FrontendHelpers.ShowGenericError('Could not delete file', xhr); }); jsonData[fieldName] = ""; } @@ -56,7 +68,7 @@ function userfieldsform(Grocy) var fileName = RandomString() + '.' + input[0].files[0].name.split('.').reverse()[0]; jsonData[fieldName] = btoa(fileName) + '_' + btoa(input[0].files[0].name); - Grocy.Api.UploadFile(input[0].files[0], 'userfiles', fileName, + self.Grocy.Api.UploadFile(input[0].files[0], 'userfiles', fileName, function(result) { }, @@ -78,7 +90,7 @@ function userfieldsform(Grocy) } }); - Grocy.Api.Put('userfields/' + $("#userfields-form").data("entity") + '/' + Grocy.EditObjectId, jsonData, + this.Grocy.Api.Put('userfields/' + $("#userfields-form").data("entity") + '/' + this.Grocy.EditObjectId, jsonData, function(result) { if (success) @@ -96,19 +108,20 @@ function userfieldsform(Grocy) ); } - Grocy.Components.UserfieldsForm.Load = function() + Load() { - if (!$("#userfields-form").length) + if (!this.$("#userfields-form").length) { return; } + var self = this; - Grocy.Api.Get('userfields/' + $("#userfields-form").data("entity") + '/' + Grocy.EditObjectId, + Grocy.Api.Get('userfields/' + this.$("#userfields-form").data("entity") + '/' + this.Grocy.EditObjectId, function(result) { $.each(result, function(key, value) { - var input = $(".userfield-input[data-userfield-name='" + key + "']"); + var input = self.$(".userfield-input[data-userfield-name='" + key + "']"); if (input.attr("type") == "checkbox" && value == 1) { @@ -117,7 +130,7 @@ function userfieldsform(Grocy) else if (input.hasAttr("multiple")) { input.val(value.split(",")); - $(".selectpicker").selectpicker("render"); + self.$(".selectpicker").selectpicker("render"); } else if (input.attr('type') == "file") { @@ -128,16 +141,16 @@ function userfieldsform(Grocy) var formGroup = input.parent().parent().parent(); formGroup.find("label.custom-file-label").text(fileName); - formGroup.find(".userfield-file-show").attr('href', Grocy.FormatUrl('/files/userfiles/' + value)); + formGroup.find(".userfield-file-show").attr('href', self.Grocy.FormatUrl('/files/userfiles/' + value)); formGroup.find('.userfield-file-show').removeClass('d-none'); formGroup.find('img.userfield-current-file') - .attr('src', Grocy.FormatUrl('/files/userfiles/' + value + '?force_serve_as=picture&best_fit_width=250&best_fit_height=250')); + .attr('src', self.Grocy.FormatUrl('/files/userfiles/' + value + '?force_serve_as=picture&best_fit_width=250&best_fit_height=250')); LoadImagesLazy(); formGroup.find('.userfield-file-delete').click( function() { - formGroup.find("label.custom-file-label").text(Grocy.translate("No file selected")); + formGroup.find("label.custom-file-label").text(self.Grocy.translate("No file selected")); formGroup.find(".userfield-file-show").addClass('d-none'); input.attr('data-old-file', fileSrc); } @@ -167,6 +180,16 @@ function userfieldsform(Grocy) input.val(value); } }); + + self.$('.datetimepicker-wrapper').each(() => + { + let picker = $(this); + + var scopeId = uuid.v4(); + picker.attr('id', scopeId); + + this.components.push(new datetimepicker(self.Grocy, "#" + scopeId)); + }); }, function(xhr) { @@ -175,9 +198,10 @@ function userfieldsform(Grocy) ); } - $(".userfield-link").keyup(function(e) + onUserfieldInputKeyUp(_this) { - var formRow = $(this).parent().parent(); + + var formRow = this.$(_this).parent().parent(); var title = formRow.find(".userfield-link-title").val(); var link = formRow.find(".userfield-link-link").val(); @@ -187,7 +211,8 @@ function userfieldsform(Grocy) }; formRow.find(".userfield-input").val(JSON.stringify(value)); - }); + + } } export { userfieldsform } \ No newline at end of file diff --git a/js/components/userpicker.js b/js/components/userpicker.js index 8f96fe84..6762f345 100644 --- a/js/components/userpicker.js +++ b/js/components/userpicker.js @@ -1,86 +1,58 @@ -function userpicker(Grocy) +import BasePicker from "./BasePicker"; + +class userpicker extends BasePicker { - - Grocy.Components.UserPicker = {}; - - Grocy.Components.UserPicker.GetPicker = function() + constructor(Grocy, scopeSelector = null) { - return $('#user_id'); + super(Grocy, "#user_id", scopeSelector); + + this.picker = this.$(this.basename); + this.input_element = this.$(this.basename + '_text_input'); + + this.initCombobox('.recipe-combobox'); + this.prefill(); } - Grocy.Components.UserPicker.GetInputElement = function() + prefill() { - return $('#user_id_text_input'); - } + var doFocus = false; + var possibleOptionElement = null; - Grocy.Components.UserPicker.GetValue = function() - { - return $('#user_id').val(); - } - - Grocy.Components.UserPicker.SetValue = function(value) - { - Grocy.Components.UserPicker.GetInputElement().val(value); - Grocy.Components.UserPicker.GetInputElement().trigger('change'); - } - - Grocy.Components.UserPicker.SetId = function(value) - { - Grocy.Components.UserPicker.GetPicker().val(value); - Grocy.Components.UserPicker.GetPicker().data('combobox').refresh(); - Grocy.Components.UserPicker.GetInputElement().trigger('change'); - } - - Grocy.Components.UserPicker.Clear = function() - { - Grocy.Components.UserPicker.SetValue(''); - Grocy.Components.UserPicker.SetId(null); - } - - $('.user-combobox').combobox({ - appendId: '_text_input', - bsVersion: '4' - }); - - var this_user_picker = Grocy.Components.UserPicker.GetPicker(); - var user_picker_doFocus = false; - var possibleOptionElement = null; - - var prefillUser = this_user_picker.parent().data('prefill-by-username').toString(); - if (typeof prefillUser !== "undefined") - { - possibleOptionElement = $("#user_id option[data-additional-searchdata*=\"" + prefillUser + "\"]").first(); - if (possibleOptionElement.length === 0) + var prefillUser = this.picker.parent().data('prefill-by-username').toString(); + if (typeof prefillUser !== "undefined") { - possibleOptionElement = $("#user_id option:contains(\"" + prefillUser + "\")").first(); + possibleOptionElement = this.$("#user_id option[data-additional-searchdata*=\"" + prefillUser + "\"]").first(); + if (possibleOptionElement.length === 0) + { + possibleOptionElement = this.$("#user_id option:contains(\"" + prefillUser + "\")").first(); + } + + if (possibleOptionElement.length > 0) + { + doFocus = true; + this.picker.val(possibleOptionElement.val()); + } } - if (possibleOptionElement.length > 0) + var prefillUserId = this.picker.parent().data('prefill-by-user-id').toString(); + if (typeof prefillUserId !== "undefined") { - user_picker_doFocus = true; - this_user_picker.val(possibleOptionElement.val()); - + possibleOptionElement = this.$("#user_id option[value='" + prefillUserId + "']").first(); + if (possibleOptionElement.length > 0) + { + doFocus = true; + this.picker.val(possibleOptionElement.val()); + } } - } - var prefillUserId = this_user_picker.parent().data('prefill-by-user-id').toString(); - if (typeof prefillUserId !== "undefined") - { - possibleOptionElement = $("#user_id option[value='" + prefillUserId + "']").first(); - if (possibleOptionElement.length > 0) + if (doFocus) { - user_picker_doFocus = true; - this_user_picker.val(possibleOptionElement.val()); + this.picker.data('combobox').refresh(); + this.picker.trigger('change'); + + this.$(this.picker.parent().data('next-input-selector').toString()) + .focus(); } } - - if (user_picker_doFocus) - { - this_user_picker.data('combobox').refresh(); - this_user_picker.trigger('change'); - - $(this_user_picker.parent().data('next-input-selector').toString()) - .focus(); - } } export { userpicker } \ No newline at end of file diff --git a/js/grocy.js b/js/grocy.js index 26df66fb..ac5ddd25 100644 --- a/js/grocy.js +++ b/js/grocy.js @@ -50,6 +50,8 @@ class GrocyClass this.Components = {}; this.initComponents = []; + this.RootGrocy = null; + // Init some classes this.Api = new GrocyApi(this); @@ -217,18 +219,20 @@ class GrocyClass this.IdleTime += 1; } - Use(componentName) + Use(componentName, scope = null) { - // initialize Components only once - if (this.initComponents.find(elem => elem == componentName)) - return; + let scopeName = scope || ""; + // initialize Components only once per scope + if (this.initComponents.find(elem => elem == componentName + scopeName)) + return this.components[componentName + scopeName]; if (Object.prototype.hasOwnProperty.call(components, componentName)) { // add-then-init to resolve circular dependencies this.initComponents.push(componentName); - var component = components[componentName](this); - this.components[component.key] = component; + var component = components[componentName](this, scope); + this.components[componentName + scopeName] = component; + return component; } else { diff --git a/js/helpers/extensions.js b/js/helpers/extensions.js index 951d6937..89765950 100644 --- a/js/helpers/extensions.js +++ b/js/helpers/extensions.js @@ -96,8 +96,5 @@ export IsJsonString, BoolVal, GetFileNameFromPath, - RemoveUriParam, - UpdateUriParam, - GetUriParam, EmptyElementWhenMatches } \ No newline at end of file diff --git a/js/lib/proxy.js b/js/lib/proxy.js index 5df1156a..65eaf073 100644 --- a/js/lib/proxy.js +++ b/js/lib/proxy.js @@ -7,7 +7,7 @@ class GrocyProxy constructor(RootGrocy, scopeSelector, config, url) { - this.rootGrocy = RootGrocy; + this.RootGrocy = RootGrocy; // proxy-local members, because they might not be set globally. this.QuantityUnits = config.QuantityUnits; @@ -38,9 +38,6 @@ class GrocyProxy this.config = config; - // scoped variants of some helpers - this.FrontendHelpers = new GrocyFrontendHelpers(this, this.Api, this.scopeSelector); - this.configProxy = Proxy.revocable(this.config, { get: function(target, prop, receiver) { @@ -56,11 +53,9 @@ class GrocyProxy }) // This is where the magic happens! - // basically, this Proxy object checks if a member - // is defined in this proxy class, and returns it - // if so. - // If not, the prop is handed over to the root - // grocy instance. + // basically, this Proxy object checks if a member is defined in this proxy class, + // and returns it if so. + // If not, the prop is handed over to the root grocy instance. this.grocyProxy = Proxy.revocable(this, { get: function(target, prop, receiver) { @@ -74,6 +69,9 @@ class GrocyProxy } } }); + + // scoped variants of some helpers + this.FrontendHelpers = new GrocyFrontendHelpers(this, RootGrocy.Api, this.scopeSelector); } Unload() @@ -82,18 +80,20 @@ class GrocyProxy this.configProxy.revoke(); } - Use(componentName) + Use(componentName, scope = null) { - // initialize Components only once - if (this.initComponents.find(elem => elem == componentName)) - return; + let scopeName = scope || ""; + // initialize Components only once per scope + if (this.initComponents.find(elem => elem == componentName + scopeName)) + return this.components[componentName + scopeName]; if (Object.prototype.hasOwnProperty.call(components, componentName)) { // add-then-init to resolve circular dependencies this.initComponents.push(componentName); - var component = components[componentName](this, this.scopeSelector); - this.components[component.key] = component; + var component = components[componentName](this, scope); + this.components[componentName + scopeName] = component; + return component; } else { @@ -125,6 +125,7 @@ class GrocyProxy return currentParam[1] === undefined ? true : decodeURIComponent(currentParam[1]); } } + return undefined; } UpdateUriParam(key, value) @@ -181,4 +182,6 @@ class GrocyProxy } this.virtualUrl = vurl.substring(1); // remove leading & } -} \ No newline at end of file +} + +export { GrocyProxy } \ No newline at end of file diff --git a/package.json b/package.json index f5ee736a..6485aa69 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "swagger-ui-dist": "^3.32.1", "tempusdominus-bootstrap-4": "https://github.com/mistressofjellyfish/bootstrap-4.git#master", "timeago": "^1.6.7", - "toastr": "^2.1.4" + "toastr": "^2.1.4", + "uuid": "^8.3.2" }, "devDependencies": { "@rollup/plugin-commonjs": "^19.0.0", diff --git a/yarn.lock b/yarn.lock index ec1c1606..fb668165 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4755,6 +4755,7 @@ fsevents@~2.3.2: tempusdominus-bootstrap-4: "https://github.com/mistressofjellyfish/bootstrap-4.git#master" timeago: ^1.6.7 toastr: ^2.1.4 + uuid: ^8.3.2 vinyl-buffer: ^1.0.1 vinyl-source-stream: ^2.0.0 languageName: unknown @@ -10818,7 +10819,7 @@ fsevents@~2.3.2: languageName: node linkType: hard -"uuid@npm:^8.2.0, uuid@npm:^8.3.0": +"uuid@npm:^8.2.0, uuid@npm:^8.3.0, uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" bin: