diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..aceae784 --- /dev/null +++ b/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": [ + "es2015" + ] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9946de1e..2d1bb9d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /public/node_modules +node_modules +.yarn /vendor /.release -embedded.txt \ No newline at end of file +embedded.txt diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 00000000..5ef68935 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,4 @@ +yarnPath: ".yarn/releases/yarn-berry.cjs" +nodeLinker: node-modules +# we explicitly do not set the old files! +# js, css delivery and bundling will be done by webpack. \ No newline at end of file diff --git a/controllers/SystemApiController.php b/controllers/SystemApiController.php index 5ff7a187..0f13d16f 100644 --- a/controllers/SystemApiController.php +++ b/controllers/SystemApiController.php @@ -76,6 +76,7 @@ class SystemApiController extends BaseApiController $requestBody = $this->GetParsedAndFilteredRequestBody($request); $this->getLocalizationService()->CheckAndAddMissingTranslationToPot($requestBody['text']); + file_put_contents("php://stderr", print_r($requestBody['text'], true)); return $this->EmptyApiResponse($response); } catch (\Exception $ex) diff --git a/gulpfile.babel.js b/gulpfile.babel.js new file mode 100644 index 00000000..e834aa2d --- /dev/null +++ b/gulpfile.babel.js @@ -0,0 +1,193 @@ +'use strict'; + +import { series, parallel, dest, src, watch, task } from 'gulp'; +import rollup from '@rollup/stream'; +import sourcemaps from 'gulp-sourcemaps'; +import source from 'vinyl-source-stream'; +import buffer from 'vinyl-buffer'; +import commonjs from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; +import rollupCss from 'rollup-plugin-css-porter'; +import gulpif from 'gulp-if'; +import uglify from 'gulp-uglify'; +import gulpsass from 'gulp-dart-sass'; // TODO: move to gulp-sass once they removed the node-sass depenency +import postcss from 'gulp-postcss'; +import glob from 'glob'; +import path from 'path'; + +// css post-processing +import cssnano from 'cssnano'; +import autoprefixer from 'autoprefixer'; +import concatCss from 'gulp-concat-css'; + +var minify = false; + +var postcss_plugins = [ + // always add autoprefixer + autoprefixer(), +]; + + +// viewjs handling +var files = glob.sync('./js/viewjs/*.js'); +var components = glob.sync('./js/viewjs/components/*.js'); + +var viewJStasks = []; + +files.forEach(function(target) +{ + task(target, cb => rollup({ + input: target, + output: { + format: 'umd', + name: path.basename(target), + sourcemap: 'inline', + }, + plugins: [resolve(), rollupCss({ + dest: './public/css/viewcss/' + path.basename(target).replace(".js", ".css") + }), commonjs()], + + }) + .pipe(source(path.basename(target), "./js/viewjs")) + .pipe(gulpif(minify, uglify())) + .pipe(buffer()) + .pipe(sourcemaps.init({ loadMaps: true })) + .pipe(sourcemaps.write('.')) + .pipe(dest('./public/viewjs'))); + viewJStasks.push(target); +}); +components.forEach(function(target) +{ + task(target, cb => rollup({ + input: target, + output: { + format: 'umd', + name: path.basename(target), + sourcemap: 'inline', + }, + plugins: [resolve(), rollupCss({ + dest: './public/css/components/' + path.basename(target).replace(".js", ".css") + }), commonjs()], + + }) + .pipe(source(path.basename(target), "./js/viewjs/components")) + .pipe(gulpif(minify, uglify())) + .pipe(buffer()) + .pipe(sourcemaps.init({ loadMaps: true })) + .pipe(sourcemaps.write('.')) + .pipe(dest('./public/viewjs/components'))); + viewJStasks.push(target); +}); + +// The `clean` function is not exported so it can be considered a private task. +// It can still be used within the `series()` composition. +function clean(cb) +{ + // body omitted + cb(); +} + +// The `build` function is exported so it is public and can be run with the `gulp` command. +// It can also be used within the `series()` composition. +function build(cb) +{ + // body omitted + return parallel(js, css, vendor, resourceFileCopy); +} + +function publish(cb) +{ + minify = true; + postcss_plugins.push(cssnano()) + return build(); +} + +function js(cb) +{ + return rollup({ + input: './js/grocy.js', + output: { + format: 'umd', + name: 'grocy.js', + sourcemap: 'inline', + }, + plugins: [resolve(), commonjs()], + + }) + .pipe(source('grocy.js', "./js")) + .pipe(gulpif(minify, uglify())) + .pipe(buffer()) + .pipe(sourcemaps.init({ loadMaps: true })) + .pipe(sourcemaps.write('.')) + .pipe(dest('./public/js')); +} + +function viewjs(cb) +{ + return parallel(viewJStasks, done => { done(); cb(); })(); +} + + +function vendor(cb) +{ + return rollup({ + input: './js/vendor.js', + output: { + format: 'umd', + name: 'grocy.js', + sourcemap: 'inline', + }, + plugins: [resolve(), commonjs()], + }) + .pipe(source('vendor.js', "./js")) + .pipe(gulpif(minify, uglify())) + .pipe(buffer()) + .pipe(sourcemaps.init({ loadMaps: true })) + .pipe(sourcemaps.write('.')) + .pipe(dest('./public/js')); +} + +function css(cb) +{ + return src('./scss/grocy.scss') + .pipe(sourcemaps.init()) + .pipe(gulpsass({ includePaths: ['./node_modules'], quietDeps: true }).on('error', gulpsass.logError)) + .pipe(concatCss('grocy.css', { includePaths: ['./node_modules'], rebaseUrls: false })) + .pipe(postcss(postcss_plugins)) + .pipe(sourcemaps.write('.')) + .pipe(dest('./public/css')) +} + +function resourceFileCopy(cb) +{ + return parallel( + cb => src([ + './node_modules/@fortawesome/fontawesome-free/webfonts/*' + ]).pipe(dest('./public/webfonts')), + cb => src('./node_modules/summernote/dist/font/*').pipe(dest('./public/css/font')), + done => { done(); cb(); } + )(); +} + +function copyLocales(cb) +{ + return parallel( + cb => src('./node_modules/timeago/locales/*.js').pipe(dest('./public/js/locales/timeago')), + cb => src('./node_modules/summernote/dist/lang/*.js').pipe(dest('./public/js/locales/summernote')), + cb => src('./node_modules/bootstrap-select/dist/js/i18n/*').pipe(dest('./public/js/locales/bootstrap-select')), + cb => src('./node_modules/fullcalendar/dist/locale/*').pipe(dest('./public/js/locales/fullcalendar')), + cb => src('./node_modules/@fullcalendar/core/locales/*').pipe(dest('./public/js/locales/fullcalendar-core')), + done => { done(); cb(); } + )(); +} + +function live(cb) +{ + watch('./scss/**/*.scss', css); + watch(['./js/**/*.js', '!!./js/viewjs/**/*.js'], js); + watch('./js/vendor.js', vendor); + //watch('./js/viewjs/**/*.js', viewjs); + viewJStasks.forEach(elem => watch(elem, series([elem]))); +} + +export { build, js, vendor, viewjs, css, live, clean, resourceFileCopy, copyLocales } \ No newline at end of file diff --git a/js/configs/datatable.js b/js/configs/datatable.js new file mode 100644 index 00000000..fbe68753 --- /dev/null +++ b/js/configs/datatable.js @@ -0,0 +1,113 @@ +import { IsJsonString } from '../helpers/extensions'; + +function setDatatableDefaults(Grocy) +{ + // 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 + var fixedOrder = { + pre: [rowGroup.dataSrc, 'asc'] + }; + + api.order.fixed(fixedOrder); + } + } + } + } + }, + '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(); + }); +} + +export { setDatatableDefaults } \ No newline at end of file diff --git a/js/configs/globalstate.js b/js/configs/globalstate.js new file mode 100644 index 00000000..a3f45ef8 --- /dev/null +++ b/js/configs/globalstate.js @@ -0,0 +1,497 @@ +import { ResizeResponsiveEmbeds } from "../helpers/embeds"; +import { IsTouchInputDevice } from "../helpers/input"; +import { BoolVal } from "../helpers/extensions"; + + +// This function sets some global state and adds some global event listeners. +function setInitialGlobalState(Grocy) +{ + // __t isn't set in global context yet, make one locally. + var __t = function(key, ...placeholder) { return Grocy.translate(key, ...placeholder) }; + + if (!Grocy.ActiveNav.isEmpty()) + { + var menuItem = $('#sidebarResponsive').find("[data-nav-for-page='" + Grocy.ActiveNav + "']"); + menuItem.addClass('active-page'); + + if (menuItem.length) + { + var parentMenuSelector = menuItem.data("sub-menu-of"); + if (typeof parentMenuSelector !== "undefined") + { + $(parentMenuSelector).collapse("show"); + $(parentMenuSelector).prev(".nav-link-collapse").addClass("active-page"); + + $(parentMenuSelector).on("shown.bs.collapse", function(e) + { + if (!menuItem.isVisibleInViewport(75)) + { + menuItem[0].scrollIntoView(); + } + }) + } + else + { + if (!menuItem.isVisibleInViewport(75)) + { + menuItem[0].scrollIntoView(); + } + } + } + } + + var observer = new MutationObserver(function(mutations) + { + mutations.forEach(function(mutation) + { + if (mutation.attributeName === "class") + { + var attributeValue = $(mutation.target).prop(mutation.attributeName); + if (attributeValue.contains("sidenav-toggled")) + { + window.localStorage.setItem("sidebar_state", "collapsed"); + } + else + { + window.localStorage.setItem("sidebar_state", "expanded"); + } + } + }); + }); + observer.observe(document.body, { + attributes: true + }); + + if (window.localStorage.getItem("sidebar_state") === "collapsed") + { + $("#sidenavToggler").click(); + } + + window.toastr.options = { + toastClass: 'alert', + closeButton: true, + timeOut: 20000, + extendedTimeOut: 5000 + }; + + window.FontAwesomeConfig = { + searchPseudoElements: true + } + + $(window).on('resize', function() + { + ResizeResponsiveEmbeds($("body").hasClass("fullscreen-card")); + }); + $("iframe").on("load", function() + { + ResizeResponsiveEmbeds($("body").hasClass("fullscreen-card")); + }); + + // Don't show tooltips on touch input devices + if (IsTouchInputDevice()) + { + var css = document.createElement("style"); + css.innerHTML = ".tooltip { display: none; }"; + document.body.appendChild(css); + } + + $(document).on("keyup paste change", "input, textarea", function() + { + $(this).closest("form").addClass("is-dirty"); + }); + $(document).on("click", "select", function() + { + $(this).closest("form").addClass("is-dirty"); + }); + + // Auto saving user setting controls + $(document).on("change", ".user-setting-control", function() + { + var element = $(this); + var settingKey = element.attr("data-setting-key"); + + if (!element[0].checkValidity()) + { + return; + } + + var inputType = "unknown"; + if (typeof element.attr("type") !== typeof undefined && element.attr("type") !== false) + { + inputType = element.attr("type").toLowerCase(); + } + + if (inputType === "checkbox") + { + value = element.is(":checked"); + } + else + { + var value = element.val(); + } + + Grocy.FrontendHelpers.SaveUserSetting(settingKey, value); + }); + + // Show file name Bootstrap custom file input + $('input.custom-file-input').on('change', function() + { + $(this).next('.custom-file-label').html(GetFileNameFromPath($(this).val())); + }); + + // Translation of "Browse"-button of Bootstrap custom file input + if ($(".custom-file-label").length > 0) + { + $("