PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.3.8
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.3.8
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 4.1.7.3.1 4.1.7.3.2 4.2.0 All 139 releases
learnpress / assets / src / js / frontend / course-builder / builder-course / builder-edit-course.js

builder-edit-course.js in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.3.8, at assets/src/js/frontend/course-builder/builder-course/builder-edit-course.js

2,552 lines 85.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import * as lpUtils from 'lpAssetsJsPath/utils.js';
2 import * as lpToastify from 'lpAssetsJsPath/lpToastify.js';
3 import SweetAlert from 'sweetalert2';
4 import { EditCourseCurriculum } from 'lpAssetsJsPath/admin/edit-course/edit-curriculum';
5 import { MetaboxExtraInfo } from './extra-info.js';
6 import { getFormState } from '../builder-form-state.js';
7 import { SWAL_ICON_DUPLICATE } from '../swal-icons.js';
8
9 export class BuilderEditCourse {
10 constructor() {
11 // Only initialize on course edit pages, not quiz/question/lesson edit pages
12 const isCourseEditPage = document.querySelector( '.cb-section__course-edit' );
13 if ( ! isCourseEditPage ) {
14 return;
15 }
16 this.init();
17 }
18
19 static selectors = {
20 elCBContent: '.lp-cb-content',
21 elTabLinks: '.lp-meta-box__course-tab__tabs li a',
22 elTabItems: '.lp-meta-box__course-tab__tabs li',
23 elTabPanels: '.lp-meta-box-course-panels',
24
25 elDataCourse: '.cb-section__course-edit',
26 elBtnUpdateCourse: '.cb-btn-update',
27 elBtnHeaderSave: '.lp-cb-save-btn',
28 elBtnDraftCourse: '.cb-btn-darft',
29 elBtnPublishCourse: '.cb-btn-publish',
30 elBtnPendingCourse: '.cb-btn-pending',
31 elBtnDropdownAction: '.cb-dropdown-item[data-status]',
32 elBtnMainAction: '.cb-btn-main-action',
33 elBtnDuplicateCourse: '.cb-btn-duplicate-course',
34 elBtnTrashCourse: '.cb-btn-trash',
35 elBtnSaveSettings: '.cb-btn-save-settings',
36 elDropdownToggle: '.cb-btn-dropdown-toggle',
37 elDropdownMenu: '.cb-dropdown-menu',
38 elHeaderActionsDropdown: '.cb-header-actions-dropdown',
39 elTitleInput: '#title',
40 elTitleCharCount: '.cb-course-edit-title__char-count',
41 elDescEditor: '#course_description_editor',
42 elDescWordCount: '.cb-course-edit-desc__word-count',
43 elStatus: '.course-status',
44 elFormSetting: '.lp-form-setting-course',
45
46 elCategoryTabs: '#course_category-tabs li a',
47 elCategoryPanels: '#taxonomy-course_category .tabs-panel',
48 elBtnToggleAddCategory: '#course_category-add-toggle',
49 elFormCategoryWrapper: '#course_category-add',
50 elInputNewCategory: '#newcourse_category',
51 elSelectParentCategory: '#newcourse_category_parent',
52 elBtnSubmitCategory: '#course_category-add-submit',
53 elCategoryChecklist: '#course_categorychecklist',
54
55 // New custom category add form selectors
56 elBtnAddCategoryNew: '.cb-course-edit-category__btn-add-new',
57 elFormCategoryAddNew: '.cb-course-edit-terms__form-add-category',
58 elInputAddCategory: '.cb-course-edit-category__input',
59 elSelectAddCategoryParent: '.cb-course-edit-category__select-parent',
60 elBtnSaveCategory: '.cb-course-edit-category__btn-save',
61 elBtnCancelCategoryNew: '.cb-course-edit-category__btn-cancel',
62 elCategorySearchInput: '.cb-course-edit-category__search-input',
63 elBtnToggleTermsSearch: '.cb-terms-header__btn-search',
64 elTermsSearchToolbar: '.cb-terms-search-toolbar',
65
66 elWrapperCheckBoxTag: '.cb-course-edit-tags__checkbox-wrapper',
67 elFormTagAddNew: '.cb-course-edit-terms__form-add-tag',
68 elBtnAddTagNew: '.cb-course-edit-tag__btn-add-new',
69 elBtnCancelTagNew: '.cb-course-edit-tag__btn-cancel',
70 elBtnSaveTag: '.cb-course-edit-tags__btn-save',
71 elInputAddTag: '.cb-course-edit-tags__input',
72 elTagsWrapper: '.cb-course-edit-tags__wrapper',
73 elTagChip: '.cb-tag-chip',
74 elTagSearchInput: '.cb-course-edit-tags__search-input',
75 elTagEmptyState: '.cb-course-edit-tags__empty',
76
77 elBtnRemoveFeatured: '.cb-remove-featured-image',
78 elBtnSetFeatured: '.cb-featured-image-dropzone:not(.has-image)',
79 elBtnChangeFeatured: '.cb-change-featured-image',
80 elFeaturedImageDropzone: '.cb-featured-image-dropzone',
81 elFeaturedImageLink: '.cb-featured-image-link',
82 elThumbnailInput: '#course_thumbnail_id',
83 elFeaturedImageContainer: '.cb-featured-image-container',
84 elPublishPanel: '.cb-course-edit-publish',
85 elPublishStatusSelect: '#cb-course-publish-status',
86 elPublishVisibilitySelect: '#cb-course-publish-visibility',
87 elPublishPasswordRow: '.cb-course-edit-publish__password-row',
88 elPublishPasswordInput: '#cb-course-publish-password',
89 elPublishDateLabel: '#cb-course-publish-date-label',
90 elPublishDateInput: '#cb-course-publish-date',
91
92 elPriceCourseData: '#price_course_data',
93 elSaleDatesFields: '.lp_sale_dates_fields',
94 elSalePriceScheduleBtn: '.lp_sale_price_schedule',
95 elCancelSaleScheduleBtn: '.lp_cancel_sale_schedule',
96 elRegularPriceInput: '#_lp_regular_price',
97 elSalePriceInput: '#_lp_sale_price',
98 elPriceInput: '#_lp_price',
99 elFormField: '.form-field',
100 elTipFloating: '.learn-press-tip-floating',
101 elCategoryDiv: '#taxonomy-course_category',
102 elCourseResultRadio: 'input[type="radio"][name="_lp_course_result"]',
103 elPassingConditionField: '._lp_passing_condition_field',
104 elBtnGetFinalQuiz: '.lp-metabox-get-final-quiz',
105 elFinalQuizMessage: '.lp-metabox-evaluate-final_quiz',
106 elExtraMetaInput: '.lp_course_extra_meta_box__input',
107 elFaqMetaInput: '.lp_course_faq_meta_box__field input',
108
109 elCBHorizontalTabs: '.lp-cb-tabs__item',
110 elCBTabPanels: '.lp-cb-tab-panel',
111
112 // Permalink component
113 elPermalinkDisplay: '.cb-permalink-display',
114 elPermalinkEditor: '.cb-permalink-editor',
115 elPermalinkEditBtn: '.cb-permalink-edit-btn',
116 elPermalinkOkBtn: '.cb-permalink-ok-btn',
117 elPermalinkCancelBtn: '.cb-permalink-cancel-btn',
118 elPermalinkSlugInput: '.cb-permalink-slug-input',
119 elPermalinkUrl: '.cb-permalink-url',
120 elPermalinkBaseUrl: '#cb-permalink-base-url',
121
122 // Preview and Admin link elements
123 elBtnPreview: '.cb-button.cb-btn-preview',
124 elAdminLink: '.lp-cb-admin-link',
125 };
126
127 init() {
128 this.isSavingCourse = false;
129
130 const editCourseCurriculum = new EditCourseCurriculum();
131 const metaboxExtraInfo = new MetaboxExtraInfo();
132 editCourseCurriculum.init();
133 metaboxExtraInfo.init();
134
135 this.initTabs();
136 this.initTabTitles();
137 this.initCategoryTabs();
138 this.initCategoryTree();
139 this.applyCategorySearch();
140 this.initTagManagement();
141 this.initSalePriceLayout();
142 this.initAssessmentMetaboxFeatures();
143 this.initTitleCharCount();
144 this.initDescWordCount();
145 this.syncPublishStatusOptions();
146 this.syncPublishDateLabel();
147 this.syncPublishVisibilityControls( false );
148 this.initHeaderActionsDropdown();
149 this.bindAiFeaturedImageListener();
150 this.events();
151 }
152
153 bindAiFeaturedImageListener() {
154 if ( BuilderEditCourse._boundAiFeaturedImageListener ) {
155 return;
156 }
157
158 document.addEventListener( 'lp-course-builder/ai-featured-image-applied', ( event ) => {
159 this.handleAiFeaturedImageApplied( event );
160 } );
161
162 BuilderEditCourse._boundAiFeaturedImageListener = true;
163 }
164
165 handleAiFeaturedImageApplied( event ) {
166 const attachmentId = parseInt( event?.detail?.attachmentId || 0, 10 );
167 const imageSrc = event?.detail?.imageSrc || '';
168
169 if ( ! attachmentId || ! imageSrc ) {
170 return;
171 }
172
173 this.setFeaturedImage( {
174 id: attachmentId,
175 url: imageSrc,
176 sizes: {
177 medium: {
178 url: imageSrc,
179 },
180 },
181 } );
182 }
183
184 events() {
185 if ( BuilderEditCourse._loadedEvents ) {
186 return;
187 }
188 BuilderEditCourse._loadedEvents = true;
189
190 lpUtils.eventHandlers( 'click', [
191 {
192 selector: BuilderEditCourse.selectors.elTabLinks,
193 class: this,
194 callBack: this.handleTabClick.name,
195 },
196 {
197 selector: BuilderEditCourse.selectors.elCategoryTabs,
198 class: this,
199 callBack: this.handleCategoryTabClick.name,
200 },
201 {
202 selector: BuilderEditCourse.selectors.elCBHorizontalTabs,
203 class: this,
204 callBack: this.handleCBHorizontalTabClick.name,
205 },
206 {
207 selector: BuilderEditCourse.selectors.elBtnToggleAddCategory,
208 class: this,
209 callBack: this.toggleAddCategoryForm.name,
210 },
211 {
212 selector: BuilderEditCourse.selectors.elBtnSubmitCategory,
213 class: this,
214 callBack: this.addNewCategory.name,
215 },
216 // Custom category add form handlers
217 {
218 selector: BuilderEditCourse.selectors.elBtnAddCategoryNew,
219 class: this,
220 callBack: this.toggleCustomAddCategoryForm.name,
221 },
222 {
223 selector: BuilderEditCourse.selectors.elBtnToggleTermsSearch,
224 class: this,
225 callBack: this.toggleTermsSearchToolbar.name,
226 },
227 {
228 selector: BuilderEditCourse.selectors.elBtnCancelCategoryNew,
229 class: this,
230 callBack: this.toggleCustomAddCategoryForm.name,
231 },
232 {
233 selector: BuilderEditCourse.selectors.elBtnSaveCategory,
234 class: this,
235 callBack: this.addNewCategoryCustom.name,
236 },
237 {
238 selector: BuilderEditCourse.selectors.elBtnMainAction,
239 class: this,
240 callBack: this.updateCourse.name,
241 },
242 {
243 selector: BuilderEditCourse.selectors.elBtnHeaderSave,
244 class: this,
245 callBack: this.updateCourse.name,
246 },
247 {
248 selector: BuilderEditCourse.selectors.elBtnDropdownAction,
249 class: this,
250 callBack: this.updateCourse.name,
251 },
252 {
253 selector: BuilderEditCourse.selectors.elBtnDuplicateCourse,
254 class: this,
255 callBack: this.duplicateCourse.name,
256 },
257 {
258 selector: BuilderEditCourse.selectors.elBtnTrashCourse,
259 class: this,
260 callBack: this.trashCourse.name,
261 },
262 {
263 selector: BuilderEditCourse.selectors.elBtnSaveSettings,
264 class: this,
265 callBack: this.saveSettings.name,
266 },
267 {
268 selector: BuilderEditCourse.selectors.elBtnGetFinalQuiz,
269 class: this,
270 callBack: this.getFinalQuiz.name,
271 },
272 {
273 selector: BuilderEditCourse.selectors.elBtnAddTagNew,
274 class: this,
275 callBack: this.toggleAddTagForm.name,
276 },
277 {
278 selector: BuilderEditCourse.selectors.elBtnCancelTagNew,
279 class: this,
280 callBack: this.toggleAddTagForm.name,
281 },
282 {
283 selector: BuilderEditCourse.selectors.elBtnSaveTag,
284 class: this,
285 callBack: this.addNewTag.name,
286 },
287 {
288 selector: BuilderEditCourse.selectors.elBtnSetFeatured,
289 class: this,
290 callBack: this.openMediaUploader.name,
291 },
292 {
293 selector: BuilderEditCourse.selectors.elFeaturedImageLink,
294 class: this,
295 callBack: this.openMediaUploader.name,
296 },
297 {
298 selector: BuilderEditCourse.selectors.elBtnChangeFeatured,
299 class: this,
300 callBack: this.openMediaUploader.name,
301 },
302 {
303 selector: BuilderEditCourse.selectors.elBtnRemoveFeatured,
304 class: this,
305 callBack: this.removeFeaturedImage.name,
306 },
307 {
308 selector: BuilderEditCourse.selectors.elSalePriceScheduleBtn,
309 class: this,
310 callBack: this.handleScheduleClick.name,
311 },
312 {
313 selector: BuilderEditCourse.selectors.elCancelSaleScheduleBtn,
314 class: this,
315 callBack: this.handleCancelSchedule.name,
316 },
317 // Permalink component events
318 {
319 selector: BuilderEditCourse.selectors.elPermalinkEditBtn,
320 class: this,
321 callBack: this.handlePermalinkEdit.name,
322 },
323 {
324 selector: BuilderEditCourse.selectors.elPermalinkOkBtn,
325 class: this,
326 callBack: this.handlePermalinkOk.name,
327 },
328 {
329 selector: BuilderEditCourse.selectors.elPermalinkCancelBtn,
330 class: this,
331 callBack: this.handlePermalinkCancel.name,
332 },
333 ] );
334
335 lpUtils.eventHandlers( 'change', [
336 {
337 selector: '.lp-meta-box input, .forminp input',
338 class: this,
339 callBack: this.showHideOptionsDependency.name,
340 },
341 {
342 selector: '#course_category-pop input[type="checkbox"]',
343 class: this,
344 callBack: this.handleMostUsedChange.name,
345 },
346 {
347 selector: 'input[name="course_tags[]"]',
348 class: this,
349 callBack: this.handleTagSelectionChange.name,
350 },
351 {
352 selector: BuilderEditCourse.selectors.elCourseResultRadio,
353 class: this,
354 callBack: this.handleCourseResultChange.name,
355 },
356 {
357 selector: BuilderEditCourse.selectors.elPublishVisibilitySelect,
358 class: this,
359 callBack: this.handlePublishVisibilityChange.name,
360 },
361 {
362 selector: BuilderEditCourse.selectors.elPublishStatusSelect,
363 class: this,
364 callBack: this.handlePublishStatusChange.name,
365 },
366 {
367 selector: BuilderEditCourse.selectors.elPublishDateInput,
368 class: this,
369 callBack: this.handlePublishDateChange.name,
370 },
371 ] );
372
373 lpUtils.eventHandlers( 'input', [
374 {
375 selector: BuilderEditCourse.selectors.elPriceCourseData,
376 class: this,
377 callBack: this.validateSalePrice.name,
378 },
379 {
380 selector: BuilderEditCourse.selectors.elTitleInput,
381 class: this,
382 callBack: this.handleTitleInput.name,
383 },
384 {
385 selector: BuilderEditCourse.selectors.elTagSearchInput,
386 class: this,
387 callBack: this.handleTagSearchInput.name,
388 },
389 {
390 selector: BuilderEditCourse.selectors.elCategorySearchInput,
391 class: this,
392 callBack: this.handleCategorySearchInput.name,
393 },
394 ] );
395
396 lpUtils.eventHandlers( 'keydown', [
397 {
398 selector: BuilderEditCourse.selectors.elInputNewCategory,
399 class: this,
400 callBack: this.addNewCategory.name,
401 checkIsEventEnter: true,
402 },
403 {
404 selector: BuilderEditCourse.selectors.elInputAddCategory,
405 class: this,
406 callBack: this.addNewCategoryCustom.name,
407 checkIsEventEnter: true,
408 },
409 {
410 selector: BuilderEditCourse.selectors.elInputAddTag,
411 class: this,
412 callBack: this.addNewTag.name,
413 checkIsEventEnter: true,
414 },
415 {
416 selector: BuilderEditCourse.selectors.elExtraMetaInput,
417 class: this,
418 callBack: this.preventEnterSubmitInMetaInput.name,
419 checkIsEventEnter: true,
420 },
421 {
422 selector: BuilderEditCourse.selectors.elFaqMetaInput,
423 class: this,
424 callBack: this.preventEnterSubmitInMetaInput.name,
425 checkIsEventEnter: true,
426 },
427 ] );
428 }
429
430 initAssessmentMetaboxFeatures() {
431 this.updatePassingConditionVisibility();
432 }
433
434 handleCourseResultChange() {
435 this.updatePassingConditionVisibility();
436 }
437
438 updatePassingConditionVisibility() {
439 const listHides = [ 'evaluate_final_quiz', 'evaluate_final_assignment' ];
440 const checkedEvaluation = document.querySelector(
441 `${ BuilderEditCourse.selectors.elCourseResultRadio }:checked`
442 );
443 const shouldHidePassing = checkedEvaluation
444 ? listHides.includes( checkedEvaluation.value )
445 : false;
446
447 document
448 .querySelectorAll( BuilderEditCourse.selectors.elPassingConditionField )
449 .forEach( ( el ) => {
450 el.style.display = shouldHidePassing ? 'none' : '';
451 } );
452 }
453
454 preventEnterSubmitInMetaInput( args ) {
455 const { e, target } = args;
456 e.preventDefault();
457
458 if ( target?.blur ) {
459 target.blur();
460 }
461 }
462
463 async getFinalQuiz( args ) {
464 const { e, target } = args;
465 e.preventDefault();
466
467 const btn = target.closest( BuilderEditCourse.selectors.elBtnGetFinalQuiz );
468 if ( ! btn || btn.dataset.loadingState === 'yes' ) {
469 return;
470 }
471
472 const defaultText = btn.textContent;
473 const loadingText = btn.dataset.loading || defaultText;
474 const currentMessage =
475 btn.parentNode?.querySelector( BuilderEditCourse.selectors.elFinalQuizMessage ) ||
476 document.querySelector( BuilderEditCourse.selectors.elFinalQuizMessage );
477
478 if ( currentMessage ) {
479 currentMessage.remove();
480 }
481
482 btn.dataset.loadingState = 'yes';
483 btn.textContent = loadingText;
484
485 try {
486 const response = await this.requestFinalQuiz( btn.dataset.postid || '' );
487 const message = response?.data || response?.message || '';
488
489 btn.textContent = defaultText;
490
491 const messageNode = document.createElement( 'div' );
492 messageNode.className = 'lp-metabox-evaluate-final_quiz';
493 messageNode.innerHTML = message;
494
495 btn.parentNode.insertBefore( messageNode, btn.nextSibling );
496 } catch ( error ) {
497 btn.textContent = defaultText;
498 if ( error?.message ) {
499 lpToastify.show( error.message, 'error' );
500 }
501 } finally {
502 delete btn.dataset.loadingState;
503 }
504 }
505
506 async requestFinalQuiz( courseId = '' ) {
507 if ( typeof wp !== 'undefined' && typeof wp.apiFetch === 'function' ) {
508 return wp.apiFetch( {
509 path: 'lp/v1/admin/course/get_final_quiz',
510 method: 'POST',
511 data: {
512 courseId,
513 isCourseBuilder: true,
514 },
515 } );
516 }
517
518 const restBase = window.lpGlobalSettings?.rest || window.lpData?.lp_rest_url || '/wp-json/';
519 const restNonce = window.lpGlobalSettings?.nonce || window.lpData?.nonce || '';
520 const normalizedRestBase = restBase.endsWith( '/' ) ? restBase : `${ restBase }/`;
521 const response = await fetch( `${ normalizedRestBase }lp/v1/admin/course/get_final_quiz`, {
522 method: 'POST',
523 credentials: 'same-origin',
524 headers: {
525 'Content-Type': 'application/json',
526 ...( restNonce ? { 'X-WP-Nonce': restNonce } : {} ),
527 },
528 body: JSON.stringify( {
529 courseId,
530 isCourseBuilder: true,
531 } ),
532 } );
533
534 let responseData = {};
535 try {
536 responseData = await response.json();
537 } catch ( error ) {
538 throw new Error( 'Cannot parse final quiz response' );
539 }
540
541 if ( ! response.ok ) {
542 throw new Error( responseData?.message || `Request failed (${ response.status })` );
543 }
544
545 return responseData;
546 }
547
548 initTagManagement() {
549 this.syncTagManagement();
550 }
551
552 getTagManagementElements() {
553 const wrapper = document.querySelector( BuilderEditCourse.selectors.elTagsWrapper );
554 if ( ! wrapper ) {
555 return null;
556 }
557
558 return {
559 wrapper,
560 searchInput: wrapper.querySelector( BuilderEditCourse.selectors.elTagSearchInput ),
561 list: wrapper.querySelector( BuilderEditCourse.selectors.elWrapperCheckBoxTag ),
562 emptyState: wrapper.querySelector( BuilderEditCourse.selectors.elTagEmptyState ),
563 };
564 }
565
566 getTagChipName( chip ) {
567 return (
568 chip.dataset.tagName ||
569 chip.querySelector( '.cb-tag-chip__name' )?.textContent?.trim()?.toLowerCase() ||
570 ''
571 );
572 }
573
574 updateTagEmptyState( emptyState, searchTerm = '', visibleCount = 0 ) {
575 if ( ! emptyState ) {
576 return;
577 }
578
579 const emptyDefault = emptyState.dataset.emptyDefault || '';
580 const emptySearch = emptyState.dataset.emptySearch || emptyDefault;
581 emptyState.textContent = searchTerm ? emptySearch : emptyDefault;
582 emptyState.classList.toggle( 'lp-hidden', visibleCount > 0 );
583 }
584
585 syncTagManagement() {
586 const elements = this.getTagManagementElements();
587 if ( ! elements || ! elements.list ) {
588 return;
589 }
590
591 const searchTerm = elements.searchInput?.value?.trim().toLowerCase() || '';
592 const chips = Array.from(
593 elements.wrapper.querySelectorAll( BuilderEditCourse.selectors.elTagChip )
594 ).sort( ( chipA, chipB ) => {
595 const chipASelected = chipA.querySelector( 'input[name="course_tags[]"]' )?.checked ? 1 : 0;
596 const chipBSelected = chipB.querySelector( 'input[name="course_tags[]"]' )?.checked ? 1 : 0;
597
598 if ( chipASelected !== chipBSelected ) {
599 return chipBSelected - chipASelected;
600 }
601
602 return this.getTagChipName( chipA ).localeCompare( this.getTagChipName( chipB ) );
603 } );
604 let visibleCount = 0;
605
606 chips.forEach( ( chip ) => {
607 const matchesSearch = ! searchTerm || this.getTagChipName( chip ).includes( searchTerm );
608 const shouldShow = matchesSearch;
609
610 chip.classList.toggle( 'lp-hidden', ! shouldShow );
611 elements.list.appendChild( chip );
612
613 if ( shouldShow ) {
614 visibleCount += 1;
615 }
616 } );
617
618 this.updateTagEmptyState( elements.emptyState, searchTerm, visibleCount );
619 }
620
621 handleTagSelectionChange() {
622 this.syncTagManagement();
623 }
624
625 handleTagSearchInput() {
626 this.syncTagManagement();
627 }
628
629 handleCategorySearchInput() {
630 this.applyCategorySearch();
631 }
632
633 toggleTermsSearchToolbar( args ) {
634 const { e, target } = args;
635 if ( e ) e.preventDefault();
636
637 const btnSearch = target?.closest( BuilderEditCourse.selectors.elBtnToggleTermsSearch );
638 if ( ! btnSearch ) {
639 return;
640 }
641
642 const toolbarTarget = btnSearch.dataset.toggleTarget || '';
643 const toolbar = toolbarTarget ? document.querySelector( toolbarTarget ) : null;
644 if ( ! toolbar ) {
645 return;
646 }
647
648 const willOpen = ! toolbar.classList.contains( 'is-open' );
649 toolbar.classList.toggle( 'is-open', willOpen );
650 btnSearch.setAttribute( 'aria-expanded', willOpen ? 'true' : 'false' );
651
652 const searchInput = toolbar.querySelector( 'input[type="search"]' );
653
654 if ( willOpen ) {
655 if ( searchInput ) {
656 setTimeout( () => searchInput.focus(), 220 );
657 }
658 return;
659 }
660
661 if ( searchInput ) {
662 searchInput.value = '';
663 }
664
665 if ( searchInput?.matches( BuilderEditCourse.selectors.elCategorySearchInput ) ) {
666 this.applyCategorySearch();
667 } else if ( searchInput?.matches( BuilderEditCourse.selectors.elTagSearchInput ) ) {
668 this.syncTagManagement();
669 }
670 }
671
672 getDirectChildByTagName( element, tagName ) {
673 return Array.from( element?.children || [] ).find( ( child ) => child.tagName === tagName );
674 }
675
676 getCategoryLabelText( li ) {
677 const label = this.getDirectChildByTagName( li, 'LABEL' );
678 return label?.textContent?.trim()?.toLowerCase() || '';
679 }
680
681 filterCategoryItem( li, searchTerm ) {
682 const childList = Array.from( li.children ).find(
683 ( child ) => child.tagName === 'UL' && child.classList.contains( 'children' )
684 );
685 const childItems = childList
686 ? Array.from( childList.children ).filter( ( child ) => child.tagName === 'LI' )
687 : [];
688 const selfMatch = ! searchTerm || this.getCategoryLabelText( li ).includes( searchTerm );
689 const childMatch = childItems.some( ( child ) => this.filterCategoryItem( child, searchTerm ) );
690 const shouldShow = ! searchTerm || selfMatch || childMatch;
691
692 li.classList.toggle( 'lp-hidden', ! shouldShow );
693
694 if ( searchTerm && childList ) {
695 li.classList.toggle( 'children-visible', selfMatch || childMatch );
696 }
697
698 return shouldShow;
699 }
700
701 applyCategorySearch() {
702 const wrapper = document.querySelector( BuilderEditCourse.selectors.elCategoryDiv );
703 const searchInput = document.querySelector( BuilderEditCourse.selectors.elCategorySearchInput );
704 if ( ! wrapper || ! searchInput ) {
705 return;
706 }
707
708 const searchTerm = searchInput.value.trim().toLowerCase();
709 const panels = wrapper.querySelectorAll( '.tabs-panel' );
710
711 panels.forEach( ( panel ) => {
712 const rootLists = Array.from( panel.children ).filter( ( child ) => child.tagName === 'UL' );
713 const items = rootLists.reduce( ( result, list ) => {
714 return result.concat(
715 Array.from( list.children ).filter( ( child ) => child.tagName === 'LI' )
716 );
717 }, [] );
718
719 if ( items.length ) {
720 items.forEach( ( item ) => this.filterCategoryItem( item, searchTerm ) );
721 }
722 } );
723
724 if ( ! searchTerm ) {
725 this.expandCheckedCategories( wrapper );
726 }
727 }
728
729 initCategoryTabs() {
730 const allTab = document.querySelector( '#course_category-tabs a[href="#course_category-all"]' );
731 if ( allTab ) {
732 allTab.closest( 'li' ).classList.add( 'tabs' );
733 const panelAll = document.querySelector( '#course_category-all' );
734 if ( panelAll ) panelAll.style.display = 'block';
735 }
736 }
737
738 handleCategoryTabClick( args ) {
739 const { e, target } = args;
740 e.preventDefault();
741 const link = target.closest( 'a' );
742 if ( ! link ) return;
743
744 const wrapper = document.querySelector( BuilderEditCourse.selectors.elCategoryDiv );
745 const tabs = wrapper.querySelectorAll( '.category-tabs li' );
746 const panels = wrapper.querySelectorAll( '.tabs-panel' );
747 const targetId = link.getAttribute( 'href' );
748
749 tabs.forEach( ( t ) => t.classList.remove( 'tabs', 'active' ) );
750 panels.forEach( ( p ) => ( p.style.display = 'none' ) );
751
752 link.closest( 'li' ).classList.add( 'tabs' );
753 const targetPanel = wrapper.querySelector( targetId );
754 if ( targetPanel ) targetPanel.style.display = 'block';
755
756 if ( targetId === '#course_category-pop' ) {
757 this.syncMostUsedTabs();
758 }
759
760 this.applyCategorySearch();
761 }
762
763 syncMostUsedTabs() {
764 const allPanel = document.querySelector( '#course_category-all' );
765 const popPanel = document.querySelector( '#course_category-pop' );
766
767 if ( ! allPanel || ! popPanel ) return;
768
769 const popInputs = popPanel.querySelectorAll( 'input[type="checkbox"]' );
770
771 popInputs.forEach( ( popInput ) => {
772 const termId = popInput.value;
773 const allInput = allPanel.querySelector( `input[value="${ termId }"]` );
774
775 if ( allInput ) {
776 popInput.checked = allInput.checked;
777 }
778 } );
779 }
780
781 handleMostUsedChange( args ) {
782 const { target } = args;
783 const termId = target.value;
784 const isChecked = target.checked;
785
786 const allInput = document.querySelector( `#course_category-all input[value="${ termId }"]` );
787
788 if ( allInput ) {
789 allInput.checked = isChecked;
790
791 if ( isChecked ) {
792 const parentLi = allInput.closest( 'li' );
793 if ( parentLi ) parentLi.classList.add( 'children-visible' );
794
795 let current = parentLi;
796 while ( current && current.parentElement.closest( 'li' ) ) {
797 current = current.parentElement.closest( 'li' );
798 current.classList.add( 'children-visible' );
799 }
800 }
801 }
802 }
803
804 toggleAddCategoryForm( args ) {
805 const { e } = args;
806 if ( e ) e.preventDefault();
807
808 const form = document.querySelector( BuilderEditCourse.selectors.elFormCategoryWrapper );
809 const input = document.querySelector( BuilderEditCourse.selectors.elInputNewCategory );
810
811 if ( form ) {
812 const isHidden = window.getComputedStyle( form ).display === 'none';
813
814 if ( isHidden ) {
815 form.style.display = 'block';
816 if ( input )
817 setTimeout( () => {
818 input.focus();
819 input.value = '';
820 }, 100 );
821 } else {
822 form.style.display = 'none';
823 }
824 }
825 }
826
827 initCategoryTree() {
828 const wrapper = document.querySelector( BuilderEditCourse.selectors.elCategoryDiv );
829 if ( ! wrapper ) return;
830
831 const childLists = wrapper.querySelectorAll( 'ul.children' );
832
833 childLists.forEach( ( ul ) => {
834 const parentLi = ul.parentElement;
835 if ( parentLi && parentLi.tagName === 'LI' ) {
836 this.addToggleBtnToLi( parentLi );
837 }
838 } );
839
840 if ( ! BuilderEditCourse._treeEventAttached ) {
841 wrapper.addEventListener( 'click', ( e ) => {
842 if ( e.target.classList.contains( 'lp-cat-toggle' ) ) {
843 e.preventDefault();
844 e.stopPropagation();
845 const li = e.target.closest( 'li' );
846 li.classList.toggle( 'children-visible' );
847 }
848 } );
849
850 wrapper.addEventListener( 'change', ( e ) => {
851 if ( e.target.type === 'checkbox' ) {
852 const li = e.target.closest( 'li' );
853 if ( li && e.target.checked ) {
854 li.classList.add( 'children-visible' );
855 }
856 }
857 } );
858
859 BuilderEditCourse._treeEventAttached = true;
860 }
861
862 this.expandCheckedCategories( wrapper );
863 }
864
865 expandCheckedCategories( wrapper ) {
866 const checkedInputs = wrapper.querySelectorAll( 'input[type="checkbox"]:checked' );
867
868 checkedInputs.forEach( ( input ) => {
869 let currentLi = input.closest( 'li' );
870
871 while ( currentLi ) {
872 const parentUl = currentLi.closest( 'ul' );
873 if ( parentUl && parentUl.classList.contains( 'children' ) ) {
874 const parentCategoryLi = parentUl.closest( 'li' );
875
876 if ( parentCategoryLi ) {
877 parentCategoryLi.classList.add( 'children-visible' );
878 currentLi = parentCategoryLi;
879 } else {
880 currentLi = null;
881 }
882 } else {
883 currentLi = null;
884 }
885 }
886 } );
887 }
888
889 addToggleBtnToLi( li ) {
890 const label = li.querySelector( 'label' );
891 if ( ! label || label.querySelector( '.lp-cat-toggle' ) ) return;
892
893 const toggleBtn = document.createElement( 'span' );
894 toggleBtn.className = 'lp-cat-toggle';
895 toggleBtn.title = 'Toggle sub-categories';
896 label.appendChild( toggleBtn );
897 }
898
899 addNewCategory( args ) {
900 const { e } = args;
901 if ( e ) e.preventDefault();
902
903 const elInput = document.querySelector( BuilderEditCourse.selectors.elInputNewCategory );
904 const elParent = document.querySelector( BuilderEditCourse.selectors.elSelectParentCategory );
905 const btnSave = document.querySelector( BuilderEditCourse.selectors.elBtnSubmitCategory );
906
907 const categoryName = elInput?.value?.trim();
908 if ( ! categoryName ) {
909 lpToastify.show( 'Please enter category name', 'error' );
910 return;
911 }
912
913 const parentId = elParent ? parseInt( elParent.value ) : 0;
914
915 lpUtils.lpSetLoadingEl( btnSave, 1 );
916
917 const dataSend = {
918 action: 'add_course_category',
919 args: { id_url: 'add-course-category' },
920 name: categoryName,
921 parent: parentId,
922 };
923
924 const callBack = {
925 success: ( response ) => {
926 const { status, message, data } = response;
927 lpToastify.show( message, status );
928
929 if ( data?.html ) {
930 const checklist = document.querySelector(
931 BuilderEditCourse.selectors.elCategoryChecklist
932 );
933
934 if ( data.parent && data.parent > 0 ) {
935 const parentInput = checklist.querySelector( `input[value="${ data.parent }"]` );
936 if ( parentInput ) {
937 const parentLi = parentInput.closest( 'li' );
938 parentLi.classList.add( 'children-visible' );
939 let ulChildren = parentLi.querySelector( ':scope > ul.children' );
940 if ( ! ulChildren ) {
941 ulChildren = document.createElement( 'ul' );
942 ulChildren.className = 'children';
943 parentLi.appendChild( ulChildren );
944 this.addToggleBtnToLi( parentLi );
945 }
946
947 ulChildren.insertAdjacentHTML( 'beforeend', data.html );
948 } else {
949 checklist.insertAdjacentHTML( 'afterbegin', data.html );
950 }
951 } else {
952 checklist.insertAdjacentHTML( 'afterbegin', data.html );
953 }
954
955 elInput.value = '';
956 if ( elParent ) elParent.value = '0';
957 this.applyCategorySearch();
958 }
959 },
960 error: ( error ) => {
961 lpToastify.show( error.message || error, 'error' );
962 },
963 completed: () => {
964 lpUtils.lpSetLoadingEl( btnSave, 0 );
965 },
966 };
967
968 window.lpAJAXG.fetchAJAX( dataSend, callBack );
969 }
970
971 /**
972 * Toggle custom category add form (between header and WP meta box).
973 * Shows/hides the form and toggles the Add New button.
974 */
975 toggleCustomAddCategoryForm( args ) {
976 const { target } = args;
977 const elBtnAdd = document.querySelector( BuilderEditCourse.selectors.elBtnAddCategoryNew );
978 const form = document.querySelector( BuilderEditCourse.selectors.elFormCategoryAddNew );
979 const isOpening = target.closest( BuilderEditCourse.selectors.elBtnAddCategoryNew );
980
981 if ( form ) {
982 if ( isOpening ) {
983 form.style.display = 'flex';
984 if ( elBtnAdd ) elBtnAdd.style.display = 'none';
985 const input = form.querySelector( BuilderEditCourse.selectors.elInputAddCategory );
986 if ( input ) setTimeout( () => input.focus(), 100 );
987 } else {
988 form.style.display = 'none';
989 if ( elBtnAdd ) elBtnAdd.style.display = 'inline-flex';
990 }
991 }
992 }
993
994 /**
995 * Add new category via custom form (outside WP meta box).
996 * Uses the same AJAX endpoint but reads from custom form fields.
997 */
998 addNewCategoryCustom( args ) {
999 const { e } = args;
1000 if ( e ) e.preventDefault();
1001
1002 const elInput = document.querySelector( BuilderEditCourse.selectors.elInputAddCategory );
1003 const elParent = document.querySelector(
1004 BuilderEditCourse.selectors.elSelectAddCategoryParent
1005 );
1006 const btnSave = document.querySelector( BuilderEditCourse.selectors.elBtnSaveCategory );
1007
1008 const categoryName = elInput?.value?.trim();
1009 if ( ! categoryName ) {
1010 lpToastify.show( 'Please enter category name', 'error' );
1011 return;
1012 }
1013
1014 const parentId = elParent ? parseInt( elParent.value ) : 0;
1015
1016 lpUtils.lpSetLoadingEl( btnSave, 1 );
1017
1018 const dataSend = {
1019 action: 'add_course_category',
1020 args: { id_url: 'add-course-category' },
1021 name: categoryName,
1022 parent: parentId,
1023 };
1024
1025 const callBack = {
1026 success: ( response ) => {
1027 const { status, message, data } = response;
1028 lpToastify.show( message, status );
1029
1030 if ( data?.html ) {
1031 const checklist = document.querySelector(
1032 BuilderEditCourse.selectors.elCategoryChecklist
1033 );
1034
1035 if ( checklist ) {
1036 if ( data.parent && data.parent > 0 ) {
1037 const parentInput = checklist.querySelector( `input[value="${ data.parent }"]` );
1038 if ( parentInput ) {
1039 const parentLi = parentInput.closest( 'li' );
1040 parentLi.classList.add( 'children-visible' );
1041 let ulChildren = parentLi.querySelector( ':scope > ul.children' );
1042 if ( ! ulChildren ) {
1043 ulChildren = document.createElement( 'ul' );
1044 ulChildren.className = 'children';
1045 parentLi.appendChild( ulChildren );
1046 this.addToggleBtnToLi( parentLi );
1047 }
1048 ulChildren.insertAdjacentHTML( 'beforeend', data.html );
1049 } else {
1050 checklist.insertAdjacentHTML( 'afterbegin', data.html );
1051 }
1052 } else {
1053 checklist.insertAdjacentHTML( 'afterbegin', data.html );
1054 }
1055 }
1056
1057 // Also update the parent select dropdown with the new category
1058 if ( data.term_id && elParent ) {
1059 const newOption = document.createElement( 'option' );
1060 newOption.value = data.term_id;
1061 newOption.textContent = categoryName;
1062 elParent.appendChild( newOption );
1063 }
1064
1065 elInput.value = '';
1066 if ( elParent ) elParent.value = '0';
1067 this.applyCategorySearch();
1068
1069 // Close the form after successful add
1070 const elBtnCancel = document.querySelector(
1071 BuilderEditCourse.selectors.elBtnCancelCategoryNew
1072 );
1073 if ( elBtnCancel ) elBtnCancel.click();
1074 }
1075 },
1076 error: ( error ) => {
1077 lpToastify.show( error.message || error, 'error' );
1078 },
1079 completed: () => {
1080 lpUtils.lpSetLoadingEl( btnSave, 0 );
1081 },
1082 };
1083
1084 window.lpAJAXG.fetchAJAX( dataSend, callBack );
1085 }
1086
1087 initSalePriceLayout() {
1088 const wrap = document.querySelector( BuilderEditCourse.selectors.elPriceCourseData );
1089 if ( ! wrap ) return;
1090
1091 const saleDatesFields = wrap.querySelectorAll( BuilderEditCourse.selectors.elSaleDatesFields );
1092 const scheduleBtn = wrap.querySelector( BuilderEditCourse.selectors.elSalePriceScheduleBtn );
1093 const cancelBtn = wrap.querySelector( BuilderEditCourse.selectors.elCancelSaleScheduleBtn );
1094
1095 let saleScheduleSet = false;
1096 const allInputs = wrap.querySelectorAll(
1097 `${ BuilderEditCourse.selectors.elSaleDatesFields } input`
1098 );
1099
1100 allInputs.forEach( ( input ) => {
1101 if ( input.value && input.value.trim() !== '' ) {
1102 saleScheduleSet = true;
1103 }
1104 } );
1105
1106 if ( saleScheduleSet ) {
1107 if ( scheduleBtn ) scheduleBtn.style.display = 'none';
1108 if ( cancelBtn ) cancelBtn.style.display = 'inline-block';
1109 saleDatesFields.forEach( ( field ) => ( field.style.display = 'block' ) );
1110 } else {
1111 if ( scheduleBtn ) scheduleBtn.style.display = 'inline-block';
1112 if ( cancelBtn ) cancelBtn.style.display = 'none';
1113 saleDatesFields.forEach( ( field ) => ( field.style.display = 'none' ) );
1114 }
1115 }
1116
1117 handleScheduleClick( args ) {
1118 const { e, target } = args;
1119 e.preventDefault();
1120 const btn = target.closest( BuilderEditCourse.selectors.elSalePriceScheduleBtn );
1121 const wrap = btn.closest( BuilderEditCourse.selectors.elPriceCourseData );
1122 if ( ! wrap ) return;
1123 const cancelBtn = wrap.querySelector( BuilderEditCourse.selectors.elCancelSaleScheduleBtn );
1124 const saleDatesFields = wrap.querySelectorAll( BuilderEditCourse.selectors.elSaleDatesFields );
1125 btn.style.display = 'none';
1126 if ( cancelBtn ) cancelBtn.style.display = 'inline-block';
1127 saleDatesFields.forEach( ( field ) => ( field.style.display = 'block' ) );
1128 }
1129
1130 handleCancelSchedule( args ) {
1131 const { e, target } = args;
1132 e.preventDefault();
1133 const btn = target.closest( BuilderEditCourse.selectors.elCancelSaleScheduleBtn );
1134 const wrap = btn.closest( BuilderEditCourse.selectors.elPriceCourseData );
1135 if ( ! wrap ) return;
1136 const scheduleBtn = wrap.querySelector( BuilderEditCourse.selectors.elSalePriceScheduleBtn );
1137 const saleDatesFields = wrap.querySelectorAll( BuilderEditCourse.selectors.elSaleDatesFields );
1138 const allInputs = wrap.querySelectorAll(
1139 `${ BuilderEditCourse.selectors.elSaleDatesFields } input`
1140 );
1141 btn.style.display = 'none';
1142 if ( scheduleBtn ) scheduleBtn.style.display = 'inline-block';
1143 saleDatesFields.forEach( ( field ) => ( field.style.display = 'none' ) );
1144 allInputs.forEach( ( input ) => ( input.value = '' ) );
1145 }
1146
1147 validateSalePrice( args ) {
1148 const { target } = args;
1149 const wrapper = target.closest( BuilderEditCourse.selectors.elPriceCourseData );
1150 if ( ! wrapper ) return;
1151 const regularPriceInput = wrapper.querySelector(
1152 BuilderEditCourse.selectors.elRegularPriceInput
1153 );
1154 const salePriceInput = wrapper.querySelector( BuilderEditCourse.selectors.elSalePriceInput );
1155 const existingTips = wrapper.querySelectorAll( BuilderEditCourse.selectors.elTipFloating );
1156 existingTips.forEach( ( tip ) => tip.remove() );
1157 if ( ! regularPriceInput || ! salePriceInput ) return;
1158 const regularVal = parseFloat( regularPriceInput.value ) || 0;
1159 const saleVal = parseFloat( salePriceInput.value ) || 0;
1160 if ( salePriceInput.value !== '' && saleVal > regularVal ) {
1161 const targetId = target.getAttribute( 'id' );
1162 const formField = target.closest( BuilderEditCourse.selectors.elFormField );
1163 const i18n =
1164 typeof lpAdminCourseEditorSettings !== 'undefined' && lpAdminCourseEditorSettings.i18n
1165 ? lpAdminCourseEditorSettings.i18n
1166 : {
1167 notice_price: 'Regular price must be greater than sale price.',
1168 notice_sale_price: 'Sale price must be less than regular price.',
1169 };
1170 const tip = document.createElement( 'div' );
1171 tip.className = 'learn-press-tip-floating';
1172 if ( targetId === BuilderEditCourse.selectors.elPriceInput ) {
1173 tip.innerHTML = i18n.notice_price;
1174 } else if ( targetId === BuilderEditCourse.selectors.elSalePriceInput ) {
1175 tip.innerHTML = i18n.notice_sale_price;
1176 }
1177 if ( formField && tip.innerHTML ) {
1178 formField.appendChild( tip );
1179 }
1180 }
1181 }
1182
1183 showHideOptionsDependency( args ) {
1184 const { target } = args;
1185 if ( target.tagName === 'INPUT' ) {
1186 if ( target.closest( '.forminp ' ) ) {
1187 const nameInput = target.name;
1188 const classDependency = nameInput.replace( 'learn_press_', '' );
1189 const elClassDependency = document.querySelectorAll( `.show_if_${ classDependency }` );
1190 if ( elClassDependency ) {
1191 elClassDependency.forEach( ( el ) => {
1192 el.classList.toggle( 'lp-option-disabled' );
1193 } );
1194 }
1195 } else if ( target.closest( '.lp-meta-box' ) ) {
1196 const elLPMetaBox = target.closest( '.lp-meta-box' );
1197 const nameInput = target.name;
1198 const elClassDependency = elLPMetaBox.querySelectorAll(
1199 `[data-dependency="${ nameInput }"]`
1200 );
1201 if ( elClassDependency ) {
1202 elClassDependency.forEach( ( el ) => {
1203 el.classList.toggle( 'lp-option-disabled' );
1204 } );
1205 }
1206 }
1207 }
1208 }
1209
1210 initTabs() {
1211 const tabLinks = document.querySelectorAll( BuilderEditCourse.selectors.elTabLinks );
1212 if ( tabLinks.length > 0 ) {
1213 this.activateTab( tabLinks[ 0 ] );
1214 }
1215 }
1216
1217 handleTabClick( args ) {
1218 const { e, target } = args;
1219 e.preventDefault();
1220 const linkElement = target.closest( 'a' );
1221 if ( linkElement ) {
1222 this.activateTab( linkElement );
1223 }
1224 }
1225
1226 activateTab( linkElement ) {
1227 const tabItems = document.querySelectorAll(
1228 BuilderEditCourse.selectors.elTabItems,
1229 );
1230 const panels = document.querySelectorAll(
1231 BuilderEditCourse.selectors.elTabPanels,
1232 );
1233 const targetId = linkElement.getAttribute( 'href' ).substring( 1 );
1234 const targetPanel = document.getElementById( targetId );
1235 if ( ! targetPanel ) return;
1236 tabItems.forEach( ( li ) => li.classList.remove( 'active' ) );
1237 panels.forEach( ( panel ) => ( panel.style.display = 'none' ) );
1238 linkElement.parentElement.classList.add( 'active' );
1239 targetPanel.style.display = 'block';
1240 }
1241
1242 /**
1243 * Handle horizontal tab click for client-side tab switching.
1244 * Uses lpShowHideEl with lp-hidden class.
1245 *
1246 * @param {Object} args - Event args containing e and target
1247 */
1248 handleCBHorizontalTabClick( args ) {
1249 const { e, target } = args;
1250 e.preventDefault();
1251
1252 const tab = target.closest( BuilderEditCourse.selectors.elCBHorizontalTabs );
1253 if ( ! tab ) return;
1254
1255 const sectionSlug = tab.dataset.tabSection;
1256 if ( ! sectionSlug ) return;
1257
1258 // Update active tab
1259 const allTabs = document.querySelectorAll( BuilderEditCourse.selectors.elCBHorizontalTabs );
1260 allTabs.forEach( ( t ) => t.classList.remove( 'is-active' ) );
1261 tab.classList.add( 'is-active' );
1262
1263 const url = new URL( window.location.href );
1264 url.searchParams.set( 'tab', sectionSlug );
1265 window.history.replaceState( {}, '', url );
1266
1267 // Show/hide panels using lpShowHideEl
1268 const allPanels = document.querySelectorAll( BuilderEditCourse.selectors.elCBTabPanels );
1269 allPanels.forEach( ( panel ) => {
1270 const isTarget = panel.dataset.section === sectionSlug;
1271 lpUtils.lpShowHideEl( panel, isTarget ? 1 : 0 );
1272 } );
1273 }
1274
1275 /**
1276 * Collect course data from all tabs for update.
1277 * Since all tabs are now rendered in DOM (client-side tab switching),
1278 * this method collects data from Overview tab (title, desc, categories, tags, thumbnail)
1279 * and Settings tab (form fields) when present.
1280 *
1281 * @return {Object} Course data object
1282 */
1283 getCourseDataForUpdate() {
1284 const data = {};
1285
1286 // Get course ID from wrapper (could be in any tab panel)
1287 const wrapperEl = document.querySelector( BuilderEditCourse.selectors.elDataCourse );
1288 data.course_id = wrapperEl ? parseInt( wrapperEl.dataset.courseId ) || 0 : 0;
1289
1290 // --- Overview Tab Data ---
1291 // Title
1292 const titleInput = document.querySelector( BuilderEditCourse.selectors.elTitleInput );
1293 data.course_title = titleInput ? titleInput.value : '';
1294
1295 // Description (TinyMCE or textarea)
1296 const descEditor = document.querySelector( BuilderEditCourse.selectors.elDescEditor );
1297 data.course_description = descEditor ? descEditor.value : '';
1298 if ( typeof tinymce !== 'undefined' ) {
1299 const editor = tinymce.get( 'course_description_editor' );
1300 if ( editor ) {
1301 data.course_description = editor.getContent();
1302 }
1303 }
1304
1305 // Categories
1306 data.course_categories = [];
1307 document
1308 .querySelectorAll( '#taxonomy-course_category input[name*="course_category"]:checked' )
1309 .forEach( ( checkbox ) => data.course_categories.push( checkbox.value ) );
1310
1311 // Tags
1312 data.course_tags = [];
1313 document
1314 .querySelectorAll( 'input[name="course_tags[]"]:checked' )
1315 .forEach( ( checkbox ) => data.course_tags.push( checkbox.value ) );
1316
1317 // Thumbnail
1318 const thumbnailInput = document.querySelector( BuilderEditCourse.selectors.elThumbnailInput );
1319 data.course_thumbnail_id = thumbnailInput ? thumbnailInput.value : '0';
1320
1321 // Permalink/Slug
1322 const permalinkInput = document.querySelector(
1323 BuilderEditCourse.selectors.elPermalinkSlugInput
1324 );
1325 if ( permalinkInput && permalinkInput.value ) {
1326 data.course_permalink = permalinkInput.value;
1327 }
1328
1329 // --- Settings Tab Data ---
1330 const elFormSetting = document.querySelector( BuilderEditCourse.selectors.elFormSetting );
1331 if ( elFormSetting ) {
1332 data.course_settings = true;
1333 const formElements = elFormSetting.querySelectorAll( 'input, select, textarea' );
1334 formElements.forEach( ( element ) => {
1335 const name = element.name || element.id;
1336 if ( ! name ) return;
1337 // Skip WP nonce and referer fields
1338 if ( name === 'learnpress_meta_box_nonce' || name === '_wp_http_referer' ) return;
1339
1340 const isArray = name.endsWith( '[]' );
1341 const fieldName = name.replace( '[]', '' );
1342
1343 if ( element.type === 'checkbox' ) {
1344 if ( isArray ) {
1345 if ( ! data[ fieldName ] ) data[ fieldName ] = [];
1346 if ( element.checked ) {
1347 data[ fieldName ].push( element.value );
1348 }
1349 } else {
1350 data[ fieldName ] = element.checked ? 'yes' : 'no';
1351 }
1352 } else if ( element.type === 'radio' ) {
1353 if ( element.checked ) {
1354 data[ fieldName ] = element.value;
1355 }
1356 } else if ( element.type === 'file' ) {
1357 if ( element.files && element.files.length > 0 ) {
1358 data[ fieldName ] = element.files;
1359 }
1360 } else {
1361 if ( isArray ) {
1362 if ( ! data.hasOwnProperty( fieldName ) ) {
1363 data[ fieldName ] = [];
1364 }
1365 if ( Array.isArray( data[ fieldName ] ) ) {
1366 data[ fieldName ].push( element.value );
1367 }
1368 } else {
1369 // Only set if not already set (first value wins)
1370 if ( ! data.hasOwnProperty( fieldName ) ) {
1371 data[ fieldName ] = element.value;
1372 }
1373 }
1374 }
1375 } );
1376 }
1377
1378 // Convert settings arrays to comma-separated strings for API
1379 // Exclude course_categories and course_tags - they're handled separately
1380 const excludeFromConversion = [ 'course_categories', 'course_tags' ];
1381 Object.keys( data ).forEach( ( key ) => {
1382 if ( Array.isArray( data[ key ] ) && ! excludeFromConversion.includes( key ) ) {
1383 data[ key ] = data[ key ].join( ',' );
1384 }
1385 } );
1386
1387 return data;
1388 }
1389
1390 updateCourse( args ) {
1391 // Context check: only handle if on course edit page
1392 if ( ! document.querySelector( BuilderEditCourse.selectors.elDataCourse ) ) {
1393 return;
1394 }
1395
1396 // Prevent double submit while request is running.
1397 if ( this.isSavingCourse ) {
1398 return;
1399 }
1400
1401 const { e, target } = args;
1402 if ( e ) {
1403 e.preventDefault();
1404 }
1405 // Find which button was clicked and determine status from data attribute
1406 const elBtnMainAction = target.closest( BuilderEditCourse.selectors.elBtnMainAction );
1407 const elBtnHeaderSave = target.closest( BuilderEditCourse.selectors.elBtnHeaderSave );
1408 const elBtnDropdownAction = target.closest( BuilderEditCourse.selectors.elBtnDropdownAction );
1409
1410 let targetStatus = 'publish';
1411 let elBtn = null;
1412
1413 // Determine status from the clicked button's data-status attribute or class
1414 if ( elBtnMainAction ) {
1415 targetStatus = elBtnMainAction.dataset.status || 'publish';
1416 elBtn = elBtnMainAction;
1417 } else if ( elBtnDropdownAction ) {
1418 targetStatus = elBtnDropdownAction.dataset.status || 'publish';
1419 elBtn = elBtnDropdownAction;
1420 } else if ( elBtnHeaderSave ) {
1421 // Header save button uses current main action status
1422 const mainBtn = document.querySelector( BuilderEditCourse.selectors.elBtnMainAction );
1423 targetStatus = mainBtn?.dataset.status || 'publish';
1424 elBtn = elBtnHeaderSave;
1425 }
1426
1427 if ( ! elBtn ) return;
1428
1429 // Course publish status is controlled by the Publish panel when available.
1430 if ( elBtnMainAction || elBtnHeaderSave ) {
1431 targetStatus = this.getStatusFromPublishPanel( targetStatus );
1432 }
1433
1434 const publishVisibilityEl = document.querySelector(
1435 BuilderEditCourse.selectors.elPublishVisibilitySelect
1436 );
1437 const publishPasswordEl = document.querySelector(
1438 BuilderEditCourse.selectors.elPublishPasswordInput
1439 );
1440 const publishVisibility = publishVisibilityEl?.value || '';
1441 const publishPassword = publishPasswordEl?.value?.trim?.() || '';
1442
1443 if ( publishVisibility === 'password' && ! publishPassword ) {
1444 lpToastify.show( 'Password is required when visibility is password protected.', 'error' );
1445 publishPasswordEl?.focus?.();
1446 return;
1447 }
1448
1449 // Check if drafting a published course
1450 if ( targetStatus === 'draft' ) {
1451 const statusEl = document.querySelector( BuilderEditCourse.selectors.elStatus );
1452 const isPublished = statusEl && statusEl.classList.contains( 'publish' );
1453 if ( isPublished ) {
1454 const confirmMsg =
1455 elBtn.dataset.confirmUnpublish ||
1456 'Saving as draft will unpublish this item. Are you sure?';
1457 if ( ! confirm( confirmMsg ) ) {
1458 return;
1459 }
1460 }
1461 }
1462
1463 const setActionLoadingState = ( isLoading ) => {
1464 const elsToToggle = [];
1465 const elHeaderSave = document.querySelector( BuilderEditCourse.selectors.elBtnHeaderSave );
1466 const elMainAction = document.querySelector( BuilderEditCourse.selectors.elBtnMainAction );
1467 const elDropdownToggle = document.querySelector(
1468 BuilderEditCourse.selectors.elDropdownToggle
1469 );
1470
1471 if ( elHeaderSave ) {
1472 elsToToggle.push( elHeaderSave );
1473 }
1474
1475 if ( elMainAction ) {
1476 elsToToggle.push( elMainAction );
1477 }
1478
1479 if ( elDropdownToggle ) {
1480 elsToToggle.push( elDropdownToggle );
1481 }
1482
1483 if ( elBtn ) {
1484 elsToToggle.push( elBtn );
1485 }
1486
1487 // Keep only unique elements.
1488 const uniqueEls = [ ...new Set( elsToToggle.filter( Boolean ) ) ];
1489
1490 uniqueEls.forEach( ( el ) => {
1491 lpUtils.lpSetLoadingEl( el, isLoading ? 1 : 0 );
1492 el.classList.toggle( 'lp-loading', Boolean( isLoading ) );
1493
1494 if ( isLoading ) {
1495 el.setAttribute( 'aria-disabled', 'true' );
1496 } else {
1497 el.removeAttribute( 'aria-disabled' );
1498 }
1499 } );
1500
1501 if ( isLoading && elDropdownToggle ) {
1502 elDropdownToggle.setAttribute( 'aria-expanded', 'false' );
1503 }
1504 };
1505
1506 this.isSavingCourse = true;
1507 setActionLoadingState(true);
1508 const courseData = this.getCourseDataForUpdate();
1509 const dataSend = {
1510 ...courseData,
1511 course_status: targetStatus,
1512 action: 'cb_save_course',
1513 args: { id_url: 'cb-save-course' },
1514 };
1515 if ( typeof lpCourseBuilder !== 'undefined' && lpCourseBuilder.nonce ) {
1516 dataSend.nonce = lpCourseBuilder.nonce;
1517 }
1518 // Handle course_categories - may be array or already a string
1519 if ( courseData.course_categories ) {
1520 dataSend.course_categories = Array.isArray( courseData.course_categories )
1521 ? courseData.course_categories.join( ',' )
1522 : courseData.course_categories;
1523 }
1524 // Handle course_tags - may be array or already a string
1525 if ( courseData.course_tags ) {
1526 dataSend.course_tags = Array.isArray( courseData.course_tags )
1527 ? courseData.course_tags.join( ',' )
1528 : courseData.course_tags;
1529 }
1530 if ( courseData.course_thumbnail_id ) {
1531 dataSend.course_thumbnail_id = courseData.course_thumbnail_id;
1532 }
1533 if ( publishVisibility ) {
1534 dataSend.course_visibility = publishVisibility;
1535 dataSend.course_password = publishVisibility === 'password' ? publishPassword : '';
1536 }
1537
1538 const publishDateInput = document.querySelector(
1539 BuilderEditCourse.selectors.elPublishDateInput
1540 );
1541 if ( publishDateInput && publishDateInput.value ) {
1542 dataSend.course_post_date = publishDateInput.value;
1543 }
1544
1545 const callBack = {
1546 success: ( response ) => {
1547 const { status, message, data } = response;
1548 lpToastify.show( message, status );
1549 if ( status === 'success' ) {
1550 this.updateHeaderTitle( courseData.course_title );
1551 // Dispatch event to reset form state (remove unsaved changes warning)
1552 document.dispatchEvent( new CustomEvent( 'lp-course-builder-saved' ) );
1553
1554 if ( data?.html ) {
1555 const elContent = elBtnMainAction.closest( BuilderEditCourse.selectors.elCBContent );
1556 const newHtml = new DOMParser().parseFromString( data.html, 'text/html' );
1557 if ( elContent ) {
1558 // elContent.outerHTML = data.html;
1559 // Temporary change each elements
1560 const elements = [
1561 ".lp-cb-header",
1562 ".cb-course-edit-column--left",
1563 ];
1564
1565 for ( const elementNew of elements ) {
1566 const el = elContent.querySelector( elementNew );
1567 if ( el ) {
1568 el.innerHTML = newHtml.querySelector( elementNew ).innerHTML;
1569 }
1570 }
1571 }
1572 }
1573 }
1574
1575 // Use redirect_url from backend if available (for new courses)
1576 if ( data?.redirect_url ) {
1577 window.location.href = data.redirect_url;
1578 }
1579 },
1580 error: ( error ) => {
1581 lpToastify.show( error.message || error, 'error' );
1582 },
1583 completed: () => {
1584 this.isSavingCourse = false;
1585 setActionLoadingState( false );
1586 },
1587 };
1588 window.lpAJAXG.fetchAJAX( dataSend, callBack );
1589 }
1590
1591 /**
1592 * Resolve immediate UI status after save to avoid stale action labels
1593 * in review-only course workflow.
1594 *
1595 * @param {string|undefined} savedStatus
1596 * @param {string} requestedStatus
1597 * @return {string}
1598 */
1599 resolveStatusForUIAfterSave( savedStatus, requestedStatus ) {
1600 const normalizedSavedStatus =
1601 typeof savedStatus === 'string' ? savedStatus.trim().toLowerCase() : '';
1602
1603 const normalizedRequestedStatus =
1604 typeof requestedStatus === 'string' ? requestedStatus.trim().toLowerCase() : '';
1605
1606 const dropdown = document.querySelector( BuilderEditCourse.selectors.elHeaderActionsDropdown );
1607 const reviewOnlyCourseAttr = ( dropdown?.dataset?.reviewOnlyCourse || '' ).toLowerCase();
1608 const isReviewOnlyCourse = [ 'yes', 'true', '1' ].includes( reviewOnlyCourseAttr );
1609
1610 if (
1611 isReviewOnlyCourse &&
1612 [ 'publish', 'private', 'future' ].includes( normalizedRequestedStatus )
1613 ) {
1614 return 'pending';
1615 }
1616
1617 return normalizedSavedStatus || normalizedRequestedStatus || 'draft';
1618 }
1619
1620 /**
1621 * Resolve target status from Publish panel controls.
1622 *
1623 * @param {string} fallbackStatus
1624 * @return {string}
1625 */
1626 getStatusFromPublishPanel( fallbackStatus = 'publish' ) {
1627 const statusSelect = document.querySelector(
1628 BuilderEditCourse.selectors.elPublishStatusSelect
1629 );
1630 const visibilitySelect = document.querySelector(
1631 BuilderEditCourse.selectors.elPublishVisibilitySelect
1632 );
1633
1634 let status = statusSelect?.value || fallbackStatus || 'publish';
1635 const visibility = visibilitySelect?.value || 'public';
1636
1637 if ( visibility === 'private' && status !== 'pending' ) {
1638 status = 'private';
1639 }
1640
1641 if ( [ 'public', 'password' ].includes( visibility ) && status === 'private' ) {
1642 status = 'publish';
1643 }
1644
1645 const allowedStatuses = [ 'publish', 'future', 'draft', 'pending', 'private' ];
1646 if ( ! allowedStatuses.includes( status ) ) {
1647 return 'publish';
1648 }
1649
1650 return status;
1651 }
1652
1653 /**
1654 * Sync Publish panel controls after save/trash.
1655 *
1656 * @param {string} status
1657 */
1658 syncPublishPanelStatus( status, visibility = '' ) {
1659 const statusSelect = document.querySelector(
1660 BuilderEditCourse.selectors.elPublishStatusSelect
1661 );
1662 const visibilitySelect = document.querySelector(
1663 BuilderEditCourse.selectors.elPublishVisibilitySelect
1664 );
1665
1666 if ( ! statusSelect && ! visibilitySelect ) {
1667 return;
1668 }
1669
1670 const normalized = ( status || '' ).toString().toLowerCase();
1671 const currentPrimaryStatus = ( statusSelect?.dataset?.primaryStatus || '' )
1672 .toString()
1673 .toLowerCase();
1674 let primaryStatusToKeep = [ 'future', 'publish' ].includes( currentPrimaryStatus )
1675 ? currentPrimaryStatus
1676 : 'publish';
1677
1678 if ( normalized === 'future' ) {
1679 primaryStatusToKeep = 'future';
1680 } else if ( [ 'publish', 'private' ].includes( normalized ) ) {
1681 primaryStatusToKeep = 'publish';
1682 }
1683
1684 if ( statusSelect ) {
1685 statusSelect.dataset.primaryStatus = primaryStatusToKeep;
1686 }
1687
1688 if ( statusSelect ) {
1689 if ( normalized === 'private' ) {
1690 statusSelect.value = 'publish';
1691 } else if ( [ 'publish', 'future', 'draft', 'pending' ].includes( normalized ) ) {
1692 statusSelect.value = normalized;
1693 }
1694 }
1695
1696 this.syncPublishStatusOptions( normalized );
1697
1698 if ( visibilitySelect ) {
1699 let visibilityToSet = visibility || visibilitySelect.value || 'public';
1700 if ( normalized === 'private' ) {
1701 visibilityToSet = 'private';
1702 } else if ( visibilityToSet === 'private' ) {
1703 visibilityToSet = 'public';
1704 }
1705 visibilitySelect.value = visibilityToSet;
1706 }
1707
1708 this.syncPublishVisibilityControls();
1709 this.syncPublishDateLabel();
1710 }
1711
1712 handlePublishVisibilityChange() {
1713 this.syncPublishVisibilityControls( true );
1714 }
1715
1716 handlePublishStatusChange() {
1717 this.syncPublishStatusOptions();
1718 this.syncPublishDateLabel();
1719 }
1720
1721 handlePublishDateChange() {
1722 this.syncPublishDateLabel();
1723 }
1724
1725 syncPublishVisibilityControls( focusPassword = false ) {
1726 const visibilitySelect = document.querySelector(
1727 BuilderEditCourse.selectors.elPublishVisibilitySelect
1728 );
1729 const passwordRow = document.querySelector( BuilderEditCourse.selectors.elPublishPasswordRow );
1730 const passwordInput = document.querySelector(
1731 BuilderEditCourse.selectors.elPublishPasswordInput
1732 );
1733
1734 if ( ! visibilitySelect || ! passwordRow ) {
1735 return;
1736 }
1737
1738 const isPassword = visibilitySelect.value === 'password';
1739 passwordRow.classList.toggle( 'lp-hidden', ! isPassword );
1740
1741 if ( focusPassword && isPassword && passwordInput ) {
1742 passwordInput.focus();
1743 }
1744 }
1745
1746 syncPublishDateLabel() {
1747 const dateInput = document.querySelector( BuilderEditCourse.selectors.elPublishDateInput );
1748 const dateLabel = document.querySelector( BuilderEditCourse.selectors.elPublishDateLabel );
1749
1750 if ( ! dateLabel ) {
1751 return;
1752 }
1753
1754 const dateValue = ( dateInput?.value || '' ).toString().trim();
1755 let isFutureDate = false;
1756
1757 if ( dateValue ) {
1758 const parsedDate = new Date( dateValue );
1759 if ( ! Number.isNaN( parsedDate.getTime() ) ) {
1760 isFutureDate = parsedDate.getTime() > Date.now();
1761 }
1762 }
1763
1764 dateLabel.textContent = isFutureDate ? 'Scheduled for' : 'Published on';
1765 }
1766
1767 syncPublishStatusOptions( preferredStatus = '' ) {
1768 const statusSelect = document.querySelector(
1769 BuilderEditCourse.selectors.elPublishStatusSelect
1770 );
1771 if ( ! statusSelect ) {
1772 return;
1773 }
1774
1775 const publishLabel = statusSelect.dataset.publishLabel || 'Published';
1776 const futureLabel = statusSelect.dataset.futureLabel || 'Scheduled';
1777 const selectedStatus = ( preferredStatus || statusSelect.value || '' ).toLowerCase();
1778 const currentPrimaryStatus = ( statusSelect.dataset.primaryStatus || '' ).toLowerCase();
1779 const draftLabel =
1780 statusSelect.querySelector( 'option[value="draft"]' )?.textContent || 'Draft';
1781 const pendingLabel =
1782 statusSelect.querySelector( 'option[value="pending"]' )?.textContent || 'Pending Review';
1783 const hasFutureOption = !! statusSelect.querySelector( 'option[value="future"]' );
1784 const hasPublishOption = !! statusSelect.querySelector( 'option[value="publish"]' );
1785
1786 let primaryValue = 'publish';
1787 if ( [ 'future', 'publish' ].includes( selectedStatus ) ) {
1788 primaryValue = selectedStatus;
1789 } else if ( [ 'future', 'publish' ].includes( currentPrimaryStatus ) ) {
1790 primaryValue = currentPrimaryStatus;
1791 } else if ( hasFutureOption && ! hasPublishOption ) {
1792 primaryValue = 'future';
1793 }
1794
1795 const primaryLabel = primaryValue === 'future' ? futureLabel : publishLabel;
1796 const valueToSelect = [ 'draft', 'pending', 'future', 'publish' ].includes( selectedStatus )
1797 ? selectedStatus
1798 : primaryValue;
1799
1800 statusSelect.dataset.primaryStatus = primaryValue;
1801
1802 statusSelect.innerHTML = '';
1803
1804 [
1805 { value: primaryValue, label: primaryLabel },
1806 { value: 'draft', label: draftLabel },
1807 { value: 'pending', label: pendingLabel },
1808 ].forEach( ( optionData ) => {
1809 const option = document.createElement( 'option' );
1810 option.value = optionData.value;
1811 option.textContent = optionData.label;
1812 statusSelect.appendChild( option );
1813 } );
1814
1815 statusSelect.value = valueToSelect;
1816 }
1817
1818 formatStatusLabel( status ) {
1819 const normalized = ( status || '' ).toString().toLowerCase();
1820 return normalized === 'future' ? 'scheduled' : normalized;
1821 }
1822
1823 /**
1824 * Update action buttons after status change.
1825 * Swaps main button and dropdown item based on new status.
1826 *
1827 * @param {string} newStatus - The new course status
1828 */
1829 updateActionButtons( newStatus ) {
1830 const dropdown = document.querySelector( BuilderEditCourse.selectors.elHeaderActionsDropdown );
1831 if ( ! dropdown ) return;
1832
1833 const mainBtn = dropdown.querySelector( '.cb-btn-main-action' );
1834 const dropdownMenu = dropdown.querySelector( BuilderEditCourse.selectors.elDropdownMenu );
1835 if ( ! mainBtn ) return;
1836
1837 const hasSingleCourseAction =
1838 dropdown.classList.contains( 'cb-header-actions-dropdown--single' ) || ! dropdownMenu;
1839
1840 if ( hasSingleCourseAction ) {
1841 mainBtn.className = 'cb-btn-update cb-btn-primary cb-btn-main-action';
1842 mainBtn.dataset.status = this.getStatusFromPublishPanel( 'publish' );
1843 mainBtn.textContent = mainBtn.dataset.titleUpdate || 'Update';
1844 dropdown.dataset.currentStatus = newStatus;
1845 return;
1846 }
1847
1848 const reviewOnlyCourseAttr = ( dropdown.dataset.reviewOnlyCourse || '' ).toLowerCase();
1849 const isReviewOnlyCourse = [ 'yes', 'true', '1' ].includes( reviewOnlyCourseAttr );
1850
1851 // Status configuration for button labels and classes
1852 const statusConfig = {
1853 publish: {
1854 mainLabel: mainBtn.dataset.titleUpdate || 'Update',
1855 mainClass: 'cb-btn-update',
1856 mainStatus: 'publish',
1857 dropdownLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1858 dropdownClass: 'cb-btn-darft',
1859 dropdownStatus: 'draft',
1860 dropdownIcon: 'dashicons-media-default',
1861 },
1862 draft: {
1863 mainLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1864 mainClass: 'cb-btn-darft',
1865 mainStatus: 'draft',
1866 dropdownLabel: mainBtn.dataset.titlePublish || 'Publish',
1867 dropdownClass: 'cb-btn-publish',
1868 dropdownStatus: 'publish',
1869 dropdownIcon: 'dashicons-visibility',
1870 },
1871 pending: {
1872 mainLabel: mainBtn.dataset.titlePublish || 'Publish',
1873 mainClass: 'cb-btn-publish',
1874 mainStatus: 'publish',
1875 dropdownLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1876 dropdownClass: 'cb-btn-darft',
1877 dropdownStatus: 'draft',
1878 dropdownIcon: 'dashicons-media-default',
1879 },
1880 trash: {
1881 mainLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1882 mainClass: 'cb-btn-darft',
1883 mainStatus: 'draft',
1884 dropdownLabel: mainBtn.dataset.titlePublish || 'Publish',
1885 dropdownClass: 'cb-btn-publish',
1886 dropdownStatus: 'publish',
1887 dropdownIcon: 'dashicons-visibility',
1888 },
1889 };
1890 const reviewStatusConfig = {
1891 publish: {
1892 mainLabel: mainBtn.dataset.titleUpdate || 'Update',
1893 mainClass: 'cb-btn-update',
1894 mainStatus: 'publish',
1895 dropdownLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1896 dropdownClass: 'cb-btn-darft',
1897 dropdownStatus: 'draft',
1898 dropdownIcon: 'dashicons-media-default',
1899 },
1900 draft: {
1901 mainLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1902 mainClass: 'cb-btn-darft',
1903 mainStatus: 'draft',
1904 dropdownLabel: mainBtn.dataset.titleSubmitReview || 'Submit for Review',
1905 dropdownClass: 'cb-btn-pending',
1906 dropdownStatus: 'pending',
1907 dropdownIcon: 'dashicons-clock',
1908 },
1909 pending: {
1910 mainLabel: mainBtn.dataset.titleSubmitReview || 'Submit for Review',
1911 mainClass: 'cb-btn-pending',
1912 mainStatus: 'pending',
1913 dropdownLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1914 dropdownClass: 'cb-btn-darft',
1915 dropdownStatus: 'draft',
1916 dropdownIcon: 'dashicons-media-default',
1917 },
1918 'auto-draft': {
1919 mainLabel: mainBtn.dataset.titleSubmitReview || 'Submit for Review',
1920 mainClass: 'cb-btn-pending',
1921 mainStatus: 'pending',
1922 dropdownLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1923 dropdownClass: 'cb-btn-darft',
1924 dropdownStatus: 'draft',
1925 dropdownIcon: 'dashicons-media-default',
1926 },
1927 trash: {
1928 mainLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1929 mainClass: 'cb-btn-darft',
1930 mainStatus: 'draft',
1931 dropdownLabel: mainBtn.dataset.titleSubmitReview || 'Submit for Review',
1932 dropdownClass: 'cb-btn-pending',
1933 dropdownStatus: 'pending',
1934 dropdownIcon: 'dashicons-clock',
1935 },
1936 private: {
1937 mainLabel: mainBtn.dataset.titleSubmitReview || 'Submit for Review',
1938 mainClass: 'cb-btn-pending',
1939 mainStatus: 'pending',
1940 dropdownLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1941 dropdownClass: 'cb-btn-darft',
1942 dropdownStatus: 'draft',
1943 dropdownIcon: 'dashicons-media-default',
1944 },
1945 };
1946
1947 const config = isReviewOnlyCourse
1948 ? reviewStatusConfig[ newStatus ] || reviewStatusConfig.draft
1949 : statusConfig[ newStatus ] || statusConfig.draft;
1950
1951 // Update main button
1952 mainBtn.className = `${ config.mainClass } cb-btn-primary cb-btn-main-action`;
1953 mainBtn.dataset.status = config.mainStatus;
1954 mainBtn.textContent = config.mainLabel;
1955
1956 // Update dropdown item (first item, excluding trash)
1957 const dropdownItems = dropdownMenu.querySelectorAll( '.cb-dropdown-item:not(.cb-btn-trash)' );
1958 if ( dropdownItems.length > 0 ) {
1959 const firstItem = dropdownItems[ 0 ];
1960 firstItem.className = `cb-dropdown-item ${ config.dropdownClass }`;
1961 firstItem.dataset.status = config.dropdownStatus;
1962 firstItem.innerHTML = `<span class="dashicons ${ config.dropdownIcon }"></span>${ config.dropdownLabel }`;
1963 }
1964
1965 // Keep dropdown color/state aligned with the main action button.
1966 dropdown.dataset.currentStatus = config.mainStatus;
1967 }
1968
1969 /**
1970 * Toggle visibility of preview button and admin link based on status.
1971 * Hide when status is 'trash', show otherwise.
1972 *
1973 * @param {string} status - Current course status
1974 */
1975 toggleTrashElements( status ) {
1976 const elBtnPreview = document.querySelector( BuilderEditCourse.selectors.elBtnPreview );
1977 const elAdminLink = document.querySelector( BuilderEditCourse.selectors.elAdminLink );
1978 const isTrash = status === 'trash';
1979
1980 if ( elBtnPreview ) {
1981 elBtnPreview.style.display = isTrash ? 'none' : '';
1982 }
1983 if ( elAdminLink ) {
1984 elAdminLink.style.display = isTrash ? 'none' : '';
1985 }
1986 }
1987
1988 async duplicateCourse( args ) {
1989 // Context check: only handle if on course edit page
1990 if ( ! document.querySelector( BuilderEditCourse.selectors.elDataCourse ) ) {
1991 return;
1992 }
1993
1994 const { target } = args;
1995 const elBtnDuplicateCourse = target.closest( BuilderEditCourse.selectors.elBtnDuplicateCourse );
1996
1997 if ( ! elBtnDuplicateCourse ) {
1998 return;
1999 }
2000
2001 const courseData = this.getCourseDataForUpdate();
2002 const courseId = parseInt( courseData?.course_id, 10 ) || 0;
2003
2004 if ( ! courseId ) {
2005 return;
2006 }
2007
2008 const result = await SweetAlert.fire( {
2009 title: elBtnDuplicateCourse.dataset.title || 'Are you sure?',
2010 text:
2011 elBtnDuplicateCourse.dataset.content || 'Are you sure you want to duplicate this course?',
2012 iconHtml: SWAL_ICON_DUPLICATE,
2013 customClass: { icon: 'lp-cb-swal-icon-html' },
2014 showCloseButton: true,
2015 showCancelButton: true,
2016 cancelButtonText: lpData.i18n.cancel,
2017 confirmButtonText: lpData.i18n.yes,
2018 reverseButtons: true,
2019 } );
2020
2021 if ( ! result.isConfirmed ) {
2022 return;
2023 }
2024
2025 lpUtils.lpSetLoadingEl( elBtnDuplicateCourse, 1 );
2026
2027 const dataSend = {
2028 action: 'duplicate_course',
2029 args: { id_url: 'duplicate-course' },
2030 course_id: courseId,
2031 };
2032
2033 const callBack = {
2034 success: ( response ) => {
2035 const { status, message, data } = response;
2036 lpToastify.show( message || 'Duplicated successfully!', status );
2037
2038 if ( status === 'success' && data?.redirect_url ) {
2039 window.location.href = data.redirect_url;
2040 }
2041 },
2042 error: ( error ) => {
2043 lpToastify.show( error.message || error, 'error' );
2044 },
2045 completed: () => {
2046 lpUtils.lpSetLoadingEl( elBtnDuplicateCourse, 0 );
2047 },
2048 };
2049
2050 window.lpAJAXG.fetchAJAX( dataSend, callBack );
2051 }
2052
2053 trashCourse( args ) {
2054 // Context check: only handle if on course edit page
2055 if ( ! document.querySelector( BuilderEditCourse.selectors.elDataCourse ) ) {
2056 return;
2057 }
2058
2059 const { target } = args;
2060 const elBtnTrashCourse = target.closest( BuilderEditCourse.selectors.elBtnTrashCourse );
2061 lpUtils.lpSetLoadingEl( elBtnTrashCourse, 1 );
2062 const courseData = this.getCourseDataForUpdate();
2063 const dataSend = {
2064 action: 'move_trash_course',
2065 args: { id_url: 'move-trash-course' },
2066 course_id: courseData.course_id,
2067 };
2068 const callBack = {
2069 success: ( response ) => {
2070 const { status, message, data } = response;
2071 lpToastify.show( message, status );
2072 if ( data?.button_title ) {
2073 const elBtnUpdate = document.querySelector(
2074 BuilderEditCourse.selectors.elBtnUpdateCourse
2075 );
2076 if ( elBtnUpdate ) elBtnUpdate.textContent = data.button_title;
2077 }
2078 if ( data?.status ) {
2079 const elStatus = document.querySelector( BuilderEditCourse.selectors.elStatus );
2080 if ( elStatus ) {
2081 elStatus.className = 'course-status ' + data.status;
2082 elStatus.textContent = this.formatStatusLabel( data.status );
2083 }
2084 this.syncPublishPanelStatus( data.status );
2085 // Toggle preview/admin link visibility for trash status
2086 this.toggleTrashElements( data.status );
2087 // Update action buttons to show "Save Draft" for trash status
2088 this.updateActionButtons( data.status );
2089 }
2090 },
2091 error: ( error ) => {
2092 lpToastify.show( error.message || error, 'error' );
2093 },
2094 completed: () => {
2095 lpUtils.lpSetLoadingEl( elBtnTrashCourse, 0 );
2096 },
2097 };
2098 window.lpAJAXG.fetchAJAX( dataSend, callBack );
2099 }
2100
2101 saveSettings( args ) {
2102 const { target } = args;
2103 const elBtnSaveSettings = target.closest( BuilderEditCourse.selectors.elBtnSaveSettings );
2104 lpUtils.lpSetLoadingEl( elBtnSaveSettings, 1 );
2105
2106 const courseData = this.getCourseDataForUpdate();
2107 const dataSend = {
2108 ...courseData,
2109 action: 'save_course_settings',
2110 args: { id_url: 'save-course-settings' },
2111 };
2112
2113 if ( typeof lpCourseBuilder !== 'undefined' && lpCourseBuilder.nonce ) {
2114 dataSend.nonce = lpCourseBuilder.nonce;
2115 }
2116
2117 const callBack = {
2118 success: ( response ) => {
2119 const { status, message } = response;
2120 lpToastify.show( message, status );
2121 },
2122 error: ( error ) => {
2123 lpToastify.show( error.message || error, 'error' );
2124 },
2125 completed: () => {
2126 lpUtils.lpSetLoadingEl( elBtnSaveSettings, 0 );
2127 },
2128 };
2129
2130 window.lpAJAXG.fetchAJAX( dataSend, callBack );
2131 }
2132
2133 toggleAddTagForm( args ) {
2134 const { target } = args;
2135 const elBtnAdd = document.querySelector( BuilderEditCourse.selectors.elBtnAddTagNew );
2136 const form = document.querySelector( BuilderEditCourse.selectors.elFormTagAddNew );
2137 const isOpening = target.closest( BuilderEditCourse.selectors.elBtnAddTagNew );
2138 if ( form ) {
2139 if ( isOpening ) {
2140 form.style.display = 'flex';
2141 if ( elBtnAdd ) elBtnAdd.style.display = 'none';
2142 const input = form.querySelector( BuilderEditCourse.selectors.elInputAddTag );
2143 if ( input ) setTimeout( () => input.focus(), 100 );
2144 } else {
2145 form.style.display = 'none';
2146 if ( elBtnAdd ) elBtnAdd.style.display = 'inline-flex';
2147 }
2148 }
2149 }
2150
2151 addNewTag( args ) {
2152 const { e } = args;
2153 const elInput = document.querySelector( BuilderEditCourse.selectors.elInputAddTag );
2154 const btnSave = document.querySelector( BuilderEditCourse.selectors.elBtnSaveTag );
2155 const tagName = elInput?.value?.trim() ?? '';
2156
2157 if ( ! tagName ) {
2158 lpToastify.show( 'Please enter tag name', 'error' );
2159 return;
2160 }
2161
2162 lpUtils.lpSetLoadingEl( btnSave, 1 );
2163
2164 const dataSend = {
2165 action: 'add_course_tag',
2166 args: { id_url: 'add-course-tag' },
2167 name: tagName,
2168 };
2169 const callBack = {
2170 success: ( response ) => {
2171 const { status, message, data } = response;
2172 lpToastify.show( message, status );
2173 if ( data?.html ) {
2174 const wrapper = document.querySelector(
2175 BuilderEditCourse.selectors.elWrapperCheckBoxTag
2176 );
2177 wrapper.insertAdjacentHTML( 'beforeend', data.html );
2178 this.syncTagManagement();
2179 elInput.value = '';
2180 const elBtnCancel = document.querySelector(
2181 BuilderEditCourse.selectors.elBtnCancelTagNew
2182 );
2183 if ( elBtnCancel ) elBtnCancel.click();
2184 }
2185 },
2186 error: ( error ) => {
2187 lpToastify.show( error.message || error, 'error' );
2188 },
2189 completed: () => {
2190 lpUtils.lpSetLoadingEl( btnSave, 0 );
2191 },
2192 };
2193 window.lpAJAXG.fetchAJAX( dataSend, callBack );
2194 }
2195
2196 openMediaUploader( args ) {
2197 if ( typeof wp === 'undefined' || typeof wp.media === 'undefined' ) return;
2198 const mediaUploader = wp.media( {
2199 title: 'Select Featured Image',
2200 button: { text: 'Use this image' },
2201 multiple: false,
2202 library: { type: 'image' },
2203 } );
2204 mediaUploader.on( 'select', () => {
2205 const attachment = mediaUploader.state().get( 'selection' ).first().toJSON();
2206 this.setFeaturedImage( attachment );
2207 } );
2208 mediaUploader.open();
2209 }
2210
2211 setFeaturedImage( attachment ) {
2212 const dropzone = document.querySelector( BuilderEditCourse.selectors.elFeaturedImageDropzone );
2213 const thumbnailInput = document.querySelector( BuilderEditCourse.selectors.elThumbnailInput );
2214 const actionsContainer = document.querySelector( '.cb-featured-image-actions' );
2215
2216 if ( ! dropzone || ! thumbnailInput ) return;
2217
2218 thumbnailInput.value = attachment.id;
2219
2220 // Mark form as having unsaved changes
2221 getFormState().markAsChanged();
2222 const imgUrl =
2223 attachment.sizes?.medium?.url || attachment.sizes?.thumbnail?.url || attachment.url;
2224
2225 // Clear dropzone content
2226 dropzone.innerHTML = '';
2227
2228 // Add image
2229 const img = document.createElement( 'img' );
2230 img.src = imgUrl;
2231 img.className = 'cb-featured-image-preview__img';
2232 img.alt = attachment.alt || '';
2233 dropzone.appendChild( img );
2234 dropzone.classList.add( 'has-image' );
2235
2236 // Show/create action buttons (order must match PHP: Remove icon first, Replace text second)
2237 if ( actionsContainer ) {
2238 actionsContainer.innerHTML = `
2239 <button type="button" class="cb-remove-featured-image"><svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_5385_4628)"><path d="M11.9 1.66699C12.2498 1.66708 12.5907 1.77723 12.8744 1.98183C13.1581 2.18643 13.3703 2.47512 13.4808 2.80699L13.9333 4.16699H16.6667C16.8877 4.16699 17.0996 4.25479 17.2559 4.41107C17.4122 4.56735 17.5 4.77931 17.5 5.00033C17.5 5.22134 17.4122 5.4333 17.2559 5.58958C17.0996 5.74586 16.8877 5.83366 16.6667 5.83366L16.6642 5.89283L15.9417 16.012C15.8966 16.6425 15.6143 17.2325 15.1517 17.6633C14.6891 18.0941 14.0805 18.3336 13.4483 18.3337H6.55167C5.91955 18.3336 5.31092 18.0941 4.84831 17.6633C4.38569 17.2325 4.10342 16.6425 4.05833 16.012L3.33583 5.89199C3.33433 5.87258 3.33349 5.85313 3.33333 5.83366C3.11232 5.83366 2.90036 5.74586 2.74408 5.58958C2.5878 5.4333 2.5 5.22134 2.5 5.00033C2.5 4.77931 2.5878 4.56735 2.74408 4.41107C2.90036 4.25479 3.11232 4.16699 3.33333 4.16699H6.06667L6.51917 2.80699C6.62975 2.47498 6.84203 2.1862 7.12592 1.98159C7.4098 1.77697 7.75089 1.66691 8.10083 1.66699H11.9ZM14.9975 5.83366H5.0025L5.72083 15.8928C5.73579 16.103 5.82981 16.2997 5.98397 16.4433C6.13812 16.587 6.34096 16.6669 6.55167 16.667H13.4483C13.659 16.6669 13.8619 16.587 14.016 16.4433C14.1702 16.2997 14.2642 16.103 14.2792 15.8928L14.9975 5.83366ZM8.33333 8.33366C8.53744 8.33369 8.73445 8.40862 8.88698 8.54425C9.03951 8.67989 9.13695 8.86678 9.16083 9.06949L9.16667 9.16699V13.3337C9.16643 13.5461 9.0851 13.7504 8.93929 13.9048C8.79349 14.0592 8.59421 14.1522 8.38217 14.1646C8.17014 14.1771 7.96135 14.1081 7.79847 13.9718C7.6356 13.8354 7.53092 13.6421 7.50583 13.4312L7.5 13.3337V9.16699C7.5 8.94598 7.5878 8.73402 7.74408 8.57774C7.90036 8.42146 8.11232 8.33366 8.33333 8.33366ZM11.6667 8.33366C11.8877 8.33366 12.0996 8.42146 12.2559 8.57774C12.4122 8.73402 12.5 8.94598 12.5 9.16699V13.3337C12.5 13.5547 12.4122 13.7666 12.2559 13.9229C12.0996 14.0792 11.8877 14.167 11.6667 14.167C11.4457 14.167 11.2337 14.0792 11.0774 13.9229C10.9211 13.7666 10.8333 13.5547 10.8333 13.3337V9.16699C10.8333 8.94598 10.9211 8.73402 11.0774 8.57774C11.2337 8.42146 11.4457 8.33366 11.6667 8.33366ZM11.9 3.33366H8.1L7.8225 4.16699H12.1775L11.9 3.33366Z" fill="currentColor"/></g><defs><clipPath id="clip0_5385_4628"><rect width="20" height="20" fill="white"/></clipPath></defs></svg></button>
2240 <button type="button" class="cb-change-featured-image">${
2241 window.lpCourseBuilder?.i18n?.replace_image || 'Replace'
2242 }</button>
2243 `;
2244 }
2245 }
2246
2247 removeFeaturedImage( args ) {
2248 const { e } = args;
2249 if ( e ) e.preventDefault();
2250
2251 const dropzone = document.querySelector( BuilderEditCourse.selectors.elFeaturedImageDropzone );
2252 const thumbnailInput = document.querySelector( BuilderEditCourse.selectors.elThumbnailInput );
2253 const actionsContainer = document.querySelector( '.cb-featured-image-actions' );
2254
2255 if ( ! dropzone ) return;
2256
2257 // Clear dropzone and show upload content
2258 dropzone.innerHTML = `
2259 <div class="cb-featured-image-upload-content">
2260 <span class="cb-featured-image-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3.29095 14.5488C3.29099 14.8637 3.33747 15.1785 3.44915 15.5527L3.48138 15.6592C3.95542 17.0735 5.26094 18.0234 6.72845 18.0234H20.0361L19.2099 20.6787C18.9761 21.5815 18.1423 22.1942 17.2294 22.1943C17.0513 22.1942 16.8735 22.1714 16.7011 22.126L2.5263 18.2881C1.43276 17.9832 0.78131 16.8383 1.06732 15.7344L3.29095 8.22949V14.5488ZM20.7079 1.80469C21.9711 1.80474 22.9998 2.84505 22.9999 4.12207V14.3164C22.9999 15.5936 21.9712 16.6337 20.7079 16.6338H6.95794C5.69489 16.6338 4.66595 15.5936 4.66595 14.3164V4.12207C4.66604 2.84502 5.69495 1.80469 6.95794 1.80469H20.7079ZM6.95794 3.65918C6.70507 3.65918 6.49903 3.86625 6.49896 4.12207V12.8701L9.02923 10.3135C9.65534 9.67964 10.6757 9.67964 11.3027 10.3135L12.412 11.4316L15.8163 7.2998C16.1206 6.93103 16.5663 6.71857 17.041 6.71582C17.5185 6.72681 17.9633 6.92104 18.2704 7.28516L21.166 10.7012V4.12207C21.1659 3.8663 20.961 3.65923 20.7079 3.65918H6.95794ZM9.24896 4.58496C10.2601 4.58496 11.0829 5.4172 11.0829 6.43945C11.0827 7.4615 10.26 8.29297 9.24896 8.29297C8.23818 8.29274 7.4162 7.46132 7.41595 6.43945C7.41595 5.41738 8.23798 4.58519 9.24896 4.58496Z" fill="#CFCFCF"/></svg></span>
2261 <p class="cb-featured-image-text"><a href="#" class="cb-featured-image-link">${
2262 window.lpCourseBuilder?.i18n?.click_to_upload || 'Click to upload'
2263 }</a></p>
2264 <p class="cb-featured-image-hint">${
2265 window.lpCourseBuilder?.i18n?.image_hint || 'JPG, JPEG, PNG less than 1MB'
2266 }</p>
2267 </div>
2268 `;
2269 dropzone.classList.remove( 'has-image' );
2270
2271 // Clear thumbnail ID
2272 if ( thumbnailInput ) {
2273 thumbnailInput.value = '';
2274
2275 // Mark form as having unsaved changes
2276 getFormState().markAsChanged();
2277 }
2278
2279 // Hide action buttons
2280 if ( actionsContainer ) {
2281 actionsContainer.innerHTML = '';
2282 }
2283 }
2284
2285 buildPermalinkDisplayUrl( baseUrl = '', slug = '', fallbackUrl = '' ) {
2286 const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl : '';
2287 const normalizedSlug = typeof slug === 'string' ? slug.trim() : '';
2288
2289 if ( normalizedBaseUrl && normalizedSlug ) {
2290 return `${ normalizedBaseUrl }${ normalizedSlug }`;
2291 }
2292
2293 return typeof fallbackUrl === 'string' ? fallbackUrl : '';
2294 }
2295
2296 /**
2297 * Handle permalink Edit button click.
2298 * Shows editor mode, hides display mode.
2299 */
2300 handlePermalinkEdit( args ) {
2301 const { e } = args;
2302 if ( e ) e.preventDefault();
2303
2304 const display = document.querySelector( BuilderEditCourse.selectors.elPermalinkDisplay );
2305 const editor = document.querySelector( BuilderEditCourse.selectors.elPermalinkEditor );
2306 const input = document.querySelector( BuilderEditCourse.selectors.elPermalinkSlugInput );
2307
2308 if ( ! display || ! editor || ! input ) return;
2309
2310 // Store original value for cancel
2311 input.dataset.originalValue = input.value;
2312
2313 // Toggle visibility
2314 display.classList.add( 'lp-hidden' );
2315 editor.classList.remove( 'lp-hidden' );
2316
2317 // Focus input and select text
2318 input.focus();
2319 input.select();
2320 }
2321
2322 /**
2323 * Handle permalink OK button click.
2324 * Validates and sanitizes slug, updates display.
2325 */
2326 handlePermalinkOk( args ) {
2327 const { e } = args;
2328 if ( e ) e.preventDefault();
2329
2330 const display = document.querySelector( BuilderEditCourse.selectors.elPermalinkDisplay );
2331 const editor = document.querySelector( BuilderEditCourse.selectors.elPermalinkEditor );
2332 const input = document.querySelector( BuilderEditCourse.selectors.elPermalinkSlugInput );
2333 const urlLink = document.querySelector( BuilderEditCourse.selectors.elPermalinkUrl );
2334 const baseUrlInput = document.querySelector( BuilderEditCourse.selectors.elPermalinkBaseUrl );
2335
2336 if ( ! display || ! editor || ! input || ! urlLink ) return;
2337
2338 // Sanitize the slug
2339 let newSlug = input.value.trim();
2340
2341 // If empty after sanitizing, restore original
2342 if ( ! newSlug ) {
2343 newSlug = input.dataset.originalValue || 'course';
2344 }
2345
2346 // Update input value with sanitized slug
2347 input.value = newSlug;
2348
2349 // Get base URL
2350 const baseUrl = baseUrlInput ? baseUrlInput.value : '';
2351 const newUrl = this.buildPermalinkDisplayUrl( baseUrl, newSlug, urlLink.textContent || '' );
2352
2353 // Keep href as the current saved link and only update display text.
2354 urlLink.textContent = newUrl;
2355
2356 // Toggle visibility back to display mode
2357 editor.classList.add( 'lp-hidden' );
2358 display.classList.remove( 'lp-hidden' );
2359
2360 // Mark form as changed if slug differs from original
2361 if ( newSlug !== input.dataset.originalValue ) {
2362 getFormState().markAsChanged();
2363 }
2364 }
2365
2366 /**
2367 * Handle permalink Cancel button click.
2368 * Restores original value and returns to display mode.
2369 */
2370 handlePermalinkCancel( args ) {
2371 const { e } = args;
2372 if ( e ) e.preventDefault();
2373
2374 const display = document.querySelector( BuilderEditCourse.selectors.elPermalinkDisplay );
2375 const editor = document.querySelector( BuilderEditCourse.selectors.elPermalinkEditor );
2376 const input = document.querySelector( BuilderEditCourse.selectors.elPermalinkSlugInput );
2377
2378 if ( ! display || ! editor || ! input ) return;
2379
2380 // Restore original value
2381 input.value = input.dataset.originalValue || '';
2382
2383 // Toggle visibility back to display mode
2384 editor.classList.add( 'lp-hidden' );
2385 display.classList.remove( 'lp-hidden' );
2386 }
2387
2388 initTabTitles() {
2389 const tabLinks = document.querySelectorAll( BuilderEditCourse.selectors.elTabLinks );
2390 tabLinks.forEach( ( link ) => {
2391 const textSpan = link.querySelector( 'span' );
2392 const title = textSpan ? textSpan.textContent.trim() : link.textContent.trim();
2393 const href = link.getAttribute( 'href' );
2394 if ( ! href ) return;
2395 const targetId = href.substring( 1 );
2396 const panel = document.getElementById( targetId );
2397 if ( panel ) {
2398 panel.setAttribute( 'data-tab-title', title );
2399 }
2400 } );
2401 }
2402
2403 initTitleCharCount() {
2404 const titleInput = document.querySelector( BuilderEditCourse.selectors.elTitleInput );
2405 if ( titleInput ) {
2406 this.updateTitleCharCount( titleInput.value );
2407 }
2408 }
2409
2410 updateTitleCharCount( text ) {
2411 const charCountEl = document.querySelector( BuilderEditCourse.selectors.elTitleCharCount );
2412 if ( ! charCountEl ) return;
2413
2414 const charCount = text.length;
2415 const charText = charCount === 1 ? 'character' : 'characters';
2416 charCountEl.textContent = `${ charCount } ${ charText }`;
2417 }
2418
2419 handleTitleInput( args ) {
2420 const { target } = args;
2421 this.updateTitleCharCount( target.value );
2422 }
2423
2424 initDescWordCount() {
2425 // Wait for TinyMCE to be ready
2426 if ( typeof tinymce !== 'undefined' ) {
2427 tinymce.on( 'AddEditor', ( e ) => {
2428 if ( e.editor.id === 'course_description_editor' ) {
2429 e.editor.on( 'init', () => {
2430 this.updateDescWordCount( e.editor );
2431
2432 // Listen for content changes
2433 e.editor.on( 'keyup change input NodeChange', () => {
2434 this.updateDescWordCount( e.editor );
2435 } );
2436 } );
2437 }
2438 } );
2439
2440 // If editor already exists
2441 const existingEditor = tinymce.get( 'course_description_editor' );
2442 if ( existingEditor ) {
2443 this.updateDescWordCount( existingEditor );
2444 existingEditor.on( 'keyup change input NodeChange', () => {
2445 this.updateDescWordCount( existingEditor );
2446 } );
2447 }
2448 }
2449 }
2450
2451 updateDescWordCount( editor ) {
2452 const wordCountEl = document.querySelector( BuilderEditCourse.selectors.elDescWordCount );
2453 if ( ! wordCountEl ) return;
2454
2455 // Use TinyMCE's built-in word count plugin
2456 const wordcount = editor.plugins.wordcount;
2457 let count = 0;
2458
2459 if ( wordcount && typeof wordcount.body !== 'undefined' ) {
2460 count = wordcount.body.getWordCount();
2461 } else if ( wordcount && typeof wordcount.getCount !== 'undefined' ) {
2462 count = wordcount.getCount();
2463 } else {
2464 // Fallback: manual count
2465 const content = editor.getContent( { format: 'text' } );
2466 count = this.countWords( content );
2467 }
2468
2469 const wordText = count === 1 ? 'word' : 'words';
2470 wordCountEl.textContent = `${ count } ${ wordText }`;
2471 }
2472
2473 updateHeaderTitle( title ) {
2474 const headerTitle = document.querySelector( '.lp-cb-header__title' );
2475 if ( headerTitle && title ) {
2476 headerTitle.textContent = title;
2477 }
2478 }
2479
2480 countWords( text ) {
2481 const trimmedText = text.replace( /<[^>]*>/g, '' ).trim();
2482 if ( trimmedText.length === 0 ) return 0;
2483 const words = trimmedText.split( /\s+/ ).filter( ( word ) => word.length > 0 );
2484 return words.length;
2485 }
2486
2487 /**
2488 * Initialize Header Actions Dropdown
2489 * Handles toggle open/close for dropdown menu in header actions
2490 */
2491 initHeaderActionsDropdown() {
2492 const dropdownWrapper = document.querySelector(
2493 BuilderEditCourse.selectors.elHeaderActionsDropdown
2494 );
2495 if ( ! dropdownWrapper ) return;
2496
2497 const toggleBtn = dropdownWrapper.querySelector( BuilderEditCourse.selectors.elDropdownToggle );
2498 const dropdownMenu = dropdownWrapper.querySelector(
2499 BuilderEditCourse.selectors.elDropdownMenu
2500 );
2501
2502 if ( ! toggleBtn || ! dropdownMenu ) return;
2503
2504 // Toggle dropdown on button click
2505 toggleBtn.addEventListener( 'click', ( e ) => {
2506 e.stopPropagation();
2507 const isOpen = dropdownMenu.classList.contains( 'is-open' );
2508
2509 if ( isOpen ) {
2510 this.closeHeaderDropdown( toggleBtn, dropdownMenu );
2511 } else {
2512 this.openHeaderDropdown( toggleBtn, dropdownMenu );
2513 }
2514 } );
2515
2516 // Close dropdown when clicking outside
2517 document.addEventListener( 'click', ( e ) => {
2518 if ( ! dropdownWrapper.contains( e.target ) ) {
2519 this.closeHeaderDropdown( toggleBtn, dropdownMenu );
2520 }
2521 } );
2522
2523 // Close dropdown on Escape key
2524 document.addEventListener( 'keydown', ( e ) => {
2525 if ( e.key === 'Escape' ) {
2526 this.closeHeaderDropdown( toggleBtn, dropdownMenu );
2527 }
2528 } );
2529
2530 // Close dropdown after clicking an item (except when it triggers an action that keeps page)
2531 const dropdownItems = dropdownMenu.querySelectorAll( '.cb-dropdown-item' );
2532 dropdownItems.forEach( ( item ) => {
2533 item.addEventListener( 'click', () => {
2534 // Small delay to allow action to process before closing
2535 setTimeout( () => {
2536 this.closeHeaderDropdown( toggleBtn, dropdownMenu );
2537 }, 100 );
2538 } );
2539 } );
2540 }
2541
2542 openHeaderDropdown( toggleBtn, dropdownMenu ) {
2543 dropdownMenu.classList.add( 'is-open' );
2544 toggleBtn.setAttribute( 'aria-expanded', 'true' );
2545 }
2546
2547 closeHeaderDropdown( toggleBtn, dropdownMenu ) {
2548 dropdownMenu.classList.remove( 'is-open' );
2549 toggleBtn.setAttribute( 'aria-expanded', 'false' );
2550 }
2551 }
2552