@asd20/ui-next 2.30.0 → 2.31.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/CHANGELOG.md +21 -0
- package/README.md +4 -0
- package/components.js +2 -0
- package/helpers.js +8 -0
- package/package.json +2 -1
- package/src/components/atoms/Asd20Messaging/index.vue +6 -1
- package/src/components/atoms/Asd20RichBodyContent/headingContext.js +255 -0
- package/src/components/atoms/Asd20RichBodyContent/index.vue +31 -0
- package/src/components/atoms/Asd20RichBodyContent/messageDocumentRenderer.js +199 -45
- package/src/components/molecules/Asd20Accordion/index.vue +38 -14
- package/src/components/molecules/Asd20BodyAccordion/index.vue +23 -2
- package/src/components/molecules/Asd20PrintContents/index.vue +42 -0
- package/src/components/molecules/Asd20PrintHeader/index.vue +54 -0
- package/src/components/molecules/Asd20PublicFormField/index.vue +149 -33
- package/src/components/molecules/Asd20TableauEmbed/index.vue +1 -0
- package/src/components/organisms/Asd20PublicFormModal/index.vue +9 -0
- package/src/components/templates/Asd20HandbookDocument/index.vue +85 -0
- package/src/components/templates/Asd20HandbookTemplate/index.vue +121 -69
- package/src/design/_document-print.scss +104 -0
- package/src/design/_print.scss +2 -0
- package/src/helpers/documentPrint.js +184 -0
- package/src/helpers/handbookDocument.js +44 -0
- package/src/helpers/publicFormDefinition.js +13 -6
- package/src/helpers/publicationIntroduction.js +53 -0
- package/templates.js +1 -0
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
class="asd20-template-handbook"
|
|
4
|
+
:data-asd20-document-root="documentPrint ? publicationDocumentId : undefined"
|
|
5
|
+
>
|
|
3
6
|
<asd20-skip-to to="#handbook-content" />
|
|
4
7
|
|
|
5
8
|
<asd20-site-navigation
|
|
9
|
+
:data-asd20-print-hide="documentPrint ? '' : undefined"
|
|
6
10
|
v-bind="siteNavigationSearchProps"
|
|
7
11
|
:navigation="navigation"
|
|
8
12
|
:action-items="actionItems"
|
|
@@ -15,6 +19,7 @@
|
|
|
15
19
|
/>
|
|
16
20
|
|
|
17
21
|
<asd20-notification-group
|
|
22
|
+
:data-asd20-print-hide="documentPrint ? '' : undefined"
|
|
18
23
|
:notifications="activeNotificationsByType.banner"
|
|
19
24
|
group-type="banner"
|
|
20
25
|
@dismiss="$emit('dismiss-notification', $event)"
|
|
@@ -42,7 +47,18 @@
|
|
|
42
47
|
</template>
|
|
43
48
|
</asd20-page-header>
|
|
44
49
|
|
|
50
|
+
<slot name="document-actions" />
|
|
51
|
+
|
|
52
|
+
<asd20-print-header
|
|
53
|
+
v-if="documentPrint"
|
|
54
|
+
:organization-title="organization?.title || organization?.name || ''"
|
|
55
|
+
:canonical-url="publicationDocumentUrl"
|
|
56
|
+
v-bind="printMetadata"
|
|
57
|
+
title=""
|
|
58
|
+
/>
|
|
59
|
+
|
|
45
60
|
<asd20-notification-group
|
|
61
|
+
:data-asd20-print-hide="documentPrint ? '' : undefined"
|
|
46
62
|
:notifications="activeNotificationsByType.inline"
|
|
47
63
|
group-type="inline"
|
|
48
64
|
@dismiss="$emit('dismiss-notification', $event)"
|
|
@@ -67,34 +83,34 @@
|
|
|
67
83
|
>
|
|
68
84
|
Topics
|
|
69
85
|
</button>
|
|
70
|
-
<
|
|
86
|
+
<div class="asd20-template-handbook__index-heading">
|
|
71
87
|
Topics
|
|
72
|
-
</
|
|
88
|
+
</div>
|
|
73
89
|
<ol
|
|
74
90
|
id="handbook-topics"
|
|
75
91
|
:class="{ 'is-open': topicsOpen }"
|
|
76
92
|
>
|
|
77
93
|
<li
|
|
78
|
-
v-for="
|
|
79
|
-
:key="
|
|
94
|
+
v-for="entry in topicEntries"
|
|
95
|
+
:key="entry.key"
|
|
80
96
|
>
|
|
81
97
|
<div class="asd20-template-handbook__topic-row">
|
|
82
98
|
<button
|
|
83
99
|
class="asd20-template-handbook__topic-button"
|
|
84
100
|
type="button"
|
|
85
|
-
:aria-current="
|
|
86
|
-
@click="selectTopic(
|
|
101
|
+
:aria-current="activeEntry && activeEntry.key === entry.key ? 'true' : null"
|
|
102
|
+
@click="selectTopic(entry.key)"
|
|
87
103
|
>
|
|
88
|
-
{{ message.heading || message.title }}
|
|
104
|
+
{{ entry.message.heading || entry.message.title }}
|
|
89
105
|
</button>
|
|
90
106
|
<button
|
|
91
|
-
v-if="subtopicsFor(
|
|
107
|
+
v-if="subtopicsFor(entry.key).length"
|
|
92
108
|
class="asd20-template-handbook__expand-button"
|
|
93
109
|
type="button"
|
|
94
|
-
:aria-expanded="expandedTopicId ===
|
|
95
|
-
:aria-controls="
|
|
96
|
-
:aria-label="`${expandedTopicId ===
|
|
97
|
-
@click="toggleTopic(
|
|
110
|
+
:aria-expanded="expandedTopicId === entry.key"
|
|
111
|
+
:aria-controls="entry.subtopicsId"
|
|
112
|
+
:aria-label="`${expandedTopicId === entry.key ? 'Hide' : 'Show'} sections in ${entry.message.heading || entry.message.title}`"
|
|
113
|
+
@click="toggleTopic(entry.key)"
|
|
98
114
|
>
|
|
99
115
|
<asd20-icon
|
|
100
116
|
name="chevron"
|
|
@@ -104,19 +120,19 @@
|
|
|
104
120
|
</button>
|
|
105
121
|
</div>
|
|
106
122
|
<ol
|
|
107
|
-
v-if="subtopicsFor(
|
|
108
|
-
v-show="expandedTopicId ===
|
|
109
|
-
:id="
|
|
123
|
+
v-if="subtopicsFor(entry.key).length"
|
|
124
|
+
v-show="expandedTopicId === entry.key"
|
|
125
|
+
:id="entry.subtopicsId"
|
|
110
126
|
class="asd20-template-handbook__subtopics"
|
|
111
127
|
>
|
|
112
128
|
<li
|
|
113
|
-
v-for="subtopic in subtopicsFor(
|
|
114
|
-
:key="subtopic.
|
|
129
|
+
v-for="subtopic in subtopicsFor(entry.key)"
|
|
130
|
+
:key="subtopic.id"
|
|
115
131
|
>
|
|
116
132
|
<button
|
|
117
133
|
class="asd20-template-handbook__subtopic-button"
|
|
118
134
|
type="button"
|
|
119
|
-
@click="selectSubtopic(
|
|
135
|
+
@click="selectSubtopic(entry.key, subtopic.id)"
|
|
120
136
|
>
|
|
121
137
|
{{ subtopic.label }}
|
|
122
138
|
</button>
|
|
@@ -126,30 +142,46 @@
|
|
|
126
142
|
</ol>
|
|
127
143
|
</nav>
|
|
128
144
|
|
|
145
|
+
<p
|
|
146
|
+
v-if="publicationDiagnostics.length"
|
|
147
|
+
class="asd20-template-handbook__publication-review"
|
|
148
|
+
role="status"
|
|
149
|
+
>
|
|
150
|
+
This handbook has content that needs publication review before PDF export.
|
|
151
|
+
</p>
|
|
152
|
+
|
|
153
|
+
<asd20-print-contents
|
|
154
|
+
v-if="documentPrint"
|
|
155
|
+
:headings="printHeadings"
|
|
156
|
+
/>
|
|
157
|
+
|
|
129
158
|
<article
|
|
130
|
-
v-for="
|
|
131
|
-
:key="
|
|
132
|
-
:data-message-id="message.id"
|
|
159
|
+
v-for="entry in topicEntries"
|
|
160
|
+
:key="entry.key"
|
|
161
|
+
:data-message-id="entry.message.id"
|
|
162
|
+
:data-topic-key="entry.key"
|
|
133
163
|
class="asd20-template-handbook__topic"
|
|
134
|
-
:class="{ 'is-active':
|
|
164
|
+
:class="{ 'is-active': activeEntry && activeEntry.key === entry.key }"
|
|
135
165
|
>
|
|
136
|
-
<h2>
|
|
137
|
-
{{ message.heading || message.title }}
|
|
166
|
+
<h2 :id="entry.titleId">
|
|
167
|
+
{{ entry.message.heading || entry.message.title }}
|
|
138
168
|
</h2>
|
|
139
169
|
<div
|
|
140
|
-
v-if="topicSummary(message)"
|
|
170
|
+
v-if="topicSummary(entry.message)"
|
|
141
171
|
class="asd20-template-handbook__summary"
|
|
142
|
-
v-html="topicSummary(message)"
|
|
172
|
+
v-html="topicSummary(entry.message)"
|
|
143
173
|
/>
|
|
144
174
|
<hr
|
|
145
|
-
v-if="topicSummary(message)"
|
|
175
|
+
v-if="topicSummary(entry.message)"
|
|
146
176
|
class="asd20-template-handbook__summary-divider"
|
|
147
177
|
/>
|
|
148
178
|
<asd20-messaging
|
|
149
|
-
v-bind="message"
|
|
179
|
+
v-bind="entry.message"
|
|
150
180
|
heading=""
|
|
151
181
|
short-description=""
|
|
152
182
|
detail-link=""
|
|
183
|
+
:heading-level-offset="1"
|
|
184
|
+
:heading-id-prefix="entry.key"
|
|
153
185
|
/>
|
|
154
186
|
</article>
|
|
155
187
|
<p v-if="!topicMessages.length">
|
|
@@ -177,7 +209,12 @@ import Asd20SiteNavigation from '../../../components/organisms/Asd20SiteNavigati
|
|
|
177
209
|
import Asd20NotificationGroup from '../../../components/organisms/Asd20NotificationGroup'
|
|
178
210
|
import Asd20QuicklinksMenu from '../../../components/organisms/Asd20QuicklinksMenu'
|
|
179
211
|
import Asd20PageFooter from '../../../components/organisms/Asd20PageFooter'
|
|
212
|
+
import Asd20PrintHeader from '../../molecules/Asd20PrintHeader'
|
|
213
|
+
import Asd20PrintContents from '../../molecules/Asd20PrintContents'
|
|
214
|
+
import { buildHandbookDocument } from '../../../helpers/handbookDocument'
|
|
215
|
+
import { installDocumentPrintLifecycle } from '../../../helpers/documentPrint'
|
|
180
216
|
import pageTemplateMixin from '../../../mixins/pageTemplateMixin'
|
|
217
|
+
import { publicationHeadingPlanKey } from '../../atoms/Asd20RichBodyContent/headingContext'
|
|
181
218
|
|
|
182
219
|
export default {
|
|
183
220
|
name: 'Asd20HandbookTemplate',
|
|
@@ -190,63 +227,71 @@ export default {
|
|
|
190
227
|
Asd20NotificationGroup,
|
|
191
228
|
Asd20QuicklinksMenu,
|
|
192
229
|
Asd20PageFooter,
|
|
230
|
+
Asd20PrintHeader,
|
|
231
|
+
Asd20PrintContents,
|
|
193
232
|
},
|
|
194
233
|
mixins: [pageTemplateMixin],
|
|
234
|
+
provide() {
|
|
235
|
+
return { [publicationHeadingPlanKey]: () => this.publicationHeadings.idPlan }
|
|
236
|
+
},
|
|
237
|
+
props: {
|
|
238
|
+
documentPrint: { type: Boolean, default: false },
|
|
239
|
+
printMetadata: { type: Object, default: () => ({}) },
|
|
240
|
+
publicationDocumentId: { type: String, default: 'handbook' },
|
|
241
|
+
publicationDocumentUrl: { type: String, default: '' },
|
|
242
|
+
publicationReservedIds: { type: Array, default: () => [] },
|
|
243
|
+
},
|
|
195
244
|
data: () => ({
|
|
196
245
|
selectedMessageId: null,
|
|
197
246
|
expandedTopicId: null,
|
|
198
|
-
subtopicsByMessageId: {},
|
|
199
247
|
topicsOpen: false,
|
|
200
248
|
}),
|
|
201
249
|
computed: {
|
|
202
|
-
|
|
203
|
-
return
|
|
250
|
+
handbookDocument() {
|
|
251
|
+
return buildHandbookDocument({
|
|
252
|
+
messages: this.messages, metaDescription: this.metaDescription,
|
|
253
|
+
documentId: this.publicationDocumentId, documentUrl: this.publicationDocumentUrl,
|
|
254
|
+
reservedIds: this.publicationReservedIds,
|
|
255
|
+
})
|
|
204
256
|
},
|
|
205
|
-
|
|
206
|
-
|
|
257
|
+
introduction() { return this.handbookDocument.introduction },
|
|
258
|
+
publicationDiagnostics() { return this.handbookDocument.diagnostics },
|
|
259
|
+
printHeadings() { return this.handbookDocument.headings },
|
|
260
|
+
handbookLead() { return this.introduction.lead },
|
|
261
|
+
topicMessages() { return this.messages.slice(1) },
|
|
262
|
+
topicEntries() { return this.handbookDocument.entries },
|
|
263
|
+
publicationHeadings() { return this.handbookDocument.headingModels },
|
|
264
|
+
activeEntry() {
|
|
265
|
+
return this.topicEntries.find(entry => entry.key === this.selectedMessageId)
|
|
266
|
+
|| this.topicEntries[0] || null
|
|
207
267
|
},
|
|
208
268
|
activeMessage() {
|
|
209
|
-
return this.
|
|
210
|
-
|| this.topicMessages[0]
|
|
211
|
-
|| null
|
|
269
|
+
return this.activeEntry?.message || null
|
|
212
270
|
},
|
|
213
271
|
},
|
|
214
272
|
watch: {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
},
|
|
219
|
-
deep: true,
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
mounted() {
|
|
223
|
-
this.$nextTick(() => this.refreshSubtopics())
|
|
273
|
+
documentPrint() { this.updatePrintLifecycle() },
|
|
274
|
+
messages: { deep: true, handler() { this.updatePrintLifecycle() } },
|
|
275
|
+
publicationDocumentId() { this.updatePrintLifecycle() },
|
|
224
276
|
},
|
|
277
|
+
mounted() { this.updatePrintLifecycle() },
|
|
278
|
+
beforeUnmount() { this.stopPrintLifecycle?.() },
|
|
225
279
|
methods: {
|
|
280
|
+
updatePrintLifecycle() {
|
|
281
|
+
this.stopPrintLifecycle?.()
|
|
282
|
+
this.stopPrintLifecycle = this.documentPrint ? installDocumentPrintLifecycle(this.$el) : null
|
|
283
|
+
},
|
|
226
284
|
topicSummary(message) {
|
|
227
285
|
return message.shortDescription || message.summary || ''
|
|
228
286
|
},
|
|
229
|
-
topicArticle(
|
|
287
|
+
topicArticle(topicKey) {
|
|
230
288
|
return Array.from(this.$el.querySelectorAll('.asd20-template-handbook__topic'))
|
|
231
|
-
.find(article => article.dataset.
|
|
289
|
+
.find(article => article.dataset.topicKey === topicKey)
|
|
232
290
|
},
|
|
233
|
-
subtopicsFor(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const nextSubtopics = {}
|
|
238
|
-
this.topicMessages.forEach(message => {
|
|
239
|
-
const article = this.topicArticle(message.id)
|
|
240
|
-
if (!article) return
|
|
241
|
-
const headings = Array.from(article.querySelectorAll('.asd20-rich-body-content h2'))
|
|
242
|
-
const subtopics = headings
|
|
243
|
-
.map((heading, index) => ({ index, label: heading.textContent.trim() }))
|
|
244
|
-
.filter(subtopic => subtopic.label)
|
|
245
|
-
if (subtopics.length) nextSubtopics[message.id] = subtopics
|
|
246
|
-
})
|
|
247
|
-
if (JSON.stringify(nextSubtopics) !== JSON.stringify(this.subtopicsByMessageId)) {
|
|
248
|
-
this.subtopicsByMessageId = nextSubtopics
|
|
249
|
-
}
|
|
291
|
+
subtopicsFor(topicKey) {
|
|
292
|
+
const index = this.topicEntries.findIndex(entry => entry.key === topicKey)
|
|
293
|
+
return this.publicationHeadings.models[index]?.headings
|
|
294
|
+
.filter(heading => heading.authoredLevel === 2 && heading.label) || []
|
|
250
295
|
},
|
|
251
296
|
toggleTopic(messageId) {
|
|
252
297
|
this.expandedTopicId = this.expandedTopicId === messageId ? null : messageId
|
|
@@ -258,14 +303,21 @@ export default {
|
|
|
258
303
|
await this.$nextTick()
|
|
259
304
|
this.topicArticle(messageId)?.scrollIntoView({ block: 'start' })
|
|
260
305
|
},
|
|
261
|
-
async selectSubtopic(messageId,
|
|
306
|
+
async selectSubtopic(messageId, headingId) {
|
|
262
307
|
this.selectedMessageId = messageId
|
|
263
308
|
this.expandedTopicId = messageId
|
|
264
309
|
this.topicsOpen = false
|
|
265
310
|
await this.$nextTick()
|
|
266
311
|
const article = this.topicArticle(messageId)
|
|
267
|
-
const
|
|
268
|
-
|
|
312
|
+
const heading = Array.from(article?.querySelectorAll('[id]') || [])
|
|
313
|
+
.find(element => element.id === headingId)
|
|
314
|
+
const toggle = heading?.closest('.asd20-accordion')
|
|
315
|
+
?.querySelector('[aria-expanded="false"][aria-controls]')
|
|
316
|
+
if (toggle) {
|
|
317
|
+
toggle.click()
|
|
318
|
+
await this.$nextTick()
|
|
319
|
+
}
|
|
320
|
+
heading?.scrollIntoView({ block: 'start' })
|
|
269
321
|
},
|
|
270
322
|
},
|
|
271
323
|
}
|
|
@@ -411,7 +463,7 @@ export default {
|
|
|
411
463
|
scroll-margin-top: space(2);
|
|
412
464
|
}
|
|
413
465
|
|
|
414
|
-
&__topic .asd20-rich-body-content
|
|
466
|
+
&__topic .asd20-rich-body-content h3 {
|
|
415
467
|
scroll-margin-top: space(2);
|
|
416
468
|
}
|
|
417
469
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Additive overrides: every content rule requires BOTH markers. The legacy
|
|
2
|
+
// _print.scss rules remain intact, including its existing unnamed page margin.
|
|
3
|
+
@media print {
|
|
4
|
+
@page asd20-document {
|
|
5
|
+
size: letter portrait;
|
|
6
|
+
margin: 0.5in;
|
|
7
|
+
@bottom-center {
|
|
8
|
+
content: 'Page ' counter(page) ' of ' counter(pages);
|
|
9
|
+
font-family: Arial, sans-serif;
|
|
10
|
+
font-size: 9pt;
|
|
11
|
+
color: #111;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
html[data-asd20-print-profile='document'] [data-asd20-document-root] {
|
|
16
|
+
page: asd20-document;
|
|
17
|
+
display: block !important;
|
|
18
|
+
color: #111 !important;
|
|
19
|
+
font-size: 10pt !important;
|
|
20
|
+
line-height: 1.45 !important;
|
|
21
|
+
print-color-adjust: exact;
|
|
22
|
+
-webkit-print-color-adjust: exact;
|
|
23
|
+
|
|
24
|
+
[data-asd20-print-hide], .asd20-print-hide,
|
|
25
|
+
.asd20-template-handbook__index, .asd20-accordion__header button,
|
|
26
|
+
.asd20-accordion__header .asd20-badge,
|
|
27
|
+
.asd20-message-video__frame, .asd20-tableau-embed__frame,
|
|
28
|
+
iframe, video, tableau-viz {
|
|
29
|
+
display: none !important;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.asd20-print-only { display: block !important; }
|
|
33
|
+
.asd20-print-contents__label { font-weight: bold; }
|
|
34
|
+
.asd20-print-contents li { break-inside: avoid; }
|
|
35
|
+
.asd20-print-contents ol { list-style: none; margin-left: 0; padding-left: 0; }
|
|
36
|
+
.asd20-print-contents a { display: flex; justify-content: space-between; gap: 1em; }
|
|
37
|
+
.asd20-print-contents__page { flex: 0 0 5ch; text-align: right; font-variant-numeric: tabular-nums; }
|
|
38
|
+
.asd20-handbook-document > header h1 { margin-top: 0 !important; }
|
|
39
|
+
|
|
40
|
+
.asd20-template-handbook__layout,
|
|
41
|
+
.asd20-template-handbook__topic,
|
|
42
|
+
.asd20-messaging, .asd20-messaging__content,
|
|
43
|
+
.asd20-accordion__content, .asd20-accordion__content-inner,
|
|
44
|
+
.asd20-accordion__viewport, .tableWrapper {
|
|
45
|
+
display: block !important;
|
|
46
|
+
position: static !important;
|
|
47
|
+
overflow: visible !important;
|
|
48
|
+
transform: none !important;
|
|
49
|
+
height: auto !important;
|
|
50
|
+
max-height: none !important;
|
|
51
|
+
min-height: 0 !important;
|
|
52
|
+
opacity: 1 !important;
|
|
53
|
+
break-inside: auto !important;
|
|
54
|
+
transition: none !important;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.asd20-template-handbook__topic { margin: 1.5rem 0 !important; }
|
|
58
|
+
h1, h2, h3, h4, h5, h6, caption, .asd20-accordion__header {
|
|
59
|
+
break-after: avoid-page;
|
|
60
|
+
break-inside: avoid-page;
|
|
61
|
+
}
|
|
62
|
+
p { orphans: 3; widows: 3; }
|
|
63
|
+
blockquote { break-inside: auto; }
|
|
64
|
+
.asd20-print-keep { break-inside: avoid-page !important; }
|
|
65
|
+
.asd20-print-break-before { break-before: page; }
|
|
66
|
+
|
|
67
|
+
img, .message-image {
|
|
68
|
+
display: block !important;
|
|
69
|
+
max-width: 100% !important;
|
|
70
|
+
height: auto !important;
|
|
71
|
+
object-fit: contain !important;
|
|
72
|
+
break-inside: avoid-page;
|
|
73
|
+
}
|
|
74
|
+
.asd20-message-image-gallery__images {
|
|
75
|
+
display: block !important;
|
|
76
|
+
overflow: visible !important;
|
|
77
|
+
max-height: none !important;
|
|
78
|
+
}
|
|
79
|
+
.asd20-message-image-gallery__images > img { max-width: 100% !important; max-height: none !important; }
|
|
80
|
+
table {
|
|
81
|
+
width: 100% !important;
|
|
82
|
+
min-width: 0 !important;
|
|
83
|
+
table-layout: fixed;
|
|
84
|
+
border-collapse: collapse;
|
|
85
|
+
break-inside: auto;
|
|
86
|
+
font-size: 9pt;
|
|
87
|
+
}
|
|
88
|
+
thead { display: table-header-group; }
|
|
89
|
+
tfoot { display: table-footer-group; }
|
|
90
|
+
tr { break-inside: avoid-page; }
|
|
91
|
+
th, td { min-width: 0 !important; overflow-wrap: anywhere; padding: 0.3em !important; border: 1px solid #888; }
|
|
92
|
+
th { background: #eee !important; color: #111 !important; }
|
|
93
|
+
a { overflow-wrap: anywhere; text-decoration: underline !important; break-inside: auto; }
|
|
94
|
+
a[href='/'] { display: inline !important; }
|
|
95
|
+
a::after { content: none !important; display: none !important; }
|
|
96
|
+
a[data-asd20-print-source]::after {
|
|
97
|
+
content: ' (' attr(data-asd20-print-source) ')' !important;
|
|
98
|
+
display: inline !important;
|
|
99
|
+
font-size: 9pt;
|
|
100
|
+
}
|
|
101
|
+
pre { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
102
|
+
ul, ol { columns: auto !important; }
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/design/_print.scss
CHANGED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { nextTick } from 'vue'
|
|
2
|
+
|
|
3
|
+
const blocks = new WeakMap()
|
|
4
|
+
const sessions = new WeakMap()
|
|
5
|
+
|
|
6
|
+
// Component registration is private; preparation is reusable by any opted-in host.
|
|
7
|
+
export function registerDocumentPrintBlock(element, prepare) {
|
|
8
|
+
blocks.set(element, prepare)
|
|
9
|
+
return () => blocks.delete(element)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function setTemporaryAttribute(element, name, value, undo) {
|
|
13
|
+
const prior = element.getAttribute(name)
|
|
14
|
+
undo.push(() => prior === null ? element.removeAttribute(name) : element.setAttribute(name, prior))
|
|
15
|
+
value === null ? element.removeAttribute(name) : element.setAttribute(name, value)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function prepareTables(root, undo, diagnostics) {
|
|
19
|
+
for (const table of root.querySelectorAll('table')) {
|
|
20
|
+
if (table.tHead || !table.tBodies.length) continue
|
|
21
|
+
const body = table.tBodies[0]
|
|
22
|
+
const rows = []
|
|
23
|
+
// A column header group is a contiguous leading run of all-TH rows. Row
|
|
24
|
+
// headers/mixed cells stay in tbody; never infer headers from bold styling.
|
|
25
|
+
for (const row of body.rows) {
|
|
26
|
+
if (!row.cells.length || !Array.from(row.cells).every(cell => cell.tagName === 'TH' &&
|
|
27
|
+
!['row', 'rowgroup'].includes(cell.getAttribute('scope')))) break
|
|
28
|
+
rows.push(row)
|
|
29
|
+
}
|
|
30
|
+
if (!rows.length) continue
|
|
31
|
+
if (rows.some((row, index) => Array.from(row.cells).some(cell => cell.rowSpan === 0 || cell.rowSpan + index > rows.length))) {
|
|
32
|
+
diagnostics.push({ code: 'table-header-span-unmapped' })
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
const following = rows.at(-1).nextSibling
|
|
36
|
+
const head = table.ownerDocument.createElement('thead')
|
|
37
|
+
body.before(head)
|
|
38
|
+
rows.forEach(row => head.append(row))
|
|
39
|
+
undo.push(() => { rows.forEach(row => body.insertBefore(row, following)); head.remove() })
|
|
40
|
+
for (const row of rows) for (const cell of row.cells) {
|
|
41
|
+
if (!cell.hasAttribute('scope')) setTemporaryAttribute(cell, 'scope', cell.colSpan > 1 ? 'colgroup' : 'col', undo)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function prepareEmbeds(root, undo, diagnostics) {
|
|
47
|
+
const doc = root.ownerDocument
|
|
48
|
+
for (const embed of root.querySelectorAll('.asd20-message-video, .asd20-tableau-embed')) {
|
|
49
|
+
const video = embed.classList.contains('asd20-message-video')
|
|
50
|
+
const source = video ? embed.dataset.src || embed.querySelector('iframe')?.src : embed.dataset.printSrc
|
|
51
|
+
const description = video ? embed.dataset.descriptiveText || embed.querySelector('.asd20-message-video__description')?.textContent :
|
|
52
|
+
embed.querySelector('.asd20-tableau-embed__description')?.textContent
|
|
53
|
+
const title = video ? embed.dataset.title || embed.querySelector('iframe')?.title : 'Interactive data visualization'
|
|
54
|
+
const alternative = doc.createElement('div')
|
|
55
|
+
alternative.className = 'asd20-print-only asd20-print-embed-alternative'
|
|
56
|
+
const text = doc.createElement('p')
|
|
57
|
+
text.textContent = `${video ? 'Video' : 'Interactive data'}: ${title || 'Untitled'}. ${description ? '' : 'A descriptive text alternative has not been supplied.'}`
|
|
58
|
+
alternative.append(text)
|
|
59
|
+
if (!description?.trim()) diagnostics.push({ code: 'embed-description-missing', source: source || '' })
|
|
60
|
+
let url
|
|
61
|
+
try { url = new URL(source) } catch { /* Report a missing safe source below. */ }
|
|
62
|
+
if (url && ['https:', 'http:'].includes(url.protocol)) {
|
|
63
|
+
const link = doc.createElement('a')
|
|
64
|
+
link.href = url.href
|
|
65
|
+
link.textContent = `${video ? 'Watch video' : 'View interactive data'}: ${url.href}`
|
|
66
|
+
alternative.append(link)
|
|
67
|
+
} else diagnostics.push({ code: 'embed-source-missing' })
|
|
68
|
+
embed.append(alternative)
|
|
69
|
+
undo.push(() => alternative.remove())
|
|
70
|
+
}
|
|
71
|
+
for (const iframe of root.querySelectorAll('iframe')) {
|
|
72
|
+
if (!iframe.closest('.asd20-message-video, .asd20-tableau-embed')) diagnostics.push({ code: 'embed-alternative-unmapped', source: iframe.src })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function beginDocumentPrint(root) {
|
|
77
|
+
if (!root?.hasAttribute('data-asd20-document-root')) throw new TypeError('Document printing requires an explicit document root.')
|
|
78
|
+
const doc = root.ownerDocument
|
|
79
|
+
if (sessions.has(doc)) throw new Error('A document print session is already active.')
|
|
80
|
+
const undo = []
|
|
81
|
+
const diagnostics = []
|
|
82
|
+
const session = { diagnostics, restore() {
|
|
83
|
+
if (sessions.get(doc) !== session) return
|
|
84
|
+
sessions.delete(doc)
|
|
85
|
+
undo.reverse().forEach(restore => restore())
|
|
86
|
+
} }
|
|
87
|
+
sessions.set(doc, session)
|
|
88
|
+
try {
|
|
89
|
+
setTemporaryAttribute(doc.documentElement, 'data-asd20-print-profile', 'document', undo)
|
|
90
|
+
setTemporaryAttribute(root, 'data-asd20-print-prepared', '', undo)
|
|
91
|
+
for (const stamp of root.querySelectorAll('[data-asd20-print-generated-at]')) {
|
|
92
|
+
if (stamp.hasAttribute('data-asd20-print-fixed-generated-at')) continue
|
|
93
|
+
const prior = stamp.textContent
|
|
94
|
+
stamp.textContent = `Generated: ${new Intl.DateTimeFormat('en-US', {
|
|
95
|
+
timeZone: 'America/Denver', dateStyle: 'medium', timeStyle: 'long',
|
|
96
|
+
}).format(new Date())}`
|
|
97
|
+
undo.push(() => { stamp.textContent = prior })
|
|
98
|
+
}
|
|
99
|
+
for (const element of [root, ...root.querySelectorAll('*')]) {
|
|
100
|
+
const prepare = blocks.get(element)
|
|
101
|
+
if (prepare) undo.push(prepare())
|
|
102
|
+
}
|
|
103
|
+
prepareTables(root, undo, diagnostics)
|
|
104
|
+
prepareEmbeds(root, undo, diagnostics)
|
|
105
|
+
if (root.querySelector('[data-cc-block]')) diagnostics.push({ code: 'rich-block-unresolved' })
|
|
106
|
+
for (const link of root.querySelectorAll('a[href]')) {
|
|
107
|
+
const href = link.getAttribute('href')
|
|
108
|
+
if (!href || href.startsWith('#') || link.closest('.asd20-print-contents') || link.hasAttribute('data-asd20-print-no-url')) continue
|
|
109
|
+
const url = new URL(href, doc.baseURI)
|
|
110
|
+
if (!['https:', 'http:'].includes(url.protocol) || link.textContent.includes(url.href)) continue
|
|
111
|
+
setTemporaryAttribute(link, 'data-asd20-print-source', url.href, undo)
|
|
112
|
+
}
|
|
113
|
+
for (const image of root.querySelectorAll('img')) setTemporaryAttribute(image, 'loading', 'eager', undo)
|
|
114
|
+
return session
|
|
115
|
+
} catch (error) { session.restore(); throw error }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function waitForAssets(root) {
|
|
119
|
+
await nextTick() // Includes legacy rich-block enhancement and print state updates.
|
|
120
|
+
const doc = root.ownerDocument
|
|
121
|
+
if (doc.fonts) {
|
|
122
|
+
await doc.fonts.ready
|
|
123
|
+
if (Array.from(doc.fonts).some(font => font.status === 'error')) throw new Error('A document font failed to load.')
|
|
124
|
+
}
|
|
125
|
+
await Promise.all(Array.from(root.querySelectorAll('img')).filter(image => !image.closest('[data-asd20-print-hide], .asd20-print-hide')).map(async image => {
|
|
126
|
+
if (typeof image.decode === 'function') await image.decode()
|
|
127
|
+
else if (!image.complete) await new Promise((resolve, reject) => {
|
|
128
|
+
image.addEventListener('load', resolve, { once: true })
|
|
129
|
+
image.addEventListener('error', reject, { once: true })
|
|
130
|
+
})
|
|
131
|
+
if (!image.naturalWidth) throw new Error(`A document image failed to load: ${image.getAttribute('src')}`)
|
|
132
|
+
}))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function prepareDocumentPrint(root, { timeoutMs = 15000 } = {}) {
|
|
136
|
+
await nextTick()
|
|
137
|
+
const session = beginDocumentPrint(root)
|
|
138
|
+
let timer
|
|
139
|
+
try {
|
|
140
|
+
await Promise.race([waitForAssets(root), new Promise((_resolve, reject) => {
|
|
141
|
+
timer = setTimeout(() => reject(new Error('Document asset readiness timed out.')), timeoutMs)
|
|
142
|
+
})])
|
|
143
|
+
if (!root.isConnected || sessions.get(root.ownerDocument) !== session) throw new Error('Document print preparation was cancelled.')
|
|
144
|
+
return session
|
|
145
|
+
} catch (error) { session.restore(); throw error }
|
|
146
|
+
finally { clearTimeout(timer) }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function printDocument(root, options) {
|
|
150
|
+
const session = await prepareDocumentPrint(root, options)
|
|
151
|
+
const win = root.ownerDocument.defaultView
|
|
152
|
+
let onAfterPrint
|
|
153
|
+
try {
|
|
154
|
+
const finished = new Promise(resolve => {
|
|
155
|
+
onAfterPrint = resolve
|
|
156
|
+
win.addEventListener('afterprint', onAfterPrint, { once: true })
|
|
157
|
+
})
|
|
158
|
+
win.print()
|
|
159
|
+
await finished
|
|
160
|
+
} finally {
|
|
161
|
+
win.removeEventListener('afterprint', onAfterPrint)
|
|
162
|
+
session.restore()
|
|
163
|
+
}
|
|
164
|
+
return session.diagnostics
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Native beforeprint cannot await fonts/images. It synchronously expands semantic
|
|
168
|
+
// blocks; controlled capture and the host's Print button must await preparation.
|
|
169
|
+
export function installDocumentPrintLifecycle(root) {
|
|
170
|
+
const win = root.ownerDocument.defaultView
|
|
171
|
+
let nativeSession
|
|
172
|
+
const before = () => {
|
|
173
|
+
if (!sessions.has(root.ownerDocument)) nativeSession = beginDocumentPrint(root)
|
|
174
|
+
}
|
|
175
|
+
const after = () => { nativeSession?.restore(); nativeSession = null }
|
|
176
|
+
win.addEventListener('beforeprint', before)
|
|
177
|
+
win.addEventListener('afterprint', after)
|
|
178
|
+
return () => {
|
|
179
|
+
win.removeEventListener('beforeprint', before)
|
|
180
|
+
win.removeEventListener('afterprint', after)
|
|
181
|
+
after()
|
|
182
|
+
sessions.get(root.ownerDocument)?.restore()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { buildPublicationIntroduction } from './publicationIntroduction'
|
|
2
|
+
import { buildPublicationHeadingModels } from '../components/atoms/Asd20RichBodyContent/messageDocumentRenderer'
|
|
3
|
+
import { createPublicationHeadingPrefix } from '../components/atoms/Asd20RichBodyContent/headingContext'
|
|
4
|
+
import { parseFragment } from 'parse5'
|
|
5
|
+
|
|
6
|
+
export function documentImageUrls(html, baseUrl) {
|
|
7
|
+
const urls = new Set()
|
|
8
|
+
const walk = node => {
|
|
9
|
+
if (node.tagName === 'img') {
|
|
10
|
+
const src = node.attrs.find(attribute => attribute.name === 'src')?.value
|
|
11
|
+
if (src && !src.startsWith('data:')) urls.add(new URL(src, baseUrl).href)
|
|
12
|
+
}
|
|
13
|
+
for (const child of node.childNodes || []) walk(child)
|
|
14
|
+
}
|
|
15
|
+
walk(parseFragment(html || ''))
|
|
16
|
+
return [...urls]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The online handbook and the isolated PDF view share ordering, exclusions and
|
|
20
|
+
// heading planning. Rendering a document never changes the authored messages.
|
|
21
|
+
export function buildHandbookDocument({ messages = [], metaDescription = '', documentId = 'handbook', documentUrl = '', reservedIds = [] } = {}) {
|
|
22
|
+
const counts = new Map()
|
|
23
|
+
const entries = messages.slice(1).map((message, index) => {
|
|
24
|
+
const identity = String(message.id ?? `position-${index}`)
|
|
25
|
+
const ordinal = counts.get(identity) || 0
|
|
26
|
+
counts.set(identity, ordinal + 1)
|
|
27
|
+
const key = createPublicationHeadingPrefix(documentId, JSON.stringify([identity, ordinal]))
|
|
28
|
+
return { message, key, titleId: `${key}-title`, subtopicsId: `${key}-subtopics` }
|
|
29
|
+
})
|
|
30
|
+
const headingModels = buildPublicationHeadingModels({
|
|
31
|
+
documentUrl,
|
|
32
|
+
bodies: entries.map(entry => ({ bodyContent: entry.message.bodyContent, bodyJson: entry.message.bodyJson, headingLevelOffset: 1, headingIdPrefix: entry.key })),
|
|
33
|
+
reservedIds: ['main', 'handbook-content', 'handbook-topics', ...reservedIds, ...entries.flatMap(entry => [entry.titleId, entry.subtopicsId])],
|
|
34
|
+
})
|
|
35
|
+
const introduction = buildPublicationIntroduction(messages[0], metaDescription)
|
|
36
|
+
return {
|
|
37
|
+
entries, introduction, headingModels,
|
|
38
|
+
headings: entries.flatMap((entry, index) => [
|
|
39
|
+
{ id: entry.titleId, label: entry.message.heading || entry.message.title, authoredLevel: 2, renderedLevel: 2 },
|
|
40
|
+
...(headingModels.models[index]?.headings || []),
|
|
41
|
+
]),
|
|
42
|
+
diagnostics: [...introduction.diagnostics, ...headingModels.headingDiagnostics],
|
|
43
|
+
}
|
|
44
|
+
}
|