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 +788 -271 4.4.5 → 4.4.10 View file →
@@ -1,341 +1,858 @@
1 1 /**
2 - * Script handle admin notices.
2 + * Admin Add-ons page JS handler as a class.
3 3 *
4 - * @since 4.1.7.3.2
5 - * @version 1.0.1
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
6 10 */
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 = [];
11 +import * as lpToastify from 'lpAssetsJsPath/lpToastify.js';
12 +import * as lpUtils from 'lpAssetsJsPath/utils.js';
17 13
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;
14 +class AdminAddons {
15 + constructor() {
16 + this.elAddonsPage = null;
17 + this.urlParams = new URLSearchParams( window.location.search );
18 + }
44 19
45 - if ( isHandling.indexOf( addonSlug ) !== -1 ) {
46 - return;
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 + } );
47 105 }
48 - isHandling.push( addonSlug );
49 106
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 - }
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 + ] );
64 169
65 - const { status, message, data } = res;
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 + } );
66 183
67 - if ( callBack ) {
68 - callBack( status, message, data );
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;
69 202 }
203 + }
70 204
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` );
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;
85 214
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 - }
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 + };
93 222
94 - elNotifyActionWrapper.classList.add( 'show' );
95 - setTimeout( () => {
96 - elNotifyActionNew.remove();
223 + const releaseHandling = () => {
224 + lpUtils.lpSetLoadingEl( el, 0 );
225 + };
97 226
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;
227 + window.lpAJAXG.fetchAJAX( params, {
228 + success: ( response ) => {
229 + releaseHandling();
110 230
111 - elAddonItems.forEach( ( elAddonItem ) => {
112 - const addonName = elAddonItem.querySelector( 'a' ).textContent;
113 - if ( elAddonItem.classList.contains( 'hide' ) ) {
114 - return;
115 - }
231 + const { status, message, data: resData } = response;
116 232
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 - } );
233 + lpToastify.show( message, status );
124 234
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' );
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 + } );
133 260 }
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 261
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 ) );
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 + );
155 272
156 - clearInterval( loadElData );
157 - }
158 -}, 1 );
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 );
159 290
160 -document.addEventListener( 'DOMContentLoaded', ( e ) => {
161 -} );
291 + // Count items for category by tab
292 + this.updateCategoryCounts( elAddonItems, tabName );
162 293
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 - }
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 + } );
173 318 }
174 319
175 - // Events actions: activate, deactivate.
176 - /*if ( el.classList.contains( 'lp-toggle-switch-label' ) ) {
177 - //e.preventDefault();
320 + updateCategoryCounts( elAddonItems, tabName ) {
321 + const categoryCounts = { all: 0 };
178 322
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 - }
323 + elAddonItems.forEach( ( elAddonItem ) => {
324 + const matchesTab =
325 + 'all' === tabName || elAddonItem.classList.contains( tabName );
326 +
327 + if ( ! matchesTab ) {
328 + return;
206 329 }
207 330
208 - if ( status === 'success' ) {
209 - if ( action === 'deactivate' ) {
210 - elAddonItem.classList.remove( 'activated' );
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 })`;
211 352 }
212 - if ( action === 'activate' ) {
213 - elAddonItem.classList.add( 'activated' );
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;
214 373 }
215 - }
216 - } );
217 - }*/
374 + } );
375 + }
218 376
219 - // Events actions: install, update, delete.
220 - if ( el.closest( '.btn-addon-action' ) ) {
221 - el = el.closest( '.btn-addon-action' );
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 +
222 393 e.preventDefault();
223 - el.classList.add( 'handling' );
394 + lpUtils.lpSetLoadingEl( el, 1 );
395 +
224 396 let purchaseCode = '';
225 - const elAddonItem = el.closest( '.lp-addon-item' );
226 - const addon = dataAddons[ elAddonItem.dataset.slug ];
397 + const elAddonItem = el.closest( selectors.elAddonItem );
398 + const addon = this.getAddonData( elAddonItem );
227 399 const action = el.dataset.action;
228 - const elItemPurchase = elAddonItem.querySelector( '.lp-addon-item__purchase' );
229 - //const elToggleSwitchInput = elAddonItem.querySelector( '.lp-toggle-switch-input' );
400 + const elItemPurchase = elAddonItem.querySelector(
401 + selectors.elItemPurchase
402 + );
230 403
231 - if ( action === 'purchase' ) {
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';
232 448 elItemPurchase.style.display = 'block';
233 - elItemPurchase.querySelector( '.purchase-install' ).style.display = 'flex';
449 + if ( ! hasPurchaseCode ) {
450 + elPurchaseCode.focus();
451 + }
452 + lpUtils.lpSetLoadingEl( el, 0 );
234 453 return;
235 - } else if ( action === 'update-purchase-code' ) {
236 - elItemPurchase.querySelector( '.purchase-update' ).style.display = 'flex';
237 - elItemPurchase.style.display = 'block';
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 );
238 479 return;
239 - } else if ( action === 'buy' ) {
240 - const link = el.dataset.link;
241 - window.open( link, '_blank' );
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 );
242 504 return;
243 505 } else if ( action === 'cancel' ) {
244 506 elItemPurchase.style.display = 'none';
507 + elItemPurchase.querySelector(
508 + selectors.elPurchaseInstall
509 + ).style.display = 'none';
510 + lpUtils.lpSetLoadingEl( el, 0 );
245 511 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 512 }
254 513
255 514 // Send request to server.
256 515 if ( elItemPurchase ) {
257 - purchaseCode = elItemPurchase.querySelector( 'input[name=purchase-code]' ).value;
516 + purchaseCode = elItemPurchase.querySelector(
517 + 'input[name=purchase-code]'
518 + ).value;
258 519 }
259 520
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' );
521 + this.addonsAction( {
522 + purchase_code: purchaseCode,
523 + action,
524 + addon,
525 + el,
526 + elAddonItem,
289 527 } );
290 528 }
291 529
292 - if ( el.classList.contains( 'nav-tab' ) ) {
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 );
293 540 e.preventDefault();
294 - const elTabs = document.querySelectorAll( '.nav-tab' );
295 - elTabs.forEach( function( elTab ) {
541 +
542 + const elTabs = this.elAddonsPage.querySelectorAll( selectors.elNavTab );
543 + elTabs.forEach( ( elTab ) => {
296 544 elTab.classList.remove( 'nav-tab-active' );
545 + elTab.setAttribute( 'aria-pressed', 'false' );
297 546 } );
298 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 +
299 553 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 = '';
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 + } );
303 582
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' );
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' );
314 729 }
315 - } );
730 + }
731 + if ( elSearchInput ) {
732 + this.updateSearchClearBtnState();
733 + elSearchInput.focus();
734 + }
735 + }
316 736
317 - setGridItems( totalItems );
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 + }
318 756 }
319 -} );
320 757
321 -/*** Event search addons. ***/
322 -document.addEventListener( 'input', ( e ) => {
323 - const el = e.target;
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 + }
324 775
325 - if ( 'lp-search-addons__input' === el.id ) {
326 - const keyword = el.value;
327 - searchAddons( keyword );
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();
328 786 }
329 787
330 - // Events change input purchase code.
331 - if ( el.classList.contains( 'enter-purchase-code' ) ) {
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;
332 838 e.preventDefault();
839 + el.classList.remove( 'is-error' );
840 + el.removeAttribute( 'aria-invalid' );
333 841 const purchaseCode = el.value;
334 - const elItemPurchase = el.closest( '.lp-addon-item__purchase' );
842 + const elItemPurchase = el.closest(
843 + AdminAddons.selectors.elItemPurchase
844 + );
335 845
336 846 if ( elItemPurchase ) {
337 - const input = elItemPurchase.querySelector( 'input[name=purchase-code]' );
847 + const input = elItemPurchase.querySelector(
848 + 'input[name=purchase-code]'
849 + );
338 850 input.value = purchaseCode;
339 851 }
340 852 }
341 -} );
853 +}
854 +
855 +const adminAddons = new AdminAddons();
856 +adminAddons.init();
857 +
858 +export default AdminAddons;