@defra/forms-model 3.0.502 → 3.0.504

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.
Files changed (38) hide show
  1. package/dist/module/__stubs__/pages.js +14 -1
  2. package/dist/module/__stubs__/pages.js.map +1 -1
  3. package/dist/module/form/form-editor/preview/controller/guidance-page-controller.js +58 -0
  4. package/dist/module/form/form-editor/preview/controller/guidance-page-controller.js.map +1 -0
  5. package/dist/module/form/form-editor/preview/controller/page-controller-base.js +363 -0
  6. package/dist/module/form/form-editor/preview/controller/page-controller-base.js.map +1 -0
  7. package/dist/module/form/form-editor/preview/controller/page-controller.js +26 -265
  8. package/dist/module/form/form-editor/preview/controller/page-controller.js.map +1 -1
  9. package/dist/module/form/form-editor/preview/index.js +2 -0
  10. package/dist/module/form/form-editor/preview/index.js.map +1 -1
  11. package/dist/module/form/form-editor/preview/types.js.map +1 -1
  12. package/dist/module/pages/helpers.js +2 -1
  13. package/dist/module/pages/helpers.js.map +1 -1
  14. package/dist/module/utils/markdown.js +23 -3
  15. package/dist/module/utils/markdown.js.map +1 -1
  16. package/dist/types/__stubs__/pages.d.ts +14 -0
  17. package/dist/types/__stubs__/pages.d.ts.map +1 -1
  18. package/dist/types/form/form-editor/preview/controller/guidance-page-controller.d.ts +10 -0
  19. package/dist/types/form/form-editor/preview/controller/guidance-page-controller.d.ts.map +1 -0
  20. package/dist/types/form/form-editor/preview/controller/page-controller-base.d.ts +198 -0
  21. package/dist/types/form/form-editor/preview/controller/page-controller-base.d.ts.map +1 -0
  22. package/dist/types/form/form-editor/preview/controller/page-controller.d.ts +3 -150
  23. package/dist/types/form/form-editor/preview/controller/page-controller.d.ts.map +1 -1
  24. package/dist/types/form/form-editor/preview/index.d.ts +2 -0
  25. package/dist/types/form/form-editor/preview/types.d.ts +3 -1
  26. package/dist/types/form/form-editor/preview/types.d.ts.map +1 -1
  27. package/dist/types/pages/helpers.d.ts.map +1 -1
  28. package/dist/types/utils/markdown.d.ts +1 -1
  29. package/dist/types/utils/markdown.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/__stubs__/pages.ts +20 -1
  32. package/src/form/form-editor/preview/controller/guidance-page-controller.js +64 -0
  33. package/src/form/form-editor/preview/controller/page-controller-base.js +388 -0
  34. package/src/form/form-editor/preview/controller/page-controller.js +28 -288
  35. package/src/form/form-editor/preview/index.js +2 -0
  36. package/src/form/form-editor/preview/types.ts +4 -1
  37. package/src/pages/helpers.ts +2 -1
  38. package/src/utils/markdown.ts +29 -3
@@ -1,10 +1,6 @@
1
- import { ComponentType } from '~/src/components/enums.js'
2
- import { HIGHLIGHT_CLASS } from '~/src/form/form-editor/preview/constants.js'
3
- import { ContentElements } from '~/src/form/form-editor/preview/content.js'
1
+ import { PreviewPageControllerBase } from '~/src/form/form-editor/preview/controller/page-controller-base.js'
4
2
  import { mapComponentToPreviewQuestion } from '~/src/form/form-editor/preview/helpers.js'
5
3
  import { Markdown } from '~/src/form/form-editor/preview/markdown.js'
6
- import { hasRepeater } from '~/src/index.js'
7
- import { hasComponents } from '~/src/pages/helpers.js'
8
4
 
9
5
  /**
10
6
  * @type {QuestionRenderer}
@@ -19,125 +15,13 @@ const questionRenderer = {
19
15
  }
20
16
  }
21
17
 
22
- /**
23
- * @implements {PageOverviewElements}
24
- */
25
- export class PagePreviewElements {
26
- /**
27
- * @type {Page}
28
- * @private
29
- */
30
- _page
31
-
32
- /**
33
- * @param {Page} page
34
- */
35
- constructor(page) {
36
- this._page = page
37
- }
38
-
39
- get heading() {
40
- return this._page.title
41
- }
42
-
43
- get guidance() {
44
- if (!hasComponents(this._page) || !this._page.components.length) {
45
- return ''
46
- }
47
-
48
- const [possibleGuidanceComponent] = this._page.components
49
-
50
- return possibleGuidanceComponent.type === ComponentType.Markdown
51
- ? possibleGuidanceComponent.content
52
- : ''
53
- }
54
-
55
- get addHeading() {
56
- return this._page.title.length > 0
57
- }
58
-
59
- get repeatQuestion() {
60
- if (hasRepeater(this._page)) {
61
- return this._page.repeat.options.title
62
- }
63
- return undefined
64
- }
65
-
66
- get hasRepeater() {
67
- return hasRepeater(this._page)
68
- }
69
- }
70
-
71
- /**
72
- * Enum for Highlight classes
73
- * @readonly
74
- * @enum {string}
75
- */
76
- const HighlightClass = {
77
- TITLE: 'title',
78
- GUIDANCE: 'guidance',
79
- REPEATER: 'repeater'
80
- }
81
-
82
- /**
83
- * @implements {PagePreviewPanelMacro}
84
- */
85
- export class PreviewPageController {
86
- static PATH = 'preview-controllers/'
87
- /**
88
- * @type {string}
89
- * @protected
90
- */
91
- _pageTemplate = PreviewPageController.PATH + 'page-controller.njk'
18
+ export class PreviewPageController extends PreviewPageControllerBase {
19
+ static PATH = PreviewPageControllerBase.PATH
92
20
  /**
93
21
  * @protected
94
22
  * @type {Question[]}
95
23
  */
96
24
  _components = []
97
- /**
98
- * @type {string}
99
- */
100
- #title = ''
101
- /**
102
- *
103
- * @type {PageRenderer}
104
- */
105
- #pageRenderer
106
- /**
107
- * @type { undefined | HighlightClass }
108
- * @protected
109
- */
110
- _highlighted = undefined
111
- /**
112
- * @type {string}
113
- * @private
114
- */
115
- _guidanceText = ''
116
- /**
117
- * @type { string }
118
- * @protected
119
- */
120
- _sectionTitle = ''
121
- /**
122
- * @type {Markdown}
123
- * @private
124
- */
125
- _emptyGuidance = PreviewPageController.createGuidanceComponent()
126
- /**
127
- *
128
- * @type {Markdown}
129
- * @protected
130
- */
131
- _guidanceComponent
132
- /**
133
- * @type {boolean}
134
- * @private
135
- */
136
- _showTitle = true
137
- /**
138
- * @type {boolean}
139
- */
140
- #isRepeater = false
141
25
 
142
26
  /**
143
27
  * @param {ComponentDef[]} components
@@ -146,29 +30,26 @@ export class PreviewPageController {
146
30
  * @param {PageRenderer} renderer
147
31
  */
148
32
  constructor(components, elements, definition, renderer) {
33
+ super(elements, renderer)
149
34
  const questions = components.map(
150
35
  mapComponentToPreviewQuestion(questionRenderer, definition)
151
36
  )
152
37
  const firstQuestion = /** @type { Markdown | undefined | Question } */ (
153
38
  questions.shift()
154
39
  )
155
-
156
40
  this._guidanceComponent =
157
41
  PreviewPageController.getOrCreateGuidanceComponent(firstQuestion)
158
42
  this._guidanceText = elements.guidance
159
43
  this._components = this.#constructComponents(firstQuestion, questions)
160
44
  this._showTitle = elements.addHeading
161
-
162
- this.#pageRenderer = renderer
163
- this.#title = elements.heading
164
45
  this._sectionTitle = elements.repeatQuestion ?? ''
165
- this.#isRepeater = elements.hasRepeater
46
+ this._isRepeater = elements.hasRepeater
166
47
  }
167
48
 
168
49
  /**
169
- * @type {typeof HighlightClass}
50
+ * @type {typeof PreviewPageControllerBase.HighlightClass}
170
51
  */
171
- static HighlightClass = HighlightClass
52
+ static HighlightClass = PreviewPageControllerBase.HighlightClass
172
53
 
173
54
  /**
174
55
  * @param { Question | Markdown | undefined} firstQuestion
@@ -181,20 +62,6 @@ export class PreviewPageController {
181
62
  : [firstQuestion, ...questions]
182
63
  }
183
64
 
184
- /**
185
- * @returns {Markdown[]}
186
- * @private
187
- */
188
- get _guidanceComponents() {
189
- if (this._guidanceText.length) {
190
- return [this._guidanceComponent]
191
- }
192
- if (this._highlighted === 'guidance') {
193
- return [this._emptyGuidance]
194
- }
195
- return []
196
- }
197
-
198
65
  /**
199
66
  * @returns {PagePreviewComponent[]}
200
67
  */
@@ -223,7 +90,7 @@ export class PreviewPageController {
223
90
  return false
224
91
  }
225
92
  // |_ one component and title not highlighted
226
- if (this.#title.trim() === this._components[0].question.trim()) {
93
+ if (this._title.trim() === this._components[0]?.question.trim()) {
227
94
  return true
228
95
  }
229
96
  // titles not the same
@@ -267,60 +134,13 @@ export class PreviewPageController {
267
134
  }
268
135
  }
269
136
 
270
- set guidanceText(text) {
271
- this._guidanceText = text
272
- this._guidanceComponent.content = text
273
- this.render()
274
- }
275
-
276
- get guidanceText() {
277
- if (!this._showTitle) {
278
- return ''
279
- }
280
- return this._guidanceText
281
- }
282
-
283
- /**
284
- *
285
- * @param {boolean} showTitle
286
- */
287
- set showTitle(showTitle) {
288
- this._showTitle = showTitle
289
- this.render()
290
- }
291
-
292
- get showTitle() {
293
- return this._showTitle
294
- }
295
-
296
- get guidance() {
297
- return {
298
- text: this.guidanceText,
299
- classes: this.#isHighlighted(HighlightClass.GUIDANCE)
300
- }
301
- }
302
-
303
- /**
304
- * @returns {{ text: string, classes: string }}
305
- */
306
- get pageTitle() {
307
- return {
308
- text: this.title,
309
- classes: this.#isHighlighted(HighlightClass.TITLE)
310
- }
311
- }
312
-
313
- render() {
314
- this.#pageRenderer.render(this._pageTemplate, this)
315
- }
316
-
317
137
  /**
318
138
  * @returns {boolean}
319
139
  */
320
140
  get titleAndFirstTitleSame() {
321
141
  return (
322
142
  this._components.length > 0 &&
323
- this.#title.trim() === this._components[0].question.trim() &&
143
+ this._title.trim() === this._components[0]?.question.trim() &&
324
144
  this.components.length === 1 &&
325
145
  this._highlighted !== 'title'
326
146
  )
@@ -328,58 +148,28 @@ export class PreviewPageController {
328
148
 
329
149
  /**
330
150
  * @returns {string}
151
+ * @protected
331
152
  */
332
- get title() {
153
+ _getTitle() {
333
154
  if (!this._showTitle || this.titleAndFirstTitleSame) {
334
155
  return ''
335
156
  }
336
- if (this.#title.length) {
337
- return this.#title
338
- }
339
- return 'Page heading'
157
+ return super._getTitle()
340
158
  }
341
159
 
342
160
  /**
343
- * @param {string} value
344
- */
345
- set title(value) {
346
- this.#title = value
347
- this.render()
348
- }
349
-
350
- highlightTitle() {
351
- this.setHighLighted(HighlightClass.TITLE)
352
- }
353
-
354
- setRepeater() {
355
- this.#isRepeater = true
356
- this.render()
357
- }
358
-
359
- unsetRepeater() {
360
- this.#isRepeater = false
361
- this.render()
362
- }
363
-
364
- get isRepeater() {
365
- return this.#isRepeater
366
- }
367
-
368
- /**
369
- * @returns {{classes: string, text: string} | undefined}
161
+ * @returns {string}
162
+ * @protected
370
163
  */
371
- get sectionTitle() {
372
- if (this.sectionTitleText === undefined) {
373
- return undefined
374
- }
375
- return {
376
- classes: this.#isHighlighted(HighlightClass.REPEATER),
377
- text: this.sectionTitleText
164
+ _getGuidanceText() {
165
+ if (!this._showTitle) {
166
+ return ''
378
167
  }
168
+ return super._getGuidanceText()
379
169
  }
380
170
 
381
171
  get repeaterText() {
382
- if (!this.#isRepeater) {
172
+ if (!this._isRepeater) {
383
173
  return undefined
384
174
  }
385
175
  if (!this._sectionTitle.length) {
@@ -389,15 +179,11 @@ export class PreviewPageController {
389
179
  }
390
180
 
391
181
  /**
392
- * @param {string | undefined} val
182
+ * @returns {string|undefined}
183
+ * @protected
393
184
  */
394
- set sectionTitleText(val) {
395
- this._sectionTitle = val ?? ''
396
- this.render()
397
- }
398
-
399
- get sectionTitleText() {
400
- if (this.#isRepeater) {
185
+ _getSectionTitleText() {
186
+ if (this._isRepeater) {
401
187
  return this.repeaterText
402
188
  }
403
189
  return undefined
@@ -408,13 +194,15 @@ export class PreviewPageController {
408
194
  return undefined
409
195
  }
410
196
  return {
411
- classes: this.#isHighlighted(HighlightClass.REPEATER),
197
+ classes: this._isHighlighted(
198
+ PreviewPageControllerBase.HighlightClass.REPEATER
199
+ ),
412
200
  text: this.repeaterButtonText
413
201
  }
414
202
  }
415
203
 
416
204
  get repeaterButtonText() {
417
- if (!this.#isRepeater) {
205
+ if (!this._isRepeater) {
418
206
  return undefined
419
207
  }
420
208
 
@@ -428,26 +216,6 @@ export class PreviewPageController {
428
216
  return firstToken.toLowerCase() + restOfStr
429
217
  }
430
218
 
431
- /**
432
- * Creates a dummy component for when guidance is highlighted
433
- * but no guidance text exists
434
- * @returns {Markdown}
435
- */
436
- static createGuidanceComponent() {
437
- const guidanceElement = new ContentElements({
438
- type: ComponentType.Markdown,
439
- title: 'Guidance component',
440
- name: 'guidanceComponent',
441
- content: 'Guidance text',
442
- options: {}
443
- })
444
- const guidanceComponent = new Markdown(guidanceElement, questionRenderer)
445
-
446
- // the dummy component should always be highlighted
447
- guidanceComponent.highlightContent()
448
- return guidanceComponent
449
- }
450
-
451
219
  /**
452
220
  * Helper method to return the guidance or a new one
453
221
  * @param { Markdown | Question | undefined } guidanceComponent
@@ -458,35 +226,7 @@ export class PreviewPageController {
458
226
  if (guidanceComponent instanceof Markdown) {
459
227
  return guidanceComponent
460
228
  }
461
- return PreviewPageController.createGuidanceComponent()
462
- }
463
-
464
- highlightGuidance() {
465
- this._guidanceComponent.highlightContent()
466
- this.setHighLighted(HighlightClass.GUIDANCE)
467
- }
468
-
469
- /**
470
- * @param {HighlightClass} highlightSection
471
- */
472
- setHighLighted(highlightSection) {
473
- this._highlighted = highlightSection
474
- this.render()
475
- }
476
-
477
- clearHighlight() {
478
- this._highlighted = undefined
479
-
480
- this._guidanceComponent.unHighlightContent()
481
- this.render()
482
- }
483
-
484
- /**
485
- * @param {string} field
486
- * @returns {string}
487
- */
488
- #isHighlighted(field) {
489
- return this._highlighted === field ? HIGHLIGHT_CLASS : ''
229
+ return PreviewPageControllerBase.createGuidanceComponent()
490
230
  }
491
231
  }
492
232
 
@@ -21,3 +21,5 @@ export * from '~/src/form/form-editor/preview/long-answer.js'
21
21
  export * from '~/src/form/form-editor/preview/uk-address.js'
22
22
  export * from '~/src/form/form-editor/preview/yes-no.js'
23
23
  export * from '~/src/form/form-editor/preview/controller/page-controller.js'
24
+ export * from '~/src/form/form-editor/preview/controller/guidance-page-controller.js'
25
+ export * from '~/src/form/form-editor/preview/controller/page-controller-base.js'
@@ -88,9 +88,12 @@ export interface ListElements extends QuestionElements {
88
88
  afterInputsHTML: string
89
89
  }
90
90
 
91
- export interface PageOverviewElements {
91
+ export interface PagePreviewBaseElements {
92
92
  heading: string
93
93
  guidance: string
94
+ }
95
+
96
+ export interface PageOverviewElements extends PagePreviewBaseElements {
94
97
  addHeading: boolean
95
98
  repeatQuestion: string | undefined
96
99
  hasRepeater: boolean
@@ -1,3 +1,5 @@
1
+ import { ComponentType } from '~/src/components/enums.js'
2
+ import { hasFormField } from '~/src/components/helpers.js'
1
3
  import { type ComponentDef } from '~/src/components/types.js'
2
4
  import {
3
5
  type Link,
@@ -6,7 +8,6 @@ import {
6
8
  type PageQuestion,
7
9
  type PageRepeat
8
10
  } from '~/src/form/form-definition/types.js'
9
- import { ComponentType, hasFormField } from '~/src/index.js'
10
11
  import {
11
12
  ControllerNames,
12
13
  ControllerTypes
@@ -1,4 +1,4 @@
1
- import { Marked } from 'marked'
1
+ import { Marked, Renderer, type Tokens } from 'marked'
2
2
 
3
3
  /**
4
4
  * Marked instance (avoids global option/extension scope)
@@ -8,10 +8,31 @@ export const marked = new Marked({
8
8
  gfm: true
9
9
  })
10
10
 
11
+ function renderLink(href: string, text: string, baseUrl?: string) {
12
+ let isLocalLink = true
13
+
14
+ if (baseUrl) {
15
+ isLocalLink = href.startsWith(baseUrl) || href.startsWith('mailto:')
16
+ }
17
+
18
+ const attrs = [`class="govuk-link"`, `href="${href}"`]
19
+
20
+ if (!isLocalLink) {
21
+ attrs.push(`target="_blank" rel="noreferrer noopener"`)
22
+ }
23
+
24
+ const label = !isLocalLink ? `${text} (opens in new tab)` : text
25
+
26
+ return `<a ${attrs.join(' ')}>${label}</a>`
27
+ }
28
+
11
29
  /**
12
30
  * Convert markdown to HTML, escaping any HTML tags first
13
31
  */
14
- export function markdownToHtml(markdown?: string | null) {
32
+ export function markdownToHtml(
33
+ markdown: string | null | undefined,
34
+ baseUrl?: string // optional in some contexts, e.g. from the designer where it might not make sense
35
+ ) {
15
36
  if (markdown === undefined || markdown === null) {
16
37
  return ''
17
38
  }
@@ -23,5 +44,10 @@ export function markdownToHtml(markdown?: string | null) {
23
44
  .replace(/"/g, '&quot;')
24
45
  .replace(/'/g, '&#39;')
25
46
 
26
- return marked.parse(escaped, { async: false })
47
+ const renderer = new Renderer()
48
+ renderer.link = ({ href, text }: Tokens.Link): string => {
49
+ return renderLink(href, text, baseUrl)
50
+ }
51
+
52
+ return marked.parse(escaped, { async: false, renderer })
27
53
  }