From dca24b45dd760af0a8c28fd0eb0c45a5d39a7246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Felix=20=C5=A0ulc?= Date: Wed, 7 Jun 2023 23:35:13 +0200 Subject: [PATCH] Assets: rewrite to TS and plugin system --- .docs/assets.md | 185 ++-- assets/ajax/index.ts | 1 + assets/ajax/naja.ts | 135 +++ assets/css/datagrid-full.css | 4 + assets/{ => css}/datagrid.css | 46 +- assets/css/happy.css.ts | 464 ++++++++++ assets/datagrid-instant-url-refresh.js | 30 - assets/datagrid-spinners.css | 118 --- assets/datagrid-spinners.js | 89 -- assets/datagrid.js | 922 -------------------- assets/datagrid.ts | 145 +++ assets/datagrids.ts | 80 ++ assets/index.ts | 6 + assets/integrations/bootstrap-select.ts | 43 + assets/integrations/happy.ts | 222 +++++ assets/integrations/index.ts | 5 + assets/integrations/sortable-js.ts | 58 ++ assets/integrations/tom-select.ts | 23 + assets/integrations/vanilla-datepicker.ts | 14 + assets/plugins/features/autosubmit.ts | 69 ++ assets/plugins/features/checkboxes.ts | 92 ++ assets/plugins/features/confirm.ts | 28 + assets/plugins/features/editable.ts | 118 +++ assets/plugins/features/inline.ts | 73 ++ assets/plugins/features/item-detail.ts | 48 + assets/plugins/features/treeView.ts | 35 + assets/plugins/index.ts | 12 + assets/plugins/integrations/datepicker.ts | 17 + assets/plugins/integrations/happy.ts | 19 + assets/plugins/integrations/nette-forms.ts | 18 + assets/plugins/integrations/selectpicker.ts | 17 + assets/plugins/integrations/sortable.ts | 61 ++ assets/types/ajax.d.ts | 136 +++ assets/types/datagrid.d.ts | 31 + assets/types/index.d.ts | 41 + assets/types/integrations.d.ts | 15 + assets/utils.ts | 179 ++++ package.json | 55 +- tsconfig.json | 32 + vite.config.js | 45 + 40 files changed, 2424 insertions(+), 1307 deletions(-) create mode 100644 assets/ajax/index.ts create mode 100644 assets/ajax/naja.ts create mode 100644 assets/css/datagrid-full.css rename assets/{ => css}/datagrid.css (96%) mode change 100755 => 100644 create mode 100644 assets/css/happy.css.ts delete mode 100644 assets/datagrid-instant-url-refresh.js delete mode 100755 assets/datagrid-spinners.css delete mode 100644 assets/datagrid-spinners.js delete mode 100644 assets/datagrid.js create mode 100644 assets/datagrid.ts create mode 100644 assets/datagrids.ts create mode 100644 assets/index.ts create mode 100644 assets/integrations/bootstrap-select.ts create mode 100644 assets/integrations/happy.ts create mode 100644 assets/integrations/index.ts create mode 100644 assets/integrations/sortable-js.ts create mode 100644 assets/integrations/tom-select.ts create mode 100644 assets/integrations/vanilla-datepicker.ts create mode 100644 assets/plugins/features/autosubmit.ts create mode 100644 assets/plugins/features/checkboxes.ts create mode 100644 assets/plugins/features/confirm.ts create mode 100644 assets/plugins/features/editable.ts create mode 100644 assets/plugins/features/inline.ts create mode 100644 assets/plugins/features/item-detail.ts create mode 100644 assets/plugins/features/treeView.ts create mode 100644 assets/plugins/index.ts create mode 100644 assets/plugins/integrations/datepicker.ts create mode 100644 assets/plugins/integrations/happy.ts create mode 100644 assets/plugins/integrations/nette-forms.ts create mode 100644 assets/plugins/integrations/selectpicker.ts create mode 100644 assets/plugins/integrations/sortable.ts create mode 100644 assets/types/ajax.d.ts create mode 100644 assets/types/datagrid.d.ts create mode 100644 assets/types/index.d.ts create mode 100644 assets/types/integrations.d.ts create mode 100644 assets/utils.ts create mode 100644 tsconfig.json create mode 100644 vite.config.js diff --git a/.docs/assets.md b/.docs/assets.md index 0a1674b90..d51a304a7 100644 --- a/.docs/assets.md +++ b/.docs/assets.md @@ -6,116 +6,103 @@ Table of contents # Assets -Datagrid needs for its precise functionality some third party scripts and styles. Install all required assets with NPM. +There are prepare JS/TS and CSS files for precise functionality. The best way is to use some frontend bundler, for example [Vite](https://vitejs.dev). -**CSS (external)** +## Installation -- bootstrap 5 -- bootstrap datepicker -- bootstrap select - -**CSS** - -- datagrid.css -- datagrid-spinners.css - -**JS (external)** - -- jquery -- nette forms -- nette ajax / naja -- bootstrap -- bootstrap datepicker -- bootstrap select - -**JS** - -- datagrid.js -- datagrid-instant-url-refresh.js -- datagrid-spinners.js - -**Icons** - -You will probably want to use some icon font, but that is in your command. -On this project website we use font awesome (you can change the icon prefix by setting new value to static property `Datagrid::$iconPrefix = 'fa fa-';`). - -**Spinners** - -As you can see, there is also a `datagrid-spinners.js` script in a datagrid repository. If you include this file within you project layout, there are some actions, that will show spinner/some other animation when waiting for ajax response. Actions, that has somehow animated spinner: - -- Group actions -- Pagination -- Changing items per page -- Toggling item detail - loading the detail for the first time - -## NPM - -``` -npm install --save ublaboo-datagrid -``` - -package.json: +You need to install datagrid's assets. For example this way. ```json { - "dependencies": { - "bootstrap-datepicker": "^1.9", - "bootstrap-select": "^1.14-beta2", - "bootstrap": "^5.0.0", - "happy-inputs": "^2.0", - "jquery": "^3.4.1", - "jquery-ui-sortable": "^1.0", - "nette-forms": "^3.0", - "nette.ajax.js": "^2.3", - "popper.js": "^1.14.7", - "ublaboo-datagrid": "^6.9" - } + "dependencies": { + "@contributte/datagrid": "git+ssh://git@github.com:contributte/datagrid.git#next" + } } ``` -## Example html when not using NPM - -```html - - - - - - - - - - - - - +## - - +## Demo - - - - - - - - - - +**package.json** - - - - - +```json +{ + "dependencies": { + "@contributte/datagrid": "git+ssh://git@github.com:contributte/datagrid.git#next", + "@fortawesome/fontawesome-free": "^6.3.0", + "bootstrap": "^5.3.0-alpha3", + "naja": "^2.5.0", + "nette-forms": "^3.3.1", + "prismjs": "^1.29.0", + "sortablejs": "^1.15.0", + "tom-select": "^2.2.2", + "vanillajs-datepicker": "^1.3.1" + }, + "devDependencies": { + "@types/bootstrap-select": "^1.13.4", + "@types/jquery": "^3.5.16", + "@types/jqueryui": "^1.12.16", + "@types/sortablejs": "^1.15.1", + "@types/vanillajs-datepicker": "^1.2.1", + "autoprefixer": "^10.4.0", + "typescript": "^4.9.5", + "vite": "^2.6.10" + }, + "scripts": { + "watch": "vite build --watch --mode=development", + "build": "vite build --mode=production" + } +} +``` - - - - +**vite.config.js** + +```js +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +export default defineConfig(({ mode }) => { + const DEV = mode === 'development'; + + return { + publicDir: './assets/public', + resolve: { + alias: { + '@': resolve(__dirname, 'assets/js'), + '~': resolve(__dirname, 'node_modules'), + }, + }, + base: '/dist/', + server: { + open: false, + hmr: false, + }, + css: { + postcss: [ + "autoprefixer" + ] + }, + build: { + manifest: true, + assetsDir: '', + outDir: './www/dist/', + emptyOutDir: true, + minify: DEV ? false : 'esbuild', + rollupOptions: { + output: { + manualChunks: undefined, + chunkFileNames: '[name].js', + entryFileNames: '[name].js', + assetFileNames: '[name].[ext]', + }, + input: { + app: './assets/js/main.js' + } + } + }, + } +}); ``` diff --git a/assets/ajax/index.ts b/assets/ajax/index.ts new file mode 100644 index 000000000..23b37df97 --- /dev/null +++ b/assets/ajax/index.ts @@ -0,0 +1 @@ +export * from "./naja"; diff --git a/assets/ajax/naja.ts b/assets/ajax/naja.ts new file mode 100644 index 000000000..fbe0658f4 --- /dev/null +++ b/assets/ajax/naja.ts @@ -0,0 +1,135 @@ +import type { Naja } from "naja"; +import type { + Ajax, + AjaxEventMap as BaseAjaxEventMap, + BaseRequestParams as AjaxBaseRequestParams, + BeforeEventDetail as BaseBeforeEventDetail, + DatagridPayload, + ErrorEventDetail as BaseErrorEventDetail, + EventDetail, + EventListener, + InteractEventDetail as BaseInteractEventDetail, + Payload, + RequestParams, + Response as AjaxResponse, + SuccessEventDetail as BaseSuccessEventDetail, +} from "../types"; +import { Datagrid } from "../datagrid"; +import { BeforeEvent, ErrorEvent, Payload as NajaPayload, SuccessEvent } from "naja/dist/Naja"; +import { InteractionEvent } from "naja/dist/core/UIHandler"; + +export interface BaseRequestParams extends AjaxBaseRequestParams, Request { + url: string; + method: string; +} + +export interface BeforeEventDetail extends BaseBeforeEventDetail { + params: EventDetail & RequestParams; +} + +export interface InteractEventDetail< + E extends HTMLElement = HTMLElement +> extends BaseInteractEventDetail, EventDetail { + element: E; +} + +export interface SuccessEventDetail< + P = DatagridPayload +> extends BaseSuccessEventDetail, EventDetail { + params: BaseRequestParams; + payload: Payload

& NajaPayload; + response: AjaxResponse & Response; +} + +export interface ErrorEventDetail< + E extends Error = Error, +> extends BaseErrorEventDetail, EventDetail { + params: BaseRequestParams; + response: (AjaxResponse & Response) | undefined; + error: E; +} + +export interface AjaxEventMap extends BaseAjaxEventMap { + before: CustomEvent; + interact: CustomEvent; + snippetUpdate: CustomEvent; + success: CustomEvent; + error: CustomEvent; +} + +export class NajaAjax extends EventTarget implements Ajax { + constructor(public client: C) { + if (!client.VERSION || client.VERSION < 2) { + throw new Error("NajaAjax supports Naja 2 and higher" + (client.VERSION ? `(version ${client.VERSION} provided)` : '')) + } + super(); + } + + onInit() { + this.client.addEventListener('before', (e) => { + return this.dispatch('before', { + params: e.detail + }); + }) + + this.client.uiHandler.addEventListener('interaction', (e) => { + if (!(e.detail.element instanceof HTMLElement)) { + throw new Error("Element is not an instanceof HTMLElement"); + } + + return this.dispatch('interact', { + ...e.detail, + element: e.detail.element as HTMLElement // Naja's event has a type of HTMLElement + }) + }) + + + this.client.addEventListener('success', (e) => { + return this.dispatch('success', { + ...e.detail, + params: e.detail.request, + payload: e.detail.payload as Payload + }); + }) + + this.client.addEventListener('error', (e) => { + return this.dispatch('error', { + ...e.detail, + params: e.detail.request, + response: e.detail.response, + }); + }) + + return this; + } + + async request(args: RequestParams): Promise

{ + return await this.client.makeRequest(args.method, args.url, args.data) as P; + } + + async submitForm(element: E): Promise

{ + return await this.client.uiHandler.submitForm(element) as P; + } + + dispatch< + K extends string, M extends BaseAjaxEventMap = AjaxEventMap + >(type: K, detail: K extends keyof M ? EventDetail : any, options?: boolean): boolean { + return this.dispatchEvent(new CustomEvent(type, {detail})); + } + + declare addEventListener: ( + type: K, + listener: EventListener, + options?: boolean | AddEventListenerOptions + ) => void; + + declare removeEventListener: ( + type: K, + listener: EventListener, + options?: boolean | AddEventListenerOptions + ) => void; + + declare dispatchEvent: ( + event: K extends keyof M ? M[K] : CustomEvent + ) => boolean; +} diff --git a/assets/css/datagrid-full.css b/assets/css/datagrid-full.css new file mode 100644 index 000000000..d42e516d7 --- /dev/null +++ b/assets/css/datagrid-full.css @@ -0,0 +1,4 @@ +@import "@fortawesome/fontawesome-free/css/all.css"; +@import 'bootstrap/dist/css/bootstrap.css'; +@import 'vanillajs-datepicker/css/datepicker-bs5.css'; +@import './datagrid.css'; diff --git a/assets/datagrid.css b/assets/css/datagrid.css old mode 100755 new mode 100644 similarity index 96% rename from assets/datagrid.css rename to assets/css/datagrid.css index 23de8ea77..73c7d1185 --- a/assets/datagrid.css +++ b/assets/css/datagrid.css @@ -1,3 +1,15 @@ +/* .datagrid--slide-toggle > td > div { + -webkit-transition: height .25s ease; + -o-transition: height .25s ease; + transition: height .25s ease; + overflow: hidden; +} */ + +.datagrid--content-row:not(.is-active) > td /*> div*/ +{ + display: none; +} + @keyframes edited { 0% { background-color: #A6E2A9 @@ -26,14 +38,6 @@ box-sizing: border-box } -.datagrid a { - text-decoration: none; -} - -.datagrid a:not(.btn):not(.dropdown-item):hover { - text-decoration: underline; -} - .datagrid .datagrid-input-group-full-width { width: 100% } @@ -225,10 +229,6 @@ opacity: 1 } -.datagrid .table > :not(:first-child) { - border-top: 0; -} - .datagrid table tbody td { vertical-align: middle } @@ -237,22 +237,6 @@ display: table } -.datagrid table tbody tr.row-item-detail { - display: none -} - -.datagrid table tbody tr.row-item-detail.toggled { - display: table-row -} - -.datagrid table tbody tr.row-item-detail .item-detail-content { - display: none -} - -.datagrid table tbody tr.row-item-detail-helper { - display: none -} - .datagrid table tbody tr .datagrid-inline-edit .form-control { margin: -3px; padding-bottom: 4px; @@ -493,7 +477,8 @@ } .datagrid select { - text-transform: none; + padding: 0; + text-transform: none } .datagrid .row-grid-bottom { @@ -526,8 +511,7 @@ display: inline-block } -.datagrid .row-grid-bottom .col-per-page .form-control, -.datagrid .row-grid-bottom .col-per-page .form-select { +.datagrid .row-grid-bottom .col-per-page .form-control { width: auto; display: inline-block } diff --git a/assets/css/happy.css.ts b/assets/css/happy.css.ts new file mode 100644 index 000000000..525ffbc50 --- /dev/null +++ b/assets/css/happy.css.ts @@ -0,0 +1,464 @@ +export const happyStyles = `/** + * From happy-inputs by paveljanda: + * https://github.com/paveljanda/happy/blob/94357b7146b5f3029cc565859a588c5832dd374a/src/happy.css + */ + +.happy-color, +.happy-checkbox, +.happy-radio { + color: #333333; +} + +.happy-color > b, +.happy-checkbox > b, +.happy-radio > b { + background-color: #333333; +} + +.happy-color.active, +.active.happy-checkbox, +.active.happy-radio { + color: #333333; +} + +.happy-color.active > b, +.active.happy-checkbox > b, +.active.happy-radio > b { + background-color: #333333; +} + +.happy-color.primary, +.primary.happy-checkbox, +.primary.happy-radio { + color: #333333; +} + +.happy-color.primary > b, +.primary.happy-checkbox > b, +.primary.happy-radio > b { + background-color: #333333; +} + +.happy-color.primary.active, +.primary.active.happy-checkbox, +.primary.active.happy-radio { + color: #4c86bb; +} + +.happy-color.primary.active > b, +.primary.active.happy-checkbox > b, +.primary.active.happy-radio > b { + background-color: #4c86bb; +} + +.happy-color.success, +.success.happy-checkbox, +.success.happy-radio { + color: #333333; +} + +.happy-color.success > b, +.success.happy-checkbox > b, +.success.happy-radio > b { + background-color: #333333; +} + +.happy-color.success.active, +.success.active.happy-checkbox, +.success.active.happy-radio { + color: #72b889; +} + +.happy-color.success.active > b, +.success.active.happy-checkbox > b, +.success.active.happy-radio > b { + background-color: #72b889; +} + +.happy-color.info, +.info.happy-checkbox, +.info.happy-radio { + color: #333333; +} + +.happy-color.info > b, +.info.happy-checkbox > b, +.info.happy-radio > b { + background-color: #333333; +} + +.happy-color.info.active, +.info.active.happy-checkbox, +.info.active.happy-radio { + color: #5bc0de; +} + +.happy-color.info.active > b, +.info.active.happy-checkbox > b, +.info.active.happy-radio > b { + background-color: #5bc0de; +} + +.happy-color.warning, +.warning.happy-checkbox, +.warning.happy-radio { + color: #333333; +} + +.happy-color.warning > b, +.warning.happy-checkbox > b, +.warning.happy-radio > b { + background-color: #333333; +} + +.happy-color.warning.active, +.warning.active.happy-checkbox, +.warning.active.happy-radio { + color: #f0bb65; +} + +.happy-color.warning.active > b, +.warning.active.happy-checkbox > b, +.warning.active.happy-radio > b { + background-color: #f0bb65; +} + +.happy-color.danger, +.danger.happy-checkbox, +.danger.happy-radio { + color: #333333; +} + +.happy-color.danger > b, +.danger.happy-checkbox > b, +.danger.happy-radio > b { + background-color: #333333; +} + +.happy-color.danger.active, +.danger.active.happy-checkbox, +.danger.active.happy-radio { + color: #ed6b6b; +} + +.happy-color.danger.active > b, +.danger.active.happy-checkbox > b, +.danger.active.happy-radio > b { + background-color: #ed6b6b; +} + +.happy-color.white, +.white.happy-checkbox, +.white.happy-radio { + color: #333333; +} + +.happy-color.white > b, +.white.happy-checkbox > b, +.white.happy-radio > b { + background-color: #333333; +} + +.happy-color.white.active, +.white.active.happy-checkbox, +.white.active.happy-radio { + color: #ffffff; +} + +.happy-color.white.active > b, +.white.active.happy-checkbox > b, +.white.active.happy-radio > b { + background-color: #ffffff; +} + +.happy-border-color, +.happy-radio { + border-color: rgba(51, 51, 51, 0.8); +} + +.happy-border-color.active, +.active.happy-radio { + border-color: #333333; +} + +.happy-border-color.primary, +.primary.happy-radio { + border-color: rgba(51, 51, 51, 0.8); +} + +.happy-border-color.primary.active, +.primary.active.happy-radio { + border-color: #4c86bb; +} + +.happy-border-color.success, +.success.happy-radio { + border-color: rgba(51, 51, 51, 0.8); +} + +.happy-border-color.success.active, +.success.active.happy-radio { + border-color: #72b889; +} + +.happy-border-color.info, +.info.happy-radio { + border-color: rgba(51, 51, 51, 0.8); +} + +.happy-border-color.info.active, +.info.active.happy-radio { + border-color: #5bc0de; +} + +.happy-border-color.warning, +.warning.happy-radio { + border-color: rgba(51, 51, 51, 0.8); +} + +.happy-border-color.warning.active, +.warning.active.happy-radio { + border-color: #f0bb65; +} + +.happy-border-color.danger, +.danger.happy-radio { + border-color: rgba(51, 51, 51, 0.8); +} + +.happy-border-color.danger.active, +.danger.active.happy-radio { + border-color: #ed6b6b; +} + +.happy-border-color.white, +.white.happy-radio { + border-color: rgba(51, 51, 51, 0.8); +} + +.happy-border-color.white.active, +.white.active.happy-radio { + border-color: #ffffff; +} + +/** + * Common + */ + +input[type="radio"].happy, +input[type="checkbox"].happy { + position: absolute; + top: -50%; + left: -50%; + opacity: 0; +} + +label:not(.selectable), +.noselect { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +label { + cursor: pointer; + position: relative; +} + +/** + * Checkbox + */ + +.happy-checkbox { + border-color: #333333; + margin-right: 0.2em; + position: relative; + display: inline-block; + line-height: 20px; + vertical-align: middle; + width: 16px; + height: 16px; + border-width: 2px; + border-style: solid; + cursor: pointer; + box-sizing: border-box; + top: -2px; + -webkit-border-radius: 2.66667px; + -moz-border-radius: 2.66667px; + border-radius: 2.66667px; +} + +.happy-checkbox svg { + position: absolute; + display: block; + top: -2px; + left: -2px; + height: 16px; + width: 16px; + opacity: 0; + -webkit-border-radius: 2.66667px; + -moz-border-radius: 2.66667px; + border-radius: 2.66667px; + background-color: #333333; + -ms-transform: scale(0.4); + -webkit-transform: scale(0.4); + transform: scale(0.4); + -ms-transition: all 180ms; + -webkit-transition: all 180ms; + transition: all 180ms; +} + +.happy-checkbox svg rect { + fill: white; +} + +.happy-checkbox svg rect:first-child { + -ms-transform: rotate(45deg); + -webkit-transform: rotate(45deg); + transform: rotate(45deg); +} + +.happy-checkbox svg rect:nth-child(2) { + -ms-transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); + /* fill: yellow; */ +} + +.happy-checkbox.thin { + border-width: 1px; +} + +.happy-checkbox.thin svg { + top: -1px; + left: -1px; +} + +.happy-checkbox.white { + border-color: #ffffff; +} + +.happy-checkbox.gray-border { + border-color: #858585; +} + +.happy-checkbox.primary-border { + border-color: #4c86bb; +} + +.happy-checkbox.success-border { + border-color: #72b889; +} + +.happy-checkbox.info-border { + border-color: #5bc0de; +} + +.happy-checkbox.warning-border { + border-color: #f0bb65; +} + +.happy-checkbox.danger-border { + border-color: #ed6b6b; +} + +.happy-checkbox.primary svg { + background-color: #4c86bb; +} + +.happy-checkbox.success svg { + background-color: #72b889; +} + +.happy-checkbox.info svg { + background-color: #5bc0de; +} + +.happy-checkbox.warning svg { + background-color: #f0bb65; +} + +.happy-checkbox.danger svg { + background-color: #ed6b6b; +} + +.happy-checkbox.white svg { + background-color: #ffffff; +} + +.happy-checkbox.white svg rect { + fill: #333333; +} + +.happy-checkbox.active { + border-color: transparent; +} + +.happy-checkbox.active svg { + opacity: 1; + -ms-transform: scale(1); + -webkit-transform: scale(1); + transform: scale(1); +} + +/** + * Radio + */ + +.happy-radio { + position: relative; + display: inline-block; + line-height: 20px; + vertical-align: middle; + width: 16px; + height: 16px; + border-width: 2px; + border-style: solid; + cursor: pointer; + box-sizing: border-box; + top: -2px; + -webkit-border-radius: 16px; + -moz-border-radius: 16px; + border-radius: 16px; +} + +.happy-radio.thin { + border-width: 1.66667px; +} + +.happy-radio b { + position: absolute; + display: block; + top: 2px; + left: 2px; + bottom: 2px; + right: 2px; + opacity: 0; + -webkit-border-radius: 10.66667px; + -moz-border-radius: 10.66667px; + border-radius: 10.66667px; + -ms-transform: scale(0.4); + -webkit-transform: scale(0.4); + transform: scale(0.4); + -ms-transition: all 180ms; + -webkit-transition: all 180ms; + transition: all 180ms; +} + +.happy-radio.active b { + opacity: 1; + -ms-transform: scale(1); + -webkit-transform: scale(1); + transform: scale(1); +} + +.happy-radio.focus { + outline: none; + -webkit-box-shadow: 0px 0px 5px 0px rgba(50, 50, 50, 0.75); + -moz-box-shadow: 0px 0px 5px 0px rgba(50, 50, 50, 0.75); + box-shadow: 0px 0px 5px 0px rgba(50, 50, 50, 0.75); +} +`; diff --git a/assets/datagrid-instant-url-refresh.js b/assets/datagrid-instant-url-refresh.js deleted file mode 100644 index f4ae43c73..000000000 --- a/assets/datagrid-instant-url-refresh.js +++ /dev/null @@ -1,30 +0,0 @@ -var dataGridRegisterAjaxCall; - -if (typeof naja !== "undefined") { - dataGridRegisterAjaxCall = function (params) { - var method = params.type || 'GET'; - var data = params.data || null; - - naja.makeRequest(method, params.url, data, { - history: 'replace' - }) - .then(params.success) - .catch(params.error); - }; - -} else { - dataGridRegisterAjaxCall = function (params) { - $.nette.ajax(params); - }; -} - -document.addEventListener('DOMContentLoaded', function () { - var element = document.querySelector('.datagrid'); - - if (element !== null) { - return dataGridRegisterAjaxCall({ - type: 'GET', - url: element.getAttribute('data-refresh-state') - }); - } -}); diff --git a/assets/datagrid-spinners.css b/assets/datagrid-spinners.css deleted file mode 100755 index 818714dfc..000000000 --- a/assets/datagrid-spinners.css +++ /dev/null @@ -1,118 +0,0 @@ -@keyframes ublaboo-spinner-icon { - 0% { - transform: rotate(0); } - 50% { - transform: rotate(180deg); } - 100% { - transform: rotate(360deg); } } - -@-webkit-keyframes ublaboo-spinner-icon { - 0% { - transform: rotate(0); } - 50% { - transform: rotate(180deg); } - 100% { - transform: rotate(360deg); } } - -.ublaboo-spinner-icon > span { - animation-duration: 2s; - animation-delay: 0; - animation-iteration-count: infinite; - animation-timing-function: ease; - animation-name: ublaboo-spinner-icon; } - -@keyframes ublaboo-spinner-small { - 0% { - transform: translate(21.3px, 2.2px); } - 11.1% { - transform: translate(8.1px, 25.2px); } - 22.2% { - transform: translate(12.7px, -0.7px); } - 33.3% { - transform: translate(17.2px, 25.2px); } - 44.4% { - transform: translate(4.2px, 2.2px); } - 55.5% { - transform: translate(24.1px, 19.5px); } - 66.6% { - transform: translate(-0.3px, 10.3px); } - 77.7% { - transform: translate(25.8px, 10.3px); } - 88.8% { - transform: translate(1.2px, 19.3px); } - 100% { - transform: translate(21.3px, 2.2px); } } - -@-webkit-keyframes ublaboo-spinner-small { - 0% { - transform: translate(21.3px, 2.2px); } - 11.1% { - transform: translate(8.1px, 25.2px); } - 22.2% { - transform: translate(12.7px, -0.7px); } - 33.3% { - transform: translate(17.2px, 25.2px); } - 44.4% { - transform: translate(4.2px, 2.2px); } - 55.5% { - transform: translate(24.1px, 19.5px); } - 66.6% { - transform: translate(-0.3px, 10.3px); } - 77.7% { - transform: translate(25.8px, 10.3px); } - 88.8% { - transform: translate(1.2px, 19.3px); } - 100% { - transform: translate(21.3px, 2.2px); } } - -@keyframes ublaboo-spinner-in { - 0% { - opacity: 0; } - 100% { - opacity: 1; } } - -@-webkit-keyframes ublaboo-spinner-in { - 0% { - opacity: 0; } - 100% { - opacity: 1; } } - -.ublaboo-spinner { - line-height: 0; - display: inline-block; - margin: auto; - position: relative; - margin: 0 1em -11px 1em; - top: 1px; - opacity: 0; - animation-duration: 150ms; - animation-delay: 0; - animation-iteration-count: 1; - animation-timing-function: ease-in; - animation-name: ublaboo-spinner-in; - animation-fill-mode: forwards; } - .ublaboo-spinner > i { - position: absolute; - background-color: #37434f; - left: 0; - top: 0; - animation-duration: 6s; - animation-delay: 0; - animation-iteration-count: infinite; - animation-timing-function: ease; } - .ublaboo-spinner > i:nth-of-type(2) { - animation-delay: -1.5s; } - .ublaboo-spinner > i:nth-of-type(3) { - animation-delay: -3s; } - .ublaboo-spinner > i:nth-of-type(4) { - animation-delay: -4.5s; } - .ublaboo-spinner.ublaboo-spinner-small { - width: 28.0px; - height: 28.0px; } - .ublaboo-spinner.ublaboo-spinner-small > i { - width: 4.0px; - height: 4.0px; - border-radius: 2px; - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - animation-name: ublaboo-spinner-small; } diff --git a/assets/datagrid-spinners.js b/assets/datagrid-spinners.js deleted file mode 100644 index b6a0b2b26..000000000 --- a/assets/datagrid-spinners.js +++ /dev/null @@ -1,89 +0,0 @@ -var dataGridRegisterExtension; - -if (typeof naja !== "undefined") { - var isNaja2 = function () { return naja && naja.VERSION && naja.VERSION >= 2 }; - var najaEventParams = function (params) { return isNaja2() ? params.detail : params }; - var najaRequest = function (params) { return isNaja2() ? params.detail.request : params.xhr }; - dataGridRegisterExtension = function (name, extension) { - var init = extension.init; - var success = extension.success; - var before = extension.before; - var complete = extension.complete; - - - var NewExtension = function NewExtension(naja, name) { - this.name = name; - - this.initialize = function (naja) { - if(init) { - naja.addEventListener('init', function (params) { - init(najaEventParams(params).defaultOptions); - }); - } - - if(success) { - naja.addEventListener('success', function (params) { - var payload = isNaja2() ? params.detail.payload : params.response; - success(payload, najaEventParams(params).options); - }); - } - - if(before) { - naja.addEventListener('before', function (params) { - before(najaRequest(params), najaEventParams(params).options); - }); - } - - if(complete) { - naja.addEventListener('complete', function (params) { - complete(najaRequest(params), najaEventParams(params).options); - }); - } - } - if (!isNaja2()) { - this.initialize(naja); - } - return this; - } - - if (isNaja2()) { - naja.registerExtension(new NewExtension(null, name)); - } else { - naja.registerExtension(NewExtension, name); - } - }; -} else if ($.nette) { - dataGridRegisterExtension = function (name, extension) { - $.nette.ext(name, extension); - }; -} - -dataGridRegisterExtension('ublaboo-spinners', { - before: function(xhr, settings) { - var el, id, row_detail, spinner_template, grid_fullname; - if (settings.nette) { - el = settings.nette.el; - spinner_template = $('

'); - if (el.is('.datagrid [name="group_action[submit]"]')) { - return el.after(spinner_template); - } else if (el.is('.datagrid a') && el.data('toggle-detail')) { - id = settings.nette.el.attr('data-toggle-detail'); - grid_fullname = settings.nette.el.attr('data-toggle-detail-grid-fullname'); - row_detail = $('.item-detail-' + grid_fullname + '-id-' + id); - if (!row_detail.hasClass('loaded')) { - return el.addClass('ublaboo-spinner-icon'); - } - } else if (el.is('.datagrid .col-pagination a')) { - return el.closest('.row-grid-bottom').find('.col-per-page').prepend(spinner_template); - } else if (el.is('.datagrid .datagrid-per-page-submit')) { - return el.closest('.row-grid-bottom').find('.col-per-page').prepend(spinner_template); - } else if (el.is('.datagrid .reset-filter')) { - return el.closest('.row-grid-bottom').find('.col-per-page').prepend(spinner_template); - } - } - }, - complete: function() { - $('.ublaboo-spinner').remove(); - return $('.ublaboo-spinner-icon').removeClass('ublaboo-spinner-icon'); - } -}); diff --git a/assets/datagrid.js b/assets/datagrid.js deleted file mode 100644 index 9adcf4083..000000000 --- a/assets/datagrid.js +++ /dev/null @@ -1,922 +0,0 @@ -var dataGridRegisterExtension, dataGridRegisterAjaxCall, dataGridLoad, dataGridSubmitForm; - -if (typeof naja !== "undefined") { - var isNaja2 = function () { return naja && naja.VERSION && naja.VERSION >= 2 }; - var najaEventParams = function (params) { return isNaja2() ? params.detail : params }; - var najaRequest = function (params) { return isNaja2() ? params.detail.request : params.xhr }; - dataGridRegisterExtension = function (name, extension) { - var init = extension.init; - var success = extension.success; - var before = extension.before; - var complete = extension.complete; - var interaction = extension.interaction; - - - var NewExtension = function NewExtension(naja, name) { - this.name = name; - - this.initialize = function (naja) { - if(init) { - naja.addEventListener('init', function (params) { - init(najaEventParams(params).defaultOptions); - }); - } - - if(success) { - naja.addEventListener('success', function (params) { - var payload = isNaja2() ? params.detail.payload : params.response; - success(payload, najaEventParams(params).options); - }); - } - - var interactionTarget = naja; - if (isNaja2()) { - interactionTarget = interactionTarget.uiHandler; - } - - interactionTarget.addEventListener('interaction', function (params) { - if (isNaja2()) { - params.detail.options.nette = { - el: $(params.detail.element) - } - } else { - params.options.nette = { - el: $(params.element) - } - } - if (interaction) { - if (!interaction(najaEventParams(params).options)){ - params.preventDefault(); - } - } - }); - - if(before) { - naja.addEventListener('before', function (params) { - if (!before(najaRequest(params), najaEventParams(params).options)) - params.preventDefault(); - }); - } - - if(complete) { - naja.addEventListener('complete', function (params) { - complete(najaRequest(params), najaEventParams(params).options); - }); - } - } - if (!isNaja2()) { - this.initialize(naja); - } - return this; - } - - if (isNaja2()) { - naja.registerExtension(new NewExtension(null, name)); - } else { - naja.registerExtension(NewExtension, name); - } - }; - - - dataGridRegisterAjaxCall = function (params) { - var method = params.type || 'GET'; - var data = params.data || null; - - naja.makeRequest(method, params.url, data, {}) - .then(params.success) - .catch(params.error); - }; - - dataGridLoad = function () { - naja.load(); - }; - - dataGridSubmitForm = function (form) { - return naja.uiHandler.submitForm(form.get(0)); - }; -} else if ($.nette) { - dataGridRegisterExtension = function (name, extension) { - $.nette.ext(name, extension); - }; - dataGridRegisterAjaxCall = function (params) { - $.nette.ajax(params); - }; - dataGridLoad = function () { - $.nette.load(); - }; - dataGridSubmitForm = function (form) { - return form.submit(); - }; -} else { - throw new Error("Include Naja.js or nette.ajax for datagrids to work!") -} - - -var datagridFilterMultiSelect, datagridGroupActionMultiSelect, datagridShiftGroupSelection, datagridSortable, datagridSortableTree, getEventDomPath, - indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; - -$(document).on('click', '[data-datagrid-confirm]:not(.ajax)', function(e) { - if (!confirm($(e.target).closest('a').attr('data-datagrid-confirm'))) { - e.stopPropagation(); - return e.preventDefault(); - } -}); - -if (typeof naja !== "undefined") { - dataGridRegisterExtension('datagrid.confirm', { - interaction: function(settings) { - var confirm_message; - if (settings.nette) { - confirm_message = settings.nette.el.data('datagrid-confirm'); - if (confirm_message) { - return confirm(confirm_message); - } - } - return true; - } - }); -} else { - dataGridRegisterExtension('datagrid.confirm', { - before: function(xhr, settings) { - var confirm_message; - if (settings.nette) { - confirm_message = settings.nette.el.data('datagrid-confirm'); - if (confirm_message) { - return confirm(confirm_message); - } - } - return true; - } - }); -} - - -$(document).on('change', 'select[data-autosubmit-per-page]', function() { - var button; - button = $(this).parent().find('input[type=submit]'); - if (button.length === 0) { - button = $(this).parent().find('button[type=submit]'); - } - return button.click(); -}).on('change', 'select[data-autosubmit]', function() { - return dataGridSubmitForm($(this).closest('form').first()); -}).on('change', 'input[data-autosubmit][data-autosubmit-change]', function(e) { - var $this, code; - code = e.which || e.keyCode || 0; - clearTimeout(window.datagrid_autosubmit_timer); - $this = $(this); - return window.datagrid_autosubmit_timer = setTimeout((function(_this) { - return function() { - return dataGridSubmitForm($this.closest('form').first()); - }; - })(this), 200); -}).on('keyup', 'input[data-autosubmit]', function(e) { - var $this, code; - code = e.which || e.keyCode || 0; - if ((code !== 13) && ((code >= 9 && code <= 40) || (code >= 112 && code <= 123))) { - return; - } - clearTimeout(window.datagrid_autosubmit_timer); - $this = $(this); - return window.datagrid_autosubmit_timer = setTimeout((function(_this) { - return function() { - return dataGridSubmitForm($this.closest('form').first()); - }; - })(this), 200); -}).on('keydown', '.datagrid-inline-edit input', function(e) { - var code; - code = e.which || e.keyCode || 0; - if (code === 13) { - e.stopPropagation(); - e.preventDefault(); - return $(this).closest('tr').find('.col-action-inline-edit [name="inline_edit[submit]"]').click(); - } -}); - -$(document).on('keydown', 'input[data-datagrid-manualsubmit]', function(e) { - var code; - code = e.which || e.keyCode || 0; - if (code === 13) { - e.stopPropagation(); - e.preventDefault(); - return dataGridSubmitForm($(this).closest('form').first()); - } -}); - -getEventDomPath = function(e) { - var node, path; - if (indexOf.call(e, path) >= 0) { - return e.path; - } - path = []; - node = e.target; - while (node !== document.body) { - if (node === null) { - break; - } - path.push(node); - node = node.parentNode; - } - return path; -}; - -datagridShiftGroupSelection = function() { - var last_checkbox; - last_checkbox = null; - return document.addEventListener('click', function(e) { - var checkboxes_rows, current_checkbox_row, el, event, i, ie, input, j, k, last_checkbox_row, last_checkbox_tbody, len, len1, len2, ref, ref1, results, row, rows; - ref = getEventDomPath(e); - for (i = 0, len = ref.length; i < len; i++) { - el = ref[i]; - if ($(el).is('.col-checkbox') && last_checkbox && e.shiftKey) { - current_checkbox_row = $(el).closest('tr'); - last_checkbox_row = last_checkbox.closest('tr'); - last_checkbox_tbody = last_checkbox_row.closest('tbody'); - checkboxes_rows = last_checkbox_tbody.find('tr').toArray(); - if (current_checkbox_row.index() > last_checkbox_row.index()) { - rows = checkboxes_rows.slice(last_checkbox_row.index(), current_checkbox_row.index()); - } else if (current_checkbox_row.index() < last_checkbox_row.index()) { - rows = checkboxes_rows.slice(current_checkbox_row.index() + 1, last_checkbox_row.index()); - } - if (!rows) { - return; - } - for (j = 0, len1 = rows.length; j < len1; j++) { - row = rows[j]; - input = $(row).find('.col-checkbox input[type=checkbox]')[0]; - if (input) { - input.checked = true; - ie = window.navigator.userAgent.indexOf("MSIE "); - if (ie) { - event = document.createEvent('Event'); - event.initEvent('change', true, true); - } else { - event = new Event('change', { - 'bubbles': true - }); - } - input.dispatchEvent(event); - } - } - } - } - ref1 = getEventDomPath(e); - results = []; - for (k = 0, len2 = ref1.length; k < len2; k++) { - el = ref1[k]; - if ($(el).is('.col-checkbox')) { - results.push(last_checkbox = $(el)); - } else { - results.push(void 0); - } - } - return results; - }); -}; - -datagridShiftGroupSelection(); - -document.addEventListener('change', function(e) { - var buttons, checked_inputs, counter, event, grid, i, ie, input, inputs, len, results, select, total; - grid = e.target.getAttribute('data-check'); - if (grid) { - checked_inputs = document.querySelectorAll('input[data-check-all-' + grid + ']:checked'); - select = document.querySelector('.datagrid-' + grid + ' select[name="group_action[group_action]"]'); - buttons = document.querySelectorAll('.datagrid-' + grid + ' .row-group-actions *[type="submit"]'); - counter = document.querySelector('.datagrid-' + grid + ' .datagrid-selected-rows-count'); - - if (checked_inputs.length) { - if (buttons) { - buttons.forEach(function (button) { - button.disabled = false; - }); - } - if (select) { - select.disabled = false; - } - total = document.querySelectorAll('input[data-check-all-' + grid + ']').length; - if (counter) { - counter.innerHTML = checked_inputs.length + '/' + total; - } - } else { - if (buttons) { - buttons.forEach(function (button) { - button.disabled = true; - }); - } - if (select) { - select.disabled = true; - select.value = ""; - } - if (counter) { - counter.innerHTML = ""; - } - } - ie = window.navigator.userAgent.indexOf("MSIE "); - if (ie) { - event = document.createEvent('Event'); - event.initEvent('change', true, true); - } else { - event = new Event('change', { - 'bubbles': true - }); - } - if (select) { - select.dispatchEvent(event); - } - } - grid = e.target.getAttribute('data-check-all'); - if (grid) { - inputs = document.querySelectorAll('input[type=checkbox][data-check-all-' + grid + ']'); - results = []; - for (i = 0, len = inputs.length; i < len; i++) { - input = inputs[i]; - input.checked = e.target.checked; - ie = window.navigator.userAgent.indexOf("MSIE "); - if (ie) { - event = document.createEvent('Event'); - event.initEvent('change', true, true); - } else { - event = new Event('change', { - 'bubbles': true - }); - } - results.push(input.dispatchEvent(event)); - } - return results; - } -}); - - -window.datagridSerializeUrl = function(obj, prefix) { -var str = []; -for(var p in obj) { - if (obj.hasOwnProperty(p)) { - var k = prefix ? prefix + "[" + p + "]" : p, v = obj[p]; - if (v !== null && v !== "") { - if (typeof v == "object") { - var r = window.datagridSerializeUrl(v, k); - if (r) { - str.push(r); - } - } else { - str.push(encodeURIComponent(k) + "=" + encodeURIComponent(v)); - } - } - } -} -return str.join("&"); -} -; - -datagridSortable = function() { - if (typeof $.fn.sortable === 'undefined') { - return; - } - return $('.datagrid [data-sortable]').sortable({ - handle: '.handle-sort', - items: 'tr', - axis: 'y', - update: function(event, ui) { - var component_prefix, data, item_id, next_id, prev_id, row, url; - row = ui.item.closest('tr[data-id]'); - item_id = row.data('id'); - prev_id = null; - next_id = null; - if (row.prev().length) { - prev_id = row.prev().data('id'); - } - if (row.next().length) { - next_id = row.next().data('id'); - } - url = $(this).data('sortable-url'); - data = {}; - component_prefix = row.closest('.datagrid').find('tbody').attr('data-sortable-parent-path'); - data[(component_prefix + '-item_id').replace(/^-/, '')] = item_id; - if (prev_id !== null) { - data[(component_prefix + '-prev_id').replace(/^-/, '')] = prev_id; - } - if (next_id !== null) { - data[(component_prefix + '-next_id').replace(/^-/, '')] = next_id; - } - return dataGridRegisterAjaxCall({ - type: 'GET', - url: url, - data: data, - error: function(jqXHR, textStatus, errorThrown) { - return alert(jqXHR.statusText); - } - }); - }, - helper: function(e, ui) { - ui.children().each(function() { - return $(this).width($(this).width()); - }); - return ui; - } - }); -}; - -$(function() { - return datagridSortable(); -}); - -if (typeof datagridSortableTree === 'undefined') { - datagridSortableTree = function() { - if (typeof $('.datagrid-tree-item-children').sortable === 'undefined') { - return; - } - return $('.datagrid-tree-item-children').sortable({ - handle: '.handle-sort', - items: '.datagrid-tree-item:not(.datagrid-tree-header)', - toleranceElement: '> .datagrid-tree-item-content', - connectWith: '.datagrid-tree-item-children', - update: function(event, ui) { - var component_prefix, data, item_id, next_id, parent, parent_id, prev_id, row, url; - $('.toggle-tree-to-delete').remove(); - row = ui.item.closest('.datagrid-tree-item[data-id]'); - item_id = row.data('id'); - prev_id = null; - next_id = null; - parent_id = null; - if (row.prev().length) { - prev_id = row.prev().data('id'); - } - if (row.next().length) { - next_id = row.next().data('id'); - } - parent = row.parent().closest('.datagrid-tree-item'); - if (parent.length) { - parent.find('.datagrid-tree-item-children').first().css({ - display: 'block' - }); - parent.addClass('has-children'); - parent_id = parent.data('id'); - } - url = $(this).data('sortable-url'); - if (!url) { - return; - } - parent.find('[data-toggle-tree]').first().removeClass('hidden'); - component_prefix = row.closest('.datagrid-tree').attr('data-sortable-parent-path'); - data = {}; - data[(component_prefix + '-item_id').replace(/^-/, '')] = item_id; - if (prev_id !== null) { - data[(component_prefix + '-prev_id').replace(/^-/, '')] = prev_id; - } - if (next_id !== null) { - data[(component_prefix + '-next_id').replace(/^-/, '')] = next_id; - } - data[(component_prefix + '-parent_id').replace(/^-/, '')] = parent_id; - return dataGridRegisterAjaxCall({ - type: 'GET', - url: url, - data: data, - error: function(jqXHR, textStatus, errorThrown) { - if (errorThrown !== 'abort') { - return alert(jqXHR.statusText); - } - } - }); - }, - stop: function(event, ui) { - return $('.toggle-tree-to-delete').removeClass('toggle-tree-to-delete'); - }, - start: function(event, ui) { - var parent; - parent = ui.item.parent().closest('.datagrid-tree-item'); - if (parent.length) { - if (parent.find('.datagrid-tree-item').length === 2) { - return parent.find('[data-toggle-tree]').addClass('toggle-tree-to-delete'); - } - } - } - }); - }; -} - -$(function() { - return datagridSortableTree(); -}); - -dataGridRegisterExtension('datagrid.happy', { - success: function() { - var c, checked_rows, class_selector, classes, event, grid, grids, i, ie, input, j, len, len1, results; - if (window.happy) { - window.happy.reset(); - } - grids = $('.datagrid'); - results = []; - for (i = 0, len = grids.length; i < len; i++) { - grid = grids[i]; - classes = grid.classList; - class_selector = ''; - for (j = 0, len1 = classes.length; j < len1; j++) { - c = classes[j]; - class_selector = class_selector + '.' + c; - } - checked_rows = document.querySelectorAll(class_selector + ' ' + 'input[data-check]:checked'); - if (checked_rows.length === 1 && checked_rows[0].getAttribute('name') === 'toggle-all') { - input = document.querySelector(class_selector + ' input[name=toggle-all]'); - if (input) { - input.checked = false; - ie = window.navigator.userAgent.indexOf("MSIE "); - if (ie) { - event = document.createEvent('Event'); - event.initEvent('change', true, true); - } else { - event = new Event('change', { - 'bubbles': true - }); - } - results.push(input.dispatchEvent(event)); - } else { - results.push(void 0); - } - } else { - results.push(void 0); - } - } - return results; - } -}); - -dataGridRegisterExtension('datagrid.sortable', { - success: function() { - return datagridSortable(); - } -}); - -dataGridRegisterExtension('datagrid.forms', { - success: function() { - return $('.datagrid').find('form').each(function() { - return window.Nette.initForm(this); - }); - } -}); - -dataGridRegisterExtension('datagrid.url', { - success: function(payload) { - var host, path, query, url; - if (payload._datagrid_url) { - if (window.history.replaceState) { - host = window.location.protocol + "//" + window.location.host; - path = window.location.pathname; - query = window.datagridSerializeUrl(payload.state).replace(/&+$/gm, ''); - if (query) { - url = host + path + "?" + query.replace(/\&*$/, ''); - } else { - url = host + path; - } - url += window.location.hash; - if (window.location.href !== url) { - return window.history.replaceState({ - path: url - }, '', url); - } - } - } - } -}); - -dataGridRegisterExtension('datagrid.sort', { - success: function(payload) { - var href, key, ref, results; - if (payload._datagrid_sort) { - ref = payload._datagrid_sort; - results = []; - for (key in ref) { - href = ref[key]; - results.push($('#datagrid-sort-' + key).attr('href', href)); - } - return results; - } - } -}); - -dataGridRegisterExtension('datargid.item_detail', { - before: function(xhr, settings) { - var id, row_detail, grid_fullname; - if (settings.nette && settings.nette.el.attr('data-toggle-detail')) { - id = settings.nette.el.attr('data-toggle-detail'); - grid_fullname = settings.nette.el.attr('data-toggle-detail-grid-fullname'); - row_detail = $('.item-detail-' + grid_fullname + '-id-' + id); - if (row_detail.hasClass('loaded')) { - if (!row_detail.find('.item-detail-content').length) { - row_detail.removeClass('toggled'); - return true; - } - if (row_detail.hasClass('toggled')) { - row_detail.find('.item-detail-content').slideToggle('fast', (function(_this) { - return function() { - return row_detail.toggleClass('toggled'); - }; - })(this)); - } else { - row_detail.toggleClass('toggled'); - row_detail.find('.item-detail-content').slideToggle('fast'); - } - return false; - } else { - return row_detail.addClass('loaded'); - } - } - return true; - }, - success: function(payload) { - var id, row_detail, grid_fullname; - if (payload._datagrid_toggle_detail && payload._datagrid_name) { - id = payload._datagrid_toggle_detail; - grid_fullname = payload._datagrid_name; - row_detail = $('.item-detail-' + grid_fullname + '-id-' + id); - row_detail.toggleClass('toggled'); - return row_detail.find('.item-detail-content').slideToggle('fast'); - } - } -}); - -dataGridRegisterExtension('datagrid.tree', { - before: function(xhr, settings) { - var children_block; - if (settings.nette && settings.nette.el.attr('data-toggle-tree')) { - settings.nette.el.toggleClass('toggle-rotate'); - children_block = settings.nette.el.closest('.datagrid-tree-item').find('.datagrid-tree-item-children').first(); - if (children_block.hasClass('loaded')) { - children_block.slideToggle('fast'); - return false; - } - } - return true; - }, - success: function(payload) { - var children_block, content, id, name, ref, snippet, template; - if (payload._datagrid_tree) { - id = payload._datagrid_tree; - children_block = $('.datagrid-tree-item[data-id="' + id + '"]').find('.datagrid-tree-item-children').first(); - children_block.addClass('loaded'); - ref = payload.snippets; - for (name in ref) { - snippet = ref[name]; - content = $(snippet); - template = $('
'); - template.attr('data-id', content.attr('data-id')); - template.append(content); - if (content.data('has-children')) { - template.addClass('has-children'); - } - children_block.append(template); - } - children_block.addClass('loaded'); - children_block.slideToggle('fast'); - dataGridLoad(); - } - return datagridSortableTree(); - } -}); - -$(document).on('click', '[data-datagrid-editable-url]', function(event) { - var attr_name, attr_value, attrs, cell, cellValue, cell_height, cell_lines, cell_padding, input, line_height, submit, valueToEdit; - cell = $(this); - if (event.target.tagName.toLowerCase() === 'a') { - return; - } - if (cell.hasClass('datagrid-inline-edit')) { - return; - } - if (!cell.hasClass('editing')) { - cell.addClass('editing'); - cellValue = cell.html().trim().replace('
', '\n'); - if (cell.attr('data-datagrid-editable-value')) { - valueToEdit = String(cell.data('datagrid-editable-value')); - } else { - valueToEdit = cellValue; - } - cell.data('originalValue', cellValue); - cell.data('valueToEdit', valueToEdit); - if (cell.data('datagrid-editable-type') === 'textarea') { - input = $(''); - cell_padding = parseInt(cell.css('padding').replace(/[^-\d\.]/g, ''), 10); - cell_height = cell.outerHeight(); - line_height = Math.round(parseFloat(cell.css('line-height'))); - cell_lines = (cell_height - (2 * cell_padding)) / line_height; - input.attr('rows', Math.round(cell_lines)); - } else if (cell.data('datagrid-editable-type') === 'select') { - input = $(cell.data('datagrid-editable-element')); - input.find("option[value='" + valueToEdit + "']").prop('selected', true); - } else { - input = $(''); - input.val(valueToEdit); - } - attrs = cell.data('datagrid-editable-attrs'); - for (attr_name in attrs) { - attr_value = attrs[attr_name]; - input.attr(attr_name, attr_value); - } - cell.removeClass('edited'); - cell.html(input); - submit = function(cell, el) { - var value; - value = el.val(); - if (value !== cell.data('valueToEdit')) { - dataGridRegisterAjaxCall({ - url: cell.data('datagrid-editable-url'), - data: { - value: value - }, - type: 'POST', - success: function(payload) { - if (cell.data('datagrid-editable-type') === 'select') { - cell.html(input.find("option[value='" + value + "']").html()); - } else { - if (payload._datagrid_editable_new_value) { - value = payload._datagrid_editable_new_value; - } - cell.html(value); - } - return cell.addClass('edited'); - }, - error: function() { - cell.html(cell.data('originalValue')); - return cell.addClass('edited-error'); - } - }); - } else { - cell.html(cell.data('originalValue')); - } - return setTimeout(function() { - return cell.removeClass('editing'); - }, 1200); - }; - cell.find('input,textarea,select').focus().on('blur', function() { - return submit(cell, $(this)); - }).on('keydown', function(e) { - if (cell.data('datagrid-editable-type') !== 'textarea') { - if (e.which === 13) { - e.stopPropagation(); - e.preventDefault(); - return submit(cell, $(this)); - } - } - if (e.which === 27) { - e.stopPropagation(); - e.preventDefault(); - cell.removeClass('editing'); - return cell.html(cell.data('originalValue')); - } - }); - return cell.find('select').on('change', function() { - return submit(cell, $(this)); - }); - } -}); - -dataGridRegisterExtension('datagrid.after_inline_edit', { - success: function(payload) { - var grid = $('.datagrid-' + payload._datagrid_name); - - if (payload._datagrid_inline_edited) { - grid.find('tr[data-id="' + payload._datagrid_inline_edited + '"] > td').addClass('edited'); - return grid.find('.datagrid-inline-edit-trigger').removeClass('hidden'); - } else if (payload._datagrid_inline_edit_cancel) { - return grid.find('.datagrid-inline-edit-trigger').removeClass('hidden'); - } - } -}); - -$(document).on('mouseup', '[data-datagrid-cancel-inline-add]', function(e) { - var code = e.which || e.keyCode || 0; - if (code === 1) { - e.stopPropagation(); - e.preventDefault(); - return $('.datagrid-row-inline-add').addClass('datagrid-row-inline-add-hidden'); - } -}); - -dataGridRegisterExtension('datagrid-toggle-inline-add', { - success: function(payload) { - var grid = $('.datagrid-' + payload._datagrid_name); - - if (payload._datagrid_inline_adding) { - var row = grid.find('.datagrid-row-inline-add'); - - if (row.hasClass('datagrid-row-inline-add-hidden')) { - row.removeClass('datagrid-row-inline-add-hidden'); - } - - row.find('input:not([readonly]),textarea:not([readonly])').first().focus(); - } - } -}); - -datagridFilterMultiSelect = function() { - var select = $('.selectpicker').first(); - - if ($.fn.selectpicker) { - let defaults = $.fn.selectpicker.defaults = { - countSelectedText: select.data('i18n-selected'), - iconBase: '', - tickIcon: select.data('selected-icon-check') - }; - - $('.selectpicker') - .removeClass('form-select form-select-sm') - .addClass('form-control form-control-sm') - .selectpicker('destroy') - .selectpicker({ - iconBase: 'fa' - }); - - return defaults; - } -}; - -$(function() { - return datagridFilterMultiSelect(); -}); - -datagridGroupActionMultiSelect = function() { - var selects; - - if (!$.fn.selectpicker) { - return; - } - - selects = $('[data-datagrid-multiselect-id]'); - - return selects.each(function() { - var id; - if ($(this).hasClass('selectpicker')) { - $(this).removeAttr('id'); - id = $(this).data('datagrid-multiselect-id'); - $(this).on('loaded.bs.select', function(e) { - $(this).parent().attr('style', 'display:none;'); - return $(this).parent().find('.hidden').removeClass('hidden').addClass('btn-default btn-secondary'); - }); - return $(this).on('rendered.bs.select', function(e) { - return $(this).parent().attr('id', id); - }); - } - }); -}; - -$(function() { - return datagridGroupActionMultiSelect(); -}); - -dataGridRegisterExtension('datagrid.fitlerMultiSelect', { - success: function() { - datagridFilterMultiSelect(); - } -}); - -dataGridRegisterExtension('datagrid.groupActionMultiSelect', { - success: function() { - return datagridGroupActionMultiSelect(); - } -}); - -dataGridRegisterExtension('datagrid.inline-editing', { - success: function(payload) { - var grid; - if (payload._datagrid_inline_editing) { - grid = $('.datagrid-' + payload._datagrid_name); - return grid.find('.datagrid-inline-edit-trigger').addClass('hidden'); - } - } -}); - -dataGridRegisterExtension('datagrid.redraw-item', { - success: function(payload) { - var row; - if (payload._datagrid_redraw_item_class) { - row = $('tr[data-id="' + payload._datagrid_redraw_item_id + '"]'); - return row.attr('class', payload._datagrid_redraw_item_class); - } - } -}); - -dataGridRegisterExtension('datagrid.reset-filter-by-column', { - success: function(payload) { - var grid, href, i, key, len, ref; - if (!payload._datagrid_name) { - return; - } - grid = $('.datagrid-' + payload._datagrid_name); - grid.find('[data-datagrid-reset-filter-by-column]').addClass('hidden'); - if (payload.non_empty_filters && payload.non_empty_filters.length) { - ref = payload.non_empty_filters; - for (i = 0, len = ref.length; i < len; i++) { - key = ref[i]; - grid.find('[data-datagrid-reset-filter-by-column="' + key + '"]').removeClass('hidden'); - } - href = grid.find('.reset-filter').attr('href'); - return grid.find('[data-datagrid-reset-filter-by-column]').each(function() { - var new_href; - key = $(this).attr('data-datagrid-reset-filter-by-column'); - new_href = href.replace('do=' + payload._datagrid_name + '-resetFilter', 'do=' + payload._datagrid_name + '-resetColumnFilter'); - new_href += '&' + payload._datagrid_name + '-key=' + key; - return $(this).attr('href', new_href); - }); - } - } -}); diff --git a/assets/datagrid.ts b/assets/datagrid.ts new file mode 100644 index 000000000..de36fcaa0 --- /dev/null +++ b/assets/datagrid.ts @@ -0,0 +1,145 @@ +import { defaultDatagridNameResolver, isEnter } from "./utils"; +import type { Ajax, DatagridEventMap, DatagridOptions, EventDetail, EventListener, } from "./types"; + +export class Datagrid extends EventTarget { + private static readonly defaultOptions: DatagridOptions = { + confirm: confirm, + resolveDatagridName: defaultDatagridNameResolver, + plugins: [], + }; + + public readonly name: string; + + public readonly ajax: Ajax; + + private readonly options: DatagridOptions; + + constructor( + public readonly el: HTMLElement, + ajax: Ajax | ((grid: Datagrid) => Ajax), + options: Partial + ) { + super(); + + this.options = { + ...Datagrid.defaultOptions, + ...options, + }; + + const name = this.resolveDatagridName(); + + if (!name) { + throw new Error("Cannot resolve name of a datagrid!"); + } + + this.name = name; + + this.ajax = typeof ajax === "function" ? ajax(this) : ajax; + + this.ajax.addEventListener("success", e => { + if (e.detail.payload?._datagrid_name === this.name && e.detail.payload?._datagrid_init) { + this.init(); + } + }); + + this.init(); + } + + public init() { + let cancelled = !this.dispatch('beforeInit', {datagrid: this}) + if (!cancelled) { + this.options.plugins.forEach((plugin) => { + plugin.onDatagridInit?.(this) + }) + } + + // Uncheck toggle-all + const checkedRows = this.el.querySelectorAll("input[data-check]:checked"); + if (checkedRows.length === 1 && checkedRows[0].getAttribute("name") === "toggle-all") { + const input = checkedRows[0]; + if (input) { + input.checked = false; + } + } + + this.el.querySelectorAll("input[data-datagrid-manualsubmit]").forEach(inputEl => { + const form = inputEl.closest("form"); + if (!form) return; + + inputEl.addEventListener("keydown", e => { + if (!isEnter(e)) return; + + e.stopPropagation(); + e.preventDefault(); + return this.ajax.submitForm(form); + }); + }); + + this.ajax.addEventListener("success", ({detail: {payload}}) => { + // todo: maybe move? + if (payload._datagrid_name && payload._datagrid_name === this.name) { + this.el.querySelector("[data-datagrid-reset-filter-by-column]") + ?.classList.add("hidden"); + + if (payload.non_empty_filters && payload.non_empty_filters.length >= 1) { + const resets = Array.from(this.el.querySelectorAll( + `[data-datagrid-reset-filter-by-column]` + )); + + const getColumnName = (el: HTMLElement) => el.getAttribute( + "data-datagrid-reset-filter-by-column" + ) + + /// tf? + for (const columnName of payload.non_empty_filters) { + resets.find(getColumnName)?.classList.remove("hidden"); + } + + const href = this.el.querySelector(".reset-filter") + ?.getAttribute("href"); + + if (href) { + resets.forEach((el) => { + const columnName = getColumnName(el); + + const newHref = href.replace("-resetFilter", "-resetColumnFilter"); + el.setAttribute("href", `${newHref}&${this.name}-key=${columnName}`); + }) + } + } + } + }) + + this.dispatch('afterInit', {datagrid: this}); + } + + public confirm(message: string): boolean { + return this.options.confirm.bind(this)(message); + } + + public resolveDatagridName(): string | null { + return this.options.resolveDatagridName.bind(this)(this.el); + } + + dispatch< + K extends string, M extends DatagridEventMap = DatagridEventMap + >(type: K, detail: K extends keyof M ? EventDetail : any, options?: boolean): boolean { + return this.dispatchEvent(new CustomEvent(type, {detail})); + } + + declare addEventListener: ( + type: K, + listener: EventListener, + options?: boolean | AddEventListenerOptions + ) => void; + + declare removeEventListener: ( + type: K, + listener: EventListener, + options?: boolean | AddEventListenerOptions + ) => void; + + declare dispatchEvent: ( + event: K extends keyof M ? M[K] : CustomEvent + ) => boolean; +} diff --git a/assets/datagrids.ts b/assets/datagrids.ts new file mode 100644 index 000000000..4aa1ec3fc --- /dev/null +++ b/assets/datagrids.ts @@ -0,0 +1,80 @@ +import { Datagrid } from "./datagrid"; +import { + AutosubmitPlugin, + CheckboxPlugin, + ConfirmPlugin, + HappyPlugin, + InlinePlugin, + NetteFormsPlugin, + SelectpickerPlugin, + SortablePlugin +} from "./plugins"; +import { Ajax, DatagridsOptions } from "./types"; +import { SortableJS } from "./integrations/sortable-js"; +import { DatepickerPlugin } from "./plugins/integrations/datepicker"; +import { BootstrapSelect, Happy, VanillaDatepicker } from "./integrations"; + +export class Datagrids { + private datagrids: Datagrid[] = []; + + readonly options: DatagridsOptions; + + readonly root: HTMLElement; + + constructor(readonly ajax: Ajax, options: Partial = {}) { + this.options = { + selector: "div.datagrid[data-refresh-state]", + datagrid: {}, + root: document.body, + ...options, + }; + + const root = typeof this.options.root === "string" + ? document.querySelector(this.options.root) + : this.options.root; + + if (!root || !(root instanceof HTMLElement)) { + throw new Error("Root element not found or is not an HTMLElement"); + } + + this.root = root; + + this.init(); + } + + init() { + this.ajax.onInit(); + (this.options.datagrid?.plugins ?? []).forEach((plugin) => plugin.onInit?.(this)); + + this.initDatagrids(); + } + + initDatagrids() { + this.datagrids = Array.from(this.root.querySelectorAll(this.options.selector)).map( + datagrid => new Datagrid(datagrid, this.ajax, this.options.datagrid) + ); + } +} + +export const createDatagrids = (ajax: Ajax, _options: Partial = {}) => { + return new Datagrids(ajax, _options); +}; + +export const createFullDatagrids = (ajax: Ajax, _options: Partial = {}) => { + return createDatagrids(ajax, { + datagrid: { + plugins: [ + new AutosubmitPlugin(), + new CheckboxPlugin(), + new ConfirmPlugin(), + new InlinePlugin(), + new NetteFormsPlugin(), + new HappyPlugin(new Happy()), + new SortablePlugin(new SortableJS()), + new DatepickerPlugin(new VanillaDatepicker()), + new SelectpickerPlugin(new BootstrapSelect()) + ], + }, + ..._options, + }) +}; diff --git a/assets/index.ts b/assets/index.ts new file mode 100644 index 000000000..39d8538bf --- /dev/null +++ b/assets/index.ts @@ -0,0 +1,6 @@ +export * from "./datagrid"; +export * from "./plugins"; +export * from "./integrations"; + +export * from "./datagrids" +export * from "./datagrid"; diff --git a/assets/integrations/bootstrap-select.ts b/assets/integrations/bootstrap-select.ts new file mode 100644 index 000000000..d551de1aa --- /dev/null +++ b/assets/integrations/bootstrap-select.ts @@ -0,0 +1,43 @@ +import { Selectpicker } from "../types"; +import { window } from "../utils"; + +export class BootstrapSelect implements Selectpicker { + initSelectpickers(elements: HTMLElement[]): void { + if (window().jQuery) { + const $ = window().jQuery; + if ($?.fn.selectpicker) { + $.fn.selectpicker.defaults = { + countSelectedText: elements[0].getAttribute("i18n-selected") ?? "", + iconBase: "fa", + tickIcon: elements[0].getAttribute("selected-icon-check") ?? "fa fa-check", + }; + + elements.forEach(element => + $(element) + .removeClass("form-select form-select-sm") + .addClass("form-control form-control-sm") + .selectpicker("destroy") + .selectpicker({}) + ); + + Array.from(elements) + .filter(element => element.hasAttribute("data-datagrid-multiselect-id")) + .forEach(element => { + const $picker = $(element); + const $parent = $picker.parent(); + + $picker.removeAttr("id"); + const id = element.getAttribute("data-datagrid-multiselect-id"); + + $picker.on("loaded.bs.select", () => { + $parent.attr("style", "display: none;"); + $parent.find(".hidden").removeClass("hidden").addClass("btn-default btn-secondary"); + }); + + $picker.on("rendered.bs.select", () => $parent.attr("id", id)); + }); + } + } + } + +} diff --git a/assets/integrations/happy.ts b/assets/integrations/happy.ts new file mode 100644 index 000000000..fb09196b7 --- /dev/null +++ b/assets/integrations/happy.ts @@ -0,0 +1,222 @@ +import { happyStyles } from "../css/happy.css"; + +/** + * Slightly cleaned up & typed version of happy-inputs by paveljanda. + */ +export class Happy { + private colors: string[] = ["primary", "success", "info", "warning", "danger", "white", "gray"]; + + private templates = { + radio: '
', + checkbox: + '
', + text: "", + textarea: "", + }; + + init() { + if (!document.querySelector('[data-happy-stylesheet]')) { + document.head.append(``) + } + this.removeBySelector(".happy-radio"); + this.removeBySelector(".happy-checkbox"); + + this.initRadio(); + this.initCheckbox(); + } + + /** + * @deprecated + */ + reset() { + this.init(); + } + + addColorToInput(input: HTMLElement, happyInput: HTMLElement, classString: string) { + if (input.classList.contains(classString)) { + happyInput.classList.add(classString); + } + + classString = `${classString}-border`; + + if (input.classList.contains(classString)) { + happyInput.classList.add(classString); + } + } + + // i... you know what, no, let "thinkess" be "thinkess" + addThinkessToInput(input: HTMLElement, happyInput: HTMLElement) { + if (input.classList.contains("thin")) { + happyInput.classList.add("thin"); + } + } + + setNames(input: HTMLElement, happyInput: HTMLElement) { + happyInput.setAttribute("data-name", input.getAttribute("name") ?? ""); + + var value = input.getAttribute("value"); + + if (value !== "undefined" && value !== null) { + happyInput.setAttribute("data-value", input.getAttribute("value") ?? ""); + } + } + + removeBySelector(selector: string) { + document.querySelectorAll(selector).forEach(el => el.parentNode?.removeChild(el)); + } + + initRadio() { + document.querySelectorAll("input[type=radio].happy").forEach(input => { + /** + * Paste happy component into html + */ + input.insertAdjacentHTML("afterend", this.templates.radio); + + const happyInput = input.nextElementSibling; + + if (happyInput instanceof HTMLElement) { + /** + * Add optional colors + */ + this.colors.forEach(color => { + this.addColorToInput(input, happyInput, color); + this.setNames(input, happyInput); + }); + + this.addThinkessToInput(input, happyInput); + } + + /** + * Init state + */ + this.checkRadioState(input); + + /** + * Set aciton functionality for native change + */ + document.addEventListener("change", this.radioOnChange.bind(this)); + }); + } + + initCheckbox() { + document.querySelectorAll("input[type=checkbox].happy").forEach(input => { + /** + * Paste happy component into html + */ + input.insertAdjacentHTML("afterend", this.templates.checkbox); + + const happyInput = input.nextElementSibling; + + /** + * Add optional colors + */ + if (happyInput instanceof HTMLElement) { + this.colors.forEach(color => { + this.addColorToInput(input, happyInput, color); + this.setNames(input, happyInput); + }); + + this.addThinkessToInput(input, happyInput); + } + + /** + * Init state + */ + this.checkCheckboxState(input); + + /** + * Set action functionality for click || native change + */ + document.addEventListener("click", this.checkCheckboxStateOnClick.bind(this)); + document.addEventListener("change", this.checkCheckboxStateOnChange.bind(this)); + }); + } + + checkCheckboxStateOnClick(event: Event) { + const target = event.target; + + // When target is SVGSVGElement (), return parentNode, + // When target is a SVGGraphicsElement (,...), find and return it's parent node + // otherwise return target itself. + const happyInput = + target instanceof SVGSVGElement + ? target.parentNode + : target instanceof SVGGraphicsElement + ? target.closest("svg")?.parentNode + : target; + + if (!(happyInput instanceof HTMLElement) || !happyInput.classList.contains("happy-checkbox")) { + return; + } + + event.preventDefault(); + + const name = happyInput.getAttribute("data-name"); + const value = happyInput.getAttribute("data-value"); + + const input = document.querySelector( + `.happy-checkbox[data-name="${name}"]` + (!!value ? `[value="${value}"]` : "") + ); + if (!(input instanceof HTMLInputElement)) return; + + const checked = happyInput.classList.contains("active"); + + input.checked = !checked; + checked ? happyInput.classList.remove("active") : happyInput.classList.add("active"); + } + + checkCheckboxStateOnChange({target}: Event) { + if (!(target instanceof HTMLInputElement)) return; + + if (target.classList.contains("happy")) { + this.checkCheckboxState(target); + } + } + + checkRadioState(input: HTMLInputElement) { + if (!input.checked || !input.hasAttribute("name")) return; + + const name = input.getAttribute("name"); + const value = input.getAttribute("value"); + + const element = document.querySelector( + `.happy-checkbox[data-name="${name}"]` + (!!value ? `[data-value="${value}"]` : "") + ); + + if (element) { + element.classList.add("active"); + } + } + + checkCheckboxState(input: HTMLInputElement) { + const name = input.getAttribute("name"); + if (!name) return; + + const value = input.getAttribute("value"); + const element = document.querySelector( + `.happy-checkbox[data-name="${name}"]` + (!!value ? `[data-value="${value}"]` : "") + ); + + if (!element) return; + + input.checked ? element.classList.add("active") : element.classList.remove("active"); + } + + radioOnChange({target}: Event) { + // Check whether target is
${snippet}
`; + + childrenBlock.innerHTML = template; + } + //children_block.addClass('loaded'); + //children_block.slideToggle('fast'); + } + } + }) + return true; + } +} diff --git a/assets/plugins/index.ts b/assets/plugins/index.ts new file mode 100644 index 000000000..99b83aed8 --- /dev/null +++ b/assets/plugins/index.ts @@ -0,0 +1,12 @@ +export * from "./integrations/datepicker"; +export * from "./integrations/happy"; +export * from "./integrations/nette-forms" +export * from "./integrations/selectpicker"; +export * from "./integrations/sortable"; + +export * from "./features/autosubmit"; +export * from "./features/checkboxes"; +export * from "./features/confirm"; +export * from "./features/editable"; +export * from "./features/inline"; +export * from "./features/item-detail"; diff --git a/assets/plugins/integrations/datepicker.ts b/assets/plugins/integrations/datepicker.ts new file mode 100644 index 000000000..5ba14ac32 --- /dev/null +++ b/assets/plugins/integrations/datepicker.ts @@ -0,0 +1,17 @@ +import { Datagrid } from "../.."; +import { DatagridPlugin, Datepicker } from "../../types"; + +export class DatepickerPlugin implements DatagridPlugin { + constructor(private datepicker: Datepicker) { + } + + onDatagridInit(datagrid: Datagrid): boolean { + const elements = datagrid.el.querySelectorAll("input[data-provide='datepicker']"); + + if (elements.length >= 1) { + this.datepicker.initDatepickers(Array.from(elements), datagrid); + } + + return true; + } +} diff --git a/assets/plugins/integrations/happy.ts b/assets/plugins/integrations/happy.ts new file mode 100644 index 000000000..e7596a8c3 --- /dev/null +++ b/assets/plugins/integrations/happy.ts @@ -0,0 +1,19 @@ +import { Datagrid } from "../.."; +import { DatagridPlugin } from "../../types"; +import { window } from "../../utils"; +import type { Happy } from "../../integrations"; + +export class HappyPlugin implements DatagridPlugin { + constructor(private happy?: Happy) { + } + + onDatagridInit(datagrid: Datagrid): boolean { + const happy = this.happy ?? window().happy ?? null; + + if (happy) { + happy.init(); + } + + return true; + } +} diff --git a/assets/plugins/integrations/nette-forms.ts b/assets/plugins/integrations/nette-forms.ts new file mode 100644 index 000000000..5c0748665 --- /dev/null +++ b/assets/plugins/integrations/nette-forms.ts @@ -0,0 +1,18 @@ +import { DatagridPlugin, Nette } from "../../types"; +import { Datagrid } from "../.."; +import { window } from "../../utils"; + +export class NetteFormsPlugin implements DatagridPlugin { + constructor(private nette?: Nette) { + } + + onDatagridInit(datagrid: Datagrid): boolean { + const nette = this.nette ?? window().Nette ?? null; + + if (nette) { + datagrid.el.querySelectorAll("form").forEach(form => nette.initForm(form)); + } + + return true; + } +} diff --git a/assets/plugins/integrations/selectpicker.ts b/assets/plugins/integrations/selectpicker.ts new file mode 100644 index 000000000..a95e2af61 --- /dev/null +++ b/assets/plugins/integrations/selectpicker.ts @@ -0,0 +1,17 @@ +import { DatagridPlugin, Selectpicker } from "../../types"; +import { Datagrid } from "../.."; + +export class SelectpickerPlugin implements DatagridPlugin { + constructor(private selectpicker: Selectpicker) { + } + + onDatagridInit(datagrid: Datagrid): boolean { + const elements = datagrid.el.querySelectorAll(".selectpicker"); + + if (elements.length >= 1) { + this.selectpicker.initSelectpickers(Array.from(elements), datagrid); + } + + return true; + } +} diff --git a/assets/plugins/integrations/sortable.ts b/assets/plugins/integrations/sortable.ts new file mode 100644 index 000000000..4a787cf70 --- /dev/null +++ b/assets/plugins/integrations/sortable.ts @@ -0,0 +1,61 @@ +import { Datagrid } from "../../datagrid"; +import { DatagridPlugin, Sortable } from "../../types"; + +export class SortablePlugin implements DatagridPlugin { + constructor(private sortable: Sortable) { + } + + onDatagridInit(datagrid: Datagrid): boolean { + datagrid.ajax.addEventListener('before', (event) => { + // TODO old ln 694... wtf? + }) + + this.sortable.initSortable(datagrid); + + datagrid.ajax.addEventListener('success', ({detail: {payload}}) => { + if (payload._datagrid_sort) { + for (const key in payload._datagrid_sort) { + const href = payload._datagrid_sort[key]; + const element = datagrid.el.querySelector(`#datagrid-sort-${key}`); + + if (element) { + // TODO: Only for BC support, to be removed + element.setAttribute("href", href); + + element.setAttribute("data-href", href); + } + } + this.sortable.initSortable(datagrid); + } + + if (payload._datagrid_tree) { + const childrenContainer = datagrid.el.querySelector( + `.datagrid-tree-item[data-id='${payload._datagrid_tree}'] .datagrid-tree-item-children` + ); + if (childrenContainer && payload.snippets) { + childrenContainer.classList.add("loaded"); + for (const key in payload.snippets) { + const snippet = payload.snippets[key]; + + const doc = new DOMParser().parseFromString(snippet, 'text/html'); + const element = doc.firstElementChild; + if (element) { + const treeItem = document.createElement("div"); + treeItem.id = key; + treeItem.classList.add("datagrid-tree-item") + treeItem.setAttribute("data-id", key); + if (element.hasAttribute("has-children")) { + treeItem.classList.add("has-children"); + } + + childrenContainer.append(treeItem); + // attachSlideToggle(childrenContainer); + } + } + } + this.sortable.initSortableTree(datagrid); + } + }) + return true; + } +} diff --git a/assets/types/ajax.d.ts b/assets/types/ajax.d.ts new file mode 100644 index 000000000..c4ae89639 --- /dev/null +++ b/assets/types/ajax.d.ts @@ -0,0 +1,136 @@ +import { EventDetail, EventListener, EventMap } from "."; +import { Datagrid } from ".."; + +export interface BaseRequestParams { + method: "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE" | "PATCH" | string; + url: string; +} + +export interface RequestParams extends BaseRequestParams { + data: D; +} + +export interface DatagridPayload { + _datagrid_name?: string; + _datagrid_toggle_detail?: string + _datagrid_inline_editing?: boolean; + _datagrid_inline_adding?: boolean; + _datagrid_inline_edited?: boolean; + _datagrid_inline_edit_cancel?: boolean; + _datagrid_url?: boolean; + _datagrid_sort?: Record; + _datagrid_tree?: string; + _datagrid_editable_new_value?: string; + _datagrid_redraw_item_id?: string; + _datagrid_redraw_item_class?: string; + _datagrid_init?: boolean; + non_empty_filters?: string[]; +} + +export interface DatagridState { + "grid-page": number | null, + "grid-perPage": number, + // TODO + "grid-sort": any | null, + "grid-filter": any | null +} + +export type Payload

= P & { + snippets?: Record; + redirect?: string; + state: S; +}; + +export interface Response { + headers: Record | Headers; + status: number; +} + +export interface BeforeEventDetail { + params: RequestParams; +} + +export interface InteractEventDetail { + element: E; +} + +export interface SuccessEventDetail

{ + params: BaseRequestParams; + payload: Payload

; + response: Response; +} + +export interface ErrorEventDetail { + params: BaseRequestParams; + response?: Response; + error?: E; +} + +export interface AjaxEventMap extends EventMap { + before: CustomEvent; + interact: CustomEvent; + snippetUpdate: CustomEvent; + success: CustomEvent; + error: CustomEvent; +} + +export interface Ajax extends EventTarget { + client: C; + + /** + * Initialization of the Ajax instance, called in createDatagrids(). + * @return this + */ + onInit(): this; + + /** + * Initializes a Datagrid instance. + * @param grid The Datagrid instance + */ + onDatagridInit?(grid: G): void; + + /** + * Sends a request to the server. + */ + request(args: RequestParams): Promise

; + + /** + * Submits a form + */ + submitForm(element: E): Promise

; + + /** + * Shortcut for dispatchEvent + * @internal + */ + dispatch( + type: K, + detail: K extends keyof M ? EventDetail : any, + options?: boolean + ): boolean; + + /** + * Note: For events dispatched directly from the underlying client, {@see Ajax.client}} + **/ + addEventListener( + type: K, + listener: EventListener, + options?: boolean | AddEventListenerOptions + ): void; + + /** + * Note: For events dispatched directly from the underlying client, {@see Ajax.client}} + **/ + removeEventListener( + type: K, + listener: EventListener, + options?: boolean | AddEventListenerOptions + ): void; + + /** + * @internal + */ + dispatchEvent( + event: K extends keyof M ? M[K] : CustomEvent + ): boolean; +} diff --git a/assets/types/datagrid.d.ts b/assets/types/datagrid.d.ts new file mode 100644 index 000000000..d62468ce5 --- /dev/null +++ b/assets/types/datagrid.d.ts @@ -0,0 +1,31 @@ +import { Datagrid, Datagrids } from ".."; +import { EventMap } from "."; + +export interface DatagridEventDetail { + datagrid: Datagrid; +} + +export interface DatagridEventMap extends EventMap { + beforeInit: CustomEvent; + afterInit: CustomEvent; +} + +export interface DatagridPlugin { + onInit?(datagrids: Datagrids): void; + + onDatagridInit?(datagrid: Datagrid): boolean; +} + +export interface DatagridOptions { + confirm(this: Datagrid, message: string): boolean; + + // Returning null will skip this datagrid + resolveDatagridName: (this: Datagrid, datagrid: HTMLElement) => string | null; + plugins: DatagridPlugin[]; +} + +export interface DatagridsOptions { + datagrid: Partial; + selector: string; + root: HTMLElement | string; +} diff --git a/assets/types/index.d.ts b/assets/types/index.d.ts new file mode 100644 index 000000000..4cd5750d7 --- /dev/null +++ b/assets/types/index.d.ts @@ -0,0 +1,41 @@ +import { Happy } from "../integrations"; +import TomSelect from "tom-select"; + +export interface Nette { + initForm: (form: HTMLFormElement) => void; +} + +export type Constructor = new (...args: any[]) => T; + +export type KeysOf = { [P in keyof T]: TVal; } + +export interface ExtendedWindow extends Window { + jQuery?: any; + Nette?: Nette; + TomSelect?: Constructor; + happy?: Happy; +} + +// https://github.com/naja-js/naja/blob/384d298a9199bf778985d1bcf5747fe8de305b22/src/utils.ts +type EventListenerFunction = ( + this: ET, + event: E +) => boolean | void | Promise; + +interface EventListenerObject { + handleEvent(event: E): void | Promise; +} + +export type EventListener = + | EventListenerFunction + | EventListenerObject + | null; + +export type EventDetail = E extends CustomEvent ? D : never; + +export interface EventMap extends Record { +} + +export * from "./datagrid"; +export * from "./integrations"; +export * from "./ajax"; diff --git a/assets/types/integrations.d.ts b/assets/types/integrations.d.ts new file mode 100644 index 000000000..15ddbcc57 --- /dev/null +++ b/assets/types/integrations.d.ts @@ -0,0 +1,15 @@ +import { Datagrid } from ".."; + +export interface Sortable { + initSortable(datagrid: Datagrid): void; + + initSortableTree(datagrid: Datagrid): void; +} + +export interface Selectpicker { + initSelectpickers(elements: HTMLElement[], datagrid: Datagrid): void; +} + +export interface Datepicker { + initDatepickers(elements: HTMLInputElement[], datagrid: Datagrid): void; +} diff --git a/assets/utils.ts b/assets/utils.ts new file mode 100644 index 000000000..c23ec2345 --- /dev/null +++ b/assets/utils.ts @@ -0,0 +1,179 @@ +import { Datagrid } from "./datagrid"; +import { ExtendedWindow } from "./types"; + +export function isPromise(p: any): p is Promise { + return typeof p === "object" && typeof p.then === "function"; +} + +export function isInKeyRange(e: KeyboardEvent, min: number, max: number): boolean { + const code = e.key.length === 1 ? e.key.charCodeAt(0) : 0; + return code >= min && code <= max; +} + +export function isEnter(e: KeyboardEvent): boolean { + return e.key === "Enter"; +} + +export function isEsc(e: KeyboardEvent): boolean { + return e.key === "Escape"; +} + +export function isFunctionKey(e: KeyboardEvent): boolean { + return e.key.length === 2 && e.key.startsWith("F"); +} + +export function window(): ExtendedWindow { + return (window ?? {}) as unknown as ExtendedWindow; +} + +export function slideDown(element: HTMLElement, cb?: (nextStateShown: boolean) => unknown) { + element.style.height = 'auto'; + + let height = element.clientHeight + "px"; + + element.style.height = '0px'; + + setTimeout(function () { + element.style.height = height; + cb?.(true); + }, 0); +} + +export function slideUp(element: HTMLElement, cb?: (nextStateShown: boolean) => unknown) { + element.style.height = '0px'; + + setTimeout(() => { + cb?.(false); + }, 250); // TODO +} + +export function slideToggle(element: HTMLElement, isVisible: boolean, cb?: (nextStateShown: boolean) => unknown) { + if (!isVisible) { + slideDown(element, cb); + } else { + slideUp(element, cb); + } +} + +export function attachSlideToggle(element: HTMLElement, control: HTMLElement, cb?: (nextStateShown: boolean) => unknown) { + if (!control.classList.contains("datagrid--slide-toggle")) { + let sliding = false; + control.classList.add("datagrid--slide-toggle"); + + slideDown(element, cb); + + control.addEventListener('click', () => { + if (sliding) return; + sliding = true; + slideToggle(element, control.classList.contains('is-active'), (active) => { + sliding = false + if (active) { + control.classList.add("is-active"); + } else { + control.classList.remove("is-active"); + } + }); + }); + } +} + +export function qs(params: Record, prefix: string = ""): string { + const encodedParams = []; + + for (const _key in params) { + const value = params[_key]; + // Cannot do !value as that would also exclude valid negative values such as 0 or false + if (value === null || value === undefined) continue; + + const key = prefix ? `${prefix}[${_key}]` : _key; + + // Skip empty strings + if (typeof value === "string" && value.trim().length < 1) continue; + + if (typeof value === "object") { + const nestedParams = qs(value, key); + // Don't include if object is empty + if (nestedParams.length >= 1) { + encodedParams.push(nestedParams); + } + + continue; + } + + encodedParams.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + } + + return encodedParams.join("&").replace(/&+$/gm, "").replace(/&*$/, ""); +} + +export function calculateCellLines(el: HTMLElement) { + const cellPadding = el.style.padding ? parseInt(el.style.padding.replace(/[^-\d\.]/g, ""), 10) : 0; + const cellHeight = el.getBoundingClientRect().height; + const lineHeight = Math.round(parseFloat(el.style.lineHeight ?? "0")); + const cellLines = Math.round((cellHeight - 2 * cellPadding) / lineHeight); + + return cellLines; +} + +// A little better debounce ;) +export function debounce unknown | Promise>( + fn: TFun, + slowdown: number = 200 +): (...args: TArgs[]) => void { + let timeout: number | null = null; + let blockedByPromise: boolean = false; + + return (...args) => { + if (blockedByPromise) return; + + timeout && clearTimeout(timeout); + timeout = setTimeout(() => { + const result = fn(...args); + + if (isPromise(result)) { + blockedByPromise = true; + result.finally(() => { + blockedByPromise = false; + }); + } + }, slowdown); + }; +} + +export function defaultDatagridNameResolver(this: Datagrid, datagrid: HTMLElement) { + // This attribute is not present by default, though if you're going to use this library + // it's recommended to add it, because when not present, the fallback way is to parse the datagrid- class, + // which is definitely far from reliable. Alternatively (mainly in case of a custom datagrid class), + // you can pass your own resolveDatagridName function to the option. + const attrName = datagrid.getAttribute("data-datagrid-name"); + if (attrName) return attrName; + + console.warn( + "Deprecated name resolution for datagrid", + datagrid, + ": Please add a data-datagrid-name attribute instead!\n" + + "Currently, the Datagrid library relies on matching the name from the 'datagrid-[name]' class, which is unreliable " + + "and may cause bugs if the default class names are not used (eg. if you add a datagrid-xx class, or change the name class completely!)\n" + + "Alternatively, you can customize the name resolution with the `resolveDatagridName` option. See TBD for more info." // TODO + ); + + const classes = datagrid.classList.value.split(" "); + + // Returns the first datagrid-XXX match + for (const className of classes) { + if (!className.startsWith("datagrid-")) continue; + + const [, ...split] = className.split("-"); + const name = split.join("-"); + + // In case nothing actually follows the prefix (className = "datagrid-") + if (name.length < 1) { + console.error(`Failed to resolve datagrid name - ambigious class name '${className}'`); + return null; + } + + return name; + } + + return null; +} diff --git a/package.json b/package.json index ae8876f5f..a1ecb2261 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,39 @@ { - "name": "ublaboo-datagrid", - "version": "6.9.1", - "description": "Assets for php composer package ublaboo/datagrid", - "keywords": [ - "ublaboo", - "contributte", - "datagrid" - ], - "author": "Pavel Janda ", - "license": "MIT", - "main": "assets/datagrid.js", - "files": [ - "assets/*" - ], - "scripts": { - "release-dry": "npm publish --dry-run", - "release": "npm publish" - } + "name": "@contributte/datagrid", + "version": "6.9.1", + "description": "Assets for contributte/datagrid", + "keywords": [ + "contributte", + "datagrid", + "tables" + ], + "license": "MIT", + "main": "assets/datagrid.js", + "files": [ + "assets/*" + ], + "scripts": { + "release-dry": "npm publish --dry-run", + "release": "npm publish" + }, + "dependencies": { + "naja": "^2.5.0" + }, + "devDependencies": { + "@fortawesome/fontawesome-free": "^6.3.0", + "bootstrap": "^5.3.0-alpha3", + "nette-forms": "^3.3.1", + "prismjs": "^1.29.0", + "sortablejs": "^1.15.0", + "tom-select": "^2.2.2", + "vanillajs-datepicker": "^1.3.1", + "@types/bootstrap-select": "^1.13.4", + "@types/jquery": "^3.5.16", + "@types/jqueryui": "^1.12.16", + "@types/sortablejs": "^1.15.1", + "@types/vanillajs-datepicker": "^1.2.1", + "autoprefixer": "^10.4.0", + "typescript": "^4.9.5", + "vite": "^2.6.10" + } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..151801a9a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "ESNext", + "moduleResolution": "Node", + "lib": [ + "ESNext", + "ESNext.AsyncIterable", + "DOM" + ], + "esModuleInterop": true, + "allowJs": true, + "sourceMap": true, + "strict": true, + "noEmit": true, + "experimentalDecorators": true, + "baseUrl": ".", + "paths": { + "~/*": [ + "./*" + ], + "@/*": [ + "./*" + ], + "@datagrid": ["./assets/datagrid/index.ts"], + "@datagrid/*": ["./assets/datagrid/*"], + }, + "types": [ + "@types/node" + ] + } +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 000000000..41dd31e11 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,45 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +export default defineConfig(({ mode }) => { + const DEV = mode === 'development'; + + return { + publicDir: './styles/public', + resolve: { + alias: { + '@': resolve(__dirname, 'styles/js'), + '~': resolve(__dirname, 'node_modules'), + '@datagrid': resolve(__dirname, 'styles/datagrid'), + }, + }, + base: '/dist/', + server: { + open: false, + hmr: false, + }, + css: { + postcss: [ + "autoprefixer" + ] + }, + build: { + manifest: true, + assetsDir: '', + outDir: './www/dist/', + emptyOutDir: true, + minify: DEV ? false : 'esbuild', + rollupOptions: { + output: { + manualChunks: undefined, + chunkFileNames: '[name].js', // DEV ? '[name].js' : '[name]-[hash].js', + entryFileNames: '[name].js', // DEV ? '[name].js' : '[name].[hash].js', + assetFileNames: '[name].[ext]', // DEV ? '[name].[ext]' : '[name].[hash].[ext]', + }, + input: { + app: './styles/js/main.js' + } + } + }, + } +});