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