PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.9
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.9
4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 All 140 releases
← All changes | assets/src/js/frontend/course-builder/builder-course/builder-edit-course.js +2571 -2551 4.3.8 → 4.4.9 View file →
@@ -1,2551 +1,2571 @@
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 -}
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 +}