@ckeditor/ckeditor5-dev-manual-server 57.4.0 → 59.0.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/dist/index.d.ts +0 -1
- package/dist/index.js +44 -146
- package/dist/manual-test-plugin/collect-pages.d.ts +1 -1
- package/package.json +1 -1
- package/theme/catalog-favorites.ts +94 -0
- package/theme/catalog-search.ts +152 -0
- package/theme/catalog.css +148 -15
- package/theme/catalog.html +33 -6
- package/theme/catalog.ts +147 -17
- package/theme/favorite-icon.ts +25 -0
- package/theme/manual-bootstrap.css +3 -3
- package/theme/manual-header.css +26 -9
- package/theme/manual-header.ts +36 -4
- package/theme/manual-instructions.css +11 -3
- package/dist/static-assets-plugin/plugin.d.ts +0 -10
- package/dist/static-assets-plugin/static-assets.d.ts +0 -9
package/theme/catalog.css
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md.
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
:root {
|
|
2
7
|
color-scheme: light;
|
|
3
8
|
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
@@ -15,6 +20,10 @@
|
|
|
15
20
|
--text-subtle: #a3a3a3;
|
|
16
21
|
--accent: #171717;
|
|
17
22
|
--accent-ring: rgb(23 23 23 / 10%);
|
|
23
|
+
--search-match-background: #e6d3fa;
|
|
24
|
+
--search-match-text: #480090;
|
|
25
|
+
--favorites-surface: #f7f0ff;
|
|
26
|
+
--favorites-border: #d9c2f0;
|
|
18
27
|
--radius-lg: 12px;
|
|
19
28
|
--radius-md: 8px;
|
|
20
29
|
--radius-sm: 6px;
|
|
@@ -36,6 +45,12 @@ a {
|
|
|
36
45
|
color: inherit;
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
::highlight(ckeditor5-manual-search-match) {
|
|
49
|
+
background-color: var(--search-match-background);
|
|
50
|
+
color: var(--search-match-text);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Application shell. */
|
|
39
54
|
.app {
|
|
40
55
|
max-width: 1280px;
|
|
41
56
|
margin: 0 auto;
|
|
@@ -75,12 +90,11 @@ a {
|
|
|
75
90
|
font-variant-numeric: tabular-nums;
|
|
76
91
|
}
|
|
77
92
|
|
|
93
|
+
/* Search. */
|
|
78
94
|
.app__search {
|
|
79
95
|
display: flex;
|
|
80
96
|
align-items: center;
|
|
81
|
-
gap: 10px;
|
|
82
97
|
height: 44px;
|
|
83
|
-
padding: 0 14px;
|
|
84
98
|
background: var(--surface);
|
|
85
99
|
border: 1px solid var(--border);
|
|
86
100
|
border-radius: var(--radius-md);
|
|
@@ -92,7 +106,19 @@ a {
|
|
|
92
106
|
box-shadow: 0 0 0 4px var(--accent-ring);
|
|
93
107
|
}
|
|
94
108
|
|
|
95
|
-
.app__search-
|
|
109
|
+
.app__search-label {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
align-self: stretch;
|
|
113
|
+
flex: 1;
|
|
114
|
+
min-width: 0;
|
|
115
|
+
gap: 10px;
|
|
116
|
+
padding: 0 10px 0 14px;
|
|
117
|
+
cursor: text;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.app__search-icon,
|
|
121
|
+
.app__search-clear {
|
|
96
122
|
flex-shrink: 0;
|
|
97
123
|
color: var(--text-subtle);
|
|
98
124
|
}
|
|
@@ -110,13 +136,33 @@ a {
|
|
|
110
136
|
}
|
|
111
137
|
|
|
112
138
|
.app__search-input::-webkit-search-cancel-button {
|
|
113
|
-
|
|
139
|
+
display: none;
|
|
114
140
|
}
|
|
115
141
|
|
|
116
142
|
.app__search-input::placeholder {
|
|
117
143
|
color: var(--text-muted);
|
|
118
144
|
}
|
|
119
145
|
|
|
146
|
+
.app__search:has(.app__search-input:placeholder-shown) .app__search-clear {
|
|
147
|
+
visibility: hidden;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.app__search-clear {
|
|
151
|
+
margin-inline-end: 10px;
|
|
152
|
+
padding: 6px;
|
|
153
|
+
line-height: 0;
|
|
154
|
+
background: none;
|
|
155
|
+
border: 0;
|
|
156
|
+
border-radius: var(--radius-sm);
|
|
157
|
+
cursor: pointer;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.app__search-clear:hover {
|
|
161
|
+
color: var(--text);
|
|
162
|
+
background: var(--surface-hover);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Catalog content. */
|
|
120
166
|
.app__list {
|
|
121
167
|
display: grid;
|
|
122
168
|
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
|
@@ -145,6 +191,7 @@ a {
|
|
|
145
191
|
color: var(--text-muted);
|
|
146
192
|
}
|
|
147
193
|
|
|
194
|
+
/* Package cards. */
|
|
148
195
|
.pkg {
|
|
149
196
|
background: var(--surface);
|
|
150
197
|
border: 1px solid var(--border);
|
|
@@ -160,11 +207,14 @@ a {
|
|
|
160
207
|
.pkg__header {
|
|
161
208
|
display: flex;
|
|
162
209
|
align-items: center;
|
|
163
|
-
|
|
164
|
-
padding: 12px 16px;
|
|
210
|
+
padding: 12px 6px 12px 16px;
|
|
165
211
|
border-bottom: 1px solid var(--border);
|
|
166
212
|
}
|
|
167
213
|
|
|
214
|
+
.pkg__header > * + * {
|
|
215
|
+
margin-inline-start: 10px;
|
|
216
|
+
}
|
|
217
|
+
|
|
168
218
|
.pkg__name {
|
|
169
219
|
flex: 1;
|
|
170
220
|
min-width: 0;
|
|
@@ -177,23 +227,91 @@ a {
|
|
|
177
227
|
}
|
|
178
228
|
|
|
179
229
|
.pkg__count {
|
|
180
|
-
flex
|
|
181
|
-
|
|
182
|
-
text-align: right;
|
|
230
|
+
flex: 0 0 38px;
|
|
231
|
+
text-align: center;
|
|
183
232
|
font-size: 12px;
|
|
184
233
|
color: var(--text-muted);
|
|
185
234
|
font-variant-numeric: tabular-nums;
|
|
186
235
|
}
|
|
187
236
|
|
|
237
|
+
.pkg__favorite,
|
|
238
|
+
.pkg__favorites-clear {
|
|
239
|
+
flex-shrink: 0;
|
|
240
|
+
line-height: 0;
|
|
241
|
+
color: var(--text-subtle);
|
|
242
|
+
background: none;
|
|
243
|
+
border: 0;
|
|
244
|
+
border-radius: var(--radius-sm);
|
|
245
|
+
cursor: pointer;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* Favorites card. */
|
|
249
|
+
.pkg--favorites,
|
|
250
|
+
.pkg--favorites:hover {
|
|
251
|
+
border-color: var(--favorites-border);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.pkg--favorites .pkg__header {
|
|
255
|
+
color: var(--search-match-text);
|
|
256
|
+
background: var(--favorites-surface);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.pkg--favorites .pkg__name {
|
|
260
|
+
font-family: inherit;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.pkg__favorites-icon {
|
|
264
|
+
flex-shrink: 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.pkg__favorites-icon path,
|
|
268
|
+
.pkg__favorite-icon path {
|
|
269
|
+
fill: none;
|
|
270
|
+
stroke: currentColor;
|
|
271
|
+
stroke-linejoin: round;
|
|
272
|
+
stroke-width: 1.5px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.pkg__favorites-icon path,
|
|
276
|
+
.favorite--active .pkg__favorite-icon path {
|
|
277
|
+
fill: currentColor;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.pkg__favorites-clear {
|
|
281
|
+
padding: 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.pkg__favorites-clear:hover {
|
|
285
|
+
color: var(--text);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.pkg__favorites-clear:not([hidden]) + .pkg__count {
|
|
289
|
+
margin-inline-start: 2px;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* Package contents. */
|
|
188
293
|
.pkg__list {
|
|
189
294
|
list-style: none;
|
|
190
295
|
margin: 0;
|
|
191
296
|
padding: 6px;
|
|
192
297
|
}
|
|
193
298
|
|
|
299
|
+
.pkg__favorites-empty {
|
|
300
|
+
padding: 10px 12px;
|
|
301
|
+
font-size: 12px;
|
|
302
|
+
color: var(--text-muted);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.pkg__item {
|
|
306
|
+
display: flex;
|
|
307
|
+
align-items: center;
|
|
308
|
+
}
|
|
309
|
+
|
|
194
310
|
.pkg__link {
|
|
195
311
|
display: flex;
|
|
196
312
|
align-items: center;
|
|
313
|
+
flex: 1;
|
|
314
|
+
min-width: 0;
|
|
197
315
|
gap: 12px;
|
|
198
316
|
padding: 10px 12px;
|
|
199
317
|
border-radius: var(--radius-sm);
|
|
@@ -206,12 +324,6 @@ a {
|
|
|
206
324
|
background: var(--surface-hover);
|
|
207
325
|
}
|
|
208
326
|
|
|
209
|
-
.pkg__link:focus-visible {
|
|
210
|
-
outline: none;
|
|
211
|
-
background: var(--surface-hover);
|
|
212
|
-
box-shadow: inset 0 0 0 2px var(--border-strong);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
327
|
.pkg__test-name {
|
|
216
328
|
flex: 1;
|
|
217
329
|
min-width: 0;
|
|
@@ -223,6 +335,27 @@ a {
|
|
|
223
335
|
white-space: nowrap;
|
|
224
336
|
}
|
|
225
337
|
|
|
338
|
+
.pkg__favorite {
|
|
339
|
+
padding: 11px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.pkg__favorite:hover {
|
|
343
|
+
color: var(--text);
|
|
344
|
+
background: var(--surface-hover);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.pkg__favorite.favorite--active {
|
|
348
|
+
color: var(--search-match-text);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.pkg__link:focus-visible,
|
|
352
|
+
.pkg__favorite:focus-visible,
|
|
353
|
+
.pkg__favorites-clear:focus-visible {
|
|
354
|
+
outline: none;
|
|
355
|
+
background: var(--surface-hover);
|
|
356
|
+
box-shadow: inset 0 0 0 2px var(--border-strong);
|
|
357
|
+
}
|
|
358
|
+
|
|
226
359
|
@media (max-width: 600px) {
|
|
227
360
|
.app {
|
|
228
361
|
padding: 0 16px 48px;
|
package/theme/catalog.html
CHANGED
|
@@ -15,12 +15,19 @@
|
|
|
15
15
|
<h1 class="app__title">CKEditor 5 manual tests</h1>
|
|
16
16
|
<p class="app__subtitle"></p>
|
|
17
17
|
</div>
|
|
18
|
-
<
|
|
19
|
-
<
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
<div class="app__search" role="search">
|
|
19
|
+
<label class="app__search-label">
|
|
20
|
+
<svg class="app__search-icon" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16">
|
|
21
|
+
<path fill="currentColor" d="M7 2a5 5 0 103.546 8.546l3.204 3.204a.75.75 0 101.06-1.06l-3.204-3.204A5 5 0 007 2zM3.5 7a3.5 3.5 0 117 0 3.5 3.5 0 01-7 0z"/>
|
|
22
|
+
</svg>
|
|
23
|
+
<input type="search" class="app__search-input" placeholder="Filter by package or test name…" autocomplete="off" autofocus>
|
|
24
|
+
</label>
|
|
25
|
+
<button type="button" class="app__search-clear" aria-label="Clear search">
|
|
26
|
+
<svg viewBox="0 0 16 16" aria-hidden="true" width="16" height="16">
|
|
27
|
+
<path fill="currentColor" d="M3.22 3.22a.75.75 0 011.06 0L8 6.94l3.72-3.72a.75.75 0 111.06 1.06L9.06 8l3.72 3.72a.75.75 0 11-1.06 1.06L8 9.06l-3.72 3.72a.75.75 0 01-1.06-1.06L6.94 8 3.22 4.28a.75.75 0 010-1.06z"/>
|
|
28
|
+
</svg>
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
24
31
|
</header>
|
|
25
32
|
<div class="app__list" role="list"></div>
|
|
26
33
|
</main>
|
|
@@ -43,11 +50,31 @@
|
|
|
43
50
|
</section>
|
|
44
51
|
</template>
|
|
45
52
|
|
|
53
|
+
<template id="manual-favorites-template">
|
|
54
|
+
<section class="pkg pkg--favorites" role="listitem">
|
|
55
|
+
<header class="pkg__header">
|
|
56
|
+
<span class="pkg__name">Favorites</span>
|
|
57
|
+
<button type="button" class="pkg__favorites-clear" aria-label="Clear all favorites" title="Clear all favorites" hidden>
|
|
58
|
+
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
|
|
59
|
+
<path fill="currentColor" d="M5.5 2.5h5l.5 1H14V5H2V3.5h3l.5-1zM3.5 6h9l-.65 8h-7.7L3.5 6zm2.25 1.5v5h1.5v-5h-1.5zm3 0v5h1.5v-5h-1.5z"/>
|
|
60
|
+
</svg>
|
|
61
|
+
</button>
|
|
62
|
+
<span class="pkg__count"></span>
|
|
63
|
+
</header>
|
|
64
|
+
<ul class="pkg__list"></ul>
|
|
65
|
+
</section>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<template id="manual-favorites-empty-template">
|
|
69
|
+
<li class="pkg__favorites-empty">Star tests to add them here.</li>
|
|
70
|
+
</template>
|
|
71
|
+
|
|
46
72
|
<template id="manual-test-template">
|
|
47
73
|
<li class="pkg__item">
|
|
48
74
|
<a class="pkg__link">
|
|
49
75
|
<span class="pkg__test-name"></span>
|
|
50
76
|
</a>
|
|
77
|
+
<button type="button" class="pkg__favorite"></button>
|
|
51
78
|
</li>
|
|
52
79
|
</template>
|
|
53
80
|
|
package/theme/catalog.ts
CHANGED
|
@@ -5,10 +5,22 @@
|
|
|
5
5
|
|
|
6
6
|
// @ts-expect-error Virtual module provided by the manual test entries plugin.
|
|
7
7
|
import { manualTestEntries, type ManualTestEntry } from 'virtual:ckeditor5-manual-entries';
|
|
8
|
+
import {
|
|
9
|
+
filterFavoriteEntries,
|
|
10
|
+
getFavoriteId,
|
|
11
|
+
loadFavoriteIds,
|
|
12
|
+
saveFavoriteIds,
|
|
13
|
+
toggleFavoriteId,
|
|
14
|
+
updateFavoriteButtonState
|
|
15
|
+
} from './catalog-favorites.js';
|
|
16
|
+
import { filterEntries, findMatchOffsets } from './catalog-search.js';
|
|
17
|
+
import { createFavoriteIcon } from './favorite-icon.js';
|
|
8
18
|
|
|
9
19
|
import './catalog.css';
|
|
10
20
|
|
|
11
21
|
const SEARCH_QUERY_PARAMETER = 'q';
|
|
22
|
+
const SEARCH_HIGHLIGHT_NAME = 'ckeditor5-manual-search-match';
|
|
23
|
+
const SEARCH_LABEL_SELECTOR = '.pkg__name, .pkg__test-name';
|
|
12
24
|
|
|
13
25
|
/**
|
|
14
26
|
* Root element for the manual test index application.
|
|
@@ -22,6 +34,7 @@ renderApp( appElement, manualTestEntries );
|
|
|
22
34
|
*/
|
|
23
35
|
function renderApp( root: HTMLElement, entries: Array<ManualTestEntry> ): void {
|
|
24
36
|
const packageCount = new Set( entries.map( entry => entry.packageName ) ).size;
|
|
37
|
+
const favoriteIds = loadFavoriteIds();
|
|
25
38
|
|
|
26
39
|
root.replaceChildren( cloneTemplateElement( 'manual-index-template' ) );
|
|
27
40
|
|
|
@@ -29,9 +42,18 @@ function renderApp( root: HTMLElement, entries: Array<ManualTestEntry> ): void {
|
|
|
29
42
|
|
|
30
43
|
const listElement = root.querySelector<HTMLElement>( '.app__list' )!;
|
|
31
44
|
const searchInput = root.querySelector<HTMLInputElement>( '.app__search-input' )!;
|
|
45
|
+
const clearSearchButton = root.querySelector<HTMLButtonElement>( '.app__search-clear' )!;
|
|
32
46
|
|
|
33
47
|
const render = ( query: string ): void => {
|
|
34
|
-
|
|
48
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
49
|
+
const renderedGroups = renderGroups( filterEntries( entries, normalizedQuery ), favoriteIds );
|
|
50
|
+
|
|
51
|
+
if ( !normalizedQuery && entries.length ) {
|
|
52
|
+
renderedGroups.unshift( renderFavoritesCard( entries, favoriteIds ) );
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
listElement.replaceChildren( ...renderedGroups );
|
|
56
|
+
updateSearchHighlights( listElement, normalizedQuery );
|
|
35
57
|
};
|
|
36
58
|
|
|
37
59
|
const setQuery = ( query: string ): void => {
|
|
@@ -39,9 +61,54 @@ function renderApp( root: HTMLElement, entries: Array<ManualTestEntry> ): void {
|
|
|
39
61
|
render( query );
|
|
40
62
|
};
|
|
41
63
|
|
|
42
|
-
|
|
64
|
+
const applySearchInput = (): void => {
|
|
43
65
|
updateSearchQueryInUrl( searchInput.value );
|
|
44
66
|
render( searchInput.value );
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
searchInput.addEventListener( 'input', applySearchInput );
|
|
70
|
+
|
|
71
|
+
listElement.addEventListener( 'click', event => {
|
|
72
|
+
const target = event.target;
|
|
73
|
+
|
|
74
|
+
if ( !( target instanceof Element ) ) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const favoriteActionButton = target.closest<HTMLButtonElement>( '.pkg__favorite, .pkg__favorites-clear' );
|
|
79
|
+
|
|
80
|
+
if ( !favoriteActionButton ) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const favoriteId = favoriteActionButton.dataset.favoriteId;
|
|
85
|
+
const shouldRestoreFocus = document.activeElement == favoriteActionButton;
|
|
86
|
+
|
|
87
|
+
if ( favoriteId ) {
|
|
88
|
+
toggleFavoriteId( favoriteIds, favoriteId );
|
|
89
|
+
} else {
|
|
90
|
+
favoriteIds.clear();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
saveFavoriteIds( favoriteIds );
|
|
94
|
+
render( searchInput.value );
|
|
95
|
+
|
|
96
|
+
if ( !shouldRestoreFocus ) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if ( favoriteId ) {
|
|
101
|
+
focusFavoriteButton( listElement, favoriteId );
|
|
102
|
+
} else {
|
|
103
|
+
searchInput.focus();
|
|
104
|
+
}
|
|
105
|
+
} );
|
|
106
|
+
|
|
107
|
+
clearSearchButton.addEventListener( 'click', () => {
|
|
108
|
+
searchInput.value = '';
|
|
109
|
+
searchInput.focus();
|
|
110
|
+
|
|
111
|
+
applySearchInput();
|
|
45
112
|
} );
|
|
46
113
|
|
|
47
114
|
window.addEventListener( 'popstate', () => {
|
|
@@ -59,6 +126,16 @@ function renderApp( root: HTMLElement, entries: Array<ManualTestEntry> ): void {
|
|
|
59
126
|
setQuery( getSearchQueryFromUrl() );
|
|
60
127
|
}
|
|
61
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Focuses the re-rendered favorite control in its package card.
|
|
131
|
+
*/
|
|
132
|
+
function focusFavoriteButton( root: HTMLElement, favoriteId: string ): void {
|
|
133
|
+
const favoriteButton = [ ...root.querySelectorAll<HTMLButtonElement>( '.pkg:not(.pkg--favorites) .pkg__favorite' ) ]
|
|
134
|
+
.find( button => button.dataset.favoriteId == favoriteId );
|
|
135
|
+
|
|
136
|
+
favoriteButton?.focus();
|
|
137
|
+
}
|
|
138
|
+
|
|
62
139
|
/**
|
|
63
140
|
* Returns the current search query from the page URL.
|
|
64
141
|
*/
|
|
@@ -86,25 +163,46 @@ function updateSearchQueryInUrl( query: string ): void {
|
|
|
86
163
|
}
|
|
87
164
|
|
|
88
165
|
/**
|
|
89
|
-
*
|
|
166
|
+
* Highlights query matches in the currently rendered package and test names.
|
|
90
167
|
*/
|
|
91
|
-
function
|
|
92
|
-
|
|
168
|
+
function updateSearchHighlights( root: HTMLElement, normalizedQuery: string ): void {
|
|
169
|
+
if ( !( 'highlights' in CSS ) ) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
93
172
|
|
|
94
|
-
|
|
95
|
-
|
|
173
|
+
CSS.highlights.delete( SEARCH_HIGHLIGHT_NAME );
|
|
174
|
+
|
|
175
|
+
if ( !normalizedQuery ) {
|
|
176
|
+
return;
|
|
96
177
|
}
|
|
97
178
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
179
|
+
const ranges: Array<Range> = [];
|
|
180
|
+
|
|
181
|
+
for ( const label of root.querySelectorAll<HTMLElement>( SEARCH_LABEL_SELECTOR ) ) {
|
|
182
|
+
const textNode = label.firstChild;
|
|
183
|
+
|
|
184
|
+
if ( !textNode || textNode.nodeType != Node.TEXT_NODE ) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for ( const { start, end } of findMatchOffsets( textNode.textContent!, normalizedQuery ) ) {
|
|
189
|
+
const range = new Range();
|
|
190
|
+
|
|
191
|
+
range.setStart( textNode, start );
|
|
192
|
+
range.setEnd( textNode, end );
|
|
193
|
+
ranges.push( range );
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if ( ranges.length ) {
|
|
198
|
+
CSS.highlights.set( SEARCH_HIGHLIGHT_NAME, new Highlight( ...ranges ) );
|
|
199
|
+
}
|
|
102
200
|
}
|
|
103
201
|
|
|
104
202
|
/**
|
|
105
203
|
* Groups visible entries by package and renders package cards.
|
|
106
204
|
*/
|
|
107
|
-
function renderGroups( entries: Array<ManualTestEntry> ): Array<HTMLElement> {
|
|
205
|
+
function renderGroups( entries: Array<ManualTestEntry>, favoriteIds: Set<string> ): Array<HTMLElement> {
|
|
108
206
|
if ( !entries.length ) {
|
|
109
207
|
return [ cloneTemplateElement( 'manual-empty-template' ) ];
|
|
110
208
|
}
|
|
@@ -118,32 +216,64 @@ function renderGroups( entries: Array<ManualTestEntry> ): Array<HTMLElement> {
|
|
|
118
216
|
groups.set( entry.packageName, group );
|
|
119
217
|
}
|
|
120
218
|
|
|
121
|
-
return [ ...groups.entries() ].map( ( [ packageName, packageEntries ] ) =>
|
|
219
|
+
return [ ...groups.entries() ].map( ( [ packageName, packageEntries ] ) =>
|
|
220
|
+
renderPackageCard( packageName, packageEntries, favoriteIds )
|
|
221
|
+
);
|
|
122
222
|
}
|
|
123
223
|
|
|
124
224
|
/**
|
|
125
225
|
* Renders a single package card with all tests belonging to that package.
|
|
126
226
|
*/
|
|
127
|
-
function renderPackageCard( packageName: string, entries: Array<ManualTestEntry> ): HTMLElement {
|
|
227
|
+
function renderPackageCard( packageName: string, entries: Array<ManualTestEntry>, favoriteIds: Set<string> ): HTMLElement {
|
|
128
228
|
const packageCard = cloneTemplateElement( 'manual-package-template' );
|
|
129
229
|
const list = packageCard.querySelector<HTMLElement>( '.pkg__list' )!;
|
|
130
230
|
|
|
131
231
|
packageCard.querySelector<HTMLElement>( '.pkg__name' )!.textContent = packageName;
|
|
132
232
|
packageCard.querySelector<HTMLElement>( '.pkg__count' )!.textContent = String( entries.length );
|
|
133
|
-
list.replaceChildren( ...entries.map( entry => renderTestItem( entry ) ) );
|
|
233
|
+
list.replaceChildren( ...entries.map( entry => renderTestItem( entry, favoriteIds ) ) );
|
|
134
234
|
|
|
135
235
|
return packageCard;
|
|
136
236
|
}
|
|
137
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Renders the favorites card, including its empty state.
|
|
240
|
+
*/
|
|
241
|
+
function renderFavoritesCard( entries: Array<ManualTestEntry>, favoriteIds: Set<string> ): HTMLElement {
|
|
242
|
+
const favoritesCard = cloneTemplateElement( 'manual-favorites-template' );
|
|
243
|
+
const favoriteEntries = filterFavoriteEntries( entries, favoriteIds );
|
|
244
|
+
const list = favoritesCard.querySelector<HTMLElement>( '.pkg__list' )!;
|
|
245
|
+
const clearButton = favoritesCard.querySelector<HTMLButtonElement>( '.pkg__favorites-clear' )!;
|
|
246
|
+
|
|
247
|
+
favoritesCard.querySelector<HTMLElement>( '.pkg__header' )!.prepend( createFavoriteIcon( 'pkg__favorites-icon' ) );
|
|
248
|
+
favoritesCard.querySelector<HTMLElement>( '.pkg__count' )!.textContent = String( favoriteEntries.length );
|
|
249
|
+
clearButton.hidden = !favoriteEntries.length;
|
|
250
|
+
|
|
251
|
+
if ( favoriteEntries.length ) {
|
|
252
|
+
list.replaceChildren( ...favoriteEntries.map( entry => renderTestItem( entry, favoriteIds, true ) ) );
|
|
253
|
+
} else {
|
|
254
|
+
list.replaceChildren( cloneTemplateElement( 'manual-favorites-empty-template' ) );
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return favoritesCard;
|
|
258
|
+
}
|
|
259
|
+
|
|
138
260
|
/**
|
|
139
261
|
* Renders a single test link inside a package card.
|
|
140
262
|
*/
|
|
141
|
-
function renderTestItem( entry: ManualTestEntry ): HTMLElement {
|
|
263
|
+
function renderTestItem( entry: ManualTestEntry, favoriteIds: Set<string>, showPackageName = false ): HTMLElement {
|
|
142
264
|
const testItem = cloneTemplateElement( 'manual-test-template' );
|
|
143
265
|
const link = testItem.querySelector<HTMLAnchorElement>( '.pkg__link' )!;
|
|
266
|
+
const favoriteButton = testItem.querySelector<HTMLButtonElement>( '.pkg__favorite' )!;
|
|
267
|
+
const favoriteId = getFavoriteId( entry );
|
|
144
268
|
|
|
145
269
|
link.href = entry.href;
|
|
146
|
-
|
|
270
|
+
|
|
271
|
+
testItem.querySelector<HTMLElement>( '.pkg__test-name' )!.textContent = showPackageName ? favoriteId : entry.slug;
|
|
272
|
+
|
|
273
|
+
favoriteButton.dataset.favoriteId = favoriteId;
|
|
274
|
+
favoriteButton.append( createFavoriteIcon( 'pkg__favorite-icon' ) );
|
|
275
|
+
|
|
276
|
+
updateFavoriteButtonState( favoriteButton, favoriteIds, favoriteId );
|
|
147
277
|
|
|
148
278
|
return testItem;
|
|
149
279
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
7
|
+
const FAVORITE_ICON_PATH = 'M8 1.5l1.94 3.93 4.34.63-3.14 3.06.74 4.32L8 11.4l-3.88 2.04.74-4.32-3.14-3.06 4.34-.63L8 1.5z';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates the favorite star icon used by the catalog and manual test header.
|
|
11
|
+
*/
|
|
12
|
+
export function createFavoriteIcon( className: string ): SVGSVGElement {
|
|
13
|
+
const icon = document.createElementNS( SVG_NAMESPACE, 'svg' );
|
|
14
|
+
const path = document.createElementNS( SVG_NAMESPACE, 'path' );
|
|
15
|
+
|
|
16
|
+
icon.classList.add( className );
|
|
17
|
+
icon.setAttribute( 'viewBox', '0 0 16 16' );
|
|
18
|
+
icon.setAttribute( 'width', '16' );
|
|
19
|
+
icon.setAttribute( 'height', '16' );
|
|
20
|
+
icon.setAttribute( 'aria-hidden', 'true' );
|
|
21
|
+
path.setAttribute( 'd', FAVORITE_ICON_PATH );
|
|
22
|
+
icon.append( path );
|
|
23
|
+
|
|
24
|
+
return icon;
|
|
25
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
.refresh-prompt {
|
|
9
9
|
position: fixed;
|
|
10
|
-
top:
|
|
10
|
+
top: 16px;
|
|
11
11
|
left: 50%;
|
|
12
12
|
z-index: 100000;
|
|
13
13
|
box-sizing: border-box;
|
|
@@ -20,14 +20,14 @@
|
|
|
20
20
|
cursor: pointer;
|
|
21
21
|
opacity: 0;
|
|
22
22
|
pointer-events: none;
|
|
23
|
-
transform: translate(-50%, calc( -100% -
|
|
23
|
+
transform: translate(-50%, calc( -100% - 16px ) );
|
|
24
24
|
transition: transform 0.18s ease-out, opacity 0.18s ease-out, background 0.1s;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
.refresh-prompt--visible {
|
|
28
28
|
opacity: 1;
|
|
29
29
|
pointer-events: auto;
|
|
30
|
-
transform: translate(-50%,
|
|
30
|
+
transform: translate(-50%, 0);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
.refresh-prompt:hover {
|
package/theme/manual-header.css
CHANGED
|
@@ -7,15 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
:host {
|
|
9
9
|
display: block;
|
|
10
|
-
--ck-manual-header-height: 44px;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
.bar {
|
|
14
|
-
position: fixed;
|
|
15
|
-
top: 0;
|
|
16
|
-
left: 0;
|
|
17
|
-
right: 0;
|
|
18
|
-
z-index: 99999;
|
|
19
13
|
display: flex;
|
|
20
14
|
align-items: stretch;
|
|
21
15
|
box-sizing: border-box;
|
|
@@ -37,7 +31,7 @@
|
|
|
37
31
|
padding: 8px 14px;
|
|
38
32
|
border: 0;
|
|
39
33
|
background: transparent;
|
|
40
|
-
border-radius:
|
|
34
|
+
border-radius: 6px;
|
|
41
35
|
text-decoration: none;
|
|
42
36
|
color: inherit;
|
|
43
37
|
font: inherit;
|
|
@@ -74,7 +68,7 @@
|
|
|
74
68
|
align-items: center;
|
|
75
69
|
flex: 1 1 auto;
|
|
76
70
|
gap: 10px;
|
|
77
|
-
padding:
|
|
71
|
+
padding: 0 14px;
|
|
78
72
|
overflow: hidden;
|
|
79
73
|
min-width: 0;
|
|
80
74
|
}
|
|
@@ -96,6 +90,29 @@
|
|
|
96
90
|
white-space: nowrap;
|
|
97
91
|
}
|
|
98
92
|
|
|
93
|
+
.favorite {
|
|
94
|
+
flex-shrink: 0;
|
|
95
|
+
justify-content: center;
|
|
96
|
+
padding: 8px;
|
|
97
|
+
margin-inline-start: -8px;
|
|
98
|
+
color: #737373;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.favorite__icon {
|
|
102
|
+
fill: none;
|
|
103
|
+
stroke: currentColor;
|
|
104
|
+
stroke-linejoin: round;
|
|
105
|
+
stroke-width: 1.5px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.favorite--active {
|
|
109
|
+
color: #6d28d9;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.favorite--active .favorite__icon {
|
|
113
|
+
fill: currentColor;
|
|
114
|
+
}
|
|
115
|
+
|
|
99
116
|
:host( :not( [ has-instructions ] ) ) .instructions-only {
|
|
100
117
|
display: none;
|
|
101
118
|
}
|
|
@@ -119,7 +136,7 @@
|
|
|
119
136
|
--panel-transition-duration: 0.18s;
|
|
120
137
|
|
|
121
138
|
position: fixed;
|
|
122
|
-
top:
|
|
139
|
+
top: 0;
|
|
123
140
|
right: 0;
|
|
124
141
|
bottom: 0;
|
|
125
142
|
z-index: 99998;
|