@@ -75,20 +75,35 @@ async function createSiteList () {
75
75
76
76
// Get the list elements
77
77
const listElement = document . getElementById ( 'sites-list' )
78
+ const gridContainer = document . getElementById ( 'grid-list' )
78
79
const templateElement = document . getElementById ( 'sites-list-template' )
80
+ const gridTemplateElement = document . getElementById ( 'grid-list-template' )
79
81
const loadingElement = document . getElementById ( 'sites-list-loading' )
82
+ const gridLoadingElement = document . getElementById ( 'grid-list-loading' )
80
83
const emptyElement = document . getElementById ( 'sites-list-empty' )
84
+ const gridEmptyElement = document . getElementById ( 'grid-list-empty' )
81
85
82
86
loadingElement . classList . add ( 'd-none' )
83
- if ( ! sites . length ) emptyElement . classList . remove ( 'd-none' )
87
+ gridLoadingElement . classList . add ( 'd-none' )
88
+
89
+ if ( ! sites . length ) {
90
+ emptyElement . classList . remove ( 'd-none' )
91
+ gridEmptyElement . classList . remove ( 'd-none' )
92
+ }
84
93
85
94
// Create a list element for every instance with handlers for launching and editing
86
95
for ( const site of sites ) {
96
+ // Create list view item
87
97
const siteElement = templateElement . content . firstElementChild . cloneNode ( true )
98
+
99
+ // Create grid view item
100
+ const gridItem = gridTemplateElement . content . firstElementChild . cloneNode ( true )
101
+
88
102
const siteName = sanitizeString ( site . config . name || site . manifest . name || site . manifest . short_name ) || new URL ( site . manifest . scope ) . host
89
103
const siteDescription = sanitizeString ( site . config . description || site . manifest . description ) || ''
90
104
const siteIcon = site . config . icon_url || getIcon ( buildIconList ( site . manifest . icons ) , 64 )
91
105
106
+ // Setup list view item
92
107
const letterElement = siteElement . querySelector ( '#sites-list-template-letter' )
93
108
if ( siteIcon ) letterElement . classList . add ( 'd-none' )
94
109
letterElement . setAttribute ( 'data-letter' , siteName [ 0 ] )
@@ -104,6 +119,71 @@ async function createSiteList () {
104
119
iconElement . classList . add ( 'd-none' )
105
120
}
106
121
122
+ // Setup grid view item
123
+ const gridLetterElement = gridItem . querySelector ( '#grid-list-template-letter' )
124
+ if ( siteIcon ) gridLetterElement . classList . add ( 'd-none' )
125
+ gridLetterElement . setAttribute ( 'data-letter' , siteName [ 0 ] )
126
+ gridLetterElement . removeAttribute ( 'id' )
127
+
128
+ const gridIconElement = gridItem . querySelector ( '#grid-list-template-icon' )
129
+ if ( ! siteIcon ) gridIconElement . classList . add ( 'd-none' )
130
+ gridIconElement . src = siteIcon
131
+ gridIconElement . setAttribute ( 'alt' , await getMessage ( 'managePageAppListIcon' ) )
132
+ gridIconElement . removeAttribute ( 'id' )
133
+ gridIconElement . onerror = ( ) => {
134
+ gridLetterElement . classList . remove ( 'd-none' )
135
+ gridIconElement . classList . add ( 'd-none' )
136
+ }
137
+
138
+ // Handle grid item clicks to show/hide buttons
139
+ const buttonsPopup = gridItem . querySelector ( '.grid-item-buttons' )
140
+ gridItem . addEventListener ( 'click' , ( event ) => {
141
+ // Don't show popup if clicking on a button
142
+ if ( event . target . closest ( '.grid-item-buttons' ) ) {
143
+ return
144
+ }
145
+
146
+ // Remove active class from all other items
147
+ document . querySelectorAll ( '.grid-item' ) . forEach ( item => {
148
+ if ( item !== gridItem ) {
149
+ item . classList . remove ( 'active' )
150
+ }
151
+ } )
152
+
153
+ // Toggle active class on clicked item
154
+ gridItem . classList . toggle ( 'active' )
155
+
156
+ if ( gridItem . classList . contains ( 'active' ) ) {
157
+ // Position the popup at click coordinates
158
+ buttonsPopup . style . top = `${ event . clientY } px`
159
+ buttonsPopup . style . left = `${ event . clientX } px`
160
+
161
+ // Adjust position if popup would go off screen
162
+ const rect = buttonsPopup . getBoundingClientRect ( )
163
+ const viewportWidth = document . documentElement . clientWidth
164
+ const viewportHeight = document . documentElement . clientHeight
165
+
166
+ if ( rect . right > viewportWidth ) {
167
+ buttonsPopup . style . left = `${ event . clientX - rect . width } px`
168
+ }
169
+ if ( rect . bottom > viewportHeight ) {
170
+ buttonsPopup . style . top = `${ event . clientY - rect . height } px`
171
+ }
172
+ }
173
+
174
+ event . stopPropagation ( )
175
+ } )
176
+
177
+ // Close popup when clicking outside
178
+ document . addEventListener ( 'click' , ( event ) => {
179
+ if ( ! event . target . closest ( '.grid-item' ) ) {
180
+ document . querySelectorAll ( '.grid-item' ) . forEach ( item => {
181
+ item . classList . remove ( 'active' )
182
+ } )
183
+ }
184
+ } )
185
+
186
+ // Set titles and descriptions
107
187
const titleElement = siteElement . querySelector ( '#sites-list-template-title' )
108
188
titleElement . innerText = siteName
109
189
titleElement . removeAttribute ( 'id' )
@@ -112,16 +192,32 @@ async function createSiteList () {
112
192
descriptionElement . innerText = siteDescription
113
193
descriptionElement . removeAttribute ( 'id' )
114
194
195
+ const gridTitleElement = gridItem . querySelector ( '#grid-list-template-title' )
196
+ gridTitleElement . innerText = siteName
197
+ gridTitleElement . removeAttribute ( 'id' )
198
+
199
+ // Setup launch buttons
115
200
const launchElement = siteElement . querySelector ( '#sites-list-template-launch' )
201
+ const gridLaunchElement = gridItem . querySelector ( '#grid-list-template-launch' )
116
202
const launchElementTooltip = await getMessage ( 'managePageAppListLaunch' )
203
+
117
204
launchElement . addEventListener ( 'click' , ( ) => { launchSite ( site ) } )
205
+ gridLaunchElement . addEventListener ( 'click' , ( ) => { launchSite ( site ) } )
206
+
118
207
launchElement . setAttribute ( 'title' , launchElementTooltip )
119
208
launchElement . setAttribute ( 'aria-label' , launchElementTooltip )
120
209
launchElement . removeAttribute ( 'id' )
121
210
211
+ gridLaunchElement . setAttribute ( 'title' , launchElementTooltip )
212
+ gridLaunchElement . setAttribute ( 'aria-label' , launchElementTooltip )
213
+ gridLaunchElement . removeAttribute ( 'id' )
214
+
215
+ // Setup edit buttons
122
216
const editElement = siteElement . querySelector ( '#sites-list-template-edit' )
217
+ const gridEditElement = gridItem . querySelector ( '#grid-list-template-edit' )
123
218
const editElementTooltip = await getMessage ( 'managePageAppListEdit' )
124
- editElement . addEventListener ( 'click' , async ( event ) => {
219
+
220
+ const editHandler = async ( event ) => {
125
221
const form = document . getElementById ( 'web-app-form' )
126
222
const submit = document . getElementById ( 'web-app-submit' )
127
223
@@ -379,14 +475,25 @@ async function createSiteList () {
379
475
// Show offcanvas element
380
476
Offcanvas . getOrCreateInstance ( document . getElementById ( 'site-edit-offcanvas' ) ) . show ( )
381
477
event . preventDefault ( )
382
- } )
478
+ }
479
+
480
+ editElement . addEventListener ( 'click' , editHandler )
481
+ gridEditElement . addEventListener ( 'click' , editHandler )
482
+
383
483
editElement . setAttribute ( 'title' , editElementTooltip )
384
484
editElement . setAttribute ( 'aria-label' , editElementTooltip )
385
485
editElement . removeAttribute ( 'id' )
386
486
487
+ gridEditElement . setAttribute ( 'title' , editElementTooltip )
488
+ gridEditElement . setAttribute ( 'aria-label' , editElementTooltip )
489
+ gridEditElement . removeAttribute ( 'id' )
490
+
491
+ // Setup remove buttons
387
492
const removeElement = siteElement . querySelector ( '#sites-list-template-remove' )
493
+ const gridRemoveElement = gridItem . querySelector ( '#grid-list-template-remove' )
388
494
const removeElementTooltip = await getMessage ( 'managePageAppListRemove' )
389
- removeElement . addEventListener ( 'click' , ( ) => {
495
+
496
+ const removeHandler = ( ) => {
390
497
const lastSiteInProfile = profiles [ site . profile ] . sites . length <= 1
391
498
392
499
document . getElementById ( 'site-remove-button' ) . onclick = async function ( ) {
@@ -433,12 +540,21 @@ async function createSiteList () {
433
540
}
434
541
435
542
Modal . getOrCreateInstance ( document . getElementById ( 'site-remove-modal' ) ) . show ( )
436
- } )
543
+ }
544
+
545
+ removeElement . addEventListener ( 'click' , removeHandler )
546
+ gridRemoveElement . addEventListener ( 'click' , removeHandler )
547
+
437
548
removeElement . setAttribute ( 'title' , removeElementTooltip )
438
549
removeElement . setAttribute ( 'aria-label' , removeElementTooltip )
439
550
removeElement . removeAttribute ( 'id' )
440
551
552
+ gridRemoveElement . setAttribute ( 'title' , removeElementTooltip )
553
+ gridRemoveElement . setAttribute ( 'aria-label' , removeElementTooltip )
554
+ gridRemoveElement . removeAttribute ( 'id' )
555
+
441
556
listElement . insertBefore ( siteElement , templateElement )
557
+ gridContainer . insertBefore ( gridItem , gridTemplateElement )
442
558
}
443
559
}
444
560
@@ -689,29 +805,38 @@ async function createProfileList () {
689
805
690
806
// Handle site and profile search
691
807
async function handleSearch ( ) {
692
- const searchHandler = function ( listElement ) {
808
+ const searchHandler = function ( listElement , gridElement ) {
693
809
document . getElementById ( 'search-box' ) . classList . remove ( 'invisible' )
694
810
695
811
document . getElementById ( 'search-input' ) . oninput = function ( ) {
812
+ const searchQuery = sanitizeString ( this . value . toLowerCase ( ) )
813
+
696
814
for ( const item of document . getElementById ( listElement ) . children ) {
697
815
const itemName = sanitizeString ( item . querySelector ( '.list-group-item-name' ) ?. innerText . toLowerCase ( ) )
698
- const searchQuery = sanitizeString ( this . value . toLowerCase ( ) )
699
-
700
816
if ( ! itemName ) continue
701
817
item . classList . toggle ( 'd-none' , itemName . indexOf ( searchQuery ) === - 1 )
702
818
}
819
+
820
+ if ( gridElement ) {
821
+ for ( const item of document . getElementById ( gridElement ) . children ) {
822
+ const itemName = sanitizeString ( item . querySelector ( '.list-group-item-name' ) ?. innerText . toLowerCase ( ) )
823
+ if ( ! itemName ) continue
824
+ item . classList . toggle ( 'd-none' , itemName . indexOf ( searchQuery ) === - 1 )
825
+ }
826
+ }
703
827
}
704
828
}
705
829
706
830
const searchHide = function ( ) {
707
831
document . getElementById ( 'search-box' ) . classList . add ( 'invisible' )
708
832
}
709
833
834
+ document . getElementById ( 'grid-tab' ) . addEventListener ( 'click' , ( ) => searchHandler ( 'grid-list' ) )
710
835
document . getElementById ( 'sites-tab' ) . addEventListener ( 'click' , ( ) => searchHandler ( 'sites-list' ) )
711
836
document . getElementById ( 'profiles-tab' ) . addEventListener ( 'click' , ( ) => searchHandler ( 'profiles-list' ) )
712
837
document . getElementById ( 'settings-tab' ) . addEventListener ( 'click' , ( ) => searchHide ( ) )
713
838
714
- searchHandler ( 'sites -list' )
839
+ searchHandler ( 'grid -list' )
715
840
}
716
841
717
842
// Handle extension settings
0 commit comments