PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.10
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.10
4.4.10 4.4.9.1 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 All 142 releases
← All changes | assets/src/js/admin/addons.js +858 -341 4.3.9 → 4.4.10 View file →
@@ -1,341 +1,858 @@
1 -/**
2 - * Script handle admin notices.
3 - *
4 - * @since 4.1.7.3.2
5 - * @version 1.0.1
6 - */
7 -import API from '../api';
8 -let elAddonsPage;
9 -let dataHtml;
10 -let dataAddons;
11 -let elLPAddons;
12 -const queryString = window.location.search;
13 -const urlParams = new URLSearchParams( queryString );
14 -const tab = urlParams.get( 'tab' );
15 -let elNotifyActionWrapper;
16 -const isHandling = [];
17 -
18 -// API get list addons.
19 -const getAddons = ( set = '' ) => {
20 - const params = tab ? `?tab=${ tab }` : `?${ set }`;
21 - fetch( API.admin.apiAddons + params, {
22 - method: 'GET',
23 - headers: {
24 - 'X-WP-Nonce': lpDataAdmin.nonce,
25 - },
26 - } ).then( ( res ) =>
27 - res.json()
28 - ).then( ( res ) => {
29 - // console.log(data);
30 - const { status, message, data } = res;
31 - if ( status === 'success' ) {
32 - dataHtml = data.html;
33 - dataAddons = data.addons;
34 - } else {
35 - dataHtml = message;
36 - }
37 - } ).catch( ( err ) => {
38 - console.log( err );
39 - } );
40 -};
41 -// API send action install, update, activate, deactivate.
42 -const addonsAction = ( data, callBack ) => {
43 - const addonSlug = data.addon.slug;
44 -
45 - if ( isHandling.indexOf( addonSlug ) !== -1 ) {
46 - return;
47 - }
48 - isHandling.push( addonSlug );
49 -
50 - fetch( API.admin.apiAddonAction, {
51 - method: 'POST',
52 - headers: {
53 - 'Content-Type': 'application/json',
54 - 'X-WP-Nonce': lpDataAdmin.nonce,
55 - },
56 - body: JSON.stringify( { ...data } ),
57 - } ).then( ( res ) =>
58 - res.json()
59 - ).then( ( res ) => {
60 - const indexAddonHanding = isHandling.indexOf( addonSlug );
61 - if ( indexAddonHanding !== -1 ) {
62 - isHandling.splice( indexAddonHanding, 1 );
63 - }
64 -
65 - const { status, message, data } = res;
66 -
67 - if ( callBack ) {
68 - callBack( status, message, data );
69 - }
70 -
71 - handleNotify( status, message );
72 - } ).catch( ( err ) => {
73 - handleNotify( 'error', `error js: ${ err }` );
74 - console.log( err );
75 - } );
76 -};
77 -// Show notify.
78 -const handleNotify = ( status, message ) => {
79 - const elNotifyAction = elNotifyActionWrapper.querySelector( '.lp-notify-action' );
80 - const elNotifyActionNew = elNotifyAction.cloneNode( true );
81 - elNotifyActionNew.classList.remove( 'clone' );
82 - elNotifyActionWrapper.insertBefore( elNotifyActionNew, elNotifyActionWrapper[ 0 ] );
83 - const elSuccess = elNotifyActionNew.querySelector( `.${ elNotifyActionNew.classList.value }__success` );
84 - const elFailed = elNotifyActionNew.querySelector( `.${ elNotifyActionNew.classList.value }__error` );
85 -
86 - if ( status === 'success' ) {
87 - elSuccess.classList.add( 'show' );
88 - elSuccess.querySelector( '.message' ).innerHTML = message;
89 - } else {
90 - elFailed.classList.add( 'show' );
91 - elFailed.querySelector( '.message' ).innerHTML = message;
92 - }
93 -
94 - elNotifyActionWrapper.classList.add( 'show' );
95 - setTimeout( () => {
96 - elNotifyActionNew.remove();
97 -
98 - const elNotifyAction = elNotifyActionWrapper.querySelectorAll( '.lp-notify-action' );
99 - if ( elNotifyAction.length === 1 ) {
100 - elNotifyActionWrapper.classList.remove( 'show' );
101 - }
102 - }, status === 'success' ? 3000 : 4000 );
103 -};
104 -// Get addons when js loaded.
105 -getAddons();
106 -// Search Addons.
107 -const searchAddons = ( name ) => {
108 - const elAddonItems = elAddonsPage.querySelectorAll( '.lp-addon-item' );
109 - let totalItems = 0;
110 -
111 - elAddonItems.forEach( ( elAddonItem ) => {
112 - const addonName = elAddonItem.querySelector( 'a' ).textContent;
113 - if ( elAddonItem.classList.contains( 'hide' ) ) {
114 - return;
115 - }
116 -
117 - if ( addonName.toLowerCase().includes( name.toLowerCase() ) ) {
118 - elAddonItem.classList.remove( 'search-not-found' );
119 - totalItems++;
120 - } else {
121 - elAddonItem.classList.add( 'search-not-found' );
122 - }
123 - } );
124 -
125 - setGridItems( totalItems );
126 -};
127 -// Set grid style items.
128 -const setGridItems = ( totalItems ) => {
129 - if ( totalItems < 4 ) {
130 - elLPAddons.classList.add( 'max-3-items' );
131 - } else {
132 - elLPAddons.classList.remove( 'max-3-items' );
133 - }
134 -};
135 -// Check element loaded and data API returned.
136 -const loadElData = setInterval( () => {
137 - if ( ! elAddonsPage || ! elNotifyActionWrapper ) {
138 - elAddonsPage = document.querySelector( '.lp-addons-page' );
139 - elNotifyActionWrapper = document.querySelector( '.lp-notify-action-wrapper' );
140 - } else if ( dataHtml && elAddonsPage && elNotifyActionWrapper ) {
141 - elAddonsPage.innerHTML = dataHtml;
142 - elLPAddons = elAddonsPage.querySelector( '#lp-addons' );
143 - const elNavTabWrapper = document.querySelector( '.lp-nav-tab-wrapper' );
144 - if ( ! elNavTabWrapper ) {
145 - clearInterval( loadElData );
146 - return;
147 - }
148 -
149 - const elNavTabWrapperClone = elNavTabWrapper.cloneNode( true );
150 - elAddonsPage.insertBefore( elNavTabWrapperClone, elAddonsPage.children[ 0 ] );
151 - elNavTabWrapperClone.style.display = 'flex';
152 - elNavTabWrapper.remove();
153 - const elNavActive = elNavTabWrapperClone.querySelector( '.nav-tab.nav-tab-active span' );
154 - setGridItems( parseInt( elNavActive.textContent ) );
155 -
156 - clearInterval( loadElData );
157 - }
158 -}, 1 );
159 -
160 -document.addEventListener( 'DOMContentLoaded', ( e ) => {
161 -} );
162 -
163 -/*** Events ***/
164 -document.addEventListener( 'click', ( e ) => {
165 - let el = e.target;
166 - const tagName = el.tagName.toLowerCase();
167 - if ( tagName === 'span' ) {
168 - e.preventDefault();
169 - const elBtnAction = el.closest( '.btn-addon-action' );
170 - if ( elBtnAction ) {
171 - elBtnAction.click();
172 - }
173 - }
174 -
175 - // Events actions: activate, deactivate.
176 - /*if ( el.classList.contains( 'lp-toggle-switch-label' ) ) {
177 - //e.preventDefault();
178 -
179 - const elAddonItem = el.closest( '.lp-addon-item' );
180 - const idLabel = el.getAttribute( 'for' );
181 - const elInput = document.querySelector( `#${ idLabel }` );
182 - const action = elInput.getAttribute( 'data-action' );
183 - const addon = dataAddons[ elAddonItem.dataset.slug ];
184 - const addonSlug = addon.slug;
185 - const parent = el.closest( '.lp-toggle-switch' );
186 - const label = parent.querySelector( `label[for=${ idLabel }]` );
187 - const dashicons = parent.querySelector( '.dashicons-update' );
188 - dashicons.style.display = 'inline-block';
189 - label.style.display = 'none';
190 - const data = { action, addon };
191 - addonsAction( data, function( status, message, data ) {
192 - const elAddon = document.querySelector( `#${ addonSlug }` );
193 - if ( elAddon ) {
194 - const parent = elAddon.closest( '.lp-toggle-switch' );
195 - if ( parent ) {
196 - const dashicons = parent.querySelector( '.dashicons-update' );
197 - dashicons.style.display = 'none';
198 - if ( action === 'deactivate' ) {
199 - elAddon.setAttribute( 'data-action', 'activate' );
200 - } else if ( action === 'activate' ) {
201 - elAddon.setAttribute( 'data-action', 'deactivate' );
202 - }
203 - const label = parent.querySelector( `label[for=${ addonSlug }]` );
204 - label.style.display = 'inline-flex';
205 - }
206 - }
207 -
208 - if ( status === 'success' ) {
209 - if ( action === 'deactivate' ) {
210 - elAddonItem.classList.remove( 'activated' );
211 - }
212 - if ( action === 'activate' ) {
213 - elAddonItem.classList.add( 'activated' );
214 - }
215 - }
216 - } );
217 - }*/
218 -
219 - // Events actions: install, update, delete.
220 - if ( el.closest( '.btn-addon-action' ) ) {
221 - el = el.closest( '.btn-addon-action' );
222 - e.preventDefault();
223 - el.classList.add( 'handling' );
224 - let purchaseCode = '';
225 - const elAddonItem = el.closest( '.lp-addon-item' );
226 - const addon = dataAddons[ elAddonItem.dataset.slug ];
227 - const action = el.dataset.action;
228 - const elItemPurchase = elAddonItem.querySelector( '.lp-addon-item__purchase' );
229 - //const elToggleSwitchInput = elAddonItem.querySelector( '.lp-toggle-switch-input' );
230 -
231 - if ( action === 'purchase' ) {
232 - elItemPurchase.style.display = 'block';
233 - elItemPurchase.querySelector( '.purchase-install' ).style.display = 'flex';
234 - return;
235 - } else if ( action === 'update-purchase-code' ) {
236 - elItemPurchase.querySelector( '.purchase-update' ).style.display = 'flex';
237 - elItemPurchase.style.display = 'block';
238 - return;
239 - } else if ( action === 'buy' ) {
240 - const link = el.dataset.link;
241 - window.open( link, '_blank' );
242 - return;
243 - } else if ( action === 'cancel' ) {
244 - elItemPurchase.style.display = 'none';
245 - return;
246 - } else if ( action === 'install' ) {
247 - if ( el.dataset.link ) {
248 - el.classList.remove( 'handling' );
249 - const link = el.dataset.link;
250 - window.open( link, '_blank' );
251 - return;
252 - }
253 - }
254 -
255 - // Send request to server.
256 - if ( elItemPurchase ) {
257 - purchaseCode = elItemPurchase.querySelector( 'input[name=purchase-code]' ).value;
258 - }
259 -
260 - const data = { purchase_code: purchaseCode, action, addon };
261 - addonsAction( data, function( status, message, data ) {
262 - if ( status === 'success' ) {
263 - if ( action === 'install' ) {
264 - elAddonItem.classList.add( 'installed', 'activated' );
265 - elAddonItem.classList.remove( 'not_installed' );
266 - elItemPurchase.style.display = 'none';
267 - /*elToggleSwitchInput.setAttribute( 'checked', 'checked' );
268 - elToggleSwitchInput.setAttribute( 'data-action', 'deactivate' );*/
269 - const elNavInstalled = document.querySelector( '.nav-tab[data-tab=installed] span' );
270 - elNavInstalled.textContent = parseInt( elNavInstalled.textContent ) + 1;
271 - const elNavNoInstalled = document.querySelector( '.nav-tab[data-tab=not_installed] span' );
272 - elNavNoInstalled.textContent = parseInt( elNavNoInstalled.textContent ) - 1;
273 - elItemPurchase.querySelector( '.purchase-install' ).style.display = 'none';
274 - elItemPurchase.querySelector( '.purchase-update' ).querySelector( '.enter-purchase-code' ).value = purchaseCode;
275 - } else if ( action === 'update' ) {
276 - const elAddonVersionCurrent = elAddonItem.querySelector( '.addon-version-current' );
277 - elAddonVersionCurrent.innerHTML = addon.version;
278 - elAddonItem.classList.remove( 'update' );
279 - } else if ( action === 'activate' ) {
280 - elAddonItem.classList.add( 'activated' );
281 - } else if ( action === 'deactivate' ) {
282 - elAddonItem.classList.remove( 'activated' );
283 - } else if ( action === 'update-purchase' ) {
284 - elItemPurchase.style.display = 'none';
285 - }
286 - }
287 -
288 - el.classList.remove( 'handling' );
289 - } );
290 - }
291 -
292 - if ( el.classList.contains( 'nav-tab' ) ) {
293 - e.preventDefault();
294 - const elTabs = document.querySelectorAll( '.nav-tab' );
295 - elTabs.forEach( function( elTab ) {
296 - elTab.classList.remove( 'nav-tab-active' );
297 - } );
298 - el.classList.add( 'nav-tab-active' );
299 - const tabName = el.dataset.tab;
300 - const elAddonItems = elAddonsPage.querySelectorAll( '.lp-addon-item' );
301 - const elSearch = elAddonsPage.querySelector( '#lp-search-addons__input' );
302 - elSearch.value = '';
303 -
304 - urlParams.set( 'tab', tabName );
305 - window.history.pushState( {}, '', `${ window.location.pathname }?${ urlParams.toString() }` );
306 - let totalItems = 0;
307 - elAddonItems.forEach( ( elAddonItem ) => {
308 - elAddonItem.classList.remove( 'search-not-found' );
309 - if ( 'all' === tabName || elAddonItem.classList.contains( tabName ) ) {
310 - elAddonItem.classList.remove( 'hide' );
311 - totalItems++;
312 - } else {
313 - elAddonItem.classList.add( 'hide' );
314 - }
315 - } );
316 -
317 - setGridItems( totalItems );
318 - }
319 -} );
320 -
321 -/*** Event search addons. ***/
322 -document.addEventListener( 'input', ( e ) => {
323 - const el = e.target;
324 -
325 - if ( 'lp-search-addons__input' === el.id ) {
326 - const keyword = el.value;
327 - searchAddons( keyword );
328 - }
329 -
330 - // Events change input purchase code.
331 - if ( el.classList.contains( 'enter-purchase-code' ) ) {
332 - e.preventDefault();
333 - const purchaseCode = el.value;
334 - const elItemPurchase = el.closest( '.lp-addon-item__purchase' );
335 -
336 - if ( elItemPurchase ) {
337 - const input = elItemPurchase.querySelector( 'input[name=purchase-code]' );
338 - input.value = purchaseCode;
339 - }
340 - }
341 -} );
1 +/**
2 + * Admin Add-ons page JS handler as a class.
3 + *
4 + * Add-ons list is rendered server-side via AdminAddonsPage::render_addons()
5 + * through TemplateAJAX::load_content_via_ajax(). This class only handles
6 + * UI interactions and addon actions via window.lpAJAXG.
7 + *
8 + * @since 4.4.7
9 + * @version 1.0.0
10 + */
11 +import * as lpToastify from 'lpAssetsJsPath/lpToastify.js';
12 +import * as lpUtils from 'lpAssetsJsPath/utils.js';
13 +
14 +class AdminAddons {
15 + constructor() {
16 + this.elAddonsPage = null;
17 + this.urlParams = new URLSearchParams( window.location.search );
18 + }
19 +
20 + static selectors = {
21 + elAddonsPage: '.lp-addons-page',
22 + elToolbar: '.lp-addons-toolbar',
23 + elLPAddons: '#lp-addons',
24 + elAddonItem: '.lp-addon-item',
25 + elBtnAction: '.btn-addon-action',
26 + elNavTab: '.nav-tab',
27 + elNavTabActive: '.nav-tab.nav-tab-active',
28 + elFilter: '.lp-addons-filter',
29 + elFilterToggle: '.lp-addons-filter__toggle',
30 + elFilterMenu: '.lp-addons-filter__menu',
31 + elCategory: '.lp-addons-category',
32 + elCategoryActive: '.lp-addons-category.active',
33 + elSearch: '.lp-addons-search',
34 + elSearchBtn: '.lp-addons-search__btn',
35 + elSearchBtnClear: '.lp-addons-search__btn-clear',
36 + elSearchClose: '.lp-addons-search__close',
37 + elSearchInput: '#lp-search-addons__input',
38 + elItemPurchase: '.lp-addon-item__purchase',
39 + elPurchaseInstall: '.purchase-install',
40 + elPurchaseCode: '.enter-purchase-code',
41 + elLicense: '.lp-addon-license',
42 + };
43 +
44 + init() {
45 + // Content is injected via AJAX into .lp-target, wait for #lp-addons.
46 + lpUtils.lpOnElementReady( AdminAddons.selectors.elLPAddons, () => {
47 + this.elAddonsPage = document.querySelector(
48 + AdminAddons.selectors.elAddonsPage
49 + );
50 + if ( ! this.elAddonsPage ) {
51 + return;
52 + }
53 +
54 + const selectors = AdminAddons.selectors;
55 + const urlTab = this.urlParams.get( 'tab' );
56 + if ( urlTab ) {
57 + const elMatchingTab = this.elAddonsPage.querySelector(
58 + `${ selectors.elNavTab }[data-tab="${ urlTab }"]`
59 + );
60 + if ( elMatchingTab ) {
61 + this.elAddonsPage
62 + .querySelectorAll( selectors.elNavTab )
63 + .forEach( ( elTab ) => {
64 + elTab.classList.remove( 'nav-tab-active' );
65 + elTab.setAttribute( 'aria-pressed', 'false' );
66 + } );
67 + elMatchingTab.classList.add( 'nav-tab-active' );
68 + elMatchingTab.setAttribute( 'aria-pressed', 'true' );
69 + }
70 + }
71 +
72 + let elActiveTab = this.elAddonsPage.querySelector(
73 + selectors.elNavTabActive
74 + );
75 + if ( ! elActiveTab ) {
76 + elActiveTab =
77 + this.elAddonsPage.querySelector(
78 + `${ selectors.elNavTab }[data-tab="all"]`
79 + ) || this.elAddonsPage.querySelector( selectors.elNavTab );
80 + if ( elActiveTab ) {
81 + elActiveTab.classList.add( 'nav-tab-active' );
82 + elActiveTab.setAttribute( 'aria-pressed', 'true' );
83 + }
84 + }
85 +
86 + let elActiveCategory = this.elAddonsPage.querySelector(
87 + selectors.elCategoryActive
88 + );
89 + if ( ! elActiveCategory ) {
90 + elActiveCategory =
91 + this.elAddonsPage.querySelector(
92 + `${ selectors.elCategory }[data-category="all"]`
93 + ) || this.elAddonsPage.querySelector( selectors.elCategory );
94 + if ( elActiveCategory ) {
95 + elActiveCategory.classList.add( 'active' );
96 + elActiveCategory.setAttribute( 'aria-pressed', 'true' );
97 + }
98 + }
99 +
100 + this.filterAddons();
101 + this.updateSearchClearBtnState();
102 + this.updateFilterToggleState();
103 + this.events();
104 + } );
105 + }
106 +
107 + /**
108 + * Register events.
109 + *
110 + * @return void
111 + */
112 + events() {
113 + lpUtils.eventHandlers( 'click', [
114 + {
115 + selector: AdminAddons.selectors.elBtnAction,
116 + class: this,
117 + callBack: this.handleBtnAction.name,
118 + },
119 + {
120 + selector: AdminAddons.selectors.elNavTab,
121 + class: this,
122 + callBack: this.handleTabClick.name,
123 + },
124 + {
125 + selector: AdminAddons.selectors.elCategory,
126 + class: this,
127 + callBack: this.handleCategoryClick.name,
128 + },
129 + {
130 + selector: AdminAddons.selectors.elFilterToggle,
131 + class: this,
132 + callBack: this.handleFilterToggleClick.name,
133 + },
134 + {
135 + selector: AdminAddons.selectors.elSearchBtn,
136 + class: this,
137 + callBack: this.handleSearchBtnClick.name,
138 + },
139 + {
140 + selector: AdminAddons.selectors.elSearchBtnClear,
141 + class: this,
142 + callBack: this.handleSearchClearClick.name,
143 + },
144 + {
145 + selector: AdminAddons.selectors.elSearchClose,
146 + class: this,
147 + callBack: this.handleSearchCloseClick.name,
148 + },
149 + ] );
150 + lpUtils.eventHandlers( 'input', [
151 + {
152 + selector: AdminAddons.selectors.elSearchInput,
153 + class: this,
154 + callBack: this.handleSearchInput.name,
155 + },
156 + {
157 + selector: AdminAddons.selectors.elPurchaseCode,
158 + class: this,
159 + callBack: this.handlePurchaseCodeInput.name,
160 + },
161 + ] );
162 + lpUtils.eventHandlers( 'keydown', [
163 + {
164 + selector: AdminAddons.selectors.elSearchInput,
165 + class: this,
166 + callBack: this.handleSearchKeydown.name,
167 + },
168 + ] );
169 +
170 + document.addEventListener( 'click', ( e ) => {
171 + if ( ! this.elAddonsPage ) {
172 + return;
173 + }
174 + const elToolbar = this.elAddonsPage.querySelector(
175 + AdminAddons.selectors.elToolbar
176 + );
177 + if ( elToolbar && elToolbar.classList.contains( 'is-filter-open' ) ) {
178 + if ( ! elToolbar.contains( e.target ) ) {
179 + this.toggleFilterMenu( false );
180 + }
181 + }
182 + } );
183 +
184 + document.addEventListener( 'keydown', ( e ) => {
185 + if ( 'Escape' === e.key ) {
186 + this.toggleFilterMenu( false );
187 + }
188 + } );
189 + }
190 +
191 + /**
192 + * Get addon data embedded on the item element.
193 + *
194 + * @param {Element} elAddonItem Addon item element.
195 + * @return {Object|null}
196 + */
197 + getAddonData( elAddonItem ) {
198 + try {
199 + return JSON.parse( elAddonItem.dataset.addon );
200 + } catch ( e ) {
201 + return null;
202 + }
203 + }
204 +
205 + /**
206 + * Send addon action to server via lpAJAXG.
207 + *
208 + * @param {Object} data { action, addon, purchase_code, el, elAddonItem }.
209 + * @return void
210 + */
211 + addonsAction( data ) {
212 + const { action, addon, el, elAddonItem } = data;
213 + const selectors = AdminAddons.selectors;
214 +
215 + const params = {
216 + action: 'addon_action',
217 + action_type: action,
218 + addon,
219 + purchase_code: data.purchase_code || '',
220 + id_url: 'addon-action',
221 + };
222 +
223 + const releaseHandling = () => {
224 + lpUtils.lpSetLoadingEl( el, 0 );
225 + };
226 +
227 + window.lpAJAXG.fetchAJAX( params, {
228 + success: ( response ) => {
229 + releaseHandling();
230 +
231 + const { status, message, data: resData } = response;
232 +
233 + lpToastify.show( message, status );
234 +
235 + if ( status === 'success' && resData?.html ) {
236 + elAddonItem.outerHTML = resData.html;
237 + } else if (
238 + 'install' === action ||
239 + 'update-purchase' === action
240 + ) {
241 + const elPurchasePanel = el.closest(
242 + selectors.elPurchaseInstall
243 + );
244 + const elPurchaseCode = elPurchasePanel
245 + ? elPurchasePanel.querySelector( selectors.elPurchaseCode )
246 + : null;
247 + if ( elPurchaseCode ) {
248 + elPurchaseCode.classList.add( 'is-error' );
249 + elPurchaseCode.setAttribute( 'aria-invalid', 'true' );
250 + }
251 + }
252 +
253 + //this.filterAddons();
254 + },
255 + error: ( error ) => {
256 + releaseHandling();
257 + lpToastify.show( `error js: ${ error }`, 'error' );
258 + },
259 + } );
260 + }
261 +
262 + /**
263 + * Filter add-ons by status, category, and search query.
264 + *
265 + * @return void
266 + */
267 + filterAddons() {
268 + const selectors = AdminAddons.selectors;
269 + const elAddonItems = this.elAddonsPage.querySelectorAll(
270 + selectors.elAddonItem
271 + );
272 +
273 + // Count items for each tab
274 + this.updateTabCounts( elAddonItems );
275 + const elActiveTab = this.elAddonsPage.querySelector(
276 + selectors.elNavTabActive
277 + );
278 + const elActiveCategory = this.elAddonsPage.querySelector(
279 + selectors.elCategoryActive
280 + );
281 + const elSearch = this.elAddonsPage.querySelector(
282 + selectors.elSearchInput
283 + );
284 + const tabName = elActiveTab ? elActiveTab.dataset.tab : 'all';
285 + const category = elActiveCategory
286 + ? elActiveCategory.dataset.category
287 + : 'all';
288 + const keyword = elSearch ? elSearch.value.trim().toLowerCase() : '';
289 + const searchTerms = keyword.split( /\s+/ ).filter( Boolean );
290 +
291 + // Count items for category by tab
292 + this.updateCategoryCounts( elAddonItems, tabName );
293 +
294 + elAddonItems.forEach( ( elAddonItem ) => {
295 + const addonInfo = JSON.parse( elAddonItem.dataset.addon );
296 +
297 + const addonName = elAddonItem
298 + .querySelector( 'a' )
299 + .textContent.toLowerCase();
300 + const textCompare = addonName + addonInfo.description;
301 +
302 + const addonCategories = ( elAddonItem.dataset.category || '' )
303 + .split( /\s+/ )
304 + .filter( Boolean );
305 + const matchesTab =
306 + 'all' === tabName || elAddonItem.classList.contains( tabName );
307 + const matchesCategory =
308 + 'all' === category || addonCategories.includes( category );
309 + const matchesSearch = searchTerms.every( ( term ) => textCompare.includes( term ) );
310 + const isVisible = matchesTab && matchesCategory && matchesSearch;
311 +
312 + elAddonItem.classList.toggle( 'hide', ! matchesTab );
313 + elAddonItem.classList.toggle(
314 + 'search-not-found',
315 + ! isVisible
316 + );
317 + } );
318 + }
319 +
320 + updateCategoryCounts( elAddonItems, tabName ) {
321 + const categoryCounts = { all: 0 };
322 +
323 + elAddonItems.forEach( ( elAddonItem ) => {
324 + const matchesTab =
325 + 'all' === tabName || elAddonItem.classList.contains( tabName );
326 +
327 + if ( ! matchesTab ) {
328 + return;
329 + }
330 +
331 + categoryCounts.all++;
332 + ( elAddonItem.dataset.category || '' )
333 + .split( /\s+/ )
334 + .filter( Boolean )
335 + .forEach( ( addonCategory ) => {
336 + categoryCounts[ addonCategory ] =
337 + ( categoryCounts[ addonCategory ] || 0 ) + 1;
338 + } );
339 + } );
340 +
341 + this.elAddonsPage
342 + .querySelectorAll( AdminAddons.selectors.elCategory )
343 + .forEach( ( elCategory ) => {
344 + const count =
345 + categoryCounts[ elCategory.dataset.category ] || 0;
346 + const elCount = elCategory.querySelector(
347 + '.lp-addons-category__count'
348 + );
349 +
350 + if ( elCount ) {
351 + elCount.textContent = `(${ count })`;
352 + }
353 +
354 + elCategory.hidden =
355 + 'all' !== elCategory.dataset.category && 0 === count;
356 + } );
357 + }
358 +
359 + updateTabCounts( elAddonItems ) {
360 + this.elAddonsPage
361 + .querySelectorAll( AdminAddons.selectors.elNavTab )
362 + .forEach( ( elNavTab ) => {
363 + const tabName = elNavTab.dataset.tab;
364 + const count = Array.from( elAddonItems ).filter(
365 + ( elAddonItem ) =>
366 + 'all' === tabName ||
367 + elAddonItem.classList.contains( tabName )
368 + ).length;
369 + const elCount = elNavTab.querySelector( 'span' );
370 +
371 + if ( elCount ) {
372 + elCount.textContent = count;
373 + }
374 + } );
375 + }
376 +
377 + /**
378 + * Handle addon action button click.
379 + *
380 + * @param {Object} args Click event arguments.
381 + * @return void
382 + */
383 + handleBtnAction( args ) {
384 + const { e, target } = args;
385 + const selectors = AdminAddons.selectors;
386 + const el = target.closest( selectors.elBtnAction );
387 +
388 + // Ignore button type link
389 + if ( 'A' === el.tagName ) {
390 + return;
391 + }
392 +
393 + e.preventDefault();
394 + lpUtils.lpSetLoadingEl( el, 1 );
395 +
396 + let purchaseCode = '';
397 + const elAddonItem = el.closest( selectors.elAddonItem );
398 + const addon = this.getAddonData( elAddonItem );
399 + const action = el.dataset.action;
400 + const elItemPurchase = elAddonItem.querySelector(
401 + selectors.elItemPurchase
402 + );
403 +
404 + if ( ! addon ) {
405 + lpUtils.lpSetLoadingEl( el, 0 );
406 + return;
407 + }
408 +
409 + if ( action === 'purchase' || action === 'update-purchase-code' ) {
410 + const elPurchaseInstall = elItemPurchase.querySelector(
411 + selectors.elPurchaseInstall
412 + );
413 + const elPurchaseCode = elPurchaseInstall.querySelector(
414 + selectors.elPurchaseCode
415 + );
416 + const elPurchaseSubmit = elPurchaseInstall.querySelector(
417 + '.lp-addon-purchase__submit'
418 + );
419 + const elPurchaseClear = elPurchaseInstall.querySelector(
420 + '.lp-addon-purchase__clear'
421 + );
422 + const elPurchaseCancelClear = elPurchaseInstall.querySelector(
423 + '.lp-addon-purchase__cancel-clear'
424 + );
425 + let hasPurchaseCode = false;
426 +
427 + if ( action === 'update-purchase-code' ) {
428 + const elLicense = elAddonItem.querySelector( selectors.elLicense );
429 + elPurchaseCode.value = elLicense
430 + ? elLicense.dataset.purchaseCodeMasked || ''
431 + : '';
432 + hasPurchaseCode = '' !== elPurchaseCode.value;
433 + elPurchaseSubmit.dataset.action = 'update-purchase';
434 + } else {
435 + elPurchaseCode.value = '';
436 + elPurchaseSubmit.dataset.action = 'install';
437 + }
438 +
439 + elPurchaseCode.disabled = hasPurchaseCode;
440 + elPurchaseSubmit.classList.toggle( 'lp-hidden', hasPurchaseCode );
441 + elPurchaseClear.classList.toggle( 'lp-hidden', ! hasPurchaseCode );
442 + elPurchaseCancelClear.classList.add( 'lp-hidden' );
443 + elPurchaseCode.classList.remove( 'is-error' );
444 + elPurchaseCode.removeAttribute( 'aria-invalid' );
445 + elItemPurchase.querySelector( 'input[name=purchase-code]' ).value =
446 + '';
447 + elPurchaseInstall.style.display = 'flex';
448 + elItemPurchase.style.display = 'block';
449 + if ( ! hasPurchaseCode ) {
450 + elPurchaseCode.focus();
451 + }
452 + lpUtils.lpSetLoadingEl( el, 0 );
453 + return;
454 + } else if ( action === 'clear-license' ) {
455 + const elPurchaseCode = elItemPurchase.querySelector(
456 + selectors.elPurchaseCode
457 + );
458 + const elPurchaseSubmit = elItemPurchase.querySelector(
459 + '.lp-addon-purchase__submit'
460 + );
461 + const elPurchaseClear = elItemPurchase.querySelector(
462 + '.lp-addon-purchase__clear'
463 + );
464 + const elPurchaseCancelClear = elItemPurchase.querySelector(
465 + '.lp-addon-purchase__cancel-clear'
466 + );
467 +
468 + elPurchaseCode.value = '';
469 + elPurchaseCode.disabled = false;
470 + elPurchaseCode.classList.remove( 'is-error' );
471 + elPurchaseCode.removeAttribute( 'aria-invalid' );
472 + elItemPurchase.querySelector( 'input[name=purchase-code]' ).value =
473 + '';
474 + elPurchaseSubmit.classList.remove( 'lp-hidden' );
475 + elPurchaseClear.classList.add( 'lp-hidden' );
476 + elPurchaseCancelClear.classList.remove( 'lp-hidden' );
477 + elPurchaseCode.focus();
478 + lpUtils.lpSetLoadingEl( el, 0 );
479 + return;
480 + } else if ( action === 'cancel-clear-license' ) {
481 + const elLicense = elAddonItem.querySelector( selectors.elLicense );
482 + const elPurchaseCode = elItemPurchase.querySelector(
483 + selectors.elPurchaseCode
484 + );
485 + const elPurchaseSubmit = elItemPurchase.querySelector(
486 + '.lp-addon-purchase__submit'
487 + );
488 + const elPurchaseClear = elItemPurchase.querySelector(
489 + '.lp-addon-purchase__clear'
490 + );
491 +
492 + elPurchaseCode.value = elLicense
493 + ? elLicense.dataset.purchaseCodeMasked || ''
494 + : '';
495 + elPurchaseCode.disabled = true;
496 + elPurchaseCode.classList.remove( 'is-error' );
497 + elPurchaseCode.removeAttribute( 'aria-invalid' );
498 + elItemPurchase.querySelector( 'input[name=purchase-code]' ).value =
499 + '';
500 + elPurchaseSubmit.classList.add( 'lp-hidden' );
501 + elPurchaseClear.classList.remove( 'lp-hidden' );
502 + el.classList.add( 'lp-hidden' );
503 + lpUtils.lpSetLoadingEl( el, 0 );
504 + return;
505 + } else if ( action === 'cancel' ) {
506 + elItemPurchase.style.display = 'none';
507 + elItemPurchase.querySelector(
508 + selectors.elPurchaseInstall
509 + ).style.display = 'none';
510 + lpUtils.lpSetLoadingEl( el, 0 );
511 + return;
512 + }
513 +
514 + // Send request to server.
515 + if ( elItemPurchase ) {
516 + purchaseCode = elItemPurchase.querySelector(
517 + 'input[name=purchase-code]'
518 + ).value;
519 + }
520 +
521 + this.addonsAction( {
522 + purchase_code: purchaseCode,
523 + action,
524 + addon,
525 + el,
526 + elAddonItem,
527 + } );
528 + }
529 +
530 + /**
531 + * Handle tab click.
532 + *
533 + * @param {Object} args Click event arguments.
534 + * @return void
535 + */
536 + handleTabClick( args ) {
537 + const { e, target } = args;
538 + const selectors = AdminAddons.selectors;
539 + const el = target.closest( selectors.elNavTab );
540 + e.preventDefault();
541 +
542 + const elTabs = this.elAddonsPage.querySelectorAll( selectors.elNavTab );
543 + elTabs.forEach( ( elTab ) => {
544 + elTab.classList.remove( 'nav-tab-active' );
545 + elTab.setAttribute( 'aria-pressed', 'false' );
546 + } );
547 + el.classList.add( 'nav-tab-active' );
548 + el.setAttribute( 'aria-pressed', 'true' );
549 + if ( el.scrollIntoView ) {
550 + el.scrollIntoView( { inline: 'center', block: 'nearest', behavior: 'smooth' } );
551 + }
552 +
553 + const tabName = el.dataset.tab;
554 + const elSearch = this.elAddonsPage.querySelector(
555 + selectors.elSearchInput
556 + );
557 + if ( elSearch ) {
558 + elSearch.value = '';
559 + }
560 + const elSearchContainer = this.elAddonsPage.querySelector(
561 + selectors.elSearch
562 + );
563 + if ( elSearchContainer ) {
564 + elSearchContainer.classList.remove( 'is-open' );
565 + const btn = elSearchContainer.querySelector(
566 + selectors.elSearchBtn
567 + );
568 + if ( btn ) {
569 + btn.setAttribute( 'aria-expanded', 'false' );
570 + }
571 + }
572 + this.elAddonsPage
573 + .querySelectorAll( selectors.elCategory )
574 + .forEach( ( elCategory ) => {
575 + const isActive = 'all' === elCategory.dataset.category;
576 + elCategory.classList.toggle( 'active', isActive );
577 + elCategory.setAttribute(
578 + 'aria-pressed',
579 + isActive ? 'true' : 'false'
580 + );
581 + } );
582 +
583 + this.urlParams.set( 'tab', tabName );
584 + window.history.pushState(
585 + {},
586 + '',
587 + `${ window.location.pathname }?${ this.urlParams.toString() }`
588 + );
589 + this.updateFilterToggleState();
590 + this.filterAddons();
591 + }
592 +
593 + /**
594 + * Handle filter toggle button click.
595 + *
596 + * @param {Object} args Click event arguments.
597 + * @return void
598 + */
599 + handleFilterToggleClick( args ) {
600 + const { e } = args;
601 + e.preventDefault();
602 + e.stopPropagation();
603 + this.toggleFilterMenu();
604 + }
605 +
606 + /**
607 + * Toggle or set filter menu dropdown open state.
608 + *
609 + * @param {boolean|undefined} open Optional boolean to force state.
610 + * @return void
611 + */
612 + toggleFilterMenu( open ) {
613 + const selectors = AdminAddons.selectors;
614 + const elToolbar = this.elAddonsPage.querySelector( selectors.elToolbar );
615 + const elFilter = this.elAddonsPage.querySelector( selectors.elFilter );
616 + if ( ! elToolbar && ! elFilter ) {
617 + return;
618 + }
619 + const toggleBtn = this.elAddonsPage.querySelector(
620 + selectors.elFilterToggle
621 + );
622 + const shouldOpen =
623 + undefined !== open
624 + ? open
625 + : elToolbar
626 + ? ! elToolbar.classList.contains( 'is-filter-open' )
627 + : ! elFilter.classList.contains( 'is-open' );
628 +
629 + if ( elToolbar ) {
630 + elToolbar.classList.toggle( 'is-filter-open', shouldOpen );
631 + }
632 + if ( elFilter ) {
633 + elFilter.classList.toggle( 'is-open', shouldOpen );
634 + }
635 + if ( toggleBtn ) {
636 + toggleBtn.setAttribute(
637 + 'aria-expanded',
638 + shouldOpen ? 'true' : 'false'
639 + );
640 + }
641 + }
642 +
643 + /**
644 + * Update filter toggle button has-filter state.
645 + *
646 + * @return void
647 + */
648 + updateFilterToggleState() {
649 + const selectors = AdminAddons.selectors;
650 + const toggleBtn = this.elAddonsPage.querySelector(
651 + selectors.elFilterToggle
652 + );
653 + if ( ! toggleBtn ) {
654 + return;
655 + }
656 + const elActiveTab = this.elAddonsPage.querySelector(
657 + selectors.elNavTabActive
658 + );
659 + const elActiveCategory = this.elAddonsPage.querySelector(
660 + selectors.elCategoryActive
661 + );
662 + const tabName = elActiveTab ? elActiveTab.dataset.tab : 'all';
663 + const category = elActiveCategory
664 + ? elActiveCategory.dataset.category
665 + : 'all';
666 + const hasFilter = 'all' !== tabName || 'all' !== category;
667 + toggleBtn.classList.toggle( 'has-filter', hasFilter );
668 + }
669 +
670 + /**
671 + * Handle category click.
672 + *
673 + * @param {Object} args Click event arguments.
674 + * @return void
675 + */
676 + handleCategoryClick( args ) {
677 + const { e, target } = args;
678 + const elCategory = target.closest(
679 + AdminAddons.selectors.elCategory
680 + );
681 + e.preventDefault();
682 +
683 + this.elAddonsPage
684 + .querySelectorAll( AdminAddons.selectors.elCategory )
685 + .forEach( ( item ) => {
686 + const isActive = item === elCategory;
687 + item.classList.toggle( 'active', isActive );
688 + item.setAttribute(
689 + 'aria-pressed',
690 + isActive ? 'true' : 'false'
691 + );
692 + } );
693 + this.updateFilterToggleState();
694 + this.filterAddons();
695 + }
696 +
697 + /**
698 + * Handle search input.
699 + *
700 + * @return void
701 + */
702 + handleSearchInput() {
703 + this.updateSearchClearBtnState();
704 + this.filterAddons();
705 + }
706 +
707 + /**
708 + * Handle search button click to open search overlay.
709 + *
710 + * @param {Object} args Click event arguments.
711 + * @return void
712 + */
713 + handleSearchBtnClick( args ) {
714 + const { e } = args;
715 + e.preventDefault();
716 + this.toggleFilterMenu( false );
717 + const selectors = AdminAddons.selectors;
718 + const elSearch = this.elAddonsPage.querySelector(
719 + selectors.elSearch
720 + );
721 + const elSearchInput = this.elAddonsPage.querySelector(
722 + selectors.elSearchInput
723 + );
724 + if ( elSearch ) {
725 + elSearch.classList.add( 'is-open' );
726 + const btn = elSearch.querySelector( selectors.elSearchBtn );
727 + if ( btn ) {
728 + btn.setAttribute( 'aria-expanded', 'true' );
729 + }
730 + }
731 + if ( elSearchInput ) {
732 + this.updateSearchClearBtnState();
733 + elSearchInput.focus();
734 + }
735 + }
736 +
737 + /**
738 + * Handle search clear button click.
739 + *
740 + * @param {Object} args Click event arguments.
741 + * @return void
742 + */
743 + handleSearchClearClick( args ) {
744 + const { e } = args;
745 + e.preventDefault();
746 + const selectors = AdminAddons.selectors;
747 + const elSearchInput = this.elAddonsPage.querySelector(
748 + selectors.elSearchInput
749 + );
750 + if ( elSearchInput ) {
751 + elSearchInput.value = '';
752 + this.updateSearchClearBtnState();
753 + this.filterAddons();
754 + elSearchInput.focus();
755 + }
756 + }
757 +
758 + /**
759 + * Update disabled state of clear search button.
760 + *
761 + * @return void
762 + */
763 + updateSearchClearBtnState() {
764 + const selectors = AdminAddons.selectors;
765 + const elSearchInput = this.elAddonsPage.querySelector(
766 + selectors.elSearchInput
767 + );
768 + const elClearBtn = this.elAddonsPage.querySelector(
769 + selectors.elSearchBtnClear
770 + );
771 + if ( elClearBtn && elSearchInput ) {
772 + elClearBtn.disabled = ! elSearchInput.value.trim();
773 + }
774 + }
775 +
776 + /**
777 + * Handle search close button click.
778 + *
779 + * @param {Object} args Click event arguments.
780 + * @return void
781 + */
782 + handleSearchCloseClick( args ) {
783 + const { e } = args;
784 + e.preventDefault();
785 + this.closeSearch();
786 + }
787 +
788 + /**
789 + * Handle search input keydown.
790 + *
791 + * @param {Object} args Keydown event arguments.
792 + * @return void
793 + */
794 + handleSearchKeydown( args ) {
795 + const { e } = args;
796 + if ( 'Escape' === e.key ) {
797 + e.preventDefault();
798 + this.closeSearch();
799 + }
800 + }
801 +
802 + /**
803 + * Close search overlay and reset query if needed.
804 + *
805 + * @return void
806 + */
807 + closeSearch() {
808 + const selectors = AdminAddons.selectors;
809 + const elSearch = this.elAddonsPage.querySelector(
810 + selectors.elSearch
811 + );
812 + const elSearchInput = this.elAddonsPage.querySelector(
813 + selectors.elSearchInput
814 + );
815 + if ( elSearch ) {
816 + elSearch.classList.remove( 'is-open' );
817 + const btn = elSearch.querySelector( selectors.elSearchBtn );
818 + if ( btn ) {
819 + btn.setAttribute( 'aria-expanded', 'false' );
820 + btn.focus();
821 + }
822 + }
823 + if ( elSearchInput && elSearchInput.value ) {
824 + elSearchInput.value = '';
825 + this.filterAddons();
826 + }
827 + this.updateSearchClearBtnState();
828 + }
829 +
830 + /**
831 + * Handle purchase code input.
832 + *
833 + * @param {Object} args Input event arguments.
834 + * @return void
835 + */
836 + handlePurchaseCodeInput( args ) {
837 + const { e, target: el } = args;
838 + e.preventDefault();
839 + el.classList.remove( 'is-error' );
840 + el.removeAttribute( 'aria-invalid' );
841 + const purchaseCode = el.value;
842 + const elItemPurchase = el.closest(
843 + AdminAddons.selectors.elItemPurchase
844 + );
845 +
846 + if ( elItemPurchase ) {
847 + const input = elItemPurchase.querySelector(
848 + 'input[name=purchase-code]'
849 + );
850 + input.value = purchaseCode;
851 + }
852 + }
853 +}
854 +
855 +const adminAddons = new AdminAddons();
856 +adminAddons.init();
857 +
858 +export default AdminAddons;