coralite-plugin-aggregation 0.7.3 → 0.9.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/LICENSE +328 -661
- package/lib/components/coralite-pagination.html +104 -0
- package/lib/index.js +52 -90
- package/package.json +3 -6
- package/types/index.js +1 -1
- package/lib/templates/coralite-pagination.html +0 -123
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<template id="coralite-pagination">
|
|
2
|
+
<nav aria-label="{{ ariaLabel }}">
|
|
3
|
+
<ul class="pagination">
|
|
4
|
+
{{ paginationLinks }}
|
|
5
|
+
</ul>
|
|
6
|
+
</nav>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script type="module">
|
|
10
|
+
import { defineComponent } from 'coralite/plugins'
|
|
11
|
+
|
|
12
|
+
export default defineComponent({
|
|
13
|
+
properties: (context) => {
|
|
14
|
+
const props = context.properties
|
|
15
|
+
|
|
16
|
+
const ariaLabel = props['aria-label'] || 'Page navigation'
|
|
17
|
+
|
|
18
|
+
const currentPage = parseInt(props['current-page'] || '1', 10)
|
|
19
|
+
const totalPages = parseInt(props['total-pages'] || '1', 10)
|
|
20
|
+
const maxVisible = parseInt(props['max-visible'] || '5', 10)
|
|
21
|
+
|
|
22
|
+
const baseUrl = props['base-url'] || ''
|
|
23
|
+
const urlPrefix = props['url-prefix'] || ''
|
|
24
|
+
const segment = props['segment'] || 'page'
|
|
25
|
+
const ellipsis = props['ellipsis'] || '...'
|
|
26
|
+
|
|
27
|
+
// Compute pagination synchronously
|
|
28
|
+
const getPageUrl = (page) => {
|
|
29
|
+
if (page === 1) {
|
|
30
|
+
return baseUrl
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const cleanPrefix = urlPrefix.endsWith('/') ? urlPrefix : `${urlPrefix}/`
|
|
34
|
+
return `${cleanPrefix}${segment}/${page}.html`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const createItem = (page, text, isActive, isDisabled) => {
|
|
38
|
+
let className = 'page-item'
|
|
39
|
+
if (isActive) className += ' active'
|
|
40
|
+
if (isDisabled) className += ' disabled'
|
|
41
|
+
|
|
42
|
+
let attr = ''
|
|
43
|
+
if (isActive) attr += ' aria-current="page"'
|
|
44
|
+
if (isDisabled) attr += ' tabindex="-1" aria-disabled="true"'
|
|
45
|
+
|
|
46
|
+
const href = isDisabled ? '#' : getPageUrl(page)
|
|
47
|
+
|
|
48
|
+
return `<li class="${className}"><a class="page-link" href="${href}"${attr}>${text}</a></li>`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let links = ''
|
|
52
|
+
|
|
53
|
+
// Previous Link
|
|
54
|
+
links += createItem(currentPage - 1, 'Previous', false, currentPage <= 1)
|
|
55
|
+
|
|
56
|
+
// Calculate Window (Start/End)
|
|
57
|
+
const half = Math.floor(maxVisible / 2)
|
|
58
|
+
let start = currentPage - half
|
|
59
|
+
let end = currentPage + half
|
|
60
|
+
|
|
61
|
+
if (start < 1) {
|
|
62
|
+
start = 1
|
|
63
|
+
end = Math.min(totalPages, maxVisible)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (end > totalPages) {
|
|
67
|
+
end = totalPages
|
|
68
|
+
start = Math.max(1, totalPages - maxVisible + 1)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const showFirst = start > 1
|
|
72
|
+
const showLast = end < totalPages
|
|
73
|
+
|
|
74
|
+
// First page + ellipsis
|
|
75
|
+
if (showFirst) {
|
|
76
|
+
links += createItem(1, '1', false, false)
|
|
77
|
+
if (start > 2) {
|
|
78
|
+
links += `<li class="page-item disabled"><span class="page-link">${ellipsis}</span></li>`
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Main window loop
|
|
83
|
+
for (let i = start; i <= end; i++) {
|
|
84
|
+
links += createItem(i, i, i === currentPage, false)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Last page + ellipsis
|
|
88
|
+
if (showLast) {
|
|
89
|
+
if (end < totalPages - 1) {
|
|
90
|
+
links += `<li class="page-item disabled"><span class="page-link">${ellipsis}</span></li>`
|
|
91
|
+
}
|
|
92
|
+
links += createItem(totalPages, totalPages, false, false)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Next Link
|
|
96
|
+
links += createItem(currentPage + 1, 'Next', false, currentPage >= totalPages)
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
ariaLabel,
|
|
100
|
+
paginationLinks: links
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
</script>
|
package/lib/index.js
CHANGED
|
@@ -4,12 +4,12 @@ import path from 'node:path'
|
|
|
4
4
|
/**
|
|
5
5
|
* Aggregates content based on configuration
|
|
6
6
|
* @param {import('../types/index.js').AggregationOptions} options
|
|
7
|
-
* @param {Object}
|
|
7
|
+
* @param {Object} context
|
|
8
8
|
* @returns {Promise<any[]>}
|
|
9
9
|
*/
|
|
10
|
-
async function aggregationMethod (options,
|
|
10
|
+
async function aggregationMethod (options, context) {
|
|
11
11
|
const {
|
|
12
|
-
|
|
12
|
+
path: paths = [],
|
|
13
13
|
template,
|
|
14
14
|
pagination,
|
|
15
15
|
filter,
|
|
@@ -17,10 +17,10 @@ async function aggregationMethod (options, contextInstance) {
|
|
|
17
17
|
limit,
|
|
18
18
|
offset = 0,
|
|
19
19
|
recursive = false,
|
|
20
|
-
|
|
20
|
+
transformProperties
|
|
21
21
|
} = options
|
|
22
22
|
|
|
23
|
-
const
|
|
23
|
+
const contextProperties = context.properties || {}
|
|
24
24
|
const pagesRoot = this.options.pages
|
|
25
25
|
|
|
26
26
|
// Collect pages
|
|
@@ -30,13 +30,13 @@ async function aggregationMethod (options, contextInstance) {
|
|
|
30
30
|
for (const relativePath of paths) {
|
|
31
31
|
const targetPath = path.join(pagesRoot, relativePath)
|
|
32
32
|
|
|
33
|
-
// Check direct path match in listByPath (non-recursive)
|
|
34
33
|
if (!recursive) {
|
|
35
34
|
const pagesInDir = this.pages.getListByPath(targetPath)
|
|
36
35
|
if (pagesInDir) {
|
|
37
36
|
for (const page of pagesInDir) {
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const pagePath = page.url ? page.url.pathname : page.path.pathname
|
|
38
|
+
if (!uniquePaths.has(pagePath)) {
|
|
39
|
+
uniquePaths.add(pagePath)
|
|
40
40
|
allPages.push(page)
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -44,11 +44,11 @@ async function aggregationMethod (options, contextInstance) {
|
|
|
44
44
|
} else {
|
|
45
45
|
// Recursive search
|
|
46
46
|
for (const page of this.pages.list) {
|
|
47
|
-
const dirname = page.path.dirname
|
|
48
|
-
// Check if dirname is targetPath or a subdirectory of targetPath
|
|
47
|
+
const dirname = page.file ? page.file.dirname : page.path.dirname
|
|
49
48
|
if (dirname === targetPath || dirname.startsWith(targetPath + path.sep)) {
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
const pagePath = page.url ? page.url.pathname : page.path.pathname
|
|
50
|
+
if (!uniquePaths.has(pagePath)) {
|
|
51
|
+
uniquePaths.add(pagePath)
|
|
52
52
|
allPages.push(page)
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -59,18 +59,17 @@ async function aggregationMethod (options, contextInstance) {
|
|
|
59
59
|
// Filter
|
|
60
60
|
if (typeof filter === 'function') {
|
|
61
61
|
allPages = allPages.filter(page => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return filter(values)
|
|
62
|
+
const pageProps = (page.result && page.result.properties) ? page.result.properties : page.properties
|
|
63
|
+
return filter(pageProps)
|
|
65
64
|
})
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
// Sort
|
|
69
68
|
if (typeof sort === 'function') {
|
|
70
69
|
allPages.sort((a, b) => {
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
return sort(
|
|
70
|
+
const propsA = (a.result && a.result.properties) ? a.result.properties : a.properties
|
|
71
|
+
const propsB = (b.result && b.result.properties) ? b.result.properties : b.properties
|
|
72
|
+
return sort(propsA, propsB)
|
|
74
73
|
})
|
|
75
74
|
}
|
|
76
75
|
|
|
@@ -81,18 +80,15 @@ async function aggregationMethod (options, contextInstance) {
|
|
|
81
80
|
let currentPage = 1
|
|
82
81
|
let totalPages = 1
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
const currentRenderContext = contextInstance.renderContext
|
|
83
|
+
const currentRenderContext = context.renderContext
|
|
86
84
|
const buildId = currentRenderContext && currentRenderContext.buildId
|
|
87
85
|
|
|
88
86
|
if (limit) {
|
|
89
87
|
if (pagination) {
|
|
90
88
|
const segment = pagination.segment || 'page'
|
|
91
|
-
const urlPathname =
|
|
89
|
+
const urlPathname = context.page.url.pathname
|
|
92
90
|
|
|
93
91
|
const escapedSegment = segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
94
|
-
|
|
95
|
-
// Try to match segment in URL: /.../segment/Number
|
|
96
92
|
const segmentRegex = new RegExp(`/${escapedSegment}/(\\d+)`)
|
|
97
93
|
const match = urlPathname.match(segmentRegex)
|
|
98
94
|
|
|
@@ -102,81 +98,55 @@ async function aggregationMethod (options, contextInstance) {
|
|
|
102
98
|
|
|
103
99
|
startIndex = offset + (currentPage - 1) * limit
|
|
104
100
|
endIndex = startIndex + limit
|
|
105
|
-
|
|
106
101
|
totalPages = Math.ceil(allPages.length / limit)
|
|
107
102
|
|
|
108
|
-
// Automatically generate subsequent pagination pages if we are on the "root" page
|
|
109
|
-
// e.g., if we are on /blog/index.html (page 1), queue up /blog/page/2, /blog/page/3...
|
|
110
103
|
if (!match && currentPage === 1 && totalPages > 1 && buildId) {
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const currentDirname = currentDocument.path.dirname
|
|
104
|
+
const currentPathname = context.page.file.pathname
|
|
105
|
+
const currentFilename = context.page.file.filename
|
|
106
|
+
const currentDirname = context.page.file.dirname
|
|
115
107
|
|
|
116
|
-
// Determine the output path structure based on rules
|
|
117
108
|
let targetDir = currentDirname
|
|
118
109
|
let urlPrefixBase = ''
|
|
119
110
|
|
|
120
|
-
// Rules:
|
|
121
|
-
// /index.html -> /page/1.html (relative to currentDirname)
|
|
122
|
-
// /blog.html -> /blog/page/1.html (creates subdirectory)
|
|
123
|
-
// /blog/index.html -> /blog/page/1.html (relative to currentDirname)
|
|
124
|
-
// /blog/today.html -> /blog/today/page/1.html (creates subdirectory)
|
|
125
|
-
|
|
126
111
|
if (currentFilename === 'index.html') {
|
|
127
|
-
// Standard case: keep in same directory
|
|
128
112
|
targetDir = currentDirname
|
|
129
|
-
|
|
130
|
-
urlPrefixBase = path.dirname(urlPathname) // e.g. /blog/
|
|
113
|
+
urlPrefixBase = path.dirname(urlPathname)
|
|
131
114
|
} else {
|
|
132
|
-
// Named file: create subdirectory with same name (minus extension)
|
|
133
115
|
const basename = path.basename(currentFilename, path.extname(currentFilename))
|
|
134
116
|
targetDir = path.join(currentDirname, basename)
|
|
135
|
-
|
|
136
|
-
// e.g. /blog.html -> /blog/
|
|
137
117
|
urlPrefixBase = urlPathname.replace(path.extname(currentFilename), '')
|
|
138
118
|
}
|
|
139
119
|
|
|
140
|
-
// Ensure trailing slash for URL prefix
|
|
141
120
|
if (!urlPrefixBase.endsWith('/')) urlPrefixBase += '/'
|
|
142
|
-
// path.dirname returns / for /index.html, but /blog for /blog/index.html.
|
|
143
|
-
// If /blog/index.html, urlPathname is /blog/index.html. dirname is /blog.
|
|
144
121
|
|
|
145
122
|
if (currentFilename === 'index.html') {
|
|
146
|
-
// If /index.html, urlPathname /index.html. dirname /. prefix /.
|
|
147
|
-
// If /blog/index.html, urlPathname /blog/index.html. dirname /blog. prefix /blog/.
|
|
148
123
|
urlPrefixBase = path.dirname(urlPathname)
|
|
149
124
|
if (!urlPrefixBase.endsWith('/')) urlPrefixBase += '/'
|
|
150
125
|
}
|
|
151
126
|
|
|
152
|
-
|
|
153
|
-
const currentItem = this.pages.getItem(currentDocument.path.pathname)
|
|
127
|
+
const currentItem = this.pages.getItem(currentPathname)
|
|
154
128
|
|
|
155
129
|
for (let i = 2; i <= totalPages; i++) {
|
|
156
130
|
const newPathname = path.join(targetDir, segment, `${i}.html`)
|
|
157
131
|
|
|
158
132
|
const virtualItem = {
|
|
159
|
-
content: currentItem ? currentItem.content : '',
|
|
133
|
+
content: currentItem ? currentItem.content : '',
|
|
160
134
|
path: {
|
|
161
135
|
pathname: newPathname,
|
|
162
136
|
dirname: path.dirname(newPathname),
|
|
163
137
|
filename: path.basename(newPathname)
|
|
164
138
|
},
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// Calculate prefix once and pass it down
|
|
169
|
-
meta_pagination_url_prefix: urlPrefixBase
|
|
139
|
+
properties: {
|
|
140
|
+
paginationBaseUrl: urlPathname,
|
|
141
|
+
paginationUrlPrefix: urlPrefixBase
|
|
170
142
|
},
|
|
171
143
|
type: 'page'
|
|
172
144
|
}
|
|
173
145
|
|
|
174
|
-
// Add to queue
|
|
175
146
|
await this.addRenderQueue(virtualItem, buildId)
|
|
176
147
|
}
|
|
177
148
|
}
|
|
178
149
|
} else {
|
|
179
|
-
// Simple limit/offset without pagination logic
|
|
180
150
|
endIndex = Math.min(startIndex + limit, allPages.length)
|
|
181
151
|
}
|
|
182
152
|
}
|
|
@@ -185,29 +155,28 @@ async function aggregationMethod (options, contextInstance) {
|
|
|
185
155
|
const resultNodes = []
|
|
186
156
|
|
|
187
157
|
for (const page of paginatedPages) {
|
|
188
|
-
const
|
|
189
|
-
let
|
|
190
|
-
|
|
191
|
-
// Apply
|
|
192
|
-
if (
|
|
193
|
-
for (const key in
|
|
194
|
-
if (Object.prototype.hasOwnProperty.call(
|
|
195
|
-
const transform =
|
|
158
|
+
const pageProps = (page.result && page.result.properties) ? page.result.properties : page.properties
|
|
159
|
+
let itemProps = { ...pageProps }
|
|
160
|
+
|
|
161
|
+
// Apply properties transformations
|
|
162
|
+
if (transformProperties && typeof transformProperties === 'object') {
|
|
163
|
+
for (const key in transformProperties) {
|
|
164
|
+
if (Object.prototype.hasOwnProperty.call(transformProperties, key)) {
|
|
165
|
+
const transform = transformProperties[key]
|
|
196
166
|
if (typeof transform === 'string') {
|
|
197
|
-
|
|
167
|
+
itemProps[key] = pageProps[transform]
|
|
198
168
|
} else if (typeof transform === 'function') {
|
|
199
|
-
|
|
169
|
+
itemProps[key] = transform(pageProps)
|
|
200
170
|
}
|
|
201
171
|
}
|
|
202
172
|
}
|
|
203
173
|
}
|
|
204
174
|
|
|
205
|
-
// Render the item template
|
|
206
175
|
if (template) {
|
|
207
176
|
const component = await this.createComponentElement({
|
|
208
177
|
id: template,
|
|
209
|
-
|
|
210
|
-
|
|
178
|
+
properties: itemProps,
|
|
179
|
+
page: context.page,
|
|
211
180
|
renderContext: currentRenderContext
|
|
212
181
|
})
|
|
213
182
|
|
|
@@ -217,39 +186,31 @@ async function aggregationMethod (options, contextInstance) {
|
|
|
217
186
|
}
|
|
218
187
|
}
|
|
219
188
|
|
|
220
|
-
// Render pagination controls
|
|
221
189
|
if (pagination) {
|
|
222
|
-
const segment = pagination.segment || 'page'
|
|
223
|
-
const escapedSegment = segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
224
|
-
|
|
225
190
|
const paginationTemplateId = pagination.template || 'coralite-pagination'
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const urlPathname = contextValues.$urlPathname
|
|
191
|
+
const urlPathname = context.page.url.pathname
|
|
192
|
+
|
|
229
193
|
let baseUrl = urlPathname
|
|
230
194
|
let urlPrefix = ''
|
|
231
195
|
|
|
232
|
-
if (
|
|
233
|
-
baseUrl =
|
|
196
|
+
if (contextProperties.paginationBaseUrl) {
|
|
197
|
+
baseUrl = contextProperties.paginationBaseUrl
|
|
234
198
|
}
|
|
235
199
|
|
|
236
|
-
if (
|
|
237
|
-
urlPrefix =
|
|
200
|
+
if (contextProperties.paginationUrlPrefix) {
|
|
201
|
+
urlPrefix = contextProperties.paginationUrlPrefix
|
|
238
202
|
} else {
|
|
239
|
-
// Page 1 logic calculation if not passed
|
|
240
203
|
if (baseUrl.endsWith('/index.html') || baseUrl.endsWith('/')) {
|
|
241
204
|
urlPrefix = path.dirname(baseUrl)
|
|
242
205
|
} else {
|
|
243
|
-
// Named file
|
|
244
206
|
const basename = path.basename(baseUrl, '.html')
|
|
245
207
|
urlPrefix = path.join(path.dirname(baseUrl), basename)
|
|
246
208
|
}
|
|
247
209
|
}
|
|
248
210
|
|
|
249
|
-
// Normalize urlPrefix to ensure trailing slash
|
|
250
211
|
if (!urlPrefix.endsWith('/')) urlPrefix += '/'
|
|
251
212
|
|
|
252
|
-
const
|
|
213
|
+
const paginationProps = {
|
|
253
214
|
'current-page': String(currentPage),
|
|
254
215
|
'total-pages': String(totalPages),
|
|
255
216
|
'base-url': baseUrl,
|
|
@@ -262,8 +223,8 @@ async function aggregationMethod (options, contextInstance) {
|
|
|
262
223
|
|
|
263
224
|
const component = await this.createComponentElement({
|
|
264
225
|
id: paginationTemplateId,
|
|
265
|
-
|
|
266
|
-
|
|
226
|
+
properties: paginationProps,
|
|
227
|
+
page: context.page,
|
|
267
228
|
renderContext: currentRenderContext
|
|
268
229
|
})
|
|
269
230
|
|
|
@@ -279,8 +240,9 @@ export const aggregation = definePlugin({
|
|
|
279
240
|
name: 'aggregation',
|
|
280
241
|
method: aggregationMethod,
|
|
281
242
|
components: [
|
|
282
|
-
|
|
243
|
+
// @ts-ignore
|
|
244
|
+
path.join(import.meta.dirname, 'components/coralite-pagination.html')
|
|
283
245
|
]
|
|
284
246
|
})
|
|
285
247
|
|
|
286
|
-
export default aggregation
|
|
248
|
+
export default aggregation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coralite-plugin-aggregation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "A Coralite plugin for dynamically collecting, filtering, sorting, and displaying content across multiple sources. Build database-free Coralite websites with automated content aggregation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -39,14 +39,11 @@
|
|
|
39
39
|
"lib",
|
|
40
40
|
"types"
|
|
41
41
|
],
|
|
42
|
-
"
|
|
43
|
-
"coralite": "^0.31.7"
|
|
44
|
-
},
|
|
45
|
-
"license": "AGPL-3.0-only",
|
|
42
|
+
"license": "MPL-2.0",
|
|
46
43
|
"devDependencies": {
|
|
47
44
|
"@stylistic/eslint-plugin-js": "^4.2.0",
|
|
48
45
|
"@stylistic/eslint-plugin-plus": "^4.2.0",
|
|
49
|
-
"coralite": "^0.
|
|
46
|
+
"coralite": "^0.33.1"
|
|
50
47
|
},
|
|
51
48
|
"scripts": {
|
|
52
49
|
"test": "node --test ./tests/index.spec.js"
|
package/types/index.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* @property {number} [limit] - The maximum number of items to return (or items per page if pagination is used).
|
|
18
18
|
* @property {number} [offset=0] - The starting index for fetching items.
|
|
19
19
|
* @property {boolean} [recursive=false] - If true, searches subdirectories of the specified paths.
|
|
20
|
-
* @property {Object.<string, (string|function(Object): *)>} [
|
|
20
|
+
* @property {Object.<string, (string|function(Object): *)>} [transformProperties] - A map of key transformations. Keys are the new property names. Values can be a string (source property name) or a function (receiving page values and returning the new value).
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
export default {}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
<template id="coralite-pagination">
|
|
2
|
-
<nav aria-label="{{ ariaLabel }}">
|
|
3
|
-
<ul class="pagination">
|
|
4
|
-
{{ paginationLinks }}
|
|
5
|
-
</ul>
|
|
6
|
-
</nav>
|
|
7
|
-
</template>
|
|
8
|
-
|
|
9
|
-
<script type="module">
|
|
10
|
-
import { defineComponent } from 'coralite'
|
|
11
|
-
|
|
12
|
-
export default defineComponent({
|
|
13
|
-
tokens: {
|
|
14
|
-
ariaLabel(context) {
|
|
15
|
-
return context['aria-label'] || 'Page navigation'
|
|
16
|
-
},
|
|
17
|
-
/**
|
|
18
|
-
* Generates HTML pagination links based on the provided context.
|
|
19
|
-
*
|
|
20
|
-
* @param {Object} context - The context object containing pagination details.
|
|
21
|
-
* @param {string} context['current-page'] - The current page number (1-based).
|
|
22
|
-
* @param {string} context['total-pages'] - The total number of pages.
|
|
23
|
-
* @param {string} context['base-url'] - The explicit URL for Page 1 (e.g., '/blog.html').
|
|
24
|
-
* @param {string} context['url-prefix'] - The directory prefix for paginated segments (e.g., '/blog').
|
|
25
|
-
* @param {string} [context.segment='page'] - The URL segment for pagination (default: 'page').
|
|
26
|
-
* @param {string} [context.max-visible='5'] - Max number of direct page links to show.
|
|
27
|
-
* @param {string} [context.ellipsis='...'] - Text to display for skipped pages.
|
|
28
|
-
* @returns {string} - A string containing the HTML <li> elements.
|
|
29
|
-
*/
|
|
30
|
-
paginationLinks(context) {
|
|
31
|
-
// Parse inputs with defaults
|
|
32
|
-
const currentPage = parseInt(context['current-page'] || '1', 10)
|
|
33
|
-
const totalPages = parseInt(context['total-pages'] || '1', 10)
|
|
34
|
-
const maxVisible = parseInt(context['max-visible'] || '5', 10)
|
|
35
|
-
|
|
36
|
-
const baseUrl = context['base-url'] || ''
|
|
37
|
-
const urlPrefix = context['url-prefix'] || ''
|
|
38
|
-
const segment = context['segment'] || 'page'
|
|
39
|
-
const ellipsis = context['ellipsis'] || '...'
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Constructs the URL for a specific page.
|
|
43
|
-
* 1. Page 1 links strictly to `base-url` (canonical root).
|
|
44
|
-
* 2. Pages > 1 follow the pattern: `{url-prefix}/{segment}/{page}.html`.
|
|
45
|
-
*/
|
|
46
|
-
const getPageUrl = (page) => {
|
|
47
|
-
if (page === 1) {
|
|
48
|
-
return baseUrl
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Ensure prefix has a trailing slash for concatenation
|
|
52
|
-
const cleanPrefix = urlPrefix.endsWith('/') ? urlPrefix : `${urlPrefix}/`
|
|
53
|
-
return `${cleanPrefix}${segment}/${page}.html`
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Helper to generate list items
|
|
57
|
-
const createItem = (page, text, isActive, isDisabled) => {
|
|
58
|
-
let className = 'page-item'
|
|
59
|
-
if (isActive) className += ' active'
|
|
60
|
-
if (isDisabled) className += ' disabled'
|
|
61
|
-
|
|
62
|
-
let attr = ''
|
|
63
|
-
if (isActive) attr += ' aria-current="page"'
|
|
64
|
-
if (isDisabled) attr += ' tabindex="-1" aria-disabled="true"'
|
|
65
|
-
|
|
66
|
-
const href = isDisabled ? '#' : getPageUrl(page)
|
|
67
|
-
|
|
68
|
-
return `<li class="${className}"><a class="page-link" href="${href}"${attr}>${text}</a></li>`
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let links = ''
|
|
72
|
-
|
|
73
|
-
// Previous Link
|
|
74
|
-
links += createItem(currentPage - 1, 'Previous', false, currentPage <= 1)
|
|
75
|
-
|
|
76
|
-
// Calculate Window (Start/End)
|
|
77
|
-
const half = Math.floor(maxVisible / 2)
|
|
78
|
-
let start = currentPage - half
|
|
79
|
-
let end = currentPage + half
|
|
80
|
-
|
|
81
|
-
if (start < 1) {
|
|
82
|
-
start = 1
|
|
83
|
-
end = Math.min(totalPages, maxVisible)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (end > totalPages) {
|
|
87
|
-
end = totalPages
|
|
88
|
-
start = Math.max(1, totalPages - maxVisible + 1)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Render Pages with ellipsis
|
|
92
|
-
const showFirst = start > 1
|
|
93
|
-
const showLast = end < totalPages
|
|
94
|
-
|
|
95
|
-
// First page + ellipsis if window doesn't touch start
|
|
96
|
-
if (showFirst) {
|
|
97
|
-
links += createItem(1, '1', false, false)
|
|
98
|
-
if (start > 2) {
|
|
99
|
-
links += `<li class="page-item disabled"><span class="page-link">${ellipsis}</span></li>`
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Main window loop
|
|
104
|
-
for (let i = start; i <= end; i++) {
|
|
105
|
-
links += createItem(i, i, i === currentPage, false)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Last page + ellipsis if window doesn't touch end
|
|
109
|
-
if (showLast) {
|
|
110
|
-
if (end < totalPages - 1) {
|
|
111
|
-
links += `<li class="page-item disabled"><span class="page-link">${ellipsis}</span></li>`
|
|
112
|
-
}
|
|
113
|
-
links += createItem(totalPages, totalPages, false, false)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Next Link
|
|
117
|
-
links += createItem(currentPage + 1, 'Next', false, currentPage >= totalPages)
|
|
118
|
-
|
|
119
|
-
return links
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
})
|
|
123
|
-
</script>
|