@peckadesign/pd-naja 1.0.0 → 1.1.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.
@@ -15,6 +15,15 @@ declare module 'naja/dist/Naja' {
15
15
  }
16
16
  }
17
17
 
18
+ interface OptionsWithPdModal extends Options {
19
+ modalOpener: Element
20
+ modalOptions: any
21
+ }
22
+
23
+ interface PdModalHistoryState extends HistoryState {
24
+ pdModal: PdModalState
25
+ }
26
+
18
27
  type CallbackFn = (callback: EventListener) => void
19
28
 
20
29
  export interface AjaxModal {
@@ -24,7 +33,7 @@ export interface AjaxModal {
24
33
  // id's of snippets, that are necessary for modal function
25
34
  reservedSnippetIds: string[]
26
35
 
27
- show(opener: Element | undefined, options: any, event: BeforeEvent | PopStateEvent): void
36
+ show(opener: Element, options: any, event: BeforeEvent | PopStateEvent): void
28
37
  hide(event: SuccessEvent | PopStateEvent): void
29
38
  isShown(): boolean
30
39
 
@@ -46,6 +55,12 @@ interface HistoryStateWrapper extends Record<string, any> {
46
55
 
47
56
  type HistoryDirection = 'forwards' | 'backwards'
48
57
 
58
+ interface PdModalState {
59
+ historyDirection: HistoryDirection
60
+ opener: string // stringified Element
61
+ options: any
62
+ }
63
+
49
64
  export class AjaxModalExtension implements Extension {
50
65
  private readonly modal: AjaxModal
51
66
  private readonly uniqueExtKey: string = 'modal'
@@ -56,6 +71,8 @@ export class AjaxModalExtension implements Extension {
56
71
  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
72
  private historyDirection: HistoryDirection = 'backwards'
58
73
 
74
+ private shouldPreventSnippetFetch = false
75
+
59
76
  private modalOptions: any = {}
60
77
 
61
78
  private original: HistoryStateWrapper[] = [] // stack of states under the modal after hiding the modal with `forwards` history mode, we need to push the previous state
@@ -97,11 +114,15 @@ export class AjaxModalExtension implements Extension {
97
114
  return options.history !== false
98
115
  }
99
116
 
100
- private isPdModalState = (state: HistoryState | Record<string, never>): boolean => {
101
- return 'pdModal' in state && state.pdModal?.isShown
117
+ private isPdModalRequest = (options: Options): options is OptionsWithPdModal => {
118
+ return Boolean(options.pdModal)
119
+ }
120
+
121
+ private isPdModalState = (state: HistoryState | Record<string, never>): state is PdModalHistoryState => {
122
+ return 'pdModal' in state
102
123
  }
103
124
 
104
- private restoreExtensionPropertiesFromState = (state: HistoryState): void => {
125
+ private restoreExtensionPropertiesFromState = (state: PdModalHistoryState): void => {
105
126
  this.historyEnabled = true // Called from popstateHandler means the history is enabled
106
127
  this.historyDirection = state.pdModal.historyDirection
107
128
  this.modalOptions = state.pdModal.options
@@ -115,9 +136,7 @@ export class AjaxModalExtension implements Extension {
115
136
  element.hasAttribute('data-naja-modal') ||
116
137
  (element as HTMLInputElement).form?.hasAttribute('data-naja-modal')
117
138
 
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) {
139
+ if (!this.isPdModalRequest(options)) {
121
140
  return
122
141
  }
123
142
 
@@ -128,14 +147,29 @@ export class AjaxModalExtension implements Extension {
128
147
  options.modalOptions = this.modalOptions
129
148
  options.modalOpener = element
130
149
 
131
- // History direction can only be set before opening the modal, then it stays the same until modal is hidden.
150
+ // If the extension is enabled and modal is not opened, we detect and store history mode. History mode cannot
151
+ // change when traversing ajax link inside modal, it stays the same until modal is hidden.
132
152
  if (!this.modal.isShown()) {
133
153
  this.historyDirection = element.getAttribute('data-naja-modal-history') === 'forwards' ? 'forwards' : 'backwards'
134
154
  }
135
155
  }
136
156
 
137
157
  private onSnippetFetch(event: FetchEvent): void {
138
- event.detail.options.pdModal = 'pdModal' in event.detail.state
158
+ // When snippet cache is off, this method is called to construct Naja request options. We retrieve modal
159
+ // `options` from original state in history if neccessary. Also, when closing the modal from `PopstateEvent`, we
160
+ // don't need to make a new request, so we call `event.preventDefault()` in that case.
161
+ const { state } = event.detail
162
+
163
+ if (this.isPdModalState(state)) {
164
+ event.detail.options.pdModal = true
165
+ event.detail.options.modalOpener = this.getElementFromString(state.pdModal.opener)
166
+ event.detail.options.modalOptions = state.pdModal.options
167
+ }
168
+
169
+ if (this.shouldPreventSnippetFetch) {
170
+ event.preventDefault()
171
+ this.shouldPreventSnippetFetch = false
172
+ }
139
173
  }
140
174
 
141
175
  private removeModalSnippetsIds(): void {
@@ -153,7 +187,7 @@ export class AjaxModalExtension implements Extension {
153
187
 
154
188
  private abortPreviousRequest(event: StartEvent): void {
155
189
  const { abortController, options } = event.detail
156
- if (options.pdModal) {
190
+ if (this.isPdModalRequest(options)) {
157
191
  this.abortControllers.get(this.uniqueExtKey)?.abort()
158
192
  this.abortControllers.set(this.uniqueExtKey, abortController)
159
193
  }
@@ -169,14 +203,21 @@ export class AjaxModalExtension implements Extension {
169
203
  private buildState(event: BuildStateEvent): void {
170
204
  const { options, state } = event.detail
171
205
 
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.
206
+ // Every time naja builds the state, and we have the modal opened, we extend the state with `pdModal` object
207
+ // containing information about modal being opened and what history mode is in use. When `options.forceRedirect`
208
+ // is set, modal might be open but the new state will be redirected outside of it.
209
+ //
210
+ // We must check this.modal.isShown instead of this.isPdModalRequest, because of initial replace state from
211
+ // Naja - in that case, options already contains information about request to be made, but we have to build the
212
+ // "previous" state yet.
175
213
  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
214
+
215
+ if (isShown) {
216
+ state.pdModal = {
217
+ historyDirection: this.historyDirection,
218
+ opener: (options.modalOpener as Element).outerHTML, // if `this.modal.isShown()` is true, the `modalOpener` has been stored
219
+ options: this.modalOptions
220
+ }
180
221
  }
181
222
 
182
223
  // If the state is build, the history is enabled. This information is needed inside modal callback where the
@@ -186,7 +227,7 @@ export class AjaxModalExtension implements Extension {
186
227
 
187
228
  private before(event: BeforeEvent) {
188
229
  const { options, request } = event.detail
189
- if (!options.pdModal) {
230
+ if (!this.isPdModalRequest(options)) {
190
231
  return
191
232
  }
192
233
 
@@ -205,7 +246,7 @@ export class AjaxModalExtension implements Extension {
205
246
  title: document.title
206
247
  }
207
248
 
208
- if (!options.pdModal) {
249
+ if (!this.isPdModalRequest(options)) {
209
250
  return
210
251
  }
211
252
 
@@ -300,6 +341,7 @@ export class AjaxModalExtension implements Extension {
300
341
 
301
342
  const isCurrentStatePdModal = this.isPdModalState(state)
302
343
  this.popstateFlag = true
344
+ this.shouldPreventSnippetFetch = false
303
345
 
304
346
  // 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
347
  // (`isPdModalState` is `false`).
@@ -321,17 +363,17 @@ export class AjaxModalExtension implements Extension {
321
363
  this.hidePopstateFlag = false
322
364
  }
323
365
 
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).
366
+ // We check if the state has `pdModal` object present on popstate. If so , we proceed to open the modal. Content
367
+ // of the modal is restored by naja itself (either from cache or by new request).
326
368
  //
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).
369
+ // If the initial state is also detected as pdModal state, we returned to some pdModal state using reload. In
370
+ // that case, we don't want to open the modal, because we might be missing some snippets. Effectively this means
371
+ // that the modal will never be opened by forward / back button if there has been some other site loaded outside
372
+ // the modal (e.g. some non-ajax link leading from modal).
331
373
  if (isCurrentStatePdModal && !this.isPdModalState(this.initialState)) {
332
374
  this.restoreExtensionPropertiesFromState(state)
333
375
 
334
- this.modal.show(undefined, state.pdModal.options, event)
376
+ this.modal.show(this.getElementFromString(state.pdModal.opener), state.pdModal.options, event)
335
377
 
336
378
  // If there is some snippet cache, we might restore modal options. If not, options will be restored based on
337
379
  // options after the ajax request. Same applies to dispatching load event - if cache is on, we dispatch the
@@ -346,16 +388,18 @@ export class AjaxModalExtension implements Extension {
346
388
  } else {
347
389
  this.historyEnabled = false // Hiding modal using forward / back button, we disable the history to prevent state duplication
348
390
 
349
- // Reload the page if the initial state has been inside modal. This prevents snippets loss e.g. during layout changes.
391
+ // Reload the page if the initial state has been inside modal. This prevents snippets loss e.g. during
392
+ // layout changes.
350
393
  if (this.isPdModalState(this.initialState)) {
351
394
  window.location.reload()
352
395
  }
353
396
 
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()
397
+ // Non-modal state and non-modal initial state we just hide the current modal and prevent snippet fetch
398
+ // (if snippet cache is off).
399
+ if (this.modal.isShown()) {
400
+ this.modal.hide(event)
401
+ this.shouldPreventSnippetFetch = true
402
+ }
359
403
  }
360
404
 
361
405
  // Keep track of current state. When `backwards` history mode is used, we eventually push this state into
@@ -372,4 +416,8 @@ export class AjaxModalExtension implements Extension {
372
416
  this.historyDirection = 'backwards'
373
417
  this.modalOptions = null
374
418
  }
419
+
420
+ private getElementFromString(stringElement: string): Element {
421
+ return new DOMParser().parseFromString(stringElement, 'text/html').body.firstElementChild as Element
422
+ }
375
423
  }
@@ -1,29 +0,0 @@
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
@@ -1,80 +0,0 @@
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/rollup.config.js DELETED
@@ -1,61 +0,0 @@
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
- ];