PluginProbe
Code Snippets / 4.0.0-beta.2
Code Snippets v4.0.0-beta.2
4.0.0-beta.2 3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 All 65 releases
code-snippets / js / entries / admin-bar.ts

admin-bar.ts in Code Snippets 4.0.0-beta.2, at js/entries/admin-bar.ts

329 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { getSnippetType } from '../utils/snippets/snippets'
2 import type { SnippetScope } from '../types/Snippet'
3
4 export type PaginationStatus = 'active' | 'inactive'
5 export type PaginationAction = 'first' | 'prev' | 'next' | 'last'
6
7 export interface SnippetResponseItem {
8 id: number
9 scope: SnippetScope
10 name?: string
11 }
12
13 export interface AdminBarConfig {
14 restUrl: string
15 nonce: string
16 perPage: number
17 isNetwork: boolean
18 excludeTypes: string[]
19 snippetPlaceholder: string
20 editUrlBase: string
21 activeNodeId: string
22 inactiveNodeId: string
23 }
24
25 declare const CODE_SNIPPETS_ADMIN_BAR: AdminBarConfig | undefined
26
27 const config = 'undefined' === typeof CODE_SNIPPETS_ADMIN_BAR ? undefined : CODE_SNIPPETS_ADMIN_BAR
28
29 const getMenuNode = (status: PaginationStatus): HTMLElement | null => {
30 if (!config) {
31 return null
32 }
33
34 const nodeId = 'active' === status ? config.activeNodeId : config.inactiveNodeId
35 return document.getElementById(nodeId)
36 }
37
38 const getPaginationControls = (status: PaginationStatus): HTMLElement | null => {
39 const menuNode = getMenuNode(status)
40 if (!menuNode) {
41 return null
42 }
43
44 return menuNode.querySelector<HTMLElement>(`.code-snippets-pagination-controls[data-status="${status}"]`)
45 }
46
47 const getPaginationState = (controls: HTMLElement): { page: number; totalPages: number } => {
48 const page = Number.parseInt(controls.dataset.page ?? '1', 10)
49 const totalPages = Number.parseInt(controls.dataset.totalPages ?? '1', 10)
50
51 return {
52 page: Number.isFinite(page) && 0 < page ? page : 1,
53 totalPages: Number.isFinite(totalPages) && 0 < totalPages ? totalPages : 1
54 }
55 }
56
57 const setLoading = (controls: HTMLElement, loading: boolean) => {
58 controls.dataset.loading = loading ? 'true' : 'false'
59 }
60
61 const buildRequestUrl = (status: PaginationStatus, page: number): string => {
62 if (!config) {
63 return ''
64 }
65
66 const url = new URL(config.restUrl)
67 url.searchParams.set('status', status)
68 url.searchParams.set('page', String(page))
69 url.searchParams.set('per_page', String(config.perPage))
70 url.searchParams.set('orderby', 'display_name')
71 url.searchParams.set('order', 'asc')
72
73 if (config.isNetwork) {
74 url.searchParams.set('network', '1')
75 }
76
77 for (const excluded of config.excludeTypes) {
78 url.searchParams.append('exclude_types[]', excluded)
79 }
80
81 return url.toString()
82 }
83
84 const fetchSnippetsPage = async (status: PaginationStatus, page: number) => {
85 if (!config) {
86 throw new Error('Missing CODE_SNIPPETS_ADMIN_BAR config')
87 }
88
89 const response = await fetch(buildRequestUrl(status, page), {
90 credentials: 'same-origin',
91 headers: {
92 'X-WP-Nonce': config.nonce
93 }
94 })
95
96 if (!response.ok) {
97 throw new Error(`Failed to fetch snippets (${response.status})`)
98 }
99
100 const totalPagesHeader = response.headers.get('X-WP-TotalPages') ?? '1'
101 const totalPages = Number.parseInt(totalPagesHeader, 10) || 1
102
103 const snippets = <SnippetResponseItem[]> await response.json()
104
105 return { snippets, totalPages }
106 }
107
108 const buildEditUrl = (snippetId: number): string => {
109 if (!config) {
110 return '#'
111 }
112
113 try {
114 const url = new URL(config.editUrlBase, window.location.href)
115 url.searchParams.set('id', String(snippetId))
116 return url.toString()
117 } catch {
118 return '#'
119 }
120 }
121
122 const buildSnippetPlaceholder = (snippetId: number): string =>
123 config?.snippetPlaceholder.replace(/%(?:\d+\$)?d/, String(snippetId)) ??
124 `Snippet #${snippetId}`
125
126 const formatSnippetTitle = (snippet: SnippetResponseItem): string => {
127 const typeLabel = getSnippetType(snippet).toUpperCase()
128 const name = snippet.name?.trim()
129 const title = ('' === name ? undefined : name) ?? buildSnippetPlaceholder(snippet.id)
130 return `(${typeLabel}) ${title}`
131 }
132
133 const setControlsLinkDisabled = (controls: HTMLElement, action: PaginationAction, disabled: boolean): void => {
134 const link = controls.querySelector<HTMLAnchorElement>(`a[data-action="${action}"]`)
135 if (link) {
136 link.setAttribute('aria-disabled', disabled ? 'true' : 'false')
137 }
138 }
139
140 const updatePaginationHrefs = (controls: HTMLElement, page: number, totalPages: number, queryArg?: string): void => {
141 if (!queryArg) {
142 return
143 }
144
145 const firstLink = controls.querySelector<HTMLAnchorElement>('a[data-action="first"]')
146 const baseHref = firstLink?.href
147 if (!baseHref) {
148 return
149 }
150
151 const buildHref = (targetPage: number) => {
152 const url = new URL(baseHref)
153
154 if (1 >= targetPage) {
155 url.searchParams.delete(queryArg)
156 } else {
157 url.searchParams.set(queryArg, String(targetPage))
158 }
159
160 return url.toString()
161 }
162
163 const getLink = (action: PaginationAction) =>
164 controls.querySelector<HTMLAnchorElement>(`a[data-action="${action}"]`)
165
166 const first = getLink('first')
167 if (first) {
168 first.href = buildHref(1)
169 }
170
171 const prev = getLink('prev')
172 if (prev) {
173 prev.href = buildHref(Math.max(1, page - 1))
174 }
175
176 const next = getLink('next')
177 if (next) {
178 next.href = buildHref(Math.min(totalPages, page + 1))
179 }
180
181 const last = getLink('last')
182 if (last) {
183 last.href = buildHref(totalPages)
184 }
185 }
186
187 const updatePaginationControls = (controls: HTMLElement, page: number, totalPages: number) => {
188 controls.dataset.page = String(page)
189 controls.dataset.totalPages = String(totalPages)
190
191 const pageLabel = controls.querySelector<HTMLElement>('.code-snippets-pagination-page')
192 if (pageLabel) {
193 pageLabel.textContent = pageLabel.textContent?.replace(/\(\d+\/\d+\)/, `(${page}/${totalPages})`) ?? pageLabel.textContent
194 }
195
196 const disableFirstPrev = 1 >= page
197 const disableNextLast = page >= totalPages
198
199 setControlsLinkDisabled(controls, 'first', disableFirstPrev)
200 setControlsLinkDisabled(controls, 'prev', disableFirstPrev)
201 setControlsLinkDisabled(controls, 'next', disableNextLast)
202 setControlsLinkDisabled(controls, 'last', disableNextLast)
203
204 updatePaginationHrefs(controls, page, totalPages, controls.dataset.queryArg)
205 }
206
207 const replaceSnippetItems = (status: PaginationStatus, snippets: SnippetResponseItem[]) => {
208 const menuNode = getMenuNode(status)
209 if (!menuNode) {
210 return
211 }
212
213 const subMenu = menuNode.querySelector<HTMLUListElement>('ul.ab-submenu')
214 if (!subMenu) {
215 return
216 }
217
218 subMenu.querySelectorAll('li.code-snippets-snippet-item').forEach(node => node.remove())
219
220 const insertAfterId = 'active' === status
221 ? 'wp-admin-bar-code-snippets-active-pagination'
222 : 'wp-admin-bar-code-snippets-inactive-pagination'
223
224 const insertAfter = subMenu.querySelector<HTMLLIElement>(`#${insertAfterId}`)
225 const fragment = document.createDocumentFragment()
226
227 for (const snippet of snippets) {
228 const li = document.createElement('li')
229 li.id = `wp-admin-bar-code-snippets-snippet-${snippet.id}`
230 li.className = 'code-snippets-snippet-item'
231
232 const a = document.createElement('a')
233 a.className = 'ab-item'
234 a.href = buildEditUrl(snippet.id)
235 a.textContent = formatSnippetTitle(snippet)
236
237 li.appendChild(a)
238 fragment.appendChild(li)
239 }
240
241 if (insertAfter?.parentNode === subMenu) {
242 subMenu.insertBefore(fragment, insertAfter.nextSibling)
243 } else {
244 subMenu.appendChild(fragment)
245 }
246 }
247
248 const navigateToPage = async (status: PaginationStatus, targetPage: number) => {
249 const controls = getPaginationControls(status)
250 if (!controls) {
251 return
252 }
253
254 const { totalPages: currentTotalPages } = getPaginationState(controls)
255 const page = Math.max(1, Math.min(targetPage, currentTotalPages))
256
257 setLoading(controls, true)
258
259 try {
260 const { snippets, totalPages } = await fetchSnippetsPage(status, page)
261 updatePaginationControls(controls, page, totalPages)
262 replaceSnippetItems(status, snippets)
263 } catch (error) {
264 console.error(error)
265 } finally {
266 setLoading(controls, false)
267 }
268 }
269
270 const handlePaginationClick = (event: MouseEvent) => {
271 const target = <Element | null> event.target
272 if (!target) {
273 return
274 }
275
276 const link = target.closest<HTMLAnchorElement>('.code-snippets-pagination-controls a[data-action]')
277 if (!link) {
278 return
279 }
280
281 const controls = link.closest<HTMLElement>('.code-snippets-pagination-controls')
282 if (!controls) {
283 return
284 }
285
286 if ('true' === controls.dataset.loading) {
287 return
288 }
289
290 const status = <PaginationStatus | undefined> controls.dataset.status
291 const action = <PaginationAction | undefined> link.dataset.action
292
293 if (!status || !action) {
294 return
295 }
296
297 event.preventDefault()
298 event.stopPropagation()
299 event.stopImmediatePropagation()
300
301 const { page, totalPages } = getPaginationState(controls)
302
303 let targetPage = page
304 switch (action) {
305 case 'first':
306 targetPage = 1
307 break
308 case 'prev':
309 targetPage = page - 1
310 break
311 case 'next':
312 targetPage = page + 1
313 break
314 case 'last':
315 targetPage = totalPages
316 break
317 }
318
319 if (targetPage === page || 1 > targetPage || targetPage > totalPages) {
320 return
321 }
322
323 void navigateToPage(status, targetPage)
324 }
325
326 if (config) {
327 document.addEventListener('click', handlePaginationClick, true)
328 }
329