diff --git a/changelog/70_UNRELEASED_xxxx.xx.xx.md b/changelog/70_UNRELEASED_xxxx.xx.xx.md
index 68b6c54c..ff05c9a2 100644
--- a/changelog/70_UNRELEASED_xxxx.xx.xx.md
+++ b/changelog/70_UNRELEASED_xxxx.xx.xx.md
@@ -95,6 +95,7 @@
- Like already possible for products/chores/batteries, locations, stores, quantity units, product groups and task categories can now be disabled to keep them for existing references without deleting them, but to hide them everywhere for selections and so on (new option "Active")
- Added a new `config.php` setting `ENERGY_UNIT` to customize the label to display energy values (was fixed `kcal` before and defaults to that, so no changed behavior when not configured)
- New logo and "Grocy" is now officially spelled with a capital initial letter (before everything was lowercase)
+- Various frontend performance enhancements
- Fixed that users were unable to delete their own API keys (when not having the `All permissions` permission)
- Fixed that button tooltips on some places didn't disappear after clicking the corresponding button
- New translations: (thanks all the translators)
diff --git a/helpers/extensions.php b/helpers/extensions.php
index 27c25f60..c83f4e4c 100644
--- a/helpers/extensions.php
+++ b/helpers/extensions.php
@@ -253,3 +253,12 @@ function string_ends_with($haystack, $needle)
return (substr($haystack, -$length) === $needle);
}
+
+global $GROCY_REQUIRED_FRONTEND_PACKAGES;
+$GROCY_REQUIRED_FRONTEND_PACKAGES = [];
+function require_frontend_packages(array $packages)
+{
+ global $GROCY_REQUIRED_FRONTEND_PACKAGES;
+
+ $GROCY_REQUIRED_FRONTEND_PACKAGES = array_unique(array_merge($GROCY_REQUIRED_FRONTEND_PACKAGES, $packages));
+}
diff --git a/public/js/grocy.js b/public/js/grocy.js
index efb466b6..336ace74 100644
--- a/public/js/grocy.js
+++ b/public/js/grocy.js
@@ -249,7 +249,14 @@ __t = function(text, ...placeholderValues)
}
}
- return sprintf(Grocy.Translator.__(text, ...placeholderValues), ...placeholderValues);
+ // sprintf can fail due to invalid placeholders
+ try
+ {
+ return sprintf(Grocy.Translator.__(text, ...placeholderValues), ...placeholderValues);
+ } catch (e)
+ {
+ return Grocy.Translator.__(text, ...placeholderValues);
+ }
}
__n = function(number, singularForm, pluralForm, isQu = false)
{
@@ -272,13 +279,15 @@ __n = function(number, singularForm, pluralForm, isQu = false)
pluralForm = singularForm;
}
+ number = Math.abs(number);
+
if (isQu)
{
- return Grocy.TranslatorQu.n__(singularForm, pluralForm, Math.abs(number), Math.abs(number))
+ return sprintf(Grocy.TranslatorQu.n__(singularForm, pluralForm, number, number), number.toString());
}
else
{
- return Grocy.Translator.n__(singularForm, pluralForm, Math.abs(number), Math.abs(number))
+ return sprintf(Grocy.Translator.n__(singularForm, pluralForm, number, number), number.toString());
}
}
@@ -665,20 +674,6 @@ $(document).on("click", ".easy-link-copy-textbox", function()
$(this).select();
});
-$("textarea.wysiwyg-editor").summernote({
- minHeight: "300px",
- lang: __t("summernote_locale"),
- callbacks: {
- onImageLinkInsert: function(url)
- {
- // Summernote workaround: Make images responsive
- // By adding the "img-fluid" class to the img tag
- $img = $('').attr({ src: url, class: "img-fluid" })
- $(this).summernote("insertNode", $img[0]);
- }
- }
-});
-
// Summernote workaround: Make embeds responsive
// By wrapping any embeded video in a container with class "embed-responsive"
$(".note-video-clip").each(function()
@@ -745,128 +740,6 @@ $(document).on("click", ".show-as-dialog-link", function(e)
// Init Bootstrap tooltips
$('[data-toggle="tooltip"]').tooltip()
-// Default DataTables initialisation settings
-var collapsedGroups = {};
-$.extend(true, $.fn.dataTable.defaults, {
- 'paginate': false,
- 'deferRender': true,
- 'language': IsJsonString(__t('datatables_localization')) ? JSON.parse(__t('datatables_localization')) : {},
- 'scrollY': false,
- 'scrollX': true,
- 'colReorder': true,
- 'stateSave': true,
- 'stateSaveParams': function(settings, data)
- {
- data.search.search = "";
-
- data.columns.forEach(column =>
- {
- column.search.search = "";
- });
- },
- 'stateSaveCallback': function(settings, data)
- {
- var settingKey = 'datatables_state_' + settings.sTableId;
- if ($.isEmptyObject(data))
- {
- //state.clear was called and unfortunately the table is not refresh, so we are reloading the page
- Grocy.FrontendHelpers.DeleteUserSetting(settingKey, true);
- } else
- {
- var stateData = JSON.stringify(data);
- Grocy.FrontendHelpers.SaveUserSetting(settingKey, stateData);
- }
- },
- 'stateLoadCallback': function(settings, data)
- {
- var settingKey = 'datatables_state_' + settings.sTableId;
-
- if (Grocy.UserSettings[settingKey] == undefined)
- {
- return null;
- }
- else
- {
- return JSON.parse(Grocy.UserSettings[settingKey]);
- }
- },
- 'preDrawCallback': function(settings)
- {
- // Currently it is not possible to save the state of rowGroup via saveState events
- var api = new $.fn.dataTable.Api(settings);
- if (typeof api.rowGroup === "function")
- {
- var settingKey = 'datatables_rowGroup_' + settings.sTableId;
- if (Grocy.UserSettings[settingKey] !== undefined)
- {
- var rowGroup = JSON.parse(Grocy.UserSettings[settingKey]);
-
- // Check if there way changed. the draw event is called often therefore we have to check if it's really necessary
- if (rowGroup.enable !== api.rowGroup().enabled()
- || ("dataSrc" in rowGroup && rowGroup.dataSrc !== api.rowGroup().dataSrc()))
- {
-
- api.rowGroup().enable(rowGroup.enable);
-
- if ("dataSrc" in rowGroup)
- {
- api.rowGroup().dataSrc(rowGroup.dataSrc);
-
- // Apply fixed order for group column
- api.order.fixed({
- pre: [rowGroup.dataSrc, 'asc']
- });
- }
- else
- {
- // Remove fixed order
- api.order.fixed({});
- }
- }
- }
- }
- },
- 'columnDefs': [
- { type: 'chinese-string', targets: '_all' }
- ],
- 'rowGroup': {
- enable: false,
- startRender: function(rows, group)
- {
- var collapsed = !!collapsedGroups[group];
- var toggleClass = collapsed ? "fa-caret-right" : "fa-caret-down";
-
- rows.nodes().each(function(row)
- {
- row.style.display = collapsed ? "none" : "";
- });
-
- return $("
")
- .append('
' + group + '
')
- .attr("data-name", group)
- .toggleClass("collapsed", collapsed);
- }
- }
-});
-$(document).on("click", "tr.dtrg-group", function()
-{
- var name = $(this).data('name');
- collapsedGroups[name] = !collapsedGroups[name];
- $("table").DataTable().draw();
-});
-$.fn.dataTable.ext.type.order["custom-sort-pre"] = function(data)
-{
- // Workaround for https://github.com/DataTables/ColReorder/issues/85
- //
- // Custom sorting can normally be provided by a "data-order" attribute on the
element,
- // however this causes issues when reordering such a column...
- //
- // This here is for a custom column type "custom-sort",
- // the custom order value needs to be provided in the first child () of the
-
- return (Number.parseFloat($(data).get(0).innerText));
-};
-
// serializeJSON defaults
$.serializeJSON.defaultOptions.checkboxUncheckedValue = "0";
@@ -912,38 +785,7 @@ $('.dropdown-item').has('.form-check input[type=checkbox]').on('click', function
{
$(e.target).find('input[type=checkbox]').click();
}
-})
-
-$('.table').on('column-sizing.dt', function(e, settings)
-{
- var dtScrollWidth = $('.dataTables_scroll').width();
- var tableWidth = $('.table').width() + 100; // Some extra padding, otherwise the scrollbar maybe only appears after a column is already completely out of the viewport
-
- if (dtScrollWidth < tableWidth)
- {
- $('.dataTables_scrollBody').addClass("no-force-overflow-visible");
- $('.dataTables_scrollBody').removeClass("force-overflow-visible");
- }
- else
- {
- $('.dataTables_scrollBody').removeClass("no-force-overflow-visible");
- $('.dataTables_scrollBody').addClass("force-overflow-visible");
- }
});
-$(document).on("show.bs.dropdown", "td .dropdown", function(e)
-{
- if ($('.dataTables_scrollBody').hasClass("no-force-overflow-visible"))
- {
- $('.dataTables_scrollBody').addClass("force-overflow-visible");
- }
-});
-$(document).on("hide.bs.dropdown", "td .dropdown", function(e)
-{
- if ($('.dataTables_scrollBody').hasClass("no-force-overflow-visible"))
- {
- $('.dataTables_scrollBody').removeClass("force-overflow-visible");
- }
-})
$(window).on("message", function(e)
{
@@ -955,223 +797,6 @@ $(window).on("message", function(e)
}
});
-$(".change-table-columns-visibility-button").on("click", function(e)
-{
- e.preventDefault();
-
- var dataTableSelector = $(e.currentTarget).attr("data-table-selector");
- var dataTable = $(dataTableSelector).DataTable();
-
- var columnCheckBoxesHtml = "";
- var rowGroupRadioBoxesHtml = "";
-
- var rowGroupDefined = typeof dataTable.rowGroup === "function";
-
- if (rowGroupDefined)
- {
- var rowGroupChecked = (dataTable.rowGroup().enabled()) ? "" : "checked";
- rowGroupRadioBoxesHtml = ' \
-
\
- \
- \
-
';
- }
-
- dataTable.columns().every(function()
- {
- var index = this.index();
- var indexForGrouping = index;
- var headerCell = $(this.header());
- var title = headerCell.text();
- var visible = this.visible();
-
- if (!title || title.trim().length == 0 || title.startsWith("Hidden") || headerCell.hasClass("d-none"))
- {
- return;
- }
-
- var shadowColumnIndex = headerCell.attr("data-shadow-rowgroup-column");
- if (shadowColumnIndex)
- {
- indexForGrouping = shadowColumnIndex;
- }
-
- var checked = "checked";
- if (!visible)
- {
- checked = "";
- }
-
- columnCheckBoxesHtml += ' \
-
';
- }
-
- bootbox.dialog({
- message: message,
- size: 'small',
- backdrop: true,
- closeButton: false,
- buttons: {
- reset: {
- label: __t('Reset'),
- className: 'btn-outline-danger float-left responsive-button',
- callback: function()
- {
- bootbox.confirm({
- message: __t("Are you sure to reset the table options?"),
- buttons: {
- cancel: {
- label: 'No',
- className: 'btn-danger'
- },
- confirm: {
- label: 'Yes',
- className: 'btn-success'
- }
- },
- callback: function(result)
- {
- if (result)
- {
- var dataTable = $(dataTableSelector).DataTable();
- var tableId = dataTable.settings()[0].sTableId;
-
- // Delete rowgroup settings
- Grocy.FrontendHelpers.DeleteUserSetting('datatables_rowGroup_' + tableId);
-
- // Delete state settings
- dataTable.state.clear();
- }
- bootbox.hideAll();
- }
- });
- }
- },
- ok: {
- label: __t('OK'),
- className: 'btn-primary responsive-button',
- callback: function()
- {
- bootbox.hideAll();
- }
- }
- }
- });
-});
-
-$(document).on("click", ".change-table-columns-visibility-toggle", function()
-{
- var dataTableSelector = $(this).attr("data-table-selector");
- var columnIndex = $(this).attr("data-column-index");
- var dataTable = $(dataTableSelector).DataTable();
-
- dataTable.columns(columnIndex).visible(this.checked);
- LoadImagesLazy();
-});
-
-
-$(document).on("click", ".change-table-columns-rowgroup-toggle", function()
-{
- var dataTableSelector = $(this).attr("data-table-selector");
- var columnIndex = $(this).attr("data-column-index");
- var dataTable = $(dataTableSelector).DataTable();
- var rowGroup;
-
- if (columnIndex == -1)
- {
- rowGroup = {
- enable: false
- };
-
- dataTable.rowGroup().enable(false);
-
- // Remove fixed order
- dataTable.order.fixed({});
- }
- else
- {
- rowGroup = {
- enable: true,
- dataSrc: columnIndex
- }
-
- dataTable.rowGroup().enable(true);
- dataTable.rowGroup().dataSrc(columnIndex);
-
- // Apply fixed order for group column
- dataTable.order.fixed({
- pre: [columnIndex, 'asc']
- });
- }
-
- var settingKey = 'datatables_rowGroup_' + dataTable.settings()[0].sTableId;
- Grocy.FrontendHelpers.SaveUserSetting(settingKey, JSON.stringify(rowGroup));
-
- dataTable.draw();
-});
-
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_RECIPES)
{
if ($(window).width() < 768)
diff --git a/public/js/grocy_datatables.js b/public/js/grocy_datatables.js
new file mode 100644
index 00000000..d44c9207
--- /dev/null
+++ b/public/js/grocy_datatables.js
@@ -0,0 +1,369 @@
+// Default DataTables initialisation settings
+var collapsedGroups = {};
+$.extend(true, $.fn.dataTable.defaults, {
+ 'paginate': false,
+ 'deferRender': true,
+ 'language': IsJsonString(__t('datatables_localization')) ? JSON.parse(__t('datatables_localization')) : {},
+ 'scrollY': false,
+ 'scrollX': true,
+ 'colReorder': true,
+ 'stateSave': true,
+ 'stateSaveParams': function(settings, data)
+ {
+ data.search.search = "";
+
+ data.columns.forEach(column =>
+ {
+ column.search.search = "";
+ });
+ },
+ 'stateSaveCallback': function(settings, data)
+ {
+ var settingKey = 'datatables_state_' + settings.sTableId;
+ if ($.isEmptyObject(data))
+ {
+ //state.clear was called and unfortunately the table is not refresh, so we are reloading the page
+ Grocy.FrontendHelpers.DeleteUserSetting(settingKey, true);
+ } else
+ {
+ var stateData = JSON.stringify(data);
+ Grocy.FrontendHelpers.SaveUserSetting(settingKey, stateData);
+ }
+ },
+ 'stateLoadCallback': function(settings, data)
+ {
+ var settingKey = 'datatables_state_' + settings.sTableId;
+
+ if (Grocy.UserSettings[settingKey] == undefined)
+ {
+ return null;
+ }
+ else
+ {
+ return JSON.parse(Grocy.UserSettings[settingKey]);
+ }
+ },
+ 'preDrawCallback': function(settings)
+ {
+ // Currently it is not possible to save the state of rowGroup via saveState events
+ var api = new $.fn.dataTable.Api(settings);
+ if (typeof api.rowGroup === "function")
+ {
+ var settingKey = 'datatables_rowGroup_' + settings.sTableId;
+ if (Grocy.UserSettings[settingKey] !== undefined)
+ {
+ var rowGroup = JSON.parse(Grocy.UserSettings[settingKey]);
+
+ // Check if there way changed. the draw event is called often therefore we have to check if it's really necessary
+ if (rowGroup.enable !== api.rowGroup().enabled()
+ || ("dataSrc" in rowGroup && rowGroup.dataSrc !== api.rowGroup().dataSrc()))
+ {
+
+ api.rowGroup().enable(rowGroup.enable);
+
+ if ("dataSrc" in rowGroup)
+ {
+ api.rowGroup().dataSrc(rowGroup.dataSrc);
+
+ // Apply fixed order for group column
+ api.order.fixed({
+ pre: [rowGroup.dataSrc, 'asc']
+ });
+ }
+ else
+ {
+ // Remove fixed order
+ api.order.fixed({});
+ }
+ }
+ }
+ }
+ },
+ 'columnDefs': [
+ { type: 'chinese-string', targets: '_all' }
+ ],
+ 'rowGroup': {
+ enable: false,
+ startRender: function(rows, group)
+ {
+ var collapsed = !!collapsedGroups[group];
+ var toggleClass = collapsed ? "fa-caret-right" : "fa-caret-down";
+
+ rows.nodes().each(function(row)
+ {
+ row.style.display = collapsed ? "none" : "";
+ });
+
+ return $("
")
+ .append('
' + group + '
')
+ .attr("data-name", group)
+ .toggleClass("collapsed", collapsed);
+ }
+ }
+});
+$(document).on("click", "tr.dtrg-group", function()
+{
+ var name = $(this).data('name');
+ collapsedGroups[name] = !collapsedGroups[name];
+ $("table").DataTable().draw();
+});
+$.fn.dataTable.ext.type.order["custom-sort-pre"] = function(data)
+{
+ // Workaround for https://github.com/DataTables/ColReorder/issues/85
+ //
+ // Custom sorting can normally be provided by a "data-order" attribute on the
element,
+ // however this causes issues when reordering such a column...
+ //
+ // This here is for a custom column type "custom-sort",
+ // the custom order value needs to be provided in the first child () of the
+
+ return (Number.parseFloat($(data).get(0).innerText));
+};
+
+$('.table').on('column-sizing.dt', function(e, settings)
+{
+ var dtScrollWidth = $('.dataTables_scroll').width();
+ var tableWidth = $('.table').width() + 100; // Some extra padding, otherwise the scrollbar maybe only appears after a column is already completely out of the viewport
+
+ if (dtScrollWidth < tableWidth)
+ {
+ $('.dataTables_scrollBody').addClass("no-force-overflow-visible");
+ $('.dataTables_scrollBody').removeClass("force-overflow-visible");
+ }
+ else
+ {
+ $('.dataTables_scrollBody').removeClass("no-force-overflow-visible");
+ $('.dataTables_scrollBody').addClass("force-overflow-visible");
+ }
+});
+$(document).on("show.bs.dropdown", "td .dropdown", function(e)
+{
+ if ($('.dataTables_scrollBody').hasClass("no-force-overflow-visible"))
+ {
+ $('.dataTables_scrollBody').addClass("force-overflow-visible");
+ }
+});
+$(document).on("hide.bs.dropdown", "td .dropdown", function(e)
+{
+ if ($('.dataTables_scrollBody').hasClass("no-force-overflow-visible"))
+ {
+ $('.dataTables_scrollBody').removeClass("force-overflow-visible");
+ }
+});
+
+$(".change-table-columns-visibility-button").on("click", function(e)
+{
+ e.preventDefault();
+
+ var dataTableSelector = $(e.currentTarget).attr("data-table-selector");
+ var dataTable = $(dataTableSelector).DataTable();
+
+ var columnCheckBoxesHtml = "";
+ var rowGroupRadioBoxesHtml = "";
+
+ var rowGroupDefined = typeof dataTable.rowGroup === "function";
+
+ if (rowGroupDefined)
+ {
+ var rowGroupChecked = (dataTable.rowGroup().enabled()) ? "" : "checked";
+ rowGroupRadioBoxesHtml = ' \
+
\
+ \
+ \
+
';
+ }
+
+ dataTable.columns().every(function()
+ {
+ var index = this.index();
+ var indexForGrouping = index;
+ var headerCell = $(this.header());
+ var title = headerCell.text();
+ var visible = this.visible();
+
+ if (!title || title.trim().length == 0 || title.startsWith("Hidden") || headerCell.hasClass("d-none"))
+ {
+ return;
+ }
+
+ var shadowColumnIndex = headerCell.attr("data-shadow-rowgroup-column");
+ if (shadowColumnIndex)
+ {
+ indexForGrouping = shadowColumnIndex;
+ }
+
+ var checked = "checked";
+ if (!visible)
+ {
+ checked = "";
+ }
+
+ columnCheckBoxesHtml += ' \
+