@simple-reporting/base 1.0.13 → 1.0.15
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/dev/package.json +14 -14
- package/dev/src/assets/scss/editor.scss +45 -0
- package/dev/src/assets/scss/general.scss +1 -0
- package/dev/tsconfig.json +1 -1
- package/dev/vite.config.ts +1 -1
- package/livingdocs/040.Media/010.table/table/download.vue +494 -0
- package/livingdocs/040.Media/010.table/table/responsive.vue +104 -0
- package/livingdocs/040.Media/010.table/table.html +2 -2
- package/livingdocs/040.Media/010.table/table.vue +26 -0
- package/livingdocs/040.Media/030.video/ld-conf.json +1 -1
- package/livingdocs/100.Misc/010.anchor/anchor.html +3 -6
- package/livingdocs/100.Misc/010.anchor/scss/editor.scss +1 -12
- package/livingdocs/110.PDF/010.pdf-pagebreak/pdf-pagebreak.html +2 -2
- package/livingdocs/110.PDF/010.pdf-pagebreak/scss/editor.scss +0 -8
- package/livingdocs/110.PDF/030.pdf-publication-title/pdf-publication-title.html +2 -5
- package/livingdocs/110.PDF/030.pdf-publication-title/scss/editor.scss +0 -11
- package/livingdocs/110.PDF/040.pdf-chapter-title/pdf-chapter-title.html +2 -5
- package/livingdocs/110.PDF/040.pdf-chapter-title/scss/editor.scss +0 -5
- package/package.json +7 -7
- package/plugins/viteSrlPlugin.d.ts +2 -0
- package/{srl/plugins/viteSrlPlugin.ts → plugins/viteSrlPlugin.js} +114 -86
- package/scripts/prepare.d.ts +1 -1
- package/scripts/prepare.js +3 -58
- package/scripts/vue/components.d.ts +1 -0
- package/scripts/vue/components.js +94 -0
- package/srl/components/Srl/Article/{DialogButton.vue → Dialog/Button.vue} +1 -1
- package/srl/components/Srl/Menu/Item/Content.vue +7 -14
- package/srl/components/Srl/Menu/Item.vue +5 -7
- package/srl/components/Srl/{Menu/List.vue → Menu.vue} +2 -3
- package/srl/components/Srl/Note/Accordion/Content.vue +1 -1
- package/srl/components/Srl/Page/Dialog.vue +1 -1
- package/srl/plugins/asyncLdComponent.ts +2 -2
- package/srl/plugins/asyncSrlComponents.ts +2 -0
- package/srl/plugins/initProject.ts +1 -1
- package/srl/plugins/vueSrlPlugin.ts +3 -20
- package/srl/tsconfig.srl.json +89 -0
- package/srl/types/components.d.ts +3 -14
- package/srl/types/global.d.ts +7 -0
- package/srl/types/nswow.d.ts +14 -14
- /package/srl/components/{Srl/Page/App.vue → App.vue} +0 -0
- /package/srl/components/Srl/Menu/Item/Content/{IconAfter.vue → Icon/After.vue} +0 -0
- /package/srl/components/Srl/Menu/Item/Content/{IconBefore.vue → Icon/Before.vue} +0 -0
- /package/srl/components/Srl/Menu/Item/Content/{ImageAfter.vue → Image/After.vue} +0 -0
- /package/srl/components/Srl/Menu/Item/Content/{ImageBefore.vue → Image/Before.vue} +0 -0
package/dev/package.json
CHANGED
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"postinstall": "srl prepare"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@simple-reporting/base": "^1.0.
|
|
23
|
-
"axios": "^1.
|
|
24
|
-
"chalk": "^5.
|
|
22
|
+
"@simple-reporting/base": "^1.0.15",
|
|
23
|
+
"axios": "^1.12.2",
|
|
24
|
+
"chalk": "^5.6.2",
|
|
25
25
|
"exceljs": "^4.4.0",
|
|
26
26
|
"file-saver": "^2.0.5",
|
|
27
27
|
"highcharts": "^11.4.8",
|
|
@@ -32,33 +32,33 @@
|
|
|
32
32
|
"standard-scss": "github:mms-neidhartschoen/nsfd_standard-scss",
|
|
33
33
|
"vite-plugin-pwa": "^0.21.2",
|
|
34
34
|
"vue": "^3.5.13",
|
|
35
|
-
"vue-i18n": "^11.1.
|
|
35
|
+
"vue-i18n": "^11.1.12",
|
|
36
36
|
"vue-router": "^4.5.1",
|
|
37
37
|
"vue3-runtime-template": "^1.0.2",
|
|
38
38
|
"xlsx": "^0.18.5"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@kne/color-palette": "^0.1.0",
|
|
42
|
-
"@rushstack/eslint-patch": "^1.
|
|
42
|
+
"@rushstack/eslint-patch": "^1.12.0",
|
|
43
43
|
"@tsconfig/node22": "^22.0.2",
|
|
44
44
|
"@types/jsdom": "^21.1.7",
|
|
45
|
-
"@types/node": "^22.
|
|
45
|
+
"@types/node": "^22.18.4",
|
|
46
46
|
"@vitejs/plugin-vue": "^5.2.4",
|
|
47
47
|
"@vitest/eslint-plugin": "1.1.20",
|
|
48
48
|
"@vue/eslint-config-prettier": "^10.2.0",
|
|
49
|
-
"@vue/eslint-config-typescript": "^14.
|
|
49
|
+
"@vue/eslint-config-typescript": "^14.6.0",
|
|
50
50
|
"@vue/test-utils": "^2.4.6",
|
|
51
51
|
"@vue/tsconfig": "^0.7.0",
|
|
52
|
-
"eslint": "^9.
|
|
52
|
+
"eslint": "^9.35.0",
|
|
53
53
|
"eslint-plugin-vue": "^9.33.0",
|
|
54
54
|
"jsdom": "^25.0.1",
|
|
55
55
|
"npm-run-all2": "^7.0.2",
|
|
56
|
-
"prettier": "^3.
|
|
57
|
-
"sass": "^1.
|
|
56
|
+
"prettier": "^3.6.2",
|
|
57
|
+
"sass": "^1.92.1",
|
|
58
58
|
"typescript": "~5.6.3",
|
|
59
|
-
"vite": "^6.3.
|
|
60
|
-
"vite-plugin-vue-devtools": "^7.7.
|
|
61
|
-
"vitest": "^2.
|
|
62
|
-
"vue-tsc": "^2.2.
|
|
59
|
+
"vite": "^6.3.6",
|
|
60
|
+
"vite-plugin-vue-devtools": "^7.7.7",
|
|
61
|
+
"vitest": "^3.2.4",
|
|
62
|
+
"vue-tsc": "^2.2.12"
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
@use 'srl';
|
|
2
2
|
@use 'web';
|
|
3
|
+
@use "placeholders";
|
|
3
4
|
|
|
4
5
|
$editor-color-srl: srl.colors-shade-200();
|
|
5
6
|
$editor-background-color-srl: srl.colors-primary-1000();
|
|
@@ -37,4 +38,48 @@ span.ns-key-figure {
|
|
|
37
38
|
// Set width of placeholder image to 100%
|
|
38
39
|
img[src^="data:image/svg+xml"] {
|
|
39
40
|
width: 100%
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.srl-editor-component {
|
|
44
|
+
@include srl.spacer-padding-block(200);
|
|
45
|
+
@extend %srl-grid-base;
|
|
46
|
+
|
|
47
|
+
&.srl-pdf-pagebreak,
|
|
48
|
+
&.srl-anchor {
|
|
49
|
+
border-top: 1px solid #{srl.colors-primary-1000()};
|
|
50
|
+
border-bottom: 1px solid #{srl.colors-primary-1000()};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&.srl-pdf-publication-title,
|
|
54
|
+
&.srl-pdf-chapter-title {
|
|
55
|
+
border-bottom: 1px solid #{srl.colors-primary-1000()};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.doc-editable.doc-no-placeholder:before {
|
|
59
|
+
display: none !important;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.srl-editor-component__text {
|
|
64
|
+
@extend %srl-regular-width;
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
|
|
68
|
+
&:after {
|
|
69
|
+
order: -1;
|
|
70
|
+
display: block;
|
|
71
|
+
@include srl.typography-editor-label-text();
|
|
72
|
+
|
|
73
|
+
.srl-anchor & {
|
|
74
|
+
content: "Anchor:";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.srl-pdf-publication-title & {
|
|
78
|
+
content: "PDF publication title:"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.srl-pdf-chapter-title & {
|
|
82
|
+
content: "PDF chapter title:"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
40
85
|
}
|
package/dev/tsconfig.json
CHANGED
package/dev/vite.config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineConfig } from 'vite'
|
|
2
2
|
import vue from '@vitejs/plugin-vue'
|
|
3
3
|
import VueDevTools from 'vite-plugin-vue-devtools'
|
|
4
|
-
import viteSrlPlugin from '
|
|
4
|
+
import viteSrlPlugin from '@simple-reporting/base/plugins/viteSrlPlugin.js'
|
|
5
5
|
import { fileURLToPath, URL } from 'node:url'
|
|
6
6
|
|
|
7
7
|
// https://vitejs.dev/config/
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* This Vue component script enables downloading an HTML table as an Excel file.
|
|
4
|
+
* It uses the `exceljs` library to convert the table into an Excel format and trigger the download.
|
|
5
|
+
*
|
|
6
|
+
* ## Props:
|
|
7
|
+
* - `titleSelector` (string, optional): CSS selector for the table titles. Default: `.srl-title-h1__text, .srl-page-title__overline, .srl-page-title__headline`.
|
|
8
|
+
* - `font` (string, optional): Font used in the Excel file. Default: `Arial`.
|
|
9
|
+
* - `defaultSheetName` (string, optional): Default name of the Excel worksheet. Default: `Table 1`.
|
|
10
|
+
* - `defaultFileName` (string, optional): Default name of the downloaded file. Default: `Table`.
|
|
11
|
+
* - `spacerRowHeight` (number, optional): Height of spacer rows in the Excel file. Default: `25`.
|
|
12
|
+
* - `formatText` (Function, optional): Function to format cell content. Default: Trims text and returns `null` if empty.
|
|
13
|
+
*
|
|
14
|
+
* ## Types:
|
|
15
|
+
* - `FormatText`: Type for the `formatText` function.
|
|
16
|
+
* - `ElementStyles`: Style properties of an HTML element.
|
|
17
|
+
* - `TextData`: Text data with styles.
|
|
18
|
+
* - `CellData`: Data and styles of a table cell.
|
|
19
|
+
* - `CellStyles`: Styles of a table cell.
|
|
20
|
+
* - `TableIdElement`: HTML div element with a `data-tableid` attribute.
|
|
21
|
+
* - `TableMatrix`: Matrix representing the table structure.
|
|
22
|
+
*
|
|
23
|
+
* ## Functions:
|
|
24
|
+
* - `downloadTable()`: Initiates the download of the table as an Excel file.
|
|
25
|
+
* - `parseColumns(worksheet: Worksheet)`: Parses the column widths of the table and sets them in the worksheet.
|
|
26
|
+
* - `parseTitles()`: Extracts the table titles and their styles.
|
|
27
|
+
* - `writeTitles(worksheet: Worksheet, titles: TextData[])`: Writes the titles to the worksheet.
|
|
28
|
+
* - `parseTable()`: Converts the HTML table into a matrix of cells.
|
|
29
|
+
* - `writeTable(worksheet: Worksheet, matrix: TableMatrix)`: Writes the table matrix to the worksheet.
|
|
30
|
+
* - `mergeCells(worksheet: Worksheet, matrix: TableMatrix, excelRows: Row[])`: Merges cells with `rowSpan` or `colSpan`.
|
|
31
|
+
* - `parseFootNotes()`: Extracts footnotes from the table.
|
|
32
|
+
* - `writeFootNotes(worksheet: Worksheet, footNotes: TextData[])`: Writes the footnotes to the worksheet.
|
|
33
|
+
* - `triggerDownload(fileName: string, buffer: ArrayBuffer)`: Triggers the download of the Excel file.
|
|
34
|
+
* - `writeSpacerRow(worksheet: Worksheet)`: Adds a spacer row to the worksheet.
|
|
35
|
+
* - `parseElementStyles(element: HTMLElement)`: Parses the styles of an HTML element.
|
|
36
|
+
* - `rgbToArgb(rgb: string, alpha: number)`: Converts an RGB color to ARGB format.
|
|
37
|
+
* - `getStyles(tableCell: HTMLTableCellElement)`: Extracts the styles of a table cell.
|
|
38
|
+
*
|
|
39
|
+
* ## Usage:
|
|
40
|
+
* This component is used to export an HTML table to an Excel file.
|
|
41
|
+
* It can be integrated into a Vue application and provides a button to trigger the export.
|
|
42
|
+
*/
|
|
43
|
+
import { ref, watch } from 'vue'
|
|
44
|
+
import { Workbook } from 'exceljs'
|
|
45
|
+
import type { Worksheet, Row } from 'exceljs/index.d.ts'
|
|
46
|
+
|
|
47
|
+
const props = withDefaults(defineProps<{
|
|
48
|
+
component?: HTMLDivElement
|
|
49
|
+
tableSelector?: string
|
|
50
|
+
titleSelector?: string
|
|
51
|
+
buttonLabel?: string
|
|
52
|
+
buttonTitle?: string
|
|
53
|
+
buttonDescription?: string
|
|
54
|
+
font?: string
|
|
55
|
+
defaultSheetName?: string
|
|
56
|
+
defaultFileName?: string
|
|
57
|
+
spacerRowHeight?: number
|
|
58
|
+
formatText?: FormatText
|
|
59
|
+
}>(),{
|
|
60
|
+
tableSelector: 'table',
|
|
61
|
+
titleSelector: '.srl-title-h1__text, .srl-page-title__overline, .srl-page-title__headline',
|
|
62
|
+
buttonLabel: 'Download',
|
|
63
|
+
buttonDescription: 'Download table as Excel file',
|
|
64
|
+
font: 'Arial',
|
|
65
|
+
defaultSheetName: 'Table 1',
|
|
66
|
+
defaultFileName: 'Table',
|
|
67
|
+
spacerRowHeight: 25,
|
|
68
|
+
formatText: (cell: HTMLTableCellElement) => {
|
|
69
|
+
return cell.innerText !== '' ? cell.innerText.trim() : null
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
type FormatText = (cell: HTMLTableCellElement) => string | null
|
|
74
|
+
|
|
75
|
+
type ElementStyles = {
|
|
76
|
+
name: string,
|
|
77
|
+
size: number,
|
|
78
|
+
color: string,
|
|
79
|
+
bold: boolean
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface TextData extends ElementStyles {
|
|
83
|
+
value: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type CellData = {
|
|
87
|
+
value: string | null,
|
|
88
|
+
styles: CellStyles,
|
|
89
|
+
colSpan: number,
|
|
90
|
+
rowSpan: number
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type CellStyles = {
|
|
94
|
+
alignment: {
|
|
95
|
+
horizontal: string,
|
|
96
|
+
vertical: string,
|
|
97
|
+
wrapText: boolean
|
|
98
|
+
},
|
|
99
|
+
border: {
|
|
100
|
+
top?: { style: string, color: { argb: string } },
|
|
101
|
+
right?: { style: string, color: { argb: string } },
|
|
102
|
+
bottom?: { style: string, color: { argb: string } },
|
|
103
|
+
left?: { style: string, color: { argb: string } }
|
|
104
|
+
},
|
|
105
|
+
font: {
|
|
106
|
+
name: string,
|
|
107
|
+
size: number,
|
|
108
|
+
color: { argb: string },
|
|
109
|
+
bold: boolean
|
|
110
|
+
},
|
|
111
|
+
fill: {
|
|
112
|
+
type?: string,
|
|
113
|
+
pattern?: string,
|
|
114
|
+
fgColor?: { argb: string }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type TableIdElement = HTMLDivElement & {
|
|
119
|
+
dataset: {
|
|
120
|
+
tableid: string
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
type TableMatrix = Array<Array<CellData>>
|
|
125
|
+
|
|
126
|
+
const rootElement = ref<HTMLDivElement>()
|
|
127
|
+
const table = ref<HTMLTableElement | null | undefined>(null)
|
|
128
|
+
|
|
129
|
+
watch(
|
|
130
|
+
props,
|
|
131
|
+
to => {
|
|
132
|
+
!to.component || init(to.component)
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
function init(componentEl: HTMLDivElement | undefined) {
|
|
137
|
+
if (!componentEl) return
|
|
138
|
+
rootElement.value = componentEl
|
|
139
|
+
table.value = componentEl.querySelector(props.tableSelector) as HTMLTableElement
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getTable(): Promise<HTMLTableElement | null | undefined> {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
let attempts = 0;
|
|
145
|
+
const checkTable = () => {
|
|
146
|
+
const table = rootElement.value?.querySelector<HTMLTableElement>('table');
|
|
147
|
+
if (table) {
|
|
148
|
+
resolve(table);
|
|
149
|
+
} else if (attempts >= 30) {
|
|
150
|
+
reject(new Error('Table not found within the maximum number of attempts.'));
|
|
151
|
+
} else {
|
|
152
|
+
attempts++;
|
|
153
|
+
setTimeout(checkTable, 10);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
checkTable();
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function downloadTable() {
|
|
162
|
+
if (!table.value) return
|
|
163
|
+
|
|
164
|
+
const matrix = parseTable()
|
|
165
|
+
const titles = parseTitles()
|
|
166
|
+
const footNotes = parseFootNotes()
|
|
167
|
+
const fileName = titles.map(t => t.value)
|
|
168
|
+
|
|
169
|
+
const headTop: HTMLTableCellElement | null | undefined = rootElement.value?.querySelector('.head_top')
|
|
170
|
+
if (headTop && headTop.textContent) {
|
|
171
|
+
fileName.push(headTop.textContent.trim())
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const tableIdElement = rootElement.value?.querySelector('[data-tableid]') as TableIdElement | null
|
|
175
|
+
|
|
176
|
+
const workbook = new Workbook()
|
|
177
|
+
const worksheet = workbook.addWorksheet( tableIdElement?.dataset.tableid || props.defaultSheetName )
|
|
178
|
+
|
|
179
|
+
parseColumns(worksheet)
|
|
180
|
+
|
|
181
|
+
writeTitles(worksheet, titles)
|
|
182
|
+
mergeCells(worksheet, matrix, writeTable(worksheet, matrix))
|
|
183
|
+
writeFootNotes(worksheet, footNotes)
|
|
184
|
+
|
|
185
|
+
triggerDownload(
|
|
186
|
+
fileName.length ? fileName.join(' - ').replaceAll(' ', '_') : props.defaultFileName,
|
|
187
|
+
await workbook.xlsx.writeBuffer()
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function parseColumns(worksheet: Worksheet): void {
|
|
192
|
+
worksheet.columns = Array.from(table.value?.querySelectorAll('col') ?? [])
|
|
193
|
+
.map(col => {
|
|
194
|
+
return {
|
|
195
|
+
width: col.offsetWidth / 7
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function parseTitles(): TextData[] {
|
|
201
|
+
const titleElements: HTMLElement[] = Array.from(document.querySelectorAll(props.titleSelector)) as HTMLElement[]
|
|
202
|
+
const tableTitle: TextData[] = []
|
|
203
|
+
titleElements.forEach( element => {
|
|
204
|
+
if (!element || !element.textContent) return
|
|
205
|
+
const styles = parseElementStyles(element)
|
|
206
|
+
tableTitle.push(Object.assign({
|
|
207
|
+
value: element.textContent.trim(),
|
|
208
|
+
}, styles))
|
|
209
|
+
})
|
|
210
|
+
return tableTitle
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function writeTitles( worksheet: Worksheet, titles: TextData[] ): void {
|
|
214
|
+
if (titles.length === 0) return
|
|
215
|
+
|
|
216
|
+
titles.forEach( title => {
|
|
217
|
+
const row = worksheet.addRow([title.value])
|
|
218
|
+
const cell = row.getCell(1)
|
|
219
|
+
cell.font = {
|
|
220
|
+
name: title.name,
|
|
221
|
+
size: title.size,
|
|
222
|
+
color: { argb: title.color },
|
|
223
|
+
bold: title.bold
|
|
224
|
+
}
|
|
225
|
+
cell.alignment = {
|
|
226
|
+
wrapText: true
|
|
227
|
+
}
|
|
228
|
+
worksheet.mergeCells(row.number, 1, row.number, worksheet.columns.length)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
//writeSpacerRow(worksheet)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function parseTable(): TableMatrix {
|
|
235
|
+
if (!table.value) return []
|
|
236
|
+
|
|
237
|
+
const matrix: TableMatrix = []
|
|
238
|
+
|
|
239
|
+
const tableRows = Array.from(table.value.querySelectorAll('tr'))
|
|
240
|
+
|
|
241
|
+
tableRows?.forEach( tableRowElem => {
|
|
242
|
+
const row: CellData[] = []
|
|
243
|
+
const tableCells: HTMLTableCellElement[] = Array.from(tableRowElem.querySelectorAll('th, td'))
|
|
244
|
+
|
|
245
|
+
tableCells.forEach( tableCellElem => {
|
|
246
|
+
|
|
247
|
+
const styles = getStyles(tableCellElem)
|
|
248
|
+
|
|
249
|
+
row.push({
|
|
250
|
+
value: props.formatText(tableCellElem),
|
|
251
|
+
styles: styles,
|
|
252
|
+
colSpan: tableCellElem.colSpan,
|
|
253
|
+
rowSpan: tableCellElem.rowSpan,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
if (tableCellElem.colSpan > 1) {
|
|
257
|
+
for (let i = 1; i < tableCellElem.colSpan; i++) {
|
|
258
|
+
row.push({
|
|
259
|
+
value: null,
|
|
260
|
+
styles: styles,
|
|
261
|
+
colSpan: 1,
|
|
262
|
+
rowSpan: 1
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
matrix.push(row)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
matrix.forEach( (row, rowIndex) => {
|
|
272
|
+
row.forEach( (cell) => {
|
|
273
|
+
const cellIndex = row.findIndex(c => c === cell)
|
|
274
|
+
if (cell.rowSpan > 1) {
|
|
275
|
+
for (let i = 1; i < cell.rowSpan; i++) {
|
|
276
|
+
const nextRow = matrix[rowIndex + i]
|
|
277
|
+
if (nextRow) {
|
|
278
|
+
for (let i = 0; i < cell.colSpan; i++) {
|
|
279
|
+
nextRow.splice(cellIndex, 0, {
|
|
280
|
+
value: null,
|
|
281
|
+
styles: cell.styles,
|
|
282
|
+
colSpan: 1,
|
|
283
|
+
rowSpan: 1
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
return matrix
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function writeTable( worksheet: Worksheet, matrix: TableMatrix ): Row[] {
|
|
296
|
+
const excelRows: Row[] = []
|
|
297
|
+
matrix.forEach( row => {
|
|
298
|
+
const excelRow = worksheet.addRow(row.map(cell => cell.value))
|
|
299
|
+
row.forEach( (cell, index) => {
|
|
300
|
+
const excelCell = excelRow.getCell(index + 1)
|
|
301
|
+
|
|
302
|
+
excelCell.alignment = cell.styles.alignment
|
|
303
|
+
excelCell.font = cell.styles.font
|
|
304
|
+
cell.styles.fill ? excelCell.fill = cell.styles.fill : null
|
|
305
|
+
excelCell.border = cell.styles.border
|
|
306
|
+
})
|
|
307
|
+
excelRows.push(excelRow)
|
|
308
|
+
})
|
|
309
|
+
return excelRows
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function mergeCells( worksheet: Worksheet, matrix: TableMatrix, excelRows: Row[] ): void {
|
|
313
|
+
matrix.forEach( (row, rowIndex) => {
|
|
314
|
+
row.forEach( (cell, cellIndex) => {
|
|
315
|
+
if (cell.rowSpan > 1 || cell.colSpan > 1) {
|
|
316
|
+
const excelRow = excelRows[rowIndex]
|
|
317
|
+
const startCell = cellIndex + 1
|
|
318
|
+
const endCell = cellIndex + cell.colSpan
|
|
319
|
+
const endRow = excelRow.number + ( cell.rowSpan - 1 )
|
|
320
|
+
worksheet.mergeCells(excelRow.number, startCell, endRow, endCell)
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function parseFootNotes(): TextData[] {
|
|
327
|
+
const footNoteData: TextData[] = []
|
|
328
|
+
const footNotes = Array.from(rootElement.value?.querySelectorAll('.srl-footnote') ?? []) as HTMLDivElement[]
|
|
329
|
+
|
|
330
|
+
footNotes.forEach(footNote => {
|
|
331
|
+
const key: HTMLSpanElement | null = footNote.querySelector('.srl-footnote__key')
|
|
332
|
+
const text: HTMLSpanElement | null = footNote.querySelector('.srl-footnote__text')
|
|
333
|
+
|
|
334
|
+
if (text && text.textContent) {
|
|
335
|
+
const row: string[] = []
|
|
336
|
+
if (key && key.textContent) {
|
|
337
|
+
row.push(key.textContent.trim())
|
|
338
|
+
}
|
|
339
|
+
row.push(text.textContent.trim())
|
|
340
|
+
const styles = parseElementStyles(text)
|
|
341
|
+
footNoteData.push(Object.assign({
|
|
342
|
+
value: row.join(' '),
|
|
343
|
+
}, styles))
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
return footNoteData
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function writeFootNotes( worksheet: Worksheet, footNotes: TextData[] ): void {
|
|
350
|
+
if (footNotes.length === 0) return
|
|
351
|
+
|
|
352
|
+
writeSpacerRow(worksheet)
|
|
353
|
+
|
|
354
|
+
footNotes.forEach(footNote => {
|
|
355
|
+
const row = worksheet.addRow([footNote.value])
|
|
356
|
+
const cell = row.getCell(1)
|
|
357
|
+
cell.font = {
|
|
358
|
+
name: footNote.name,
|
|
359
|
+
size: footNote.size,
|
|
360
|
+
color: { argb: footNote.color },
|
|
361
|
+
bold: footNote.bold
|
|
362
|
+
}
|
|
363
|
+
cell.alignment = {
|
|
364
|
+
horizontal: 'left',
|
|
365
|
+
vertical: 'top',
|
|
366
|
+
wrapText: true
|
|
367
|
+
}
|
|
368
|
+
worksheet.mergeCells(row.number, 1, row.number, worksheet.columns.length)
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function triggerDownload( fileName: string, buffer: ArrayBuffer ): void {
|
|
373
|
+
const blob = new Blob([buffer], {
|
|
374
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
375
|
+
})
|
|
376
|
+
const link = document.createElement('a')
|
|
377
|
+
link.href = URL.createObjectURL(blob)
|
|
378
|
+
link.download = fileName + '.xlsx'
|
|
379
|
+
link.click()
|
|
380
|
+
link.remove()
|
|
381
|
+
URL.revokeObjectURL(link.href)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function writeSpacerRow(worksheet: Worksheet): void {
|
|
385
|
+
const row = worksheet.addRow([])
|
|
386
|
+
row.height = props.spacerRowHeight
|
|
387
|
+
worksheet.mergeCells(row.number, 1, row.number, worksheet.columns.length)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function parseElementStyles(element: HTMLElement): ElementStyles | null {
|
|
391
|
+
const styles = window.getComputedStyle(element)
|
|
392
|
+
return {
|
|
393
|
+
name: props.font,
|
|
394
|
+
size: parseInt(styles.fontSize) * 0.75,
|
|
395
|
+
color: rgbToArgb(styles.color),
|
|
396
|
+
bold: parseInt(styles.fontWeight) > 400
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function rgbToArgb(rgb: string, alpha: number = 1): string {
|
|
401
|
+
const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([0-9.]+))?\)/)
|
|
402
|
+
if (!match) {
|
|
403
|
+
throw new Error("Dies ist keine gültige RGB- oder RGBA-Farbe")
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const r = parseInt(match[1])
|
|
407
|
+
const g = parseInt(match[2])
|
|
408
|
+
const b = parseInt(match[3])
|
|
409
|
+
const a = match[4] !== undefined ? Math.round(parseFloat(match[4]) * 255) : Math.round(alpha * 255)
|
|
410
|
+
|
|
411
|
+
const aHex = a.toString(16).padStart(2, '0')
|
|
412
|
+
const rHex = r.toString(16).padStart(2, '0')
|
|
413
|
+
const gHex = g.toString(16).padStart(2, '0')
|
|
414
|
+
const bHex = b.toString(16).padStart(2, '0')
|
|
415
|
+
|
|
416
|
+
return `${aHex}${rHex}${gHex}${bHex}`
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function getStyles( tableCell: HTMLTableCellElement ): CellStyles {
|
|
420
|
+
const styles: CellStyles = {
|
|
421
|
+
alignment: {
|
|
422
|
+
horizontal: window.getComputedStyle(tableCell).textAlign,
|
|
423
|
+
vertical: window.getComputedStyle(tableCell).verticalAlign,
|
|
424
|
+
wrapText: true
|
|
425
|
+
},
|
|
426
|
+
border: {},
|
|
427
|
+
font: {
|
|
428
|
+
name: 'Arial',
|
|
429
|
+
size: parseInt(window.getComputedStyle(tableCell).fontSize) * 0.75,
|
|
430
|
+
color: { argb: rgbToArgb(window.getComputedStyle(tableCell).color) },
|
|
431
|
+
bold: parseInt(window.getComputedStyle(tableCell).fontWeight) > 400
|
|
432
|
+
},
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const brdTopWidth = parseInt(window.getComputedStyle(tableCell).borderTopWidth)
|
|
436
|
+
if (brdTopWidth) {
|
|
437
|
+
styles.border.top = {
|
|
438
|
+
style: brdTopWidth < 3 ? 'thin' : brdTopWidth < 6 ? 'medium' : 'thick',
|
|
439
|
+
color: {argb: rgbToArgb(window.getComputedStyle(tableCell).borderTopColor)}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const brdRightWidth = parseInt(window.getComputedStyle(tableCell).borderRightWidth)
|
|
444
|
+
if (brdRightWidth) {
|
|
445
|
+
styles.border.right = {
|
|
446
|
+
style: brdRightWidth < 3 ? 'thin' : brdRightWidth < 6 ? 'medium' : 'thick',
|
|
447
|
+
color: {argb: rgbToArgb(window.getComputedStyle(tableCell).borderRightColor)}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const brdBottomWidth = parseInt(window.getComputedStyle(tableCell).borderBottomWidth)
|
|
452
|
+
if (brdBottomWidth) {
|
|
453
|
+
styles.border.bottom = {
|
|
454
|
+
style: brdBottomWidth < 3 ? 'thin' : brdBottomWidth < 6 ? 'medium' : 'thick',
|
|
455
|
+
color: {argb: rgbToArgb(window.getComputedStyle(tableCell).borderBottomColor)}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const brdLeftWidth = parseInt(window.getComputedStyle(tableCell).borderLeftWidth)
|
|
460
|
+
if (brdLeftWidth) {
|
|
461
|
+
styles.border.left = {
|
|
462
|
+
style: brdLeftWidth < 3 ? 'thin' : brdLeftWidth < 6 ? 'medium' : 'thick',
|
|
463
|
+
color: {argb: rgbToArgb(window.getComputedStyle(tableCell).borderLeftColor)}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const bgColor = rgbToArgb(window.getComputedStyle(tableCell).backgroundColor)
|
|
468
|
+
if (bgColor.substring(0,2) !== '00') {
|
|
469
|
+
styles.fill = {
|
|
470
|
+
type: 'pattern',
|
|
471
|
+
pattern: 'solid',
|
|
472
|
+
fgColor: {argb: bgColor}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return styles
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
</script>
|
|
480
|
+
|
|
481
|
+
<template>
|
|
482
|
+
<button
|
|
483
|
+
v-if="table"
|
|
484
|
+
type="button"
|
|
485
|
+
:title="$te('table.download.title') ? $t('table.download.title') : props.buttonTitle ?? props.buttonLabel"
|
|
486
|
+
:aria-label="$te('table.download.label') ? $t('table.download.label') : props.buttonLabel"
|
|
487
|
+
:aria-description="$te('table.download.description') ? $t('table.download.description') : props.buttonDescription"
|
|
488
|
+
class="srl-button"
|
|
489
|
+
@click="downloadTable"
|
|
490
|
+
>
|
|
491
|
+
<i class="srl-icon srl-icon-download" aria-hidden="true"></i>
|
|
492
|
+
<span v-text="$te('table.download.label') ? $t('table.download.label') : props.buttonLabel" />
|
|
493
|
+
</button>
|
|
494
|
+
</template>
|