PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.2
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.2
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.4.2, at assets/src/js/frontend/course-builder/builder-course/builder-edit-course.js

2,572 lines 83.5 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 === 'select-multiple' ) {
1357 // Collect every selected option, not just the first one (element.value).
1358 const selectedValues = Array.from( element.selectedOptions )
1359 .map( ( option ) => option.value )
1360 .filter( ( value ) => value !== '' );
1361 if ( ! Array.isArray( data[ fieldName ] ) ) {
1362 data[ fieldName ] = [];
1363 }
1364 data[ fieldName ].push( ...selectedValues );
1365 } else if ( element.type === 'file' ) {
1366 if ( element.files && element.files.length > 0 ) {
1367 data[ fieldName ] = element.files;
1368 }
1369 } else {
1370 if ( isArray ) {
1371 if ( ! data.hasOwnProperty( fieldName ) ) {
1372 data[ fieldName ] = [];
1373 }
1374 if ( Array.isArray( data[ fieldName ] ) ) {
1375 data[ fieldName ].push( element.value );
1376 }
1377 } else {
1378 // Only set if not already set (first value wins)
1379 if ( ! data.hasOwnProperty( fieldName ) ) {
1380 data[ fieldName ] = element.value;
1381 }
1382 }
1383 }
1384 } );
1385 }
1386
1387 // Convert settings arrays to comma-separated strings for API
1388 // Exclude course_categories and course_tags - they're handled separately.
1389 // Exclude the extra-info fields too: their values are free text that may
1390 // contain commas, so they must stay as arrays (one entry per input) and
1391 // not be comma-joined, or the server splits a single value into several.
1392 const excludeFromConversion = [
1393 'course_categories',
1394 'course_tags',
1395 '_lp_requirements',
1396 '_lp_target_audiences',
1397 '_lp_key_features',
1398 '_lp_faqs_question',
1399 '_lp_faqs_answer',
1400 ];
1401 Object.keys( data ).forEach( ( key ) => {
1402 if ( Array.isArray( data[ key ] ) && ! excludeFromConversion.includes( key ) ) {
1403 data[ key ] = data[ key ].join( ',' );
1404 }
1405 } );
1406
1407 return data;
1408 }
1409
1410 updateCourse( args ) {
1411 // Context check: only handle if on course edit page
1412 if ( ! document.querySelector( BuilderEditCourse.selectors.elDataCourse ) ) {
1413 return;
1414 }
1415
1416 // Prevent double submit while request is running.
1417 if ( this.isSavingCourse ) {
1418 return;
1419 }
1420
1421 const { e, target } = args;
1422 if ( e ) {
1423 e.preventDefault();
1424 }
1425 // Find which button was clicked and determine status from data attribute
1426 const elBtnMainAction = target.closest( BuilderEditCourse.selectors.elBtnMainAction );
1427 const elBtnHeaderSave = target.closest( BuilderEditCourse.selectors.elBtnHeaderSave );
1428 const elBtnDropdownAction = target.closest( BuilderEditCourse.selectors.elBtnDropdownAction );
1429
1430 let targetStatus = 'publish';
1431 let elBtn = null;
1432
1433 // Determine status from the clicked button's data-status attribute or class
1434 if ( elBtnMainAction ) {
1435 targetStatus = elBtnMainAction.dataset.status || 'publish';
1436 elBtn = elBtnMainAction;
1437 } else if ( elBtnDropdownAction ) {
1438 targetStatus = elBtnDropdownAction.dataset.status || 'publish';
1439 elBtn = elBtnDropdownAction;
1440 } else if ( elBtnHeaderSave ) {
1441 // Header save button uses current main action status
1442 const mainBtn = document.querySelector( BuilderEditCourse.selectors.elBtnMainAction );
1443 targetStatus = mainBtn?.dataset.status || 'publish';
1444 elBtn = elBtnHeaderSave;
1445 }
1446
1447 if ( ! elBtn ) return;
1448
1449 // Course publish status is controlled by the Publish panel when available.
1450 if ( elBtnMainAction || elBtnHeaderSave ) {
1451 targetStatus = this.getStatusFromPublishPanel( targetStatus );
1452 }
1453
1454 const publishVisibilityEl = document.querySelector(
1455 BuilderEditCourse.selectors.elPublishVisibilitySelect
1456 );
1457 const publishPasswordEl = document.querySelector(
1458 BuilderEditCourse.selectors.elPublishPasswordInput
1459 );
1460 const publishVisibility = publishVisibilityEl?.value || '';
1461 const publishPassword = publishPasswordEl?.value?.trim?.() || '';
1462
1463 if ( publishVisibility === 'password' && ! publishPassword ) {
1464 lpToastify.show( 'Password is required when visibility is password protected.', 'error' );
1465 publishPasswordEl?.focus?.();
1466 return;
1467 }
1468
1469 // Check if drafting a published course
1470 if ( targetStatus === 'draft' ) {
1471 const statusEl = document.querySelector( BuilderEditCourse.selectors.elStatus );
1472 const isPublished = statusEl && statusEl.classList.contains( 'publish' );
1473 if ( isPublished ) {
1474 const confirmMsg =
1475 elBtn.dataset.confirmUnpublish ||
1476 'Saving as draft will unpublish this item. Are you sure?';
1477 if ( ! confirm( confirmMsg ) ) {
1478 return;
1479 }
1480 }
1481 }
1482
1483 const setActionLoadingState = ( isLoading ) => {
1484 const elsToToggle = [];
1485 const elHeaderSave = document.querySelector( BuilderEditCourse.selectors.elBtnHeaderSave );
1486 const elMainAction = document.querySelector( BuilderEditCourse.selectors.elBtnMainAction );
1487 const elDropdownToggle = document.querySelector(
1488 BuilderEditCourse.selectors.elDropdownToggle
1489 );
1490
1491 if ( elHeaderSave ) {
1492 elsToToggle.push( elHeaderSave );
1493 }
1494
1495 if ( elMainAction ) {
1496 elsToToggle.push( elMainAction );
1497 }
1498
1499 if ( elDropdownToggle ) {
1500 elsToToggle.push( elDropdownToggle );
1501 }
1502
1503 if ( elBtn ) {
1504 elsToToggle.push( elBtn );
1505 }
1506
1507 // Keep only unique elements.
1508 const uniqueEls = [ ...new Set( elsToToggle.filter( Boolean ) ) ];
1509
1510 uniqueEls.forEach( ( el ) => {
1511 lpUtils.lpSetLoadingEl( el, isLoading ? 1 : 0 );
1512 el.classList.toggle( 'lp-loading', Boolean( isLoading ) );
1513
1514 if ( isLoading ) {
1515 el.setAttribute( 'aria-disabled', 'true' );
1516 } else {
1517 el.removeAttribute( 'aria-disabled' );
1518 }
1519 } );
1520
1521 if ( isLoading && elDropdownToggle ) {
1522 elDropdownToggle.setAttribute( 'aria-expanded', 'false' );
1523 }
1524 };
1525
1526 this.isSavingCourse = true;
1527 setActionLoadingState(true);
1528 const courseData = this.getCourseDataForUpdate();
1529 const dataSend = {
1530 ...courseData,
1531 course_status: targetStatus,
1532 action: 'cb_save_course',
1533 args: { id_url: 'cb-save-course' },
1534 };
1535 if ( typeof lpCourseBuilder !== 'undefined' && lpCourseBuilder.nonce ) {
1536 dataSend.nonce = lpCourseBuilder.nonce;
1537 }
1538 // Handle course_categories - may be array or already a string
1539 if ( courseData.course_categories ) {
1540 dataSend.course_categories = Array.isArray( courseData.course_categories )
1541 ? courseData.course_categories.join( ',' )
1542 : courseData.course_categories;
1543 }
1544 // Handle course_tags - may be array or already a string
1545 if ( courseData.course_tags ) {
1546 dataSend.course_tags = Array.isArray( courseData.course_tags )
1547 ? courseData.course_tags.join( ',' )
1548 : courseData.course_tags;
1549 }
1550 if ( courseData.course_thumbnail_id ) {
1551 dataSend.course_thumbnail_id = courseData.course_thumbnail_id;
1552 }
1553 if ( publishVisibility ) {
1554 dataSend.course_visibility = publishVisibility;
1555 dataSend.course_password = publishVisibility === 'password' ? publishPassword : '';
1556 }
1557
1558 const publishDateInput = document.querySelector(
1559 BuilderEditCourse.selectors.elPublishDateInput
1560 );
1561 if ( publishDateInput && publishDateInput.value ) {
1562 dataSend.course_post_date = publishDateInput.value;
1563 }
1564
1565 const callBack = {
1566 success: ( response ) => {
1567 const { status, message, data } = response;
1568 lpToastify.show( message, status );
1569 if ( status === 'success' ) {
1570 this.updateHeaderTitle( courseData.course_title );
1571 // Dispatch event to reset form state (remove unsaved changes warning)
1572 document.dispatchEvent( new CustomEvent( 'lp-course-builder-saved' ) );
1573
1574 if ( data?.html ) {
1575 const elContent = elBtnMainAction.closest( BuilderEditCourse.selectors.elCBContent );
1576 const newHtml = new DOMParser().parseFromString( data.html, 'text/html' );
1577 if ( elContent ) {
1578 // elContent.outerHTML = data.html;
1579 // Temporary change each elements
1580 const elements = [
1581 ".lp-cb-header",
1582 ".cb-course-edit-column--left",
1583 ];
1584
1585 for ( const elementNew of elements ) {
1586 const el = elContent.querySelector( elementNew );
1587 if ( el ) {
1588 el.innerHTML = newHtml.querySelector( elementNew ).innerHTML;
1589 }
1590 }
1591 }
1592 }
1593 }
1594
1595 // Use redirect_url from backend if available (for new courses)
1596 if ( data?.redirect_url ) {
1597 window.location.href = data.redirect_url;
1598 }
1599 },
1600 error: ( error ) => {
1601 lpToastify.show( error.message || error, 'error' );
1602 },
1603 completed: () => {
1604 this.isSavingCourse = false;
1605 setActionLoadingState( false );
1606 },
1607 };
1608 window.lpAJAXG.fetchAJAX( dataSend, callBack );
1609 }
1610
1611 /**
1612 * Resolve immediate UI status after save to avoid stale action labels
1613 * in review-only course workflow.
1614 *
1615 * @param {string|undefined} savedStatus
1616 * @param {string} requestedStatus
1617 * @return {string}
1618 */
1619 resolveStatusForUIAfterSave( savedStatus, requestedStatus ) {
1620 const normalizedSavedStatus =
1621 typeof savedStatus === 'string' ? savedStatus.trim().toLowerCase() : '';
1622
1623 const normalizedRequestedStatus =
1624 typeof requestedStatus === 'string' ? requestedStatus.trim().toLowerCase() : '';
1625
1626 const dropdown = document.querySelector( BuilderEditCourse.selectors.elHeaderActionsDropdown );
1627 const reviewOnlyCourseAttr = ( dropdown?.dataset?.reviewOnlyCourse || '' ).toLowerCase();
1628 const isReviewOnlyCourse = [ 'yes', 'true', '1' ].includes( reviewOnlyCourseAttr );
1629
1630 if (
1631 isReviewOnlyCourse &&
1632 [ 'publish', 'private', 'future' ].includes( normalizedRequestedStatus )
1633 ) {
1634 return 'pending';
1635 }
1636
1637 return normalizedSavedStatus || normalizedRequestedStatus || 'draft';
1638 }
1639
1640 /**
1641 * Resolve target status from Publish panel controls.
1642 *
1643 * @param {string} fallbackStatus
1644 * @return {string}
1645 */
1646 getStatusFromPublishPanel( fallbackStatus = 'publish' ) {
1647 const statusSelect = document.querySelector(
1648 BuilderEditCourse.selectors.elPublishStatusSelect
1649 );
1650 const visibilitySelect = document.querySelector(
1651 BuilderEditCourse.selectors.elPublishVisibilitySelect
1652 );
1653
1654 let status = statusSelect?.value || fallbackStatus || 'publish';
1655 const visibility = visibilitySelect?.value || 'public';
1656
1657 if ( visibility === 'private' && status !== 'pending' ) {
1658 status = 'private';
1659 }
1660
1661 if ( [ 'public', 'password' ].includes( visibility ) && status === 'private' ) {
1662 status = 'publish';
1663 }
1664
1665 const allowedStatuses = [ 'publish', 'future', 'draft', 'pending', 'private' ];
1666 if ( ! allowedStatuses.includes( status ) ) {
1667 return 'publish';
1668 }
1669
1670 return status;
1671 }
1672
1673 /**
1674 * Sync Publish panel controls after save/trash.
1675 *
1676 * @param {string} status
1677 */
1678 syncPublishPanelStatus( status, visibility = '' ) {
1679 const statusSelect = document.querySelector(
1680 BuilderEditCourse.selectors.elPublishStatusSelect
1681 );
1682 const visibilitySelect = document.querySelector(
1683 BuilderEditCourse.selectors.elPublishVisibilitySelect
1684 );
1685
1686 if ( ! statusSelect && ! visibilitySelect ) {
1687 return;
1688 }
1689
1690 const normalized = ( status || '' ).toString().toLowerCase();
1691 const currentPrimaryStatus = ( statusSelect?.dataset?.primaryStatus || '' )
1692 .toString()
1693 .toLowerCase();
1694 let primaryStatusToKeep = [ 'future', 'publish' ].includes( currentPrimaryStatus )
1695 ? currentPrimaryStatus
1696 : 'publish';
1697
1698 if ( normalized === 'future' ) {
1699 primaryStatusToKeep = 'future';
1700 } else if ( [ 'publish', 'private' ].includes( normalized ) ) {
1701 primaryStatusToKeep = 'publish';
1702 }
1703
1704 if ( statusSelect ) {
1705 statusSelect.dataset.primaryStatus = primaryStatusToKeep;
1706 }
1707
1708 if ( statusSelect ) {
1709 if ( normalized === 'private' ) {
1710 statusSelect.value = 'publish';
1711 } else if ( [ 'publish', 'future', 'draft', 'pending' ].includes( normalized ) ) {
1712 statusSelect.value = normalized;
1713 }
1714 }
1715
1716 this.syncPublishStatusOptions( normalized );
1717
1718 if ( visibilitySelect ) {
1719 let visibilityToSet = visibility || visibilitySelect.value || 'public';
1720 if ( normalized === 'private' ) {
1721 visibilityToSet = 'private';
1722 } else if ( visibilityToSet === 'private' ) {
1723 visibilityToSet = 'public';
1724 }
1725 visibilitySelect.value = visibilityToSet;
1726 }
1727
1728 this.syncPublishVisibilityControls();
1729 this.syncPublishDateLabel();
1730 }
1731
1732 handlePublishVisibilityChange() {
1733 this.syncPublishVisibilityControls( true );
1734 }
1735
1736 handlePublishStatusChange() {
1737 this.syncPublishStatusOptions();
1738 this.syncPublishDateLabel();
1739 }
1740
1741 handlePublishDateChange() {
1742 this.syncPublishDateLabel();
1743 }
1744
1745 syncPublishVisibilityControls( focusPassword = false ) {
1746 const visibilitySelect = document.querySelector(
1747 BuilderEditCourse.selectors.elPublishVisibilitySelect
1748 );
1749 const passwordRow = document.querySelector( BuilderEditCourse.selectors.elPublishPasswordRow );
1750 const passwordInput = document.querySelector(
1751 BuilderEditCourse.selectors.elPublishPasswordInput
1752 );
1753
1754 if ( ! visibilitySelect || ! passwordRow ) {
1755 return;
1756 }
1757
1758 const isPassword = visibilitySelect.value === 'password';
1759 passwordRow.classList.toggle( 'lp-hidden', ! isPassword );
1760
1761 if ( focusPassword && isPassword && passwordInput ) {
1762 passwordInput.focus();
1763 }
1764 }
1765
1766 syncPublishDateLabel() {
1767 const dateInput = document.querySelector( BuilderEditCourse.selectors.elPublishDateInput );
1768 const dateLabel = document.querySelector( BuilderEditCourse.selectors.elPublishDateLabel );
1769
1770 if ( ! dateLabel ) {
1771 return;
1772 }
1773
1774 const dateValue = ( dateInput?.value || '' ).toString().trim();
1775 let isFutureDate = false;
1776
1777 if ( dateValue ) {
1778 const parsedDate = new Date( dateValue );
1779 if ( ! Number.isNaN( parsedDate.getTime() ) ) {
1780 isFutureDate = parsedDate.getTime() > Date.now();
1781 }
1782 }
1783
1784 dateLabel.textContent = isFutureDate ? 'Scheduled for' : 'Published on';
1785 }
1786
1787 syncPublishStatusOptions( preferredStatus = '' ) {
1788 const statusSelect = document.querySelector(
1789 BuilderEditCourse.selectors.elPublishStatusSelect
1790 );
1791 if ( ! statusSelect ) {
1792 return;
1793 }
1794
1795 const publishLabel = statusSelect.dataset.publishLabel || 'Published';
1796 const futureLabel = statusSelect.dataset.futureLabel || 'Scheduled';
1797 const selectedStatus = ( preferredStatus || statusSelect.value || '' ).toLowerCase();
1798 const currentPrimaryStatus = ( statusSelect.dataset.primaryStatus || '' ).toLowerCase();
1799 const draftLabel =
1800 statusSelect.querySelector( 'option[value="draft"]' )?.textContent || 'Draft';
1801 const pendingLabel =
1802 statusSelect.querySelector( 'option[value="pending"]' )?.textContent || 'Pending Review';
1803 const hasFutureOption = !! statusSelect.querySelector( 'option[value="future"]' );
1804 const hasPublishOption = !! statusSelect.querySelector( 'option[value="publish"]' );
1805
1806 let primaryValue = 'publish';
1807 if ( [ 'future', 'publish' ].includes( selectedStatus ) ) {
1808 primaryValue = selectedStatus;
1809 } else if ( [ 'future', 'publish' ].includes( currentPrimaryStatus ) ) {
1810 primaryValue = currentPrimaryStatus;
1811 } else if ( hasFutureOption && ! hasPublishOption ) {
1812 primaryValue = 'future';
1813 }
1814
1815 const primaryLabel = primaryValue === 'future' ? futureLabel : publishLabel;
1816 const valueToSelect = [ 'draft', 'pending', 'future', 'publish' ].includes( selectedStatus )
1817 ? selectedStatus
1818 : primaryValue;
1819
1820 statusSelect.dataset.primaryStatus = primaryValue;
1821
1822 statusSelect.innerHTML = '';
1823
1824 [
1825 { value: primaryValue, label: primaryLabel },
1826 { value: 'draft', label: draftLabel },
1827 { value: 'pending', label: pendingLabel },
1828 ].forEach( ( optionData ) => {
1829 const option = document.createElement( 'option' );
1830 option.value = optionData.value;
1831 option.textContent = optionData.label;
1832 statusSelect.appendChild( option );
1833 } );
1834
1835 statusSelect.value = valueToSelect;
1836 }
1837
1838 formatStatusLabel( status ) {
1839 const normalized = ( status || '' ).toString().toLowerCase();
1840 return normalized === 'future' ? 'scheduled' : normalized;
1841 }
1842
1843 /**
1844 * Update action buttons after status change.
1845 * Swaps main button and dropdown item based on new status.
1846 *
1847 * @param {string} newStatus - The new course status
1848 */
1849 updateActionButtons( newStatus ) {
1850 const dropdown = document.querySelector( BuilderEditCourse.selectors.elHeaderActionsDropdown );
1851 if ( ! dropdown ) return;
1852
1853 const mainBtn = dropdown.querySelector( '.cb-btn-main-action' );
1854 const dropdownMenu = dropdown.querySelector( BuilderEditCourse.selectors.elDropdownMenu );
1855 if ( ! mainBtn ) return;
1856
1857 const hasSingleCourseAction =
1858 dropdown.classList.contains( 'cb-header-actions-dropdown--single' ) || ! dropdownMenu;
1859
1860 if ( hasSingleCourseAction ) {
1861 mainBtn.className = 'cb-btn-update cb-btn-primary cb-btn-main-action';
1862 mainBtn.dataset.status = this.getStatusFromPublishPanel( 'publish' );
1863 mainBtn.textContent = mainBtn.dataset.titleUpdate || 'Update';
1864 dropdown.dataset.currentStatus = newStatus;
1865 return;
1866 }
1867
1868 const reviewOnlyCourseAttr = ( dropdown.dataset.reviewOnlyCourse || '' ).toLowerCase();
1869 const isReviewOnlyCourse = [ 'yes', 'true', '1' ].includes( reviewOnlyCourseAttr );
1870
1871 // Status configuration for button labels and classes
1872 const statusConfig = {
1873 publish: {
1874 mainLabel: mainBtn.dataset.titleUpdate || 'Update',
1875 mainClass: 'cb-btn-update',
1876 mainStatus: 'publish',
1877 dropdownLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1878 dropdownClass: 'cb-btn-darft',
1879 dropdownStatus: 'draft',
1880 dropdownIcon: 'dashicons-media-default',
1881 },
1882 draft: {
1883 mainLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1884 mainClass: 'cb-btn-darft',
1885 mainStatus: 'draft',
1886 dropdownLabel: mainBtn.dataset.titlePublish || 'Publish',
1887 dropdownClass: 'cb-btn-publish',
1888 dropdownStatus: 'publish',
1889 dropdownIcon: 'dashicons-visibility',
1890 },
1891 pending: {
1892 mainLabel: mainBtn.dataset.titlePublish || 'Publish',
1893 mainClass: 'cb-btn-publish',
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 trash: {
1901 mainLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1902 mainClass: 'cb-btn-darft',
1903 mainStatus: 'draft',
1904 dropdownLabel: mainBtn.dataset.titlePublish || 'Publish',
1905 dropdownClass: 'cb-btn-publish',
1906 dropdownStatus: 'publish',
1907 dropdownIcon: 'dashicons-visibility',
1908 },
1909 };
1910 const reviewStatusConfig = {
1911 publish: {
1912 mainLabel: mainBtn.dataset.titleUpdate || 'Update',
1913 mainClass: 'cb-btn-update',
1914 mainStatus: 'publish',
1915 dropdownLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1916 dropdownClass: 'cb-btn-darft',
1917 dropdownStatus: 'draft',
1918 dropdownIcon: 'dashicons-media-default',
1919 },
1920 draft: {
1921 mainLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1922 mainClass: 'cb-btn-darft',
1923 mainStatus: 'draft',
1924 dropdownLabel: mainBtn.dataset.titleSubmitReview || 'Submit for Review',
1925 dropdownClass: 'cb-btn-pending',
1926 dropdownStatus: 'pending',
1927 dropdownIcon: 'dashicons-clock',
1928 },
1929 pending: {
1930 mainLabel: mainBtn.dataset.titleSubmitReview || 'Submit for Review',
1931 mainClass: 'cb-btn-pending',
1932 mainStatus: 'pending',
1933 dropdownLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1934 dropdownClass: 'cb-btn-darft',
1935 dropdownStatus: 'draft',
1936 dropdownIcon: 'dashicons-media-default',
1937 },
1938 'auto-draft': {
1939 mainLabel: mainBtn.dataset.titleSubmitReview || 'Submit for Review',
1940 mainClass: 'cb-btn-pending',
1941 mainStatus: 'pending',
1942 dropdownLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1943 dropdownClass: 'cb-btn-darft',
1944 dropdownStatus: 'draft',
1945 dropdownIcon: 'dashicons-media-default',
1946 },
1947 trash: {
1948 mainLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1949 mainClass: 'cb-btn-darft',
1950 mainStatus: 'draft',
1951 dropdownLabel: mainBtn.dataset.titleSubmitReview || 'Submit for Review',
1952 dropdownClass: 'cb-btn-pending',
1953 dropdownStatus: 'pending',
1954 dropdownIcon: 'dashicons-clock',
1955 },
1956 private: {
1957 mainLabel: mainBtn.dataset.titleSubmitReview || 'Submit for Review',
1958 mainClass: 'cb-btn-pending',
1959 mainStatus: 'pending',
1960 dropdownLabel: mainBtn.dataset.titleDraft || 'Save Draft',
1961 dropdownClass: 'cb-btn-darft',
1962 dropdownStatus: 'draft',
1963 dropdownIcon: 'dashicons-media-default',
1964 },
1965 };
1966
1967 const config = isReviewOnlyCourse
1968 ? reviewStatusConfig[ newStatus ] || reviewStatusConfig.draft
1969 : statusConfig[ newStatus ] || statusConfig.draft;
1970
1971 // Update main button
1972 mainBtn.className = `${ config.mainClass } cb-btn-primary cb-btn-main-action`;
1973 mainBtn.dataset.status = config.mainStatus;
1974 mainBtn.textContent = config.mainLabel;
1975
1976 // Update dropdown item (first item, excluding trash)
1977 const dropdownItems = dropdownMenu.querySelectorAll( '.cb-dropdown-item:not(.cb-btn-trash)' );
1978 if ( dropdownItems.length > 0 ) {
1979 const firstItem = dropdownItems[ 0 ];
1980 firstItem.className = `cb-dropdown-item ${ config.dropdownClass }`;
1981 firstItem.dataset.status = config.dropdownStatus;
1982 firstItem.innerHTML = `<span class="dashicons ${ config.dropdownIcon }"></span>${ config.dropdownLabel }`;
1983 }
1984
1985 // Keep dropdown color/state aligned with the main action button.
1986 dropdown.dataset.currentStatus = config.mainStatus;
1987 }
1988
1989 /**
1990 * Toggle visibility of preview button and admin link based on status.
1991 * Hide when status is 'trash', show otherwise.
1992 *
1993 * @param {string} status - Current course status
1994 */
1995 toggleTrashElements( status ) {
1996 const elBtnPreview = document.querySelector( BuilderEditCourse.selectors.elBtnPreview );
1997 const elAdminLink = document.querySelector( BuilderEditCourse.selectors.elAdminLink );
1998 const isTrash = status === 'trash';
1999
2000 if ( elBtnPreview ) {
2001 elBtnPreview.style.display = isTrash ? 'none' : '';
2002 }
2003 if ( elAdminLink ) {
2004 elAdminLink.style.display = isTrash ? 'none' : '';
2005 }
2006 }
2007
2008 async duplicateCourse( args ) {
2009 // Context check: only handle if on course edit page
2010 if ( ! document.querySelector( BuilderEditCourse.selectors.elDataCourse ) ) {
2011 return;
2012 }
2013
2014 const { target } = args;
2015 const elBtnDuplicateCourse = target.closest( BuilderEditCourse.selectors.elBtnDuplicateCourse );
2016
2017 if ( ! elBtnDuplicateCourse ) {
2018 return;
2019 }
2020
2021 const courseData = this.getCourseDataForUpdate();
2022 const courseId = parseInt( courseData?.course_id, 10 ) || 0;
2023
2024 if ( ! courseId ) {
2025 return;
2026 }
2027
2028 const result = await SweetAlert.fire( {
2029 title: elBtnDuplicateCourse.dataset.title || 'Are you sure?',
2030 text:
2031 elBtnDuplicateCourse.dataset.content || 'Are you sure you want to duplicate this course?',
2032 iconHtml: SWAL_ICON_DUPLICATE,
2033 customClass: { icon: 'lp-cb-swal-icon-html' },
2034 showCloseButton: true,
2035 showCancelButton: true,
2036 cancelButtonText: lpData.i18n.cancel,
2037 confirmButtonText: lpData.i18n.yes,
2038 reverseButtons: true,
2039 } );
2040
2041 if ( ! result.isConfirmed ) {
2042 return;
2043 }
2044
2045 lpUtils.lpSetLoadingEl( elBtnDuplicateCourse, 1 );
2046
2047 const dataSend = {
2048 action: 'duplicate_course',
2049 args: { id_url: 'duplicate-course' },
2050 course_id: courseId,
2051 };
2052
2053 const callBack = {
2054 success: ( response ) => {
2055 const { status, message, data } = response;
2056 lpToastify.show( message || 'Duplicated successfully!', status );
2057
2058 if ( status === 'success' && data?.redirect_url ) {
2059 window.location.href = data.redirect_url;
2060 }
2061 },
2062 error: ( error ) => {
2063 lpToastify.show( error.message || error, 'error' );
2064 },
2065 completed: () => {
2066 lpUtils.lpSetLoadingEl( elBtnDuplicateCourse, 0 );
2067 },
2068 };
2069
2070 window.lpAJAXG.fetchAJAX( dataSend, callBack );
2071 }
2072
2073 trashCourse( args ) {
2074 // Context check: only handle if on course edit page
2075 if ( ! document.querySelector( BuilderEditCourse.selectors.elDataCourse ) ) {
2076 return;
2077 }
2078
2079 const { target } = args;
2080 const elBtnTrashCourse = target.closest( BuilderEditCourse.selectors.elBtnTrashCourse );
2081 lpUtils.lpSetLoadingEl( elBtnTrashCourse, 1 );
2082 const courseData = this.getCourseDataForUpdate();
2083 const dataSend = {
2084 action: 'move_trash_course',
2085 args: { id_url: 'move-trash-course' },
2086 course_id: courseData.course_id,
2087 };
2088 const callBack = {
2089 success: ( response ) => {
2090 const { status, message, data } = response;
2091 lpToastify.show( message, status );
2092 if ( data?.button_title ) {
2093 const elBtnUpdate = document.querySelector(
2094 BuilderEditCourse.selectors.elBtnUpdateCourse
2095 );
2096 if ( elBtnUpdate ) elBtnUpdate.textContent = data.button_title;
2097 }
2098 if ( data?.status ) {
2099 const elStatus = document.querySelector( BuilderEditCourse.selectors.elStatus );
2100 if ( elStatus ) {
2101 elStatus.className = 'course-status ' + data.status;
2102 elStatus.textContent = this.formatStatusLabel( data.status );
2103 }
2104 this.syncPublishPanelStatus( data.status );
2105 // Toggle preview/admin link visibility for trash status
2106 this.toggleTrashElements( data.status );
2107 // Update action buttons to show "Save Draft" for trash status
2108 this.updateActionButtons( data.status );
2109 }
2110 },
2111 error: ( error ) => {
2112 lpToastify.show( error.message || error, 'error' );
2113 },
2114 completed: () => {
2115 lpUtils.lpSetLoadingEl( elBtnTrashCourse, 0 );
2116 },
2117 };
2118 window.lpAJAXG.fetchAJAX( dataSend, callBack );
2119 }
2120
2121 saveSettings( args ) {
2122 const { target } = args;
2123 const elBtnSaveSettings = target.closest( BuilderEditCourse.selectors.elBtnSaveSettings );
2124 lpUtils.lpSetLoadingEl( elBtnSaveSettings, 1 );
2125
2126 const courseData = this.getCourseDataForUpdate();
2127 const dataSend = {
2128 ...courseData,
2129 action: 'save_course_settings',
2130 args: { id_url: 'save-course-settings' },
2131 };
2132
2133 if ( typeof lpCourseBuilder !== 'undefined' && lpCourseBuilder.nonce ) {
2134 dataSend.nonce = lpCourseBuilder.nonce;
2135 }
2136
2137 const callBack = {
2138 success: ( response ) => {
2139 const { status, message } = response;
2140 lpToastify.show( message, status );
2141 },
2142 error: ( error ) => {
2143 lpToastify.show( error.message || error, 'error' );
2144 },
2145 completed: () => {
2146 lpUtils.lpSetLoadingEl( elBtnSaveSettings, 0 );
2147 },
2148 };
2149
2150 window.lpAJAXG.fetchAJAX( dataSend, callBack );
2151 }
2152
2153 toggleAddTagForm( args ) {
2154 const { target } = args;
2155 const elBtnAdd = document.querySelector( BuilderEditCourse.selectors.elBtnAddTagNew );
2156 const form = document.querySelector( BuilderEditCourse.selectors.elFormTagAddNew );
2157 const isOpening = target.closest( BuilderEditCourse.selectors.elBtnAddTagNew );
2158 if ( form ) {
2159 if ( isOpening ) {
2160 form.style.display = 'flex';
2161 if ( elBtnAdd ) elBtnAdd.style.display = 'none';
2162 const input = form.querySelector( BuilderEditCourse.selectors.elInputAddTag );
2163 if ( input ) setTimeout( () => input.focus(), 100 );
2164 } else {
2165 form.style.display = 'none';
2166 if ( elBtnAdd ) elBtnAdd.style.display = 'inline-flex';
2167 }
2168 }
2169 }
2170
2171 addNewTag( args ) {
2172 const { e } = args;
2173 const elInput = document.querySelector( BuilderEditCourse.selectors.elInputAddTag );
2174 const btnSave = document.querySelector( BuilderEditCourse.selectors.elBtnSaveTag );
2175 const tagName = elInput?.value?.trim() ?? '';
2176
2177 if ( ! tagName ) {
2178 lpToastify.show( 'Please enter tag name', 'error' );
2179 return;
2180 }
2181
2182 lpUtils.lpSetLoadingEl( btnSave, 1 );
2183
2184 const dataSend = {
2185 action: 'add_course_tag',
2186 args: { id_url: 'add-course-tag' },
2187 name: tagName,
2188 };
2189 const callBack = {
2190 success: ( response ) => {
2191 const { status, message, data } = response;
2192 lpToastify.show( message, status );
2193 if ( data?.html ) {
2194 const wrapper = document.querySelector(
2195 BuilderEditCourse.selectors.elWrapperCheckBoxTag
2196 );
2197 wrapper.insertAdjacentHTML( 'beforeend', data.html );
2198 this.syncTagManagement();
2199 elInput.value = '';
2200 const elBtnCancel = document.querySelector(
2201 BuilderEditCourse.selectors.elBtnCancelTagNew
2202 );
2203 if ( elBtnCancel ) elBtnCancel.click();
2204 }
2205 },
2206 error: ( error ) => {
2207 lpToastify.show( error.message || error, 'error' );
2208 },
2209 completed: () => {
2210 lpUtils.lpSetLoadingEl( btnSave, 0 );
2211 },
2212 };
2213 window.lpAJAXG.fetchAJAX( dataSend, callBack );
2214 }
2215
2216 openMediaUploader( args ) {
2217 if ( typeof wp === 'undefined' || typeof wp.media === 'undefined' ) return;
2218 const mediaUploader = wp.media( {
2219 title: 'Select Featured Image',
2220 button: { text: 'Use this image' },
2221 multiple: false,
2222 library: { type: 'image' },
2223 } );
2224 mediaUploader.on( 'select', () => {
2225 const attachment = mediaUploader.state().get( 'selection' ).first().toJSON();
2226 this.setFeaturedImage( attachment );
2227 } );
2228 mediaUploader.open();
2229 }
2230
2231 setFeaturedImage( attachment ) {
2232 const dropzone = document.querySelector( BuilderEditCourse.selectors.elFeaturedImageDropzone );
2233 const thumbnailInput = document.querySelector( BuilderEditCourse.selectors.elThumbnailInput );
2234 const actionsContainer = document.querySelector( '.cb-featured-image-actions' );
2235
2236 if ( ! dropzone || ! thumbnailInput ) return;
2237
2238 thumbnailInput.value = attachment.id;
2239
2240 // Mark form as having unsaved changes
2241 getFormState().markAsChanged();
2242 const imgUrl =
2243 attachment.sizes?.medium?.url || attachment.sizes?.thumbnail?.url || attachment.url;
2244
2245 // Clear dropzone content
2246 dropzone.innerHTML = '';
2247
2248 // Add image
2249 const img = document.createElement( 'img' );
2250 img.src = imgUrl;
2251 img.className = 'cb-featured-image-preview__img';
2252 img.alt = attachment.alt || '';
2253 dropzone.appendChild( img );
2254 dropzone.classList.add( 'has-image' );
2255
2256 // Show/create action buttons (order must match PHP: Remove icon first, Replace text second)
2257 if ( actionsContainer ) {
2258 actionsContainer.innerHTML = `
2259 <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>
2260 <button type="button" class="cb-change-featured-image">${
2261 window.lpCourseBuilder?.i18n?.replace_image || 'Replace'
2262 }</button>
2263 `;
2264 }
2265 }
2266
2267 removeFeaturedImage( args ) {
2268 const { e } = args;
2269 if ( e ) e.preventDefault();
2270
2271 const dropzone = document.querySelector( BuilderEditCourse.selectors.elFeaturedImageDropzone );
2272 const thumbnailInput = document.querySelector( BuilderEditCourse.selectors.elThumbnailInput );
2273 const actionsContainer = document.querySelector( '.cb-featured-image-actions' );
2274
2275 if ( ! dropzone ) return;
2276
2277 // Clear dropzone and show upload content
2278 dropzone.innerHTML = `
2279 <div class="cb-featured-image-upload-content">
2280 <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>
2281 <p class="cb-featured-image-text"><a href="#" class="cb-featured-image-link">${
2282 window.lpCourseBuilder?.i18n?.click_to_upload || 'Click to upload'
2283 }</a></p>
2284 <p class="cb-featured-image-hint">${
2285 window.lpCourseBuilder?.i18n?.image_hint || 'JPG, JPEG, PNG less than 1MB'
2286 }</p>
2287 </div>
2288 `;
2289 dropzone.classList.remove( 'has-image' );
2290
2291 // Clear thumbnail ID
2292 if ( thumbnailInput ) {
2293 thumbnailInput.value = '';
2294
2295 // Mark form as having unsaved changes
2296 getFormState().markAsChanged();
2297 }
2298
2299 // Hide action buttons
2300 if ( actionsContainer ) {
2301 actionsContainer.innerHTML = '';
2302 }
2303 }
2304
2305 buildPermalinkDisplayUrl( baseUrl = '', slug = '', fallbackUrl = '' ) {
2306 const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl : '';
2307 const normalizedSlug = typeof slug === 'string' ? slug.trim() : '';
2308
2309 if ( normalizedBaseUrl && normalizedSlug ) {
2310 return `${ normalizedBaseUrl }${ normalizedSlug }`;
2311 }
2312
2313 return typeof fallbackUrl === 'string' ? fallbackUrl : '';
2314 }
2315
2316 /**
2317 * Handle permalink Edit button click.
2318 * Shows editor mode, hides display mode.
2319 */
2320 handlePermalinkEdit( args ) {
2321 const { e } = args;
2322 if ( e ) e.preventDefault();
2323
2324 const display = document.querySelector( BuilderEditCourse.selectors.elPermalinkDisplay );
2325 const editor = document.querySelector( BuilderEditCourse.selectors.elPermalinkEditor );
2326 const input = document.querySelector( BuilderEditCourse.selectors.elPermalinkSlugInput );
2327
2328 if ( ! display || ! editor || ! input ) return;
2329
2330 // Store original value for cancel
2331 input.dataset.originalValue = input.value;
2332
2333 // Toggle visibility
2334 display.classList.add( 'lp-hidden' );
2335 editor.classList.remove( 'lp-hidden' );
2336
2337 // Focus input and select text
2338 input.focus();
2339 input.select();
2340 }
2341
2342 /**
2343 * Handle permalink OK button click.
2344 * Validates and sanitizes slug, updates display.
2345 */
2346 handlePermalinkOk( args ) {
2347 const { e } = args;
2348 if ( e ) e.preventDefault();
2349
2350 const display = document.querySelector( BuilderEditCourse.selectors.elPermalinkDisplay );
2351 const editor = document.querySelector( BuilderEditCourse.selectors.elPermalinkEditor );
2352 const input = document.querySelector( BuilderEditCourse.selectors.elPermalinkSlugInput );
2353 const urlLink = document.querySelector( BuilderEditCourse.selectors.elPermalinkUrl );
2354 const baseUrlInput = document.querySelector( BuilderEditCourse.selectors.elPermalinkBaseUrl );
2355
2356 if ( ! display || ! editor || ! input || ! urlLink ) return;
2357
2358 // Sanitize the slug
2359 let newSlug = input.value.trim();
2360
2361 // If empty after sanitizing, restore original
2362 if ( ! newSlug ) {
2363 newSlug = input.dataset.originalValue || 'course';
2364 }
2365
2366 // Update input value with sanitized slug
2367 input.value = newSlug;
2368
2369 // Get base URL
2370 const baseUrl = baseUrlInput ? baseUrlInput.value : '';
2371 const newUrl = this.buildPermalinkDisplayUrl( baseUrl, newSlug, urlLink.textContent || '' );
2372
2373 // Keep href as the current saved link and only update display text.
2374 urlLink.textContent = newUrl;
2375
2376 // Toggle visibility back to display mode
2377 editor.classList.add( 'lp-hidden' );
2378 display.classList.remove( 'lp-hidden' );
2379
2380 // Mark form as changed if slug differs from original
2381 if ( newSlug !== input.dataset.originalValue ) {
2382 getFormState().markAsChanged();
2383 }
2384 }
2385
2386 /**
2387 * Handle permalink Cancel button click.
2388 * Restores original value and returns to display mode.
2389 */
2390 handlePermalinkCancel( args ) {
2391 const { e } = args;
2392 if ( e ) e.preventDefault();
2393
2394 const display = document.querySelector( BuilderEditCourse.selectors.elPermalinkDisplay );
2395 const editor = document.querySelector( BuilderEditCourse.selectors.elPermalinkEditor );
2396 const input = document.querySelector( BuilderEditCourse.selectors.elPermalinkSlugInput );
2397
2398 if ( ! display || ! editor || ! input ) return;
2399
2400 // Restore original value
2401 input.value = input.dataset.originalValue || '';
2402
2403 // Toggle visibility back to display mode
2404 editor.classList.add( 'lp-hidden' );
2405 display.classList.remove( 'lp-hidden' );
2406 }
2407
2408 initTabTitles() {
2409 const tabLinks = document.querySelectorAll( BuilderEditCourse.selectors.elTabLinks );
2410 tabLinks.forEach( ( link ) => {
2411 const textSpan = link.querySelector( 'span' );
2412 const title = textSpan ? textSpan.textContent.trim() : link.textContent.trim();
2413 const href = link.getAttribute( 'href' );
2414 if ( ! href ) return;
2415 const targetId = href.substring( 1 );
2416 const panel = document.getElementById( targetId );
2417 if ( panel ) {
2418 panel.setAttribute( 'data-tab-title', title );
2419 }
2420 } );
2421 }
2422
2423 initTitleCharCount() {
2424 const titleInput = document.querySelector( BuilderEditCourse.selectors.elTitleInput );
2425 if ( titleInput ) {
2426 this.updateTitleCharCount( titleInput.value );
2427 }
2428 }
2429
2430 updateTitleCharCount( text ) {
2431 const charCountEl = document.querySelector( BuilderEditCourse.selectors.elTitleCharCount );
2432 if ( ! charCountEl ) return;
2433
2434 const charCount = text.length;
2435 const charText = charCount === 1 ? 'character' : 'characters';
2436 charCountEl.textContent = `${ charCount } ${ charText }`;
2437 }
2438
2439 handleTitleInput( args ) {
2440 const { target } = args;
2441 this.updateTitleCharCount( target.value );
2442 }
2443
2444 initDescWordCount() {
2445 // Wait for TinyMCE to be ready
2446 if ( typeof tinymce !== 'undefined' ) {
2447 tinymce.on( 'AddEditor', ( e ) => {
2448 if ( e.editor.id === 'course_description_editor' ) {
2449 e.editor.on( 'init', () => {
2450 this.updateDescWordCount( e.editor );
2451
2452 // Listen for content changes
2453 e.editor.on( 'keyup change input NodeChange', () => {
2454 this.updateDescWordCount( e.editor );
2455 } );
2456 } );
2457 }
2458 } );
2459
2460 // If editor already exists
2461 const existingEditor = tinymce.get( 'course_description_editor' );
2462 if ( existingEditor ) {
2463 this.updateDescWordCount( existingEditor );
2464 existingEditor.on( 'keyup change input NodeChange', () => {
2465 this.updateDescWordCount( existingEditor );
2466 } );
2467 }
2468 }
2469 }
2470
2471 updateDescWordCount( editor ) {
2472 const wordCountEl = document.querySelector( BuilderEditCourse.selectors.elDescWordCount );
2473 if ( ! wordCountEl ) return;
2474
2475 // Use TinyMCE's built-in word count plugin
2476 const wordcount = editor.plugins.wordcount;
2477 let count = 0;
2478
2479 if ( wordcount && typeof wordcount.body !== 'undefined' ) {
2480 count = wordcount.body.getWordCount();
2481 } else if ( wordcount && typeof wordcount.getCount !== 'undefined' ) {
2482 count = wordcount.getCount();
2483 } else {
2484 // Fallback: manual count
2485 const content = editor.getContent( { format: 'text' } );
2486 count = this.countWords( content );
2487 }
2488
2489 const wordText = count === 1 ? 'word' : 'words';
2490 wordCountEl.textContent = `${ count } ${ wordText }`;
2491 }
2492
2493 updateHeaderTitle( title ) {
2494 const headerTitle = document.querySelector( '.lp-cb-header__title' );
2495 if ( headerTitle && title ) {
2496 headerTitle.textContent = title;
2497 }
2498 }
2499
2500 countWords( text ) {
2501 const trimmedText = text.replace( /<[^>]*>/g, '' ).trim();
2502 if ( trimmedText.length === 0 ) return 0;
2503 const words = trimmedText.split( /\s+/ ).filter( ( word ) => word.length > 0 );
2504 return words.length;
2505 }
2506
2507 /**
2508 * Initialize Header Actions Dropdown
2509 * Handles toggle open/close for dropdown menu in header actions
2510 */
2511 initHeaderActionsDropdown() {
2512 const dropdownWrapper = document.querySelector(
2513 BuilderEditCourse.selectors.elHeaderActionsDropdown
2514 );
2515 if ( ! dropdownWrapper ) return;
2516
2517 const toggleBtn = dropdownWrapper.querySelector( BuilderEditCourse.selectors.elDropdownToggle );
2518 const dropdownMenu = dropdownWrapper.querySelector(
2519 BuilderEditCourse.selectors.elDropdownMenu
2520 );
2521
2522 if ( ! toggleBtn || ! dropdownMenu ) return;
2523
2524 // Toggle dropdown on button click
2525 toggleBtn.addEventListener( 'click', ( e ) => {
2526 e.stopPropagation();
2527 const isOpen = dropdownMenu.classList.contains( 'is-open' );
2528
2529 if ( isOpen ) {
2530 this.closeHeaderDropdown( toggleBtn, dropdownMenu );
2531 } else {
2532 this.openHeaderDropdown( toggleBtn, dropdownMenu );
2533 }
2534 } );
2535
2536 // Close dropdown when clicking outside
2537 document.addEventListener( 'click', ( e ) => {
2538 if ( ! dropdownWrapper.contains( e.target ) ) {
2539 this.closeHeaderDropdown( toggleBtn, dropdownMenu );
2540 }
2541 } );
2542
2543 // Close dropdown on Escape key
2544 document.addEventListener( 'keydown', ( e ) => {
2545 if ( e.key === 'Escape' ) {
2546 this.closeHeaderDropdown( toggleBtn, dropdownMenu );
2547 }
2548 } );
2549
2550 // Close dropdown after clicking an item (except when it triggers an action that keeps page)
2551 const dropdownItems = dropdownMenu.querySelectorAll( '.cb-dropdown-item' );
2552 dropdownItems.forEach( ( item ) => {
2553 item.addEventListener( 'click', () => {
2554 // Small delay to allow action to process before closing
2555 setTimeout( () => {
2556 this.closeHeaderDropdown( toggleBtn, dropdownMenu );
2557 }, 100 );
2558 } );
2559 } );
2560 }
2561
2562 openHeaderDropdown( toggleBtn, dropdownMenu ) {
2563 dropdownMenu.classList.add( 'is-open' );
2564 toggleBtn.setAttribute( 'aria-expanded', 'true' );
2565 }
2566
2567 closeHeaderDropdown( toggleBtn, dropdownMenu ) {
2568 dropdownMenu.classList.remove( 'is-open' );
2569 toggleBtn.setAttribute( 'aria-expanded', 'false' );
2570 }
2571 }
2572