@peckadesign/pd-naja 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +14 -0
- package/.eslintrc.js +12 -0
- package/.github/workflows/ci.yml +29 -0
- package/.github/workflows/publish.yml +80 -0
- package/.prettierrc +6 -0
- package/README.md +90 -0
- package/package.json +51 -0
- package/rollup.config.js +61 -0
- package/src/extensions/AjaxModalExtension.ts +375 -0
- package/src/extensions/SpinnerExtension.ts +132 -0
- package/src/index.esm.ts +6 -0
- package/src/utils/Control.ts +18 -0
- package/src/utils/ControlManager.ts +42 -0
- package/tsconfig.json +20 -0
package/.editorconfig
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# editorconfig.org
|
|
2
|
+
root = true
|
|
3
|
+
|
|
4
|
+
[*]
|
|
5
|
+
indent_style = tab
|
|
6
|
+
tab_width = unset
|
|
7
|
+
indent_size = tab
|
|
8
|
+
end_of_line = lf
|
|
9
|
+
charset = utf-8
|
|
10
|
+
trim_trailing_whitespace = true
|
|
11
|
+
insert_final_newline = true
|
|
12
|
+
|
|
13
|
+
[*.md]
|
|
14
|
+
trim_trailing_whitespace = false
|
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require('@rushstack/eslint-patch/modern-module-resolution')
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
root: true,
|
|
5
|
+
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'prettier'],
|
|
6
|
+
rules: {
|
|
7
|
+
'@typescript-eslint/ban-ts-comment': 'off',
|
|
8
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
9
|
+
'prettier/prettier': 1
|
|
10
|
+
},
|
|
11
|
+
ignorePatterns: ['*.js']
|
|
12
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
types: [opened, synchronize]
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
lint:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
name: ESLint
|
|
10
|
+
env:
|
|
11
|
+
CI: true
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout 🛎
|
|
14
|
+
uses: actions/checkout@v3
|
|
15
|
+
|
|
16
|
+
- name: Setup Node 📦
|
|
17
|
+
uses: actions/setup-node@v3
|
|
18
|
+
with:
|
|
19
|
+
node-version: lts/*
|
|
20
|
+
cache: npm
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies 👨🏻💻
|
|
23
|
+
run: npm ci
|
|
24
|
+
|
|
25
|
+
- name: Lint ✨
|
|
26
|
+
run: npm run lint
|
|
27
|
+
|
|
28
|
+
- name: Build 🔨
|
|
29
|
+
run: npm run build
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags:
|
|
5
|
+
- '*'
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: Build
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout 🛎
|
|
13
|
+
uses: actions/checkout@v3
|
|
14
|
+
|
|
15
|
+
- name: Setup Node 📦
|
|
16
|
+
uses: actions/setup-node@v3
|
|
17
|
+
with:
|
|
18
|
+
node-version: lts/*
|
|
19
|
+
cache: npm
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies 👨🏻💻
|
|
22
|
+
run: npm ci
|
|
23
|
+
|
|
24
|
+
- name: Build 🔨
|
|
25
|
+
run: npm run build
|
|
26
|
+
|
|
27
|
+
- name: Download artifacts 🧩
|
|
28
|
+
uses: actions/upload-artifact@v3
|
|
29
|
+
with:
|
|
30
|
+
name: dist-files
|
|
31
|
+
path: dist/
|
|
32
|
+
|
|
33
|
+
release:
|
|
34
|
+
name: Release
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
needs: [build]
|
|
37
|
+
steps:
|
|
38
|
+
- name: Checkout 🛎
|
|
39
|
+
uses: actions/checkout@v3
|
|
40
|
+
|
|
41
|
+
- name: Download artifacts 🧩
|
|
42
|
+
uses: actions/download-artifact@v3
|
|
43
|
+
with:
|
|
44
|
+
name: dist-files
|
|
45
|
+
path: dist/
|
|
46
|
+
|
|
47
|
+
- name: Create release draft 🕊️
|
|
48
|
+
uses: softprops/action-gh-release@v1
|
|
49
|
+
with:
|
|
50
|
+
draft: true
|
|
51
|
+
files: |
|
|
52
|
+
dist/PdModal.js
|
|
53
|
+
dist/PdModal.js.map
|
|
54
|
+
dist/PdModal.min.js
|
|
55
|
+
dist/PdModal.min.js.map
|
|
56
|
+
|
|
57
|
+
publish:
|
|
58
|
+
name: Publish
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
needs: [build]
|
|
61
|
+
steps:
|
|
62
|
+
- name: Checkout 🛎
|
|
63
|
+
uses: actions/checkout@v3
|
|
64
|
+
|
|
65
|
+
- name: Download artifacts 🧩
|
|
66
|
+
uses: actions/download-artifact@v3
|
|
67
|
+
with:
|
|
68
|
+
name: dist-files
|
|
69
|
+
path: dist/
|
|
70
|
+
|
|
71
|
+
- name: Setup Node 📦
|
|
72
|
+
uses: actions/setup-node@v3
|
|
73
|
+
with:
|
|
74
|
+
node-version: lts/*
|
|
75
|
+
registry-url: 'https://registry.npmjs.org'
|
|
76
|
+
|
|
77
|
+
- name: Publish release 🕊️
|
|
78
|
+
run: npm publish --access public
|
|
79
|
+
env:
|
|
80
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
|
package/.prettierrc
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# pd-naja
|
|
2
|
+
|
|
3
|
+
1. [Quick start](#quick-start)
|
|
4
|
+
2. [Utilities](#utilities)
|
|
5
|
+
3. [Extensions](#extensions)
|
|
6
|
+
1. [AjaxModalExtension](#ajaxmodalextension)
|
|
7
|
+
2. [SpinnerExtension](#spinnerextension)
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
```
|
|
11
|
+
$ npm install naja @peckadesign/pd-naja
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import naja from 'naja'
|
|
16
|
+
import { ExtensionName } from '@peckadesign/pd-naja'
|
|
17
|
+
|
|
18
|
+
naja.registerExtension(new ExtensionName())
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { controlManager } from '@peckadesign/pd-naja'
|
|
23
|
+
import SomeControl from '@/js/Controls/SomeControl' // `SomeControl` must implement `Control` interface
|
|
24
|
+
import SomeAnotherControl from '@/js/Controls/SomeControl'
|
|
25
|
+
|
|
26
|
+
// This control is only initialized on page load
|
|
27
|
+
controlManager.addControlOnLoad(SomeControl)
|
|
28
|
+
|
|
29
|
+
// This control will also get initialized after Naja requests
|
|
30
|
+
controlManager.addControlOnLive(SomeControl)
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Utilities
|
|
35
|
+
This package provides easy ways to add JS components reactive to Naja ajax events.
|
|
36
|
+
|
|
37
|
+
`Control` is meant to be used for standalone components, that might be dependent on ajax. It should be used together with `ControlManager`. Class implementing the `Control` interface **should export its instance**. Then the intended lifecycle of class is as follows:
|
|
38
|
+
|
|
39
|
+
1. `constructor` is called immediately and is also called only once. This is the ideal place for e.g. adding common event handlers to the `body`, or modifying necessary DOM properties on elements not affected by ajax.
|
|
40
|
+
|
|
41
|
+
2. The instantiated class is then added to ControlManager either by `addControlOnLoad` or `addControlOnLive`. This ensures, that on `DOMContentLoaded` the `initialize` function of class is called. This method should implement initialization of the control dependent on fully loaded DOM. The `context` argument is equal to `document` in this call.
|
|
42
|
+
|
|
43
|
+
3. In case of `addControlOnLive` used, the `initialize` method is also called for each success ajax request. The `context` argument is equal to modified nette snippet.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## Extensions
|
|
47
|
+
|
|
48
|
+
### `AjaxModalExtension`
|
|
49
|
+
This extension allows you to implement modal window with browser history using Naja. Extension itself is agnostic of used modal, you can use any modal plugin you want as long as you provide simple adapter implementing `AjaxModal` interface. Class implementing `AjaxModal` is only parameter recieved by constructor.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import naja from 'naja'
|
|
53
|
+
import { AjaxModalExtension } from '@peckadesign/pd-naja'
|
|
54
|
+
import { modal } from '@/js/App/PdModal' // modal must implement AjaxModal interface
|
|
55
|
+
|
|
56
|
+
naja.registerExtension(
|
|
57
|
+
new AjaxModalExtension(modal)
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### Interface `AjaxModal`
|
|
62
|
+
| Property | Description |
|
|
63
|
+
|-----------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
64
|
+
| `element: Element` | Most outer element of the modal window. |
|
|
65
|
+
| `reservedSnippetIds: string[]` | List of snippet id's, that are neccessary for modal to work, e.g. `snippet--modal`. |
|
|
66
|
+
| `show(opener: Element \| undefined, options: any, event: BeforeEvent \| PopStateEvent): void` | Method that is called for opening the modal. `opener` is the element causing the opening, event is the associated event. |
|
|
67
|
+
| `hide(event: SuccessEvent \| PopStateEvent): void` | Method that is called for closing the modal either as a result of `closeModal` property in ajax response or as a result of navigating using browser history. |
|
|
68
|
+
| `isShown(): boolean` | Should return wheter the modal is opened or not. |
|
|
69
|
+
| `onShow: (callback: EventListener) => void`<br/>`onHide: (callback: EventListener) => void`<br/>`onHidden: (callback: EventListener) => void` | The extension needs to attach some handlers when show, hide or hidden happens. These functions should provide a way to add listeners to such events. |
|
|
70
|
+
| `dispatchLoad?: (options: any, event: SuccessEvent \| PopStateEvent) => void` | If you provide this method, extension will call it whenever the content is changed (loaded). |
|
|
71
|
+
| `getOptions(opener: Element): any` | If you need to change some modal settings based on opener element, you can do that in this function. Return value of this function is stored in `localStorage` (or wherever is Naja configured to store states) and also in `HistoryState`. Bear in mind that this introduces some limitations of what can be returned (e.g. no `Element` is allowed). |
|
|
72
|
+
| `setOptions(options: any): void` | Counterpart of `getOptions` method, you can use this method to restore modal settings based on `options`. |
|
|
73
|
+
|
|
74
|
+
### `SpinnerExtension`
|
|
75
|
+
|
|
76
|
+
This extension allows you to add configurable loading indicator to ajax request. Constructor recieves following 4 parameters:
|
|
77
|
+
|
|
78
|
+
| Parameter | Description |
|
|
79
|
+
|--------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
80
|
+
| `spinner: ((props?: any) => Element) \| Element` | Mandatory parameter. It should either be function return the spinner element, or directly element. |
|
|
81
|
+
| `getSpinnerProps: ((initator: Element) => any) \| undefined = undefined` | If you provide `spinner` as a function, you might also provide function to get settings from ajax initiator. Returned value is passed as a `props` to `spinner()` call. |
|
|
82
|
+
| `ajaxSpinnerWrapSelector = '.ajax-wrap'` | See below. |
|
|
83
|
+
|`ajaxSpinnerPlaceholderSelector = '.ajax-spinner'`| See below |
|
|
84
|
+
|
|
85
|
+
The logic for spinner placeholder is as follows:
|
|
86
|
+
1. Extension can be turned off by using `data-naja-spinner="off"`.
|
|
87
|
+
2. If there is `data-naja-spinner` with different value, this value is used as a selector for element into which the spinner element is appended.
|
|
88
|
+
3. If there is no `data-naja-spinner`, closest `ajaxSpinnerWrapSelector` is being searched for and:
|
|
89
|
+
1. If there is `ajaxSpinnerPlaceholderSelector` inside, this element is used for placing spinner element.
|
|
90
|
+
2. If not, the spinner element is appended into `ajaxSpinnerWrapSelector` itself.
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@peckadesign/pd-naja",
|
|
3
|
+
"title": "PD Naja utils & extensions",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"private": false,
|
|
6
|
+
"description": "Utilities and extensions created for use with Naja library.",
|
|
7
|
+
"module": "dist/PdNaja.esm.js",
|
|
8
|
+
"types": "dist/index.esm.d.ts",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/peckadesign/pd-naja.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"naja",
|
|
15
|
+
"extension",
|
|
16
|
+
"ajax",
|
|
17
|
+
"typescript",
|
|
18
|
+
"vanilla",
|
|
19
|
+
"javascript"
|
|
20
|
+
],
|
|
21
|
+
"author": {
|
|
22
|
+
"name": "PeckaDesign, s.r.o",
|
|
23
|
+
"email": "support@peckadesign.cz"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/peckadesign/pd-naja/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/peckadesign/pd-naja#readme",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"naja": "^2.5.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "NODE_ENV=production rollup -c --bundleConfigAsCjs",
|
|
35
|
+
"lint": "NODE_ENV=development eslint src --ext .ts --max-warnings=0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@rollup/plugin-babel": "^6.0.3",
|
|
39
|
+
"@rollup/plugin-commonjs": "^24.1.0",
|
|
40
|
+
"@rollup/plugin-node-resolve": "^15.0.2",
|
|
41
|
+
"@rollup/plugin-typescript": "^11.1.0",
|
|
42
|
+
"@rushstack/eslint-patch": "^1.2.0",
|
|
43
|
+
"eslint": "^8.40.0",
|
|
44
|
+
"eslint-config-prettier": "^8.8.0",
|
|
45
|
+
"eslint-config-typescript": "^3.0.0",
|
|
46
|
+
"eslint-plugin-prettier": "^4.2.1",
|
|
47
|
+
"prettier": "^2.8.8",
|
|
48
|
+
"rollup": "^3.21.6",
|
|
49
|
+
"typescript": "^5.0.4"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import babel from '@rollup/plugin-babel';
|
|
2
|
+
import commonjs from '@rollup/plugin-commonjs';
|
|
3
|
+
import resolve from '@rollup/plugin-node-resolve';
|
|
4
|
+
import typescript from '@rollup/plugin-typescript';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
import pkg from './package.json';
|
|
8
|
+
const output = {
|
|
9
|
+
banner: `/**\n * ${pkg.title} - ${pkg.description} \n * ${pkg.homepage}\n *\n * @author ${pkg.author.name} <${pkg.author.email}>\n * @license ${pkg.license}\n *\n * @version ${pkg.version}\n */\n`,
|
|
10
|
+
sourcemap: true,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const babelPlugin = babel({
|
|
14
|
+
exclude: /node_modules/,
|
|
15
|
+
include: 'src/**',
|
|
16
|
+
babelHelpers: 'runtime',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export default [
|
|
20
|
+
{
|
|
21
|
+
// ESM build for modern tools like webpack
|
|
22
|
+
input: 'src/index.esm.ts',
|
|
23
|
+
output: {
|
|
24
|
+
...output,
|
|
25
|
+
file: pkg.module,
|
|
26
|
+
format: 'esm',
|
|
27
|
+
},
|
|
28
|
+
external: [
|
|
29
|
+
/@babel\/runtime/,
|
|
30
|
+
...Object.keys(pkg.dependencies || {}),
|
|
31
|
+
...Object.keys(pkg.peerDependencies || {}),
|
|
32
|
+
],
|
|
33
|
+
plugins: [
|
|
34
|
+
resolve(),
|
|
35
|
+
commonjs(),
|
|
36
|
+
typescript(),
|
|
37
|
+
babelPlugin,
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
// type declaration files for ESM build
|
|
42
|
+
input: 'src/index.esm.ts',
|
|
43
|
+
output: {
|
|
44
|
+
...output,
|
|
45
|
+
dir: path.dirname(pkg.module),
|
|
46
|
+
format: 'esm',
|
|
47
|
+
},
|
|
48
|
+
external: [
|
|
49
|
+
/@babel\/runtime/,
|
|
50
|
+
...Object.keys(pkg.dependencies || {}),
|
|
51
|
+
...Object.keys(pkg.peerDependencies || {}),
|
|
52
|
+
],
|
|
53
|
+
plugins: [
|
|
54
|
+
typescript({
|
|
55
|
+
declaration: true,
|
|
56
|
+
declarationDir: path.dirname(pkg.module),
|
|
57
|
+
emitDeclarationOnly: true,
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { Naja, BeforeEvent, CompleteEvent, StartEvent, SuccessEvent, Options, Extension } from 'naja/dist/Naja'
|
|
2
|
+
import { BuildStateEvent, HistoryState } from 'naja/dist/core/HistoryHandler'
|
|
3
|
+
import { InteractionEvent } from 'naja/dist/core/UIHandler'
|
|
4
|
+
import { FetchEvent } from 'naja/dist/core/SnippetCache'
|
|
5
|
+
|
|
6
|
+
declare module 'naja/dist/Naja' {
|
|
7
|
+
interface Options {
|
|
8
|
+
pdModal?: boolean
|
|
9
|
+
modalOpener?: Element
|
|
10
|
+
modalOptions?: any
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Payload {
|
|
14
|
+
closeModal?: boolean
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type CallbackFn = (callback: EventListener) => void
|
|
19
|
+
|
|
20
|
+
export interface AjaxModal {
|
|
21
|
+
// main element of the modal
|
|
22
|
+
element: Element
|
|
23
|
+
|
|
24
|
+
// id's of snippets, that are necessary for modal function
|
|
25
|
+
reservedSnippetIds: string[]
|
|
26
|
+
|
|
27
|
+
show(opener: Element | undefined, options: any, event: BeforeEvent | PopStateEvent): void
|
|
28
|
+
hide(event: SuccessEvent | PopStateEvent): void
|
|
29
|
+
isShown(): boolean
|
|
30
|
+
|
|
31
|
+
onShow: CallbackFn
|
|
32
|
+
onHide: CallbackFn
|
|
33
|
+
onHidden: CallbackFn
|
|
34
|
+
|
|
35
|
+
dispatchLoad?: (options: any, event: SuccessEvent | PopStateEvent) => void
|
|
36
|
+
|
|
37
|
+
getOptions(opener: Element): any
|
|
38
|
+
setOptions(options: any): void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface HistoryStateWrapper extends Record<string, any> {
|
|
42
|
+
location: string
|
|
43
|
+
state: HistoryState
|
|
44
|
+
title: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type HistoryDirection = 'forwards' | 'backwards'
|
|
48
|
+
|
|
49
|
+
export class AjaxModalExtension implements Extension {
|
|
50
|
+
private readonly modal: AjaxModal
|
|
51
|
+
private readonly uniqueExtKey: string = 'modal'
|
|
52
|
+
|
|
53
|
+
private popstateFlag = false
|
|
54
|
+
private hidePopstateFlag = false
|
|
55
|
+
|
|
56
|
+
private historyEnabled = false // (dis)allows `pushState` after hiding the modal when going back in history (popstate), we don't want to push new state into history same is true also when the history is disabled for request altogether
|
|
57
|
+
private historyDirection: HistoryDirection = 'backwards'
|
|
58
|
+
|
|
59
|
+
private modalOptions: any = {}
|
|
60
|
+
|
|
61
|
+
private original: HistoryStateWrapper[] = [] // stack of states under the modal after hiding the modal with `forwards` history mode, we need to push the previous state
|
|
62
|
+
private lastState: HistoryStateWrapper | null = null
|
|
63
|
+
private initialState: HistoryState | Record<string, never>
|
|
64
|
+
|
|
65
|
+
private readonly abortControllers: Map<string, AbortController> = new Map()
|
|
66
|
+
|
|
67
|
+
public constructor(modal: AjaxModal) {
|
|
68
|
+
// Extension popstate has to be executed before naja popstate, so we can correctly detect if the pdModal is
|
|
69
|
+
// opened. Therefore, we bind the callback before the extension initialization itself.
|
|
70
|
+
window.addEventListener('popstate', this.popstateHandler.bind(this))
|
|
71
|
+
|
|
72
|
+
this.modal = modal
|
|
73
|
+
this.initialState = history.state || {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public initialize(naja: Naja): void {
|
|
77
|
+
naja.uiHandler.addEventListener('interaction', this.checkExtensionEnabled.bind(this))
|
|
78
|
+
|
|
79
|
+
naja.historyHandler.addEventListener('buildState', this.buildState.bind(this))
|
|
80
|
+
|
|
81
|
+
naja.snippetCache.addEventListener('fetch', this.onSnippetFetch.bind(this))
|
|
82
|
+
|
|
83
|
+
naja.addEventListener('before', this.before.bind(this))
|
|
84
|
+
naja.addEventListener('start', this.abortPreviousRequest.bind(this))
|
|
85
|
+
naja.addEventListener('success', this.success.bind(this))
|
|
86
|
+
naja.addEventListener('complete', this.clearRequest.bind(this))
|
|
87
|
+
|
|
88
|
+
this.modal.onShow(this.showHandler.bind(this))
|
|
89
|
+
this.modal.onHide(() => {
|
|
90
|
+
this.removeModalSnippetsIds()
|
|
91
|
+
this.abortControllers.get('modal')?.abort()
|
|
92
|
+
})
|
|
93
|
+
this.modal.onHidden(this.hiddenHandler.bind(this))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private isRequestWithHistory = (options: Options): boolean => {
|
|
97
|
+
return options.history !== false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private isPdModalState = (state: HistoryState | Record<string, never>): boolean => {
|
|
101
|
+
return 'pdModal' in state && state.pdModal?.isShown
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private restoreExtensionPropertiesFromState = (state: HistoryState): void => {
|
|
105
|
+
this.historyEnabled = true // Called from popstateHandler means the history is enabled
|
|
106
|
+
this.historyDirection = state.pdModal.historyDirection
|
|
107
|
+
this.modalOptions = state.pdModal.options
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private checkExtensionEnabled(event: InteractionEvent): void {
|
|
111
|
+
const { element, options } = event.detail
|
|
112
|
+
|
|
113
|
+
options.pdModal =
|
|
114
|
+
this.modal.isShown() ||
|
|
115
|
+
element.hasAttribute('data-naja-modal') ||
|
|
116
|
+
(element as HTMLInputElement).form?.hasAttribute('data-naja-modal')
|
|
117
|
+
|
|
118
|
+
// If the extension is enabled and modal is not opened, we detect and store history mode. History mode cannot
|
|
119
|
+
// change when traversing ajax link inside modal.
|
|
120
|
+
if (!options.pdModal) {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// `modalOptions` will be stored in state, therefore no `Element` is allowed. These options are also forwarded
|
|
125
|
+
// to other Naja event handlers via `options`. We also store the element separately to be used there as well.
|
|
126
|
+
this.modalOptions = this.modal.getOptions(element)
|
|
127
|
+
|
|
128
|
+
options.modalOptions = this.modalOptions
|
|
129
|
+
options.modalOpener = element
|
|
130
|
+
|
|
131
|
+
// History direction can only be set before opening the modal, then it stays the same until modal is hidden.
|
|
132
|
+
if (!this.modal.isShown()) {
|
|
133
|
+
this.historyDirection = element.getAttribute('data-naja-modal-history') === 'forwards' ? 'forwards' : 'backwards'
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private onSnippetFetch(event: FetchEvent): void {
|
|
138
|
+
event.detail.options.pdModal = 'pdModal' in event.detail.state
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private removeModalSnippetsIds(): void {
|
|
142
|
+
// When closing the modal, we don't want to update any snippets inside it, when other requests finishes. This
|
|
143
|
+
// will ensure, that snippets that might be also outside modal (e.g. flash messages) will be redrawn outside the
|
|
144
|
+
// modal. Therefore, we remove the id attributes, so no snippet is found.
|
|
145
|
+
//
|
|
146
|
+
// Some snippets are necessary for modal function
|
|
147
|
+
this.modal.element.querySelectorAll('[id^="snippet-"]').forEach((snippet) => {
|
|
148
|
+
if (!this.modal.reservedSnippetIds.includes(snippet.id)) {
|
|
149
|
+
snippet.removeAttribute('id')
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private abortPreviousRequest(event: StartEvent): void {
|
|
155
|
+
const { abortController, options } = event.detail
|
|
156
|
+
if (options.pdModal) {
|
|
157
|
+
this.abortControllers.get(this.uniqueExtKey)?.abort()
|
|
158
|
+
this.abortControllers.set(this.uniqueExtKey, abortController)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private clearRequest(event: CompleteEvent): void {
|
|
163
|
+
const { request } = event.detail
|
|
164
|
+
if (!request.signal.aborted) {
|
|
165
|
+
this.abortControllers.delete(this.uniqueExtKey)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private buildState(event: BuildStateEvent): void {
|
|
170
|
+
const { options, state } = event.detail
|
|
171
|
+
|
|
172
|
+
// Every time naja builds the state, we extend it with `pdModal` object containing information about modal being
|
|
173
|
+
// opened and what history mode is in use. When options.forceRedirect is set, modal might be open but the new
|
|
174
|
+
// state will be redirected outside it.
|
|
175
|
+
const isShown: boolean = this.modal.isShown() && !options.forceRedirect
|
|
176
|
+
state.pdModal = {
|
|
177
|
+
isShown,
|
|
178
|
+
historyDirection: isShown ? this.historyDirection : null,
|
|
179
|
+
options: isShown ? this.modalOptions : null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// If the state is build, the history is enabled. This information is needed inside modal callback where the
|
|
183
|
+
// options are not available, so we store this internally in extension.
|
|
184
|
+
this.historyEnabled = true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private before(event: BeforeEvent) {
|
|
188
|
+
const { options, request } = event.detail
|
|
189
|
+
if (!options.pdModal) {
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.modal.show(options.modalOpener, options.modalOptions, event)
|
|
194
|
+
|
|
195
|
+
request.headers.append('Pd-Modal-Opened', String(Number(this.modal.isShown())))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private success(event: SuccessEvent) {
|
|
199
|
+
const { options, payload } = event.detail
|
|
200
|
+
|
|
201
|
+
this.popstateFlag = false
|
|
202
|
+
this.lastState = {
|
|
203
|
+
location: location.href,
|
|
204
|
+
state: history.state,
|
|
205
|
+
title: document.title
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!options.pdModal) {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const requestHistory = this.isRequestWithHistory(options)
|
|
213
|
+
|
|
214
|
+
// If the history is disabled for current request, we will disable it for all ajax links / forms in modal as well.
|
|
215
|
+
if (!requestHistory && event.target) {
|
|
216
|
+
const ajaxified = this.modal.element.querySelectorAll<HTMLElement>((event.target as Naja).uiHandler?.selector)
|
|
217
|
+
|
|
218
|
+
ajaxified.forEach((element: HTMLElement) => {
|
|
219
|
+
element.setAttribute('data-naja-history', 'off')
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (payload.closeModal) {
|
|
224
|
+
this.modal.hide(event)
|
|
225
|
+
} else {
|
|
226
|
+
this.modal.setOptions(this.modalOptions)
|
|
227
|
+
|
|
228
|
+
if (this.modal.dispatchLoad) {
|
|
229
|
+
this.modal.dispatchLoad(this.modalOptions, event)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private showHandler() {
|
|
235
|
+
this.modal.setOptions(this.modalOptions)
|
|
236
|
+
|
|
237
|
+
// If the modal history mode is `forwards`, we store the state under the modal, so we can push it as a new state
|
|
238
|
+
// after hiding the modal.
|
|
239
|
+
if (this.historyDirection === 'forwards') {
|
|
240
|
+
if (this.popstateFlag && this.lastState) {
|
|
241
|
+
this.original.push(this.lastState)
|
|
242
|
+
} else {
|
|
243
|
+
const state: HistoryStateWrapper = {
|
|
244
|
+
location: location.href,
|
|
245
|
+
state: history.state,
|
|
246
|
+
title: document.title
|
|
247
|
+
}
|
|
248
|
+
this.original.push(state)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private hiddenHandler() {
|
|
254
|
+
// This method is called after modal has been hidden. It either pushes a new state into history (mode `forwards`)
|
|
255
|
+
// or calls `history.back()` to start go-back procedure.
|
|
256
|
+
//
|
|
257
|
+
// New state is pushed only if we are able to retrieve the state under the modal (which should have been stored
|
|
258
|
+
// previously) and the modal is not being closed using forward / back buttons in browser.
|
|
259
|
+
if (!this.historyEnabled) {
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (this.historyDirection === 'backwards') {
|
|
264
|
+
// We don't know how many states we need to return. We go one by one, see popstate handler. This go-back
|
|
265
|
+
// procedure is detected using `hidePopstateFlag`.
|
|
266
|
+
this.hidePopstateFlag = true
|
|
267
|
+
this.cleanData()
|
|
268
|
+
window.history.back()
|
|
269
|
+
} else if (this.historyDirection === 'forwards') {
|
|
270
|
+
const state = this.original.pop()
|
|
271
|
+
this.original = []
|
|
272
|
+
|
|
273
|
+
if (state) {
|
|
274
|
+
// When closing the modal using forward / back buttons in browser, the current state is the same as the
|
|
275
|
+
// one stored in `this.original`. If that's the case, we don't push anything as it would duplicate the
|
|
276
|
+
// state in history.
|
|
277
|
+
if (history.state === undefined || history.state.href !== state.location) {
|
|
278
|
+
history.pushState(state.state, state.title, state.location)
|
|
279
|
+
document.title = state.title
|
|
280
|
+
|
|
281
|
+
this.popstateFlag = false
|
|
282
|
+
this.lastState = {
|
|
283
|
+
location: state.location,
|
|
284
|
+
state: state.state,
|
|
285
|
+
title: state.title
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
this.cleanData()
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private popstateHandler(event: PopStateEvent): void {
|
|
295
|
+
const state: HistoryState = event.state || this.initialState
|
|
296
|
+
|
|
297
|
+
if (typeof state === 'undefined' || !this.modal) {
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const isCurrentStatePdModal = this.isPdModalState(state)
|
|
302
|
+
this.popstateFlag = true
|
|
303
|
+
|
|
304
|
+
// We don't know how many states we go back. So we go one by one until the new state is not modal state
|
|
305
|
+
// (`isPdModalState` is `false`).
|
|
306
|
+
if (this.hidePopstateFlag) {
|
|
307
|
+
// We don't want the naja popstate callback to be executed (or any other popstate handler).
|
|
308
|
+
event.stopImmediatePropagation()
|
|
309
|
+
|
|
310
|
+
if (isCurrentStatePdModal) {
|
|
311
|
+
window.history.back()
|
|
312
|
+
|
|
313
|
+
return
|
|
314
|
+
} else {
|
|
315
|
+
// Todo check if this is really necessary. When used with nette.ajax / history.nette.ajax, this was necessary in some cases, where the title hasn't been restored correctly.
|
|
316
|
+
if (state.title) {
|
|
317
|
+
document.title = state.title
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.hidePopstateFlag = false
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// We check if the state has pdModal object present on popstate. If so (and the pdModal.isShown is true), we proceed
|
|
325
|
+
// to open the modal. Content of the modal is restored by naja itself (either from cache or by new request).
|
|
326
|
+
//
|
|
327
|
+
// If the initial state is also detected as pdModal state, we returned to some pdModal state using reload. In that
|
|
328
|
+
// case, we don't want to open the modal, because we might be missing some snippets. Effectively this means that the
|
|
329
|
+
// modal will never be opened by forward / back button if there has been some other site loaded outside the modal
|
|
330
|
+
// (e.g. some non-ajax link leading from modal).
|
|
331
|
+
if (isCurrentStatePdModal && !this.isPdModalState(this.initialState)) {
|
|
332
|
+
this.restoreExtensionPropertiesFromState(state)
|
|
333
|
+
|
|
334
|
+
this.modal.show(undefined, state.pdModal.options, event)
|
|
335
|
+
|
|
336
|
+
// If there is some snippet cache, we might restore modal options. If not, options will be restored based on
|
|
337
|
+
// options after the ajax request. Same applies to dispatching load event - if cache is on, we dispatch the
|
|
338
|
+
// event immediately, otherwise it will be dispatched after ajax request.
|
|
339
|
+
if (state.snippets?.storage !== 'off') {
|
|
340
|
+
this.modal.setOptions(state.pdModal.options)
|
|
341
|
+
|
|
342
|
+
if (this.modal.dispatchLoad) {
|
|
343
|
+
this.modal.dispatchLoad(this.modalOptions, event)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
this.historyEnabled = false // Hiding modal using forward / back button, we disable the history to prevent state duplication
|
|
348
|
+
|
|
349
|
+
// Reload the page if the initial state has been inside modal. This prevents snippets loss e.g. during layout changes.
|
|
350
|
+
if (this.isPdModalState(this.initialState)) {
|
|
351
|
+
window.location.reload()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Non-modal state and non-modal initial state, we just hide the current modal.
|
|
355
|
+
this.modal.hide(event)
|
|
356
|
+
|
|
357
|
+
// We don't want the naja popstate callback to be executed (or any other popstate handler).
|
|
358
|
+
event.stopImmediatePropagation()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Keep track of current state. When `backwards` history mode is used, we eventually push this state into
|
|
362
|
+
// `this.original`.
|
|
363
|
+
this.lastState = {
|
|
364
|
+
location: location.href,
|
|
365
|
+
state: state,
|
|
366
|
+
title: document.title
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private cleanData(): void {
|
|
371
|
+
this.historyEnabled = false
|
|
372
|
+
this.historyDirection = 'backwards'
|
|
373
|
+
this.modalOptions = null
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { CompleteEvent, Extension, Naja, StartEvent } from 'naja/dist/Naja'
|
|
2
|
+
import { InteractionEvent } from 'naja/dist/core/UIHandler'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @author Radek Šerý
|
|
6
|
+
*
|
|
7
|
+
* Spinner - loading indicator:
|
|
8
|
+
* 1. Extension can be turned off by using `data-naja-spinner="off"`.
|
|
9
|
+
* 2. If there is `data-naja-spinner` with different value, this value is used as a selector for element into which the spinner element is appended.
|
|
10
|
+
* 3. If there is no `data-naja-spinner`, closest `ajaxSpinnerWrapSelector` is being searched for and:
|
|
11
|
+
* i. If there is `ajaxSpinnerPlaceholderSelector` inside, this element is used for placing spinner element.
|
|
12
|
+
* ii. If not, the spinner element is appended into `ajaxSpinnerWrapSelector` itself.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
declare module 'naja/dist/Naja' {
|
|
16
|
+
interface Options {
|
|
17
|
+
spinnerInitiator?: Element
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type spinnerType = ((props?: any) => Element) | Element
|
|
22
|
+
type spinnerPropsFn = ((initiator: Element) => any) | undefined
|
|
23
|
+
|
|
24
|
+
export class SpinnerExtension implements Extension {
|
|
25
|
+
public readonly spinner: spinnerType
|
|
26
|
+
public readonly getSpinnerProps?: spinnerPropsFn
|
|
27
|
+
|
|
28
|
+
public readonly ajaxSpinnerWrapSelector: string
|
|
29
|
+
public readonly ajaxSpinnerPlaceholderSelector: string
|
|
30
|
+
|
|
31
|
+
public constructor(
|
|
32
|
+
spinner: spinnerType,
|
|
33
|
+
getSpinnerProps: spinnerPropsFn = undefined,
|
|
34
|
+
ajaxSpinnerWrapSelector = '.ajax-wrap',
|
|
35
|
+
ajaxSpinnerPlaceholderSelector = '.ajax-spinner'
|
|
36
|
+
) {
|
|
37
|
+
this.spinner = spinner
|
|
38
|
+
this.getSpinnerProps = getSpinnerProps
|
|
39
|
+
|
|
40
|
+
this.ajaxSpinnerWrapSelector = ajaxSpinnerWrapSelector
|
|
41
|
+
this.ajaxSpinnerPlaceholderSelector = ajaxSpinnerPlaceholderSelector
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public initialize(naja: Naja): void {
|
|
45
|
+
naja.uiHandler.addEventListener('interaction', this.getSpinnerInitiator.bind(this))
|
|
46
|
+
|
|
47
|
+
naja.addEventListener('start', this.showSpinners.bind(this))
|
|
48
|
+
naja.addEventListener('complete', this.hideSpinners.bind(this))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private getSpinnerInitiator(event: InteractionEvent): void {
|
|
52
|
+
event.detail.options.spinnerInitiator = event.detail.element
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private showSpinners(event: StartEvent): void {
|
|
56
|
+
const { options } = event.detail
|
|
57
|
+
|
|
58
|
+
if (!options.spinnerInitiator) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const spinnerInitiator = options.spinnerInitiator
|
|
63
|
+
const placeholders = this.getPlaceholders(spinnerInitiator)
|
|
64
|
+
|
|
65
|
+
if (placeholders.length === 0) {
|
|
66
|
+
return
|
|
67
|
+
} else {
|
|
68
|
+
options.spinnerQueue = options.spinnerQueue || []
|
|
69
|
+
|
|
70
|
+
placeholders.forEach((placeholder) => {
|
|
71
|
+
let spinner: Element
|
|
72
|
+
|
|
73
|
+
if (typeof this.spinner === 'function') {
|
|
74
|
+
spinner = this.getSpinnerProps ? this.spinner(this.getSpinnerProps(spinnerInitiator)) : this.spinner()
|
|
75
|
+
} else {
|
|
76
|
+
spinner = this.spinner
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
placeholder.appendChild(spinner)
|
|
80
|
+
options.spinnerQueue.push(spinner)
|
|
81
|
+
|
|
82
|
+
spinner.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 100 })
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private hideSpinners(event: CompleteEvent): void {
|
|
88
|
+
const { options } = event.detail
|
|
89
|
+
|
|
90
|
+
if (options.forceRedirect) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
options.spinnerQueue?.forEach((spinner: Element) => {
|
|
95
|
+
const animation = spinner.animate({ opacity: 0 }, { duration: 100 })
|
|
96
|
+
animation.finished.then(() => spinner.remove())
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private getPlaceholders(element: Element): Element[] {
|
|
101
|
+
if (!element) {
|
|
102
|
+
return []
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const spinner = element.getAttribute('data-naja-spinner') || null
|
|
106
|
+
let placeholders: Element[] = []
|
|
107
|
+
|
|
108
|
+
if (spinner === 'off') {
|
|
109
|
+
return []
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
placeholders = this.getPlaceholdersByQuerySelector(spinner)
|
|
113
|
+
|
|
114
|
+
return placeholders.length ? placeholders : this.getPlaceholdersByDOM(element)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private getPlaceholdersByQuerySelector(selector: string | null): Element[] {
|
|
118
|
+
return selector ? Array.from(document.querySelectorAll(selector)) : []
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private getPlaceholdersByDOM(element: Element): Element[] {
|
|
122
|
+
const wrap = element.closest(this.ajaxSpinnerWrapSelector)
|
|
123
|
+
|
|
124
|
+
if (wrap === null) {
|
|
125
|
+
return []
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const placeholders = wrap.querySelectorAll(this.ajaxSpinnerPlaceholderSelector)
|
|
129
|
+
|
|
130
|
+
return placeholders.length ? Array.from(placeholders) : [wrap]
|
|
131
|
+
}
|
|
132
|
+
}
|
package/src/index.esm.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// `Control` is meant to be used for standalone components, that might be dependent on ajax. It should be used together
|
|
2
|
+
// with ControlManager. Class implementing the `Control` interface should export its instance. Then the intended
|
|
3
|
+
// lifecycle of class is as follows:
|
|
4
|
+
//
|
|
5
|
+
// 1. `constructor` is called immediately and is also called only once. This is the ideal place for e.g. adding common
|
|
6
|
+
// event handlers to the `body`, or modifying necessary DOM properties on elements not affected by ajax.
|
|
7
|
+
|
|
8
|
+
// 2. The instantiated class is then added to ControlManager either by `addControlOnLoad` or `addControlOnLive`. This
|
|
9
|
+
// ensures, that on `DOMContentLoaded` the `initialize` function of class is called. This method should implement
|
|
10
|
+
// initialization of the control dependent on fully loaded DOM. The `context` argument is equal to `document` in this
|
|
11
|
+
// call.
|
|
12
|
+
//
|
|
13
|
+
// 3. In case of `addControlOnLive` used, the `initialize` method is also called for each success ajax request. The
|
|
14
|
+
// `context` argument is equal to modified nette snippet.
|
|
15
|
+
//
|
|
16
|
+
export default interface Control {
|
|
17
|
+
initialize(context: Element | Document): void
|
|
18
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import naja from 'naja'
|
|
2
|
+
import { AfterUpdateEvent } from 'naja/dist/core/SnippetHandler'
|
|
3
|
+
import Control from './Control'
|
|
4
|
+
|
|
5
|
+
let instance: ControlManager | null = null
|
|
6
|
+
|
|
7
|
+
export default class ControlManager {
|
|
8
|
+
private onLoadControl: Control[] = []
|
|
9
|
+
private onLiveControl: Control[] = []
|
|
10
|
+
|
|
11
|
+
public constructor() {
|
|
12
|
+
if (instance === null) {
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
14
|
+
instance = this
|
|
15
|
+
naja.snippetHandler.addEventListener('afterUpdate', this.onSnippetUpdate.bind(this))
|
|
16
|
+
}
|
|
17
|
+
return instance
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public onLoad(): void {
|
|
21
|
+
this.initialize(this.onLoadControl)
|
|
22
|
+
this.initialize(this.onLiveControl)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private onSnippetUpdate(event: AfterUpdateEvent): void {
|
|
26
|
+
this.initialize(this.onLiveControl, event.detail.snippet)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private initialize(controls: Control[], snippet?: Element | Document): void {
|
|
30
|
+
controls.forEach((control) => {
|
|
31
|
+
control.initialize(snippet || document)
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public addControlOnLoad(control: Control): void {
|
|
36
|
+
this.onLoadControl.push(control)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public addControlOnLive(control: Control): void {
|
|
40
|
+
this.onLiveControl.push(control)
|
|
41
|
+
}
|
|
42
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2021",
|
|
4
|
+
"module": "ES2015",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
|
|
8
|
+
"strict": true,
|
|
9
|
+
"alwaysStrict": true,
|
|
10
|
+
|
|
11
|
+
"rootDir": "src/",
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
|
|
14
|
+
"noEmitOnError": true,
|
|
15
|
+
"jsx": "react"
|
|
16
|
+
},
|
|
17
|
+
"exclude": [
|
|
18
|
+
"node_modules"
|
|
19
|
+
]
|
|
20
|
+
}
|