PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.9
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.9
4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 All 140 releases
← All changes | assets/src/js/frontend/course-builder/builder-question/builder-edit-question.js +1239 -1247 4.3.9.1 → 4.4.9 View file →
@@ -1,1247 +1,1239 @@
1 -import * as lpUtils from 'lpAssetsJsPath/utils.js';
2 -import * as lpToastify from 'lpAssetsJsPath/lpToastify.js';
3 -import SweetAlert from 'sweetalert2';
4 -import { EditQuestion } from 'lpAssetsJsPath/admin/edit-question.js';
5 -import { SWAL_ICON_DUPLICATE, SWAL_ICON_TRASH_DRAFT } from '../swal-icons.js';
6 -import { getFormState } from '../builder-form-state.js';
7 -
8 -export class BuilderEditQuestion {
9 - constructor() {
10 - this.editQuestion = null;
11 - // Events use document-level event delegation, so always register them
12 - // The page context check happens in individual handlers via target.closest()
13 - this.init();
14 - }
15 -
16 - static selectors = {
17 - elDataQuestion: '.cb-section__question-edit',
18 - elBtnUpdateQuestion: '.cb-btn-update',
19 - elBtnDraftQuestion: '.cb-btn-draft',
20 - elBtnDuplicateQuestion: '.cb-btn-duplicate-question',
21 - elBtnTrashQuestion: '.cb-btn-trash',
22 - elBtnMainAction: '.cb-btn-main-action',
23 - elQuestionStatus: '.question-status',
24 - idTitle: 'title',
25 - idDescEditor: 'question_description_editor',
26 - elPublishStatusSelect: '#cb-question-publish-status',
27 - elFormSetting: '.lp-form-setting-question',
28 - // Permalink component
29 - elPermalinkDisplay: '.cb-permalink-display',
30 - elPermalinkEditor: '.cb-permalink-editor',
31 - elPermalinkEditBtn: '.cb-permalink-edit-btn',
32 - elPermalinkOkBtn: '.cb-permalink-ok-btn',
33 - elPermalinkCancelBtn: '.cb-permalink-cancel-btn',
34 - elPermalinkSlugInput: '.cb-permalink-slug-input',
35 - elPermalinkUrl: '.cb-permalink-url',
36 - elPermalinkBaseUrl: '#cb-permalink-base-url',
37 - // Question edit selectors
38 - elEditQuestionWrap: '.lp-edit-question-wrap',
39 - elQuestionEditMain: '.lp-question-edit-main',
40 - // Tab handling selectors
41 - elCBHorizontalTabs: '.lp-cb-tabs__item',
42 - elCBTabPanels: '.lp-cb-tab-panel',
43 - // Dropdown selectors
44 - elDropdownToggle: '.cb-btn-dropdown-toggle',
45 - elDropdownMenu: '.cb-dropdown-menu',
46 - elDropdownItem: '.cb-dropdown-item',
47 - elHeaderActionsDropdown: '.cb-header-actions-dropdown',
48 - elQuestionActionExpanded: '.course-action-expanded',
49 - };
50 -
51 - init() {
52 - this.initQuestionAnswersSettings();
53 - this.initTabs();
54 - this.initHeaderActionsDropdown();
55 - this.syncHeaderActionWithPublishPanel();
56 - this.events();
57 - }
58 -
59 - /**
60 - * Initialize header actions dropdown (toggle behavior)
61 - */
62 - initHeaderActionsDropdown() {
63 - // Close dropdown when clicking outside
64 - document.addEventListener( 'click', ( e ) => {
65 - const dropdown = document.querySelector(
66 - BuilderEditQuestion.selectors.elHeaderActionsDropdown
67 - );
68 - if ( dropdown && ! dropdown.contains( e.target ) ) {
69 - const menu = dropdown.querySelector( BuilderEditQuestion.selectors.elDropdownMenu );
70 - const toggle = dropdown.querySelector( BuilderEditQuestion.selectors.elDropdownToggle );
71 - if ( menu ) {
72 - menu.classList.remove( 'is-open' );
73 - }
74 - if ( toggle ) {
75 - toggle.setAttribute( 'aria-expanded', 'false' );
76 - }
77 - }
78 - } );
79 - }
80 -
81 - /**
82 - * Handle dropdown toggle click
83 - */
84 - handleDropdownToggle( args ) {
85 - if ( this.hasPublishDrivenSingleAction() ) {
86 - return;
87 - }
88 -
89 - const { target } = args;
90 - const toggleBtn = target.closest( BuilderEditQuestion.selectors.elDropdownToggle );
91 -
92 - if ( ! toggleBtn ) {
93 - return;
94 - }
95 -
96 - const dropdown = toggleBtn.closest( BuilderEditQuestion.selectors.elHeaderActionsDropdown );
97 - if ( ! dropdown ) {
98 - return;
99 - }
100 -
101 - const menu = dropdown.querySelector( BuilderEditQuestion.selectors.elDropdownMenu );
102 - if ( menu ) {
103 - menu.classList.toggle( 'is-open' );
104 - const isOpen = menu.classList.contains( 'is-open' );
105 - toggleBtn.setAttribute( 'aria-expanded', isOpen ? 'true' : 'false' );
106 - }
107 - }
108 -
109 - /**
110 - * Handle dropdown item click (Save Draft, Publish from dropdown menu)
111 - */
112 - handleDropdownItemClick( args ) {
113 - // Context check: only handle if on question edit page
114 - if ( ! this.isQuestionContext() ) {
115 - return;
116 - }
117 -
118 - if ( this.hasPublishDrivenSingleAction() ) {
119 - return;
120 - }
121 -
122 - const { target } = args;
123 - const dropdownItem = target.closest( BuilderEditQuestion.selectors.elDropdownItem );
124 -
125 - if ( ! dropdownItem ) {
126 - return;
127 - }
128 -
129 - // Skip if this is trash button - it has its own handler
130 - if (
131 - dropdownItem.classList.contains( 'cb-btn-trash' ) ||
132 - dropdownItem.classList.contains( 'cb-btn-duplicate-question' )
133 - ) {
134 - return;
135 - }
136 -
137 - const status = dropdownItem.dataset.status;
138 - if ( ! status ) {
139 - return;
140 - }
141 -
142 - // Close the dropdown menu
143 - const menu = dropdownItem.closest( BuilderEditQuestion.selectors.elDropdownMenu );
144 - if ( menu ) {
145 - menu.classList.remove( 'is-open' );
146 - }
147 -
148 - // Save with the specified status
149 - this.saveQuestionWithStatus( dropdownItem, status );
150 - }
151 -
152 - /**
153 - * Save question with specified status (publish/draft)
154 - * @param {HTMLElement} btnEl - The button element that was clicked
155 - * @param {string} status - The status to save (publish/draft)
156 - */
157 - async saveQuestionWithStatus( btnEl, status ) {
158 - const canContinue = await this.confirmUnpublishIfNeeded( status, btnEl );
159 - if ( ! canContinue ) {
160 - return;
161 - }
162 -
163 - // Validate title before saving
164 - if ( ! this.validateTitleBeforeUpdate() ) {
165 - return;
166 - }
167 -
168 - lpUtils.lpSetLoadingEl( btnEl, 1 );
169 -
170 - const questionData = this.getQuestionDataForUpdate();
171 -
172 - const dataSend = {
173 - ...questionData,
174 - action: 'builder_update_question',
175 - args: {
176 - id_url: 'builder-update-question',
177 - },
178 - question_status: status,
179 - };
180 -
181 - if ( typeof lpQuestionBuilder !== 'undefined' && lpQuestionBuilder.nonce ) {
182 - dataSend.nonce = lpQuestionBuilder.nonce;
183 - }
184 -
185 - const callBack = {
186 - success: ( response ) => {
187 - const { status: respStatus, message, data } = response;
188 - lpToastify.show( message, respStatus );
189 -
190 - if ( respStatus === 'success' ) {
191 - // Update action button text
192 - this.updateActionButtons( data?.status || status );
193 - this.syncPublishPanelStatus( data?.status || status );
194 - this.updatePermalinkUIAfterSave( data );
195 -
196 - // Reset form state before potential redirect.
197 - document.dispatchEvent( new CustomEvent( 'lp-course-builder-saved' ) );
198 -
199 - if ( data?.question_id_new ) {
200 - const currentUrl = window.location.href;
201 - window.location.href = currentUrl.replace(
202 - /post-new\/?/,
203 - `${ data.question_id_new }/`
204 - );
205 - }
206 -
207 - if ( data?.status ) {
208 - const elStatus = document.querySelector(
209 - BuilderEditQuestion.selectors.elQuestionStatus
210 - );
211 - if ( elStatus ) {
212 - elStatus.className = 'question-status ' + data.status;
213 - elStatus.textContent = data.status;
214 - }
215 -
216 - if ( data.status === 'trash' || data.status === 'draft' ) {
217 - const curriculumItem = document.querySelector(
218 - `.question-item[data-question-id="${ questionData.question_id }"]`
219 - );
220 - if ( curriculumItem ) {
221 - const elCurriculumQuestion = curriculumItem.closest( '.question' );
222 - if ( elCurriculumQuestion ) {
223 - elCurriculumQuestion.remove();
224 - }
225 - }
226 - }
227 - }
228 - }
229 - },
230 - error: ( error ) => {
231 - lpToastify.show( error.message || error, 'error' );
232 - },
233 - completed: () => {
234 - lpUtils.lpSetLoadingEl( btnEl, 0 );
235 - },
236 - };
237 -
238 - window.lpAJAXG.fetchAJAX( dataSend, callBack );
239 - }
240 -
241 - /**
242 - * Initialize horizontal tabs for client-side tab switching
243 - */
244 - initTabs() {
245 - const tabs = document.querySelectorAll( BuilderEditQuestion.selectors.elCBHorizontalTabs );
246 - if ( tabs.length === 0 ) {
247 - return;
248 - }
249 -
250 - // Activate first tab by default if none is active
251 - const activeTab = document.querySelector(
252 - `${ BuilderEditQuestion.selectors.elCBHorizontalTabs }.is-active`
253 - );
254 - if ( ! activeTab && tabs.length > 0 ) {
255 - tabs[ 0 ].classList.add( 'is-active' );
256 - const section = tabs[ 0 ].getAttribute( 'data-tab-section' );
257 - if ( section ) {
258 - const panel = document.querySelector(
259 - `${ BuilderEditQuestion.selectors.elCBTabPanels }[data-section="${ section }"]`
260 - );
261 - if ( panel ) {
262 - panel.classList.remove( 'lp-hidden' );
263 - }
264 - }
265 - }
266 - }
267 -
268 - /**
269 - * Handle horizontal tab click for client-side tab switching
270 - */
271 - handleTabClick( args ) {
272 - const { e, target } = args;
273 - const tabLink = target.closest( BuilderEditQuestion.selectors.elCBHorizontalTabs );
274 -
275 - if ( ! tabLink ) {
276 - return;
277 - }
278 -
279 - e.preventDefault();
280 -
281 - const section = tabLink.getAttribute( 'data-tab-section' );
282 - if ( ! section ) {
283 - return;
284 - }
285 -
286 - // Update active tab
287 - const allTabs = document.querySelectorAll( BuilderEditQuestion.selectors.elCBHorizontalTabs );
288 - allTabs.forEach( ( tab ) => tab.classList.remove( 'is-active' ) );
289 - tabLink.classList.add( 'is-active' );
290 -
291 - const url = new URL( window.location.href );
292 - url.searchParams.set( 'tab', section );
293 - window.history.replaceState( {}, '', url );
294 -
295 - // Show/hide panels
296 - const allPanels = document.querySelectorAll( BuilderEditQuestion.selectors.elCBTabPanels );
297 - allPanels.forEach( ( panel ) => {
298 - if ( panel.getAttribute( 'data-section' ) === section ) {
299 - panel.classList.remove( 'lp-hidden' );
300 - } else {
301 - panel.classList.add( 'lp-hidden' );
302 - }
303 - } );
304 - }
305 -
306 - /**
307 - * Initialize Question Answers Settings
308 - * This will init EditQuestion class for the question answer management
309 - */
310 - initQuestionAnswersSettings() {
311 - lpUtils.lpOnElementReady(
312 - BuilderEditQuestion.selectors.elQuestionEditMain,
313 - ( elQuestionEditMain ) => {
314 - // Initialize EditQuestion for question answer editing
315 - if ( ! this.editQuestion ) {
316 - this.editQuestion = new EditQuestion();
317 - this.editQuestion.init();
318 - }
319 -
320 - // Init sortable for question answers
321 - if ( this.editQuestion ) {
322 - this.editQuestion.sortAbleQuestionAnswer( elQuestionEditMain );
323 - }
324 - }
325 - );
326 - }
327 -
328 - /**
329 - * Re-initialize when question type changes
330 - */
331 - reinitQuestionHandlers( elQuestionEditMain ) {
332 - if ( this.editQuestion && elQuestionEditMain ) {
333 - this.editQuestion.sortAbleQuestionAnswer( elQuestionEditMain );
334 - this.editQuestion.initTinyMCE();
335 - }
336 - }
337 -
338 - /**
339 - * Re-initialize for popup context
340 - * This is called when popup is opened multiple times to ensure
341 - * TinyMCE and other handlers are properly re-initialized
342 - *
343 - * @param {HTMLElement} container - The popup container element
344 - */
345 - reinit( container ) {
346 - const elQuestionEditMain = container
347 - ? container.querySelector( BuilderEditQuestion.selectors.elQuestionEditMain )
348 - : document.querySelector( BuilderEditQuestion.selectors.elQuestionEditMain );
349 -
350 - if ( ! elQuestionEditMain ) {
351 - return;
352 - }
353 -
354 - // Re-create EditQuestion instance to ensure fresh initialization
355 - // This is necessary because TinyMCE instances were destroyed when popup closed
356 - if ( this.editQuestion ) {
357 - // Destroy existing TinyMCE instances in the container first
358 - if ( typeof tinymce !== 'undefined' && container ) {
359 - const textareas = container.querySelectorAll( 'textarea.lp-meta-box__editor' );
360 - textareas.forEach( ( textarea ) => {
361 - const editorId = textarea.id;
362 - if ( editorId ) {
363 - const editor = tinymce.get( editorId );
364 - if ( editor ) {
365 - editor.remove();
366 - }
367 - if ( typeof wp !== 'undefined' && wp.editor && wp.editor.remove ) {
368 - wp.editor.remove( editorId );
369 - }
370 - }
371 - } );
372 - }
373 - }
374 -
375 - // Create fresh EditQuestion instance
376 - this.editQuestion = new EditQuestion();
377 - this.editQuestion.init();
378 -
379 - // Re-init sortable and TinyMCE
380 - this.editQuestion.sortAbleQuestionAnswer( elQuestionEditMain );
381 -
382 - // Use setTimeout to ensure DOM is ready for TinyMCE
383 - setTimeout( () => {
384 - if ( this.editQuestion ) {
385 - this.editQuestion.initTinyMCE();
386 - }
387 - }, 100 );
388 - }
389 -
390 - events() {
391 - if ( BuilderEditQuestion._loadedEvents ) {
392 - return;
393 - }
394 - BuilderEditQuestion._loadedEvents = true;
395 -
396 - lpUtils.eventHandlers( 'click', [
397 - {
398 - selector: BuilderEditQuestion.selectors.elBtnMainAction,
399 - class: this,
400 - callBack: this.updateQuestion.name,
401 - },
402 - {
403 - selector: BuilderEditQuestion.selectors.elBtnDraftQuestion,
404 - class: this,
405 - callBack: this.saveDraftQuestion.name,
406 - },
407 - {
408 - selector: BuilderEditQuestion.selectors.elBtnDuplicateQuestion,
409 - class: this,
410 - callBack: this.duplicateQuestion.name,
411 - },
412 - {
413 - selector: BuilderEditQuestion.selectors.elBtnTrashQuestion,
414 - class: this,
415 - callBack: this.trashQuestion.name,
416 - },
417 - {
418 - selector: BuilderEditQuestion.selectors.elCBHorizontalTabs,
419 - class: this,
420 - callBack: this.handleTabClick.name,
421 - },
422 - {
423 - selector: BuilderEditQuestion.selectors.elDropdownToggle,
424 - class: this,
425 - callBack: this.handleDropdownToggle.name,
426 - },
427 - {
428 - selector: BuilderEditQuestion.selectors.elDropdownItem,
429 - class: this,
430 - callBack: this.handleDropdownItemClick.name,
431 - },
432 - {
433 - selector: BuilderEditQuestion.selectors.elPermalinkEditBtn,
434 - class: this,
435 - callBack: this.handlePermalinkEdit.name,
436 - },
437 - {
438 - selector: BuilderEditQuestion.selectors.elPermalinkOkBtn,
439 - class: this,
440 - callBack: this.handlePermalinkOk.name,
441 - },
442 - {
443 - selector: BuilderEditQuestion.selectors.elPermalinkCancelBtn,
444 - class: this,
445 - callBack: this.handlePermalinkCancel.name,
446 - },
447 - ] );
448 -
449 - lpUtils.eventHandlers( 'change', [
450 - {
451 - selector: BuilderEditQuestion.selectors.elPublishStatusSelect,
452 - class: this,
453 - callBack: this.handlePublishStatusChange.name,
454 - },
455 - ] );
456 - }
457 -
458 - handlePublishStatusChange() {
459 - this.syncHeaderActionWithPublishPanel();
460 - getFormState().markAsChanged();
461 - }
462 -
463 - /**
464 - * Validate title is not empty before update
465 - * @return {boolean} True if valid, false if invalid
466 - */
467 - validateTitleBeforeUpdate() {
468 - const titleInput = document.getElementById( BuilderEditQuestion.selectors.idTitle );
469 - if ( ! titleInput ) {
470 - return true;
471 - }
472 -
473 - const title = titleInput.value.trim();
474 - if ( ! title ) {
475 - lpToastify.show( 'Question title is required.', 'error' );
476 - titleInput.focus();
477 - return false;
478 - }
479 - return true;
480 - }
481 -
482 - /**
483 - * Update action buttons after status change, matching course edit logic.
484 - * Updates both main button and dropdown items based on new status.
485 - * @param {string} newStatus - The new question status
486 - */
487 - updateActionButtons( newStatus ) {
488 - const dropdown = document.querySelector(
489 - BuilderEditQuestion.selectors.elHeaderActionsDropdown
490 - );
491 - if ( ! dropdown ) return;
492 -
493 - const mainBtn = dropdown.querySelector( '.cb-btn-main-action' );
494 - const dropdownMenu = dropdown.querySelector( BuilderEditQuestion.selectors.elDropdownMenu );
495 - if ( ! mainBtn ) return;
496 -
497 - if ( this.hasPublishDrivenSingleAction() ) {
498 - this.syncHeaderActionWithPublishPanel();
499 - return;
500 - }
501 -
502 - if ( ! dropdownMenu ) return;
503 -
504 - // Status configuration for button labels and classes
505 - const statusConfig = {
506 - publish: {
507 - mainLabel: mainBtn.dataset.titleUpdate || 'Update',
508 - mainClass: 'cb-btn-update',
509 - mainStatus: 'publish',
510 - dropdownLabel: mainBtn.dataset.titleDraft || 'Save Draft',
511 - dropdownClass: 'cb-btn-darft',
512 - dropdownStatus: 'draft',
513 - dropdownIcon: 'dashicons-media-default',
514 - },
515 - draft: {
516 - mainLabel: mainBtn.dataset.titleDraft || 'Save Draft',
517 - mainClass: 'cb-btn-darft',
518 - mainStatus: 'draft',
519 - dropdownLabel: mainBtn.dataset.titlePublish || 'Publish',
520 - dropdownClass: 'cb-btn-publish',
521 - dropdownStatus: 'publish',
522 - dropdownIcon: 'dashicons-visibility',
523 - },
524 - pending: {
525 - mainLabel: 'Submit for Review',
526 - mainClass: 'cb-btn-pending',
527 - mainStatus: 'pending',
528 - dropdownLabel: mainBtn.dataset.titleDraft || 'Save Draft',
529 - dropdownClass: 'cb-btn-darft',
530 - dropdownStatus: 'draft',
531 - dropdownIcon: 'dashicons-media-default',
532 - },
533 - };
534 -
535 - const config = statusConfig[ newStatus ] || statusConfig.draft;
536 -
537 - // Update main button
538 - mainBtn.className = `${ config.mainClass } cb-btn-primary cb-btn-main-action`;
539 - mainBtn.dataset.status = config.mainStatus;
540 - mainBtn.textContent = config.mainLabel;
541 -
542 - // Update dropdown item (first item, excluding trash)
543 - const dropdownItems = dropdownMenu.querySelectorAll( '.cb-dropdown-item:not(.cb-btn-trash)' );
544 - if ( dropdownItems.length > 0 ) {
545 - const firstItem = dropdownItems[ 0 ];
546 - firstItem.className = `cb-dropdown-item ${ config.dropdownClass }`;
547 - firstItem.dataset.status = config.dropdownStatus;
548 - firstItem.innerHTML = `<span class="dashicons ${ config.dropdownIcon }"></span>${ config.dropdownLabel }`;
549 - }
550 -
551 - // Update dropdown data-current-status
552 - dropdown.dataset.currentStatus = newStatus;
553 - }
554 -
555 - /**
556 - * Check if we're in question edit context
557 - * @return {boolean}
558 - */
559 - isQuestionContext() {
560 - return !! document.querySelector( BuilderEditQuestion.selectors.elDataQuestion );
561 - }
562 -
563 - getQuestionDataForUpdate() {
564 - const data = {};
565 -
566 - const wrapperEl = document.querySelector( BuilderEditQuestion.selectors.elDataQuestion );
567 -
568 - data.question_id = wrapperEl ? parseInt( wrapperEl.dataset.questionId ) || 0 : 0;
569 -
570 - const titleInput = document.getElementById( BuilderEditQuestion.selectors.idTitle );
571 - data.question_title = titleInput ? titleInput.value : '';
572 -
573 - const descEditor = document.getElementById( BuilderEditQuestion.selectors.idDescEditor );
574 - data.question_description = descEditor ? descEditor.value : '';
575 -
576 - if ( typeof tinymce !== 'undefined' ) {
577 - const editor = tinymce.get( BuilderEditQuestion.selectors.idDescEditor );
578 - if ( editor ) {
579 - data.question_description = editor.getContent();
580 - }
581 - }
582 -
583 - const permalinkInput = document.querySelector(
584 - BuilderEditQuestion.selectors.elPermalinkSlugInput
585 - );
586 - if ( permalinkInput && permalinkInput.value ) {
587 - data.question_permalink = permalinkInput.value;
588 - }
589 -
590 - const elFormSetting = document.querySelector( BuilderEditQuestion.selectors.elFormSetting );
591 -
592 - if ( elFormSetting ) {
593 - data.question_settings = true;
594 - const formElements = elFormSetting.querySelectorAll( 'input, select, textarea' );
595 -
596 - formElements.forEach( ( element ) => {
597 - const name = element.name || element.id;
598 -
599 - if ( ! name ) {
600 - return;
601 - }
602 -
603 - if ( name === 'learnpress_meta_box_nonce' || name === '_wp_http_referer' ) {
604 - return;
605 - }
606 -
607 - if ( element.type === 'checkbox' ) {
608 - const fieldName = name.replace( '[]', '' );
609 - if ( ! data.hasOwnProperty( fieldName ) ) {
610 - data[ fieldName ] = element.checked ? 'yes' : 'no';
611 - }
612 - } else if ( element.type === 'radio' ) {
613 - if ( element.checked ) {
614 - const fieldName = name.replace( '[]', '' );
615 - data[ fieldName ] = element.value;
616 - }
617 - } else if ( element.type === 'file' ) {
618 - const fieldName = name.replace( '[]', '' );
619 - if ( element.files && element.files.length > 0 ) {
620 - data[ fieldName ] = element.files;
621 - }
622 - } else {
623 - const fieldName = name.replace( '[]', '' );
624 -
625 - if ( name.endsWith( '[]' ) ) {
626 - if ( ! data.hasOwnProperty( fieldName ) ) {
627 - data[ fieldName ] = [];
628 - }
629 -
630 - if ( Array.isArray( data[ fieldName ] ) ) {
631 - data[ fieldName ].push( element.value );
632 - }
633 - } else {
634 - if ( ! data.hasOwnProperty( fieldName ) ) {
635 - data[ fieldName ] = element.value;
636 - }
637 - }
638 - }
639 - } );
640 -
641 - Object.keys( data ).forEach( ( key ) => {
642 - if ( Array.isArray( data[ key ] ) ) {
643 - data[ key ] = data[ key ].join( ',' );
644 - }
645 - } );
646 - }
647 -
648 - return data;
649 - }
650 -
651 - getStatusFromPublishPanel( fallbackStatus = 'publish' ) {
652 - const statusSelect = document.querySelector(
653 - BuilderEditQuestion.selectors.elPublishStatusSelect
654 - );
655 - if ( ! statusSelect ) {
656 - return fallbackStatus;
657 - }
658 -
659 - const selectedStatus = statusSelect.value;
660 - if ( selectedStatus === 'publish' || selectedStatus === 'draft' ) {
661 - return selectedStatus;
662 - }
663 -
664 - return fallbackStatus;
665 - }
666 -
667 - hasPublishDrivenSingleAction() {
668 - if ( ! document.querySelector( BuilderEditQuestion.selectors.elPublishStatusSelect ) ) {
669 - return false;
670 - }
671 -
672 - const wrapperEl = document.querySelector( BuilderEditQuestion.selectors.elDataQuestion );
673 - const questionId = wrapperEl ? parseInt( wrapperEl.dataset.questionId ) || 0 : 0;
674 -
675 - return questionId > 0;
676 - }
677 -
678 - syncHeaderActionWithPublishPanel() {
679 - if ( ! this.hasPublishDrivenSingleAction() ) {
680 - return;
681 - }
682 -
683 - const dropdown = document.querySelector(
684 - BuilderEditQuestion.selectors.elHeaderActionsDropdown
685 - );
686 - if ( ! dropdown ) {
687 - return;
688 - }
689 -
690 - const mainBtn = dropdown.querySelector( '.cb-btn-main-action' );
691 - if ( ! mainBtn ) {
692 - return;
693 - }
694 -
695 - const toggleBtn = dropdown.querySelector( BuilderEditQuestion.selectors.elDropdownToggle );
696 - const dropdownMenu = dropdown.querySelector( BuilderEditQuestion.selectors.elDropdownMenu );
697 - const status = this.getStatusFromPublishPanel( mainBtn.dataset.status || 'publish' );
698 -
699 - mainBtn.classList.remove( 'cb-btn-darft', 'cb-btn-publish', 'cb-btn-pending' );
700 - mainBtn.classList.add( 'cb-btn-update', 'cb-btn-primary', 'cb-btn-main-action' );
701 - mainBtn.textContent = mainBtn.dataset.titleUpdate || 'Update';
702 - mainBtn.dataset.status = status;
703 - dropdown.dataset.currentStatus = status;
704 - dropdown.classList.add( 'cb-header-actions-dropdown--single' );
705 -
706 - if ( toggleBtn ) {
707 - toggleBtn.style.display = 'none';
708 - }
709 - if ( dropdownMenu ) {
710 - dropdownMenu.style.display = 'none';
711 - }
712 - }
713 -
714 - syncPublishPanelStatus( status ) {
715 - const statusSelect = document.querySelector(
716 - BuilderEditQuestion.selectors.elPublishStatusSelect
717 - );
718 - if ( ! statusSelect ) {
719 - return;
720 - }
721 -
722 - statusSelect.value = status === 'publish' ? 'publish' : 'draft';
723 - this.syncHeaderActionWithPublishPanel();
724 - }
725 -
726 - updatePermalinkUIAfterSave( data = {} ) {
727 - const slugInput = document.querySelector( BuilderEditQuestion.selectors.elPermalinkSlugInput );
728 - const urlLink = document.querySelector( BuilderEditQuestion.selectors.elPermalinkUrl );
729 - const baseUrlInput = document.querySelector( BuilderEditQuestion.selectors.elPermalinkBaseUrl );
730 - const permalinkDisplayUrl = this.buildPermalinkDisplayUrl(
731 - baseUrlInput ? baseUrlInput.value : '',
732 - data?.question_slug,
733 - data?.question_permalink
734 - );
735 -
736 - if ( slugInput && data?.question_slug ) {
737 - slugInput.value = data.question_slug;
738 - slugInput.dataset.originalValue = data.question_slug;
739 - }
740 -
741 - if ( urlLink && data?.question_permalink ) {
742 - urlLink.href = data.question_permalink;
743 - urlLink.textContent = permalinkDisplayUrl || data.question_permalink;
744 - } else if ( urlLink && permalinkDisplayUrl ) {
745 - urlLink.textContent = permalinkDisplayUrl;
746 - }
747 - }
748 -
749 - slugify( str ) {
750 - return ( str || '' )
751 - .toString()
752 - .normalize( 'NFD' )
753 - .replace( /[\u0300-\u036f]/g, '' )
754 - .replace( /đ/g, 'd' )
755 - .replace( /Đ/g, 'D' )
756 - .toLowerCase()
757 - .replace( /\s+/g, '-' )
758 - .replace( /[^\w-]+/g, '' )
759 - .replace( /--+/g, '-' )
760 - .replace( /^-+/, '' )
761 - .replace( /-+$/, '' );
762 - }
763 -
764 - buildPermalinkDisplayUrl( baseUrl = '', slug = '', fallbackUrl = '' ) {
765 - const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl : '';
766 - const normalizedSlug = typeof slug === 'string' ? slug.trim() : '';
767 -
768 - if ( normalizedBaseUrl && normalizedSlug ) {
769 - return `${ normalizedBaseUrl }${ normalizedSlug }`;
770 - }
771 -
772 - return typeof fallbackUrl === 'string' ? fallbackUrl : '';
773 - }
774 -
775 - handlePermalinkEdit( args ) {
776 - if ( ! this.isQuestionContext() ) {
777 - return;
778 - }
779 -
780 - const { e } = args;
781 - if ( e ) e.preventDefault();
782 -
783 - const display = document.querySelector( BuilderEditQuestion.selectors.elPermalinkDisplay );
784 - const editor = document.querySelector( BuilderEditQuestion.selectors.elPermalinkEditor );
785 - const input = document.querySelector( BuilderEditQuestion.selectors.elPermalinkSlugInput );
786 -
787 - if ( ! display || ! editor || ! input ) return;
788 -
789 - input.dataset.originalValue = input.value;
790 - display.classList.add( 'lp-hidden' );
791 - editor.classList.remove( 'lp-hidden' );
792 - input.focus();
793 - input.select();
794 - }
795 -
796 - handlePermalinkOk( args ) {
797 - if ( ! this.isQuestionContext() ) {
798 - return;
799 - }
800 -
801 - const { e } = args;
802 - if ( e ) e.preventDefault();
803 -
804 - const display = document.querySelector( BuilderEditQuestion.selectors.elPermalinkDisplay );
805 - const editor = document.querySelector( BuilderEditQuestion.selectors.elPermalinkEditor );
806 - const input = document.querySelector( BuilderEditQuestion.selectors.elPermalinkSlugInput );
807 - const urlLink = document.querySelector( BuilderEditQuestion.selectors.elPermalinkUrl );
808 - const baseUrlInput = document.querySelector( BuilderEditQuestion.selectors.elPermalinkBaseUrl );
809 -
810 - if ( ! display || ! editor || ! input || ! urlLink ) return;
811 -
812 - let newSlug = this.slugify( input.value.trim() );
813 - if ( ! newSlug ) {
814 - newSlug = input.dataset.originalValue || 'question';
815 - }
816 -
817 - input.value = newSlug;
818 - const baseUrl = baseUrlInput ? baseUrlInput.value : '';
819 - const newUrl = this.buildPermalinkDisplayUrl( baseUrl, newSlug, urlLink.textContent || '' );
820 -
821 - // Keep href as the current saved link and only update display text.
822 - urlLink.textContent = newUrl;
823 - editor.classList.add( 'lp-hidden' );
824 - display.classList.remove( 'lp-hidden' );
825 -
826 - if ( newSlug !== input.dataset.originalValue ) {
827 - getFormState().markAsChanged();
828 - }
829 - }
830 -
831 - handlePermalinkCancel( args ) {
832 - if ( ! this.isQuestionContext() ) {
833 - return;
834 - }
835 -
836 - const { e } = args;
837 - if ( e ) e.preventDefault();
838 -
839 - const display = document.querySelector( BuilderEditQuestion.selectors.elPermalinkDisplay );
840 - const editor = document.querySelector( BuilderEditQuestion.selectors.elPermalinkEditor );
841 - const input = document.querySelector( BuilderEditQuestion.selectors.elPermalinkSlugInput );
842 -
843 - if ( ! display || ! editor || ! input ) return;
844 -
845 - input.value = input.dataset.originalValue || '';
846 - editor.classList.add( 'lp-hidden' );
847 - display.classList.remove( 'lp-hidden' );
848 - }
849 -
850 - async updateQuestion( args ) {
851 - // Context check: only handle if on question edit page
852 - if ( ! this.isQuestionContext() ) {
853 - return;
854 - }
855 -
856 - const { target } = args;
857 - const elBtnMainAction = target.closest( BuilderEditQuestion.selectors.elBtnMainAction );
858 -
859 - if ( ! elBtnMainAction ) {
860 - return;
861 - }
862 -
863 - // Validate title before update
864 - if ( ! this.validateTitleBeforeUpdate() ) {
865 - return;
866 - }
867 -
868 - // Resolve status from Overview publish panel first, fallback to button data-status.
869 - const targetStatus = this.getStatusFromPublishPanel(
870 - elBtnMainAction.dataset.status || 'publish'
871 - );
872 - const canContinue = await this.confirmUnpublishIfNeeded( targetStatus, elBtnMainAction );
873 - if ( ! canContinue ) {
874 - return;
875 - }
876 -
877 - lpUtils.lpSetLoadingEl( elBtnMainAction, 1 );
878 -
879 - const questionData = this.getQuestionDataForUpdate();
880 -
881 - const dataSend = {
882 - ...questionData,
883 - action: 'builder_update_question',
884 - args: {
885 - id_url: 'builder-update-question',
886 - },
887 - question_status: targetStatus,
888 - };
889 -
890 - if ( typeof lpQuestionBuilder !== 'undefined' && lpQuestionBuilder.nonce ) {
891 - dataSend.nonce = lpQuestionBuilder.nonce;
892 - }
893 -
894 - const callBack = {
895 - success: ( response ) => {
896 - const { status, message, data } = response;
897 - lpToastify.show( message, status );
898 -
899 - if ( status === 'success' ) {
900 - // Update action button text with actual status from server
901 - this.updateActionButtons( data?.status || targetStatus );
902 - this.syncPublishPanelStatus( data?.status || targetStatus );
903 - this.updatePermalinkUIAfterSave( data );
904 -
905 - // Reset form state before potential redirect.
906 - document.dispatchEvent( new CustomEvent( 'lp-course-builder-saved' ) );
907 -
908 - if ( data?.redirect_url ) {
909 - window.location.href = data.redirect_url;
910 - }
911 -
912 - if ( data?.status ) {
913 - const elStatus = document.querySelector(
914 - BuilderEditQuestion.selectors.elQuestionStatus
915 - );
916 - if ( elStatus ) {
917 - elStatus.className = 'question-status ' + data.status;
918 - elStatus.textContent = data.status;
919 - }
920 -
921 - if ( data.status === 'trash' || data.status === 'draft' ) {
922 - const curriculumItem = document.querySelector(
923 - `.question-item[data-question-id="${ questionData.question_id }"]`
924 - );
925 - if ( curriculumItem ) {
926 - const elCurriculumQuestion = curriculumItem.closest( '.question' );
927 - if ( elCurriculumQuestion ) {
928 - elCurriculumQuestion.remove();
929 - }
930 - }
931 - }
932 - }
933 - }
934 - },
935 - error: ( error ) => {
936 - lpToastify.show( error.message || error, 'error' );
937 - },
938 - completed: () => {
939 - lpUtils.lpSetLoadingEl( elBtnMainAction, 0 );
940 - },
941 - };
942 -
943 - window.lpAJAXG.fetchAJAX( dataSend, callBack );
944 - }
945 -
946 - async confirmUnpublishIfNeeded( targetStatus, triggerEl ) {
947 - if ( targetStatus !== 'draft' ) {
948 - return true;
949 - }
950 -
951 - const statusEl = document.querySelector( BuilderEditQuestion.selectors.elQuestionStatus );
952 - const isPublished = statusEl && statusEl.classList.contains( 'publish' );
953 - if ( ! isPublished ) {
954 - return true;
955 - }
956 -
957 - const confirmMsg =
958 - triggerEl?.dataset?.confirmUnpublish ||
959 - 'Saving as draft will unpublish this item. Are you sure?';
960 - const result = await SweetAlert.fire( {
961 - title: confirmMsg,
962 - iconHtml: SWAL_ICON_TRASH_DRAFT,
963 - customClass: { icon: 'lp-cb-swal-icon-html' },
964 - showCloseButton: true,
965 - showCancelButton: true,
966 - cancelButtonText: lpData.i18n.cancel,
967 - confirmButtonText: lpData.i18n.yes,
968 - reverseButtons: true,
969 - } );
970 -
971 - return !! result.isConfirmed;
972 - }
973 -
974 - async saveDraftQuestion( args ) {
975 - // Context check: only handle if on question edit page
976 - if ( ! this.isQuestionContext() ) {
977 - return;
978 - }
979 -
980 - const { target } = args;
981 - const elBtnDraftQuestion = target.closest( BuilderEditQuestion.selectors.elBtnDraftQuestion );
982 -
983 - if ( ! elBtnDraftQuestion ) {
984 - return;
985 - }
986 -
987 - // Check if published to show confirm unpublish modal
988 - const statusEl = document.querySelector( BuilderEditQuestion.selectors.elQuestionStatus );
989 - const isPublished = statusEl && statusEl.classList.contains( 'publish' );
990 - if ( isPublished ) {
991 - const confirmMsg =
992 - elBtnDraftQuestion.dataset.confirmUnpublish ||
993 - 'Saving as draft will unpublish this item. Are you sure?';
994 - const result = await SweetAlert.fire( {
995 - title: confirmMsg,
996 - iconHtml: SWAL_ICON_TRASH_DRAFT,
997 - customClass: { icon: 'lp-cb-swal-icon-html' },
998 - showCloseButton: true,
999 - showCancelButton: true,
1000 - cancelButtonText: lpData.i18n.cancel,
1001 - confirmButtonText: lpData.i18n.yes,
1002 - reverseButtons: true,
1003 - } );
1004 -
1005 - if ( ! result.isConfirmed ) {
1006 - return;
1007 - }
1008 - }
1009 -
1010 - // Validate title before saving draft
1011 - if ( ! this.validateTitleBeforeUpdate() ) {
1012 - return;
1013 - }
1014 -
1015 - const elActionExpanded = document.querySelector(
1016 - BuilderEditQuestion.selectors.elQuestionActionExpanded
1017 - );
1018 -
1019 - lpUtils.lpSetLoadingEl( elActionExpanded, 1 );
1020 -
1021 - const questionData = this.getQuestionDataForUpdate();
1022 -
1023 - const dataSend = {
1024 - ...questionData,
1025 - action: 'builder_update_question',
1026 - args: {
1027 - id_url: 'builder-update-question',
1028 - },
1029 - question_status: 'draft',
1030 - };
1031 -
1032 - if ( typeof lpQuestionBuilder !== 'undefined' && lpQuestionBuilder.nonce ) {
1033 - dataSend.nonce = lpQuestionBuilder.nonce;
1034 - }
1035 -
1036 - const callBack = {
1037 - success: ( response ) => {
1038 - const { status, message, data } = response;
1039 - lpToastify.show( message, status );
1040 -
1041 - if ( status === 'success' ) {
1042 - // Update action button text
1043 - this.updateActionButtons( 'draft' );
1044 - this.syncPublishPanelStatus( data?.status || 'draft' );
1045 - this.updatePermalinkUIAfterSave( data );
1046 -
1047 - // Reset form state before potential redirect.
1048 - document.dispatchEvent( new CustomEvent( 'lp-course-builder-saved' ) );
1049 -
1050 - if ( data?.question_id_new ) {
1051 - const currentUrl = window.location.href;
1052 - window.location.href = currentUrl.replace(
1053 - /post-new\/?/,
1054 - `${ data.question_id_new }/`
1055 - );
1056 - }
1057 -
1058 - if ( data?.status ) {
1059 - const elStatus = document.querySelector(
1060 - BuilderEditQuestion.selectors.elQuestionStatus
1061 - );
1062 - if ( elStatus ) {
1063 - elStatus.className = 'question-status ' + data.status;
1064 - elStatus.textContent = data.status;
1065 - }
1066 -
1067 - if ( data.status === 'trash' || data.status === 'draft' ) {
1068 - const curriculumItem = document.querySelector(
1069 - `.question-item[data-question-id="${ questionData.question_id }"]`
1070 - );
1071 - if ( curriculumItem ) {
1072 - const elCurriculumQuestion = curriculumItem.closest( '.question' );
1073 - if ( elCurriculumQuestion ) {
1074 - elCurriculumQuestion.remove();
1075 - }
1076 - }
1077 - }
1078 - }
1079 - }
1080 - },
1081 - error: ( error ) => {
1082 - lpToastify.show( error.message || error, 'error' );
1083 - },
1084 - completed: () => {
1085 - lpUtils.lpSetLoadingEl( elActionExpanded, 0 );
1086 - },
1087 - };
1088 -
1089 - window.lpAJAXG.fetchAJAX( dataSend, callBack );
1090 - }
1091 -
1092 - async trashQuestion( args ) {
1093 - // Context check: only handle if on question edit page
1094 - if ( ! this.isQuestionContext() ) {
1095 - return;
1096 - }
1097 -
1098 - const { target } = args;
1099 - const elBtnTrashQuestion = target.closest( BuilderEditQuestion.selectors.elBtnTrashQuestion );
1100 - if ( ! elBtnTrashQuestion ) {
1101 - return;
1102 - }
1103 -
1104 - const result = await SweetAlert.fire( {
1105 - title: 'Are you sure you want to trash this question?',
1106 - iconHtml: SWAL_ICON_TRASH_DRAFT,
1107 - customClass: { icon: 'lp-cb-swal-icon-html' },
1108 - showCloseButton: true,
1109 - showCancelButton: true,
1110 - cancelButtonText: lpData.i18n.cancel,
1111 - confirmButtonText: lpData.i18n.yes,
1112 - reverseButtons: true,
1113 - } );
1114 -
1115 - if ( ! result.isConfirmed ) {
1116 - return;
1117 - }
1118 -
1119 - const elActionExpanded = document.querySelector(
1120 - BuilderEditQuestion.selectors.elQuestionActionExpanded
1121 - );
1122 - lpUtils.lpSetLoadingEl( elActionExpanded, 1 );
1123 -
1124 - const questionData = this.getQuestionDataForUpdate();
1125 - const dataSend = {
1126 - action: 'move_trash_question',
1127 - args: {
1128 - id_url: 'move-trash-question',
1129 - },
1130 - question_id: questionData.question_id || 0,
1131 - };
1132 -
1133 - const callBack = {
1134 - success: ( response ) => {
1135 - const { status, message, data } = response;
1136 - lpToastify.show( message, status );
1137 -
1138 - if ( status === 'success' ) {
1139 - if ( data?.redirect_url ) {
1140 - window.location.href = data.redirect_url;
1141 - }
1142 -
1143 - if ( data?.status ) {
1144 - const elStatus = document.querySelector(
1145 - BuilderEditQuestion.selectors.elQuestionStatus
1146 - );
1147 - if ( elStatus ) {
1148 - elStatus.className = 'question-status ' + data.status;
1149 - elStatus.textContent = data.status;
1150 - }
1151 - if ( data.status === 'trash' || data.status === 'draft' ) {
1152 - const curriculumItem = document.querySelector(
1153 - `.question-item[data-question-id="${ questionData.question_id }"]`
1154 - );
1155 - if ( curriculumItem ) {
1156 - const elCurriculumQuestion = curriculumItem.closest( '.question' );
1157 - if ( elCurriculumQuestion ) {
1158 - elCurriculumQuestion.remove();
1159 - }
1160 - }
1161 - }
1162 - }
1163 - }
1164 - },
1165 - error: ( error ) => {
1166 - lpToastify.show( error.message || error, 'error' );
1167 - },
1168 - completed: () => {
1169 - lpUtils.lpSetLoadingEl( elActionExpanded, 0 );
1170 - },
1171 - };
1172 -
1173 - window.lpAJAXG.fetchAJAX( dataSend, callBack );
1174 - }
1175 -
1176 - async duplicateQuestion( args ) {
1177 - // Context check: only handle if on question edit page
1178 - if ( ! this.isQuestionContext() ) {
1179 - return;
1180 - }
1181 -
1182 - const { target } = args;
1183 - const elBtnDuplicateQuestion = target.closest(
1184 - BuilderEditQuestion.selectors.elBtnDuplicateQuestion
1185 - );
1186 - if ( ! elBtnDuplicateQuestion ) {
1187 - return;
1188 - }
1189 -
1190 - const questionData = this.getQuestionDataForUpdate();
1191 - const questionId = parseInt( questionData?.question_id, 10 ) || 0;
1192 - if ( ! questionId ) {
1193 - return;
1194 - }
1195 -
1196 - const result = await SweetAlert.fire( {
1197 - title: elBtnDuplicateQuestion.dataset.title || 'Are you sure?',
1198 - text:
1199 - elBtnDuplicateQuestion.dataset.content ||
1200 - 'Are you sure you want to duplicate this question?',
1201 - iconHtml: SWAL_ICON_DUPLICATE,
1202 - customClass: { icon: 'lp-cb-swal-icon-html' },
1203 - showCloseButton: true,
1204 - showCancelButton: true,
1205 - cancelButtonText: lpData.i18n.cancel,
1206 - confirmButtonText: lpData.i18n.yes,
1207 - reverseButtons: true,
1208 - } );
1209 -
1210 - if ( ! result.isConfirmed ) {
1211 - return;
1212 - }
1213 -
1214 - const elActionExpanded = document.querySelector(
1215 - BuilderEditQuestion.selectors.elQuestionActionExpanded
1216 - );
1217 -
1218 - lpUtils.lpSetLoadingEl( elActionExpanded, 1 );
1219 -
1220 - const dataSend = {
1221 - action: 'duplicate_question',
1222 - args: {
1223 - id_url: 'duplicate-question',
1224 - },
1225 - question_id: questionId,
1226 - };
1227 -
1228 - const callBack = {
1229 - success: ( response ) => {
1230 - const { status, message, data } = response;
1231 - lpToastify.show( message || 'Duplicated successfully!', status );
1232 -
1233 - if ( status === 'success' && data?.redirect_url ) {
1234 - window.location.href = data.redirect_url;
1235 - }
1236 - },
1237 - error: ( error ) => {
1238 - lpToastify.show( error.message || error, 'error' );
1239 - },
1240 - completed: () => {
1241 - lpUtils.lpSetLoadingEl( elActionExpanded, 0 );
1242 - },
1243 - };
1244 -
1245 - window.lpAJAXG.fetchAJAX( dataSend, callBack );
1246 - }
1247 -}
1 +import * as lpUtils from 'lpAssetsJsPath/utils.js';
2 +import * as lpToastify from 'lpAssetsJsPath/lpToastify.js';
3 +import SweetAlert from 'sweetalert2';
4 +import { EditQuestion } from 'lpAssetsJsPath/admin/edit-question.js';
5 +import { SWAL_ICON_DUPLICATE, SWAL_ICON_TRASH_DRAFT } from '../swal-icons.js';
6 +import { getFormState } from '../builder-form-state.js';
7 +
8 +export class BuilderEditQuestion {
9 + constructor() {
10 + this.editQuestion = null;
11 + // Events use document-level event delegation, so always register them
12 + // The page context check happens in individual handlers via target.closest()
13 + this.init();
14 + }
15 +
16 + static selectors = {
17 + elDataQuestion: '.cb-section__question-edit',
18 + elBtnUpdateQuestion: '.cb-btn-update',
19 + elBtnDraftQuestion: '.cb-btn-draft',
20 + elBtnDuplicateQuestion: '.cb-btn-duplicate-question',
21 + elBtnTrashQuestion: '.cb-btn-trash',
22 + elBtnMainAction: '.cb-btn-main-action',
23 + elQuestionStatus: '.question-status',
24 + idTitle: 'title',
25 + idDescEditor: 'question_description_editor',
26 + elPublishStatusSelect: '#cb-question-publish-status',
27 + elFormSetting: '.lp-form-setting-question',
28 + // Permalink component
29 + elPermalinkDisplay: '.cb-permalink-display',
30 + elPermalinkEditor: '.cb-permalink-editor',
31 + elPermalinkEditBtn: '.cb-permalink-edit-btn',
32 + elPermalinkOkBtn: '.cb-permalink-ok-btn',
33 + elPermalinkCancelBtn: '.cb-permalink-cancel-btn',
34 + elPermalinkSlugInput: '.cb-permalink-slug-input',
35 + elPermalinkUrl: '.cb-permalink-url',
36 + elPermalinkBaseUrl: '#cb-permalink-base-url',
37 + // Question edit selectors
38 + elEditQuestionWrap: '.lp-edit-question-wrap',
39 + elQuestionEditMain: '.lp-question-edit-main',
40 + // Tab handling selectors
41 + elCBHorizontalTabs: '.lp-cb-tabs__item',
42 + elCBTabPanels: '.lp-cb-tab-panel',
43 + // Dropdown selectors
44 + elDropdownToggle: '.cb-btn-dropdown-toggle',
45 + elDropdownMenu: '.cb-dropdown-menu',
46 + elDropdownItem: '.cb-dropdown-item',
47 + elHeaderActionsDropdown: '.cb-header-actions-dropdown',
48 + elQuestionActionExpanded: '.course-action-expanded',
49 + };
50 +
51 + init() {
52 + this.initQuestionAnswersSettings();
53 + this.initTabs();
54 + this.initHeaderActionsDropdown();
55 + this.syncHeaderActionWithPublishPanel();
56 + this.events();
57 + }
58 +
59 + /**
60 + * Initialize header actions dropdown (toggle behavior)
61 + */
62 + initHeaderActionsDropdown() {
63 + // Close dropdown when clicking outside
64 + document.addEventListener( 'click', ( e ) => {
65 + const dropdown = document.querySelector(
66 + BuilderEditQuestion.selectors.elHeaderActionsDropdown
67 + );
68 + if ( dropdown && ! dropdown.contains( e.target ) ) {
69 + const menu = dropdown.querySelector( BuilderEditQuestion.selectors.elDropdownMenu );
70 + const toggle = dropdown.querySelector( BuilderEditQuestion.selectors.elDropdownToggle );
71 + if ( menu ) {
72 + menu.classList.remove( 'is-open' );
73 + }
74 + if ( toggle ) {
75 + toggle.setAttribute( 'aria-expanded', 'false' );
76 + }
77 + }
78 + } );
79 + }
80 +
81 + /**
82 + * Handle dropdown toggle click
83 + */
84 + handleDropdownToggle( args ) {
85 + if ( this.hasPublishDrivenSingleAction() ) {
86 + return;
87 + }
88 +
89 + const { target } = args;
90 + const toggleBtn = target.closest( BuilderEditQuestion.selectors.elDropdownToggle );
91 +
92 + if ( ! toggleBtn ) {
93 + return;
94 + }
95 +
96 + const dropdown = toggleBtn.closest( BuilderEditQuestion.selectors.elHeaderActionsDropdown );
97 + if ( ! dropdown ) {
98 + return;
99 + }
100 +
101 + const menu = dropdown.querySelector( BuilderEditQuestion.selectors.elDropdownMenu );
102 + if ( menu ) {
103 + menu.classList.toggle( 'is-open' );
104 + const isOpen = menu.classList.contains( 'is-open' );
105 + toggleBtn.setAttribute( 'aria-expanded', isOpen ? 'true' : 'false' );
106 + }
107 + }
108 +
109 + /**
110 + * Handle dropdown item click (Save Draft, Publish from dropdown menu)
111 + */
112 + handleDropdownItemClick( args ) {
113 + // Context check: only handle if on question edit page
114 + if ( ! this.isQuestionContext() ) {
115 + return;
116 + }
117 +
118 + if ( this.hasPublishDrivenSingleAction() ) {
119 + return;
120 + }
121 +
122 + const { target } = args;
123 + const dropdownItem = target.closest( BuilderEditQuestion.selectors.elDropdownItem );
124 +
125 + if ( ! dropdownItem ) {
126 + return;
127 + }
128 +
129 + // Skip if this is trash button - it has its own handler
130 + if (
131 + dropdownItem.classList.contains( 'cb-btn-trash' ) ||
132 + dropdownItem.classList.contains( 'cb-btn-duplicate-question' )
133 + ) {
134 + return;
135 + }
136 +
137 + const status = dropdownItem.dataset.status;
138 + if ( ! status ) {
139 + return;
140 + }
141 +
142 + // Close the dropdown menu
143 + const menu = dropdownItem.closest( BuilderEditQuestion.selectors.elDropdownMenu );
144 + if ( menu ) {
145 + menu.classList.remove( 'is-open' );
146 + }
147 +
148 + // Save with the specified status
149 + this.saveQuestionWithStatus( dropdownItem, status );
150 + }
151 +
152 + /**
153 + * Save question with specified status (publish/draft)
154 + * @param {HTMLElement} btnEl - The button element that was clicked
155 + * @param {string} status - The status to save (publish/draft)
156 + */
157 + async saveQuestionWithStatus( btnEl, status ) {
158 + const canContinue = await this.confirmUnpublishIfNeeded( status, btnEl );
159 + if ( ! canContinue ) {
160 + return;
161 + }
162 +
163 + // Validate title before saving
164 + if ( ! this.validateTitleBeforeUpdate() ) {
165 + return;
166 + }
167 +
168 + lpUtils.lpSetLoadingEl( btnEl, 1 );
169 +
170 + const questionData = this.getQuestionDataForUpdate();
171 +
172 + const dataSend = {
173 + ...questionData,
174 + action: 'builder_update_question',
175 + args: {
176 + id_url: 'builder-update-question',
177 + },
178 + question_status: status,
179 + };
180 +
181 + if ( typeof lpQuestionBuilder !== 'undefined' && lpQuestionBuilder.nonce ) {
182 + dataSend.nonce = lpQuestionBuilder.nonce;
183 + }
184 +
185 + const callBack = {
186 + success: ( response ) => {
187 + const { status: respStatus, message, data } = response;
188 + lpToastify.show( message, respStatus );
189 +
190 + if ( respStatus === 'success' ) {
191 + // Update action button text
192 + this.updateActionButtons( data?.status || status );
193 + this.syncPublishPanelStatus( data?.status || status );
194 + this.updatePermalinkUIAfterSave( data );
195 +
196 + // Reset form state before potential redirect.
197 + document.dispatchEvent( new CustomEvent( 'lp-course-builder-saved' ) );
198 +
199 + if ( data?.redirect_url ) {
200 + window.location.href = data.redirect_url;
201 + }
202 +
203 + if ( data?.status ) {
204 + const elStatus = document.querySelector(
205 + BuilderEditQuestion.selectors.elQuestionStatus
206 + );
207 + if ( elStatus ) {
208 + elStatus.className = 'question-status ' + data.status;
209 + elStatus.textContent = data.status;
210 + }
211 +
212 + if ( data.status === 'trash' || data.status === 'draft' ) {
213 + const curriculumItem = document.querySelector(
214 + `.question-item[data-question-id="${ questionData.question_id }"]`
215 + );
216 + if ( curriculumItem ) {
217 + const elCurriculumQuestion = curriculumItem.closest( '.question' );
218 + if ( elCurriculumQuestion ) {
219 + elCurriculumQuestion.remove();
220 + }
221 + }
222 + }
223 + }
224 + }
225 + },
226 + error: ( error ) => {
227 + lpToastify.show( error.message || error, 'error' );
228 + },
229 + completed: () => {
230 + lpUtils.lpSetLoadingEl( btnEl, 0 );
231 + },
232 + };
233 +
234 + window.lpAJAXG.fetchAJAX( dataSend, callBack );
235 + }
236 +
237 + /**
238 + * Initialize horizontal tabs for client-side tab switching
239 + */
240 + initTabs() {
241 + const tabs = document.querySelectorAll( BuilderEditQuestion.selectors.elCBHorizontalTabs );
242 + if ( tabs.length === 0 ) {
243 + return;
244 + }
245 +
246 + // Activate first tab by default if none is active
247 + const activeTab = document.querySelector(
248 + `${ BuilderEditQuestion.selectors.elCBHorizontalTabs }.is-active`
249 + );
250 + if ( ! activeTab && tabs.length > 0 ) {
251 + tabs[ 0 ].classList.add( 'is-active' );
252 + const section = tabs[ 0 ].getAttribute( 'data-tab-section' );
253 + if ( section ) {
254 + const panel = document.querySelector(
255 + `${ BuilderEditQuestion.selectors.elCBTabPanels }[data-section="${ section }"]`
256 + );
257 + if ( panel ) {
258 + panel.classList.remove( 'lp-hidden' );
259 + }
260 + }
261 + }
262 + }
263 +
264 + /**
265 + * Handle horizontal tab click for client-side tab switching
266 + */
267 + handleTabClick( args ) {
268 + const { e, target } = args;
269 + const tabLink = target.closest( BuilderEditQuestion.selectors.elCBHorizontalTabs );
270 +
271 + if ( ! tabLink ) {
272 + return;
273 + }
274 +
275 + e.preventDefault();
276 +
277 + const section = tabLink.getAttribute( 'data-tab-section' );
278 + if ( ! section ) {
279 + return;
280 + }
281 +
282 + // Update active tab
283 + const allTabs = document.querySelectorAll( BuilderEditQuestion.selectors.elCBHorizontalTabs );
284 + allTabs.forEach( ( tab ) => tab.classList.remove( 'is-active' ) );
285 + tabLink.classList.add( 'is-active' );
286 +
287 + const url = new URL( window.location.href );
288 + url.searchParams.set( 'tab', section );
289 + window.history.replaceState( {}, '', url );
290 +
291 + // Show/hide panels
292 + const allPanels = document.querySelectorAll( BuilderEditQuestion.selectors.elCBTabPanels );
293 + allPanels.forEach( ( panel ) => {
294 + if ( panel.getAttribute( 'data-section' ) === section ) {
295 + panel.classList.remove( 'lp-hidden' );
296 + } else {
297 + panel.classList.add( 'lp-hidden' );
298 + }
299 + } );
300 + }
301 +
302 + /**
303 + * Initialize Question Answers Settings
304 + * This will init EditQuestion class for the question answer management
305 + */
306 + initQuestionAnswersSettings() {
307 + lpUtils.lpOnElementReady(
308 + BuilderEditQuestion.selectors.elQuestionEditMain,
309 + ( elQuestionEditMain ) => {
310 + // Initialize EditQuestion for question answer editing
311 + if ( ! this.editQuestion ) {
312 + this.editQuestion = new EditQuestion();
313 + this.editQuestion.init();
314 + }
315 +
316 + // Init sortable for question answers
317 + if ( this.editQuestion ) {
318 + this.editQuestion.sortAbleQuestionAnswer( elQuestionEditMain );
319 + }
320 + }
321 + );
322 + }
323 +
324 + /**
325 + * Re-initialize when question type changes
326 + */
327 + reinitQuestionHandlers( elQuestionEditMain ) {
328 + if ( this.editQuestion && elQuestionEditMain ) {
329 + this.editQuestion.sortAbleQuestionAnswer( elQuestionEditMain );
330 + this.editQuestion.initTinyMCE();
331 + }
332 + }
333 +
334 + /**
335 + * Re-initialize for popup context
336 + * This is called when popup is opened multiple times to ensure
337 + * TinyMCE and other handlers are properly re-initialized
338 + *
339 + * @param {HTMLElement} container - The popup container element
340 + */
341 + reinit( container ) {
342 + const elQuestionEditMain = container
343 + ? container.querySelector( BuilderEditQuestion.selectors.elQuestionEditMain )
344 + : document.querySelector( BuilderEditQuestion.selectors.elQuestionEditMain );
345 +
346 + if ( ! elQuestionEditMain ) {
347 + return;
348 + }
349 +
350 + // Re-create EditQuestion instance to ensure fresh initialization
351 + // This is necessary because TinyMCE instances were destroyed when popup closed
352 + if ( this.editQuestion ) {
353 + // Destroy existing TinyMCE instances in the container first
354 + if ( typeof tinymce !== 'undefined' && container ) {
355 + const textareas = container.querySelectorAll( 'textarea.lp-meta-box__editor' );
356 + textareas.forEach( ( textarea ) => {
357 + const editorId = textarea.id;
358 + if ( editorId ) {
359 + const editor = tinymce.get( editorId );
360 + if ( editor ) {
361 + editor.remove();
362 + }
363 + if ( typeof wp !== 'undefined' && wp.editor && wp.editor.remove ) {
364 + wp.editor.remove( editorId );
365 + }
366 + }
367 + } );
368 + }
369 + }
370 +
371 + // Create fresh EditQuestion instance
372 + this.editQuestion = new EditQuestion();
373 + this.editQuestion.init();
374 +
375 + // Re-init sortable and TinyMCE
376 + this.editQuestion.sortAbleQuestionAnswer( elQuestionEditMain );
377 +
378 + // Use setTimeout to ensure DOM is ready for TinyMCE
379 + setTimeout( () => {
380 + if ( this.editQuestion ) {
381 + this.editQuestion.initTinyMCE();
382 + }
383 + }, 100 );
384 + }
385 +
386 + events() {
387 + if ( BuilderEditQuestion._loadedEvents ) {
388 + return;
389 + }
390 + BuilderEditQuestion._loadedEvents = true;
391 +
392 + lpUtils.eventHandlers( 'click', [
393 + {
394 + selector: BuilderEditQuestion.selectors.elBtnMainAction,
395 + class: this,
396 + callBack: this.updateQuestion.name,
397 + },
398 + {
399 + selector: BuilderEditQuestion.selectors.elBtnDraftQuestion,
400 + class: this,
401 + callBack: this.saveDraftQuestion.name,
402 + },
403 + {
404 + selector: BuilderEditQuestion.selectors.elBtnDuplicateQuestion,
405 + class: this,
406 + callBack: this.duplicateQuestion.name,
407 + },
408 + {
409 + selector: BuilderEditQuestion.selectors.elBtnTrashQuestion,
410 + class: this,
411 + callBack: this.trashQuestion.name,
412 + },
413 + {
414 + selector: BuilderEditQuestion.selectors.elCBHorizontalTabs,
415 + class: this,
416 + callBack: this.handleTabClick.name,
417 + },
418 + {
419 + selector: BuilderEditQuestion.selectors.elDropdownToggle,
420 + class: this,
421 + callBack: this.handleDropdownToggle.name,
422 + },
423 + {
424 + selector: BuilderEditQuestion.selectors.elDropdownItem,
425 + class: this,
426 + callBack: this.handleDropdownItemClick.name,
427 + },
428 + {
429 + selector: BuilderEditQuestion.selectors.elPermalinkEditBtn,
430 + class: this,
431 + callBack: this.handlePermalinkEdit.name,
432 + },
433 + {
434 + selector: BuilderEditQuestion.selectors.elPermalinkOkBtn,
435 + class: this,
436 + callBack: this.handlePermalinkOk.name,
437 + },
438 + {
439 + selector: BuilderEditQuestion.selectors.elPermalinkCancelBtn,
440 + class: this,
441 + callBack: this.handlePermalinkCancel.name,
442 + },
443 + ] );
444 +
445 + lpUtils.eventHandlers( 'change', [
446 + {
447 + selector: BuilderEditQuestion.selectors.elPublishStatusSelect,
448 + class: this,
449 + callBack: this.handlePublishStatusChange.name,
450 + },
451 + ] );
452 + }
453 +
454 + handlePublishStatusChange() {
455 + this.syncHeaderActionWithPublishPanel();
456 + getFormState().markAsChanged();
457 + }
458 +
459 + /**
460 + * Validate title is not empty before update
461 + * @return {boolean} True if valid, false if invalid
462 + */
463 + validateTitleBeforeUpdate() {
464 + const titleInput = document.getElementById( BuilderEditQuestion.selectors.idTitle );
465 + if ( ! titleInput ) {
466 + return true;
467 + }
468 +
469 + const title = titleInput.value.trim();
470 + if ( ! title ) {
471 + lpToastify.show( 'Question title is required.', 'error' );
472 + titleInput.focus();
473 + return false;
474 + }
475 + return true;
476 + }
477 +
478 + /**
479 + * Update action buttons after status change, matching course edit logic.
480 + * Updates both main button and dropdown items based on new status.
481 + * @param {string} newStatus - The new question status
482 + */
483 + updateActionButtons( newStatus ) {
484 + const dropdown = document.querySelector(
485 + BuilderEditQuestion.selectors.elHeaderActionsDropdown
486 + );
487 + if ( ! dropdown ) return;
488 +
489 + const mainBtn = dropdown.querySelector( '.cb-btn-main-action' );
490 + const dropdownMenu = dropdown.querySelector( BuilderEditQuestion.selectors.elDropdownMenu );
491 + if ( ! mainBtn ) return;
492 +
493 + if ( this.hasPublishDrivenSingleAction() ) {
494 + this.syncHeaderActionWithPublishPanel();
495 + return;
496 + }
497 +
498 + if ( ! dropdownMenu ) return;
499 +
500 + // Status configuration for button labels and classes
501 + const statusConfig = {
502 + publish: {
503 + mainLabel: mainBtn.dataset.titleUpdate || 'Update',
504 + mainClass: 'cb-btn-update',
505 + mainStatus: 'publish',
506 + dropdownLabel: mainBtn.dataset.titleDraft || 'Save Draft',
507 + dropdownClass: 'cb-btn-darft',
508 + dropdownStatus: 'draft',
509 + dropdownIcon: 'dashicons-media-default',
510 + },
511 + draft: {
512 + mainLabel: mainBtn.dataset.titleDraft || 'Save Draft',
513 + mainClass: 'cb-btn-darft',
514 + mainStatus: 'draft',
515 + dropdownLabel: mainBtn.dataset.titlePublish || 'Publish',
516 + dropdownClass: 'cb-btn-publish',
517 + dropdownStatus: 'publish',
518 + dropdownIcon: 'dashicons-visibility',
519 + },
520 + pending: {
521 + mainLabel: 'Submit for Review',
522 + mainClass: 'cb-btn-pending',
523 + mainStatus: 'pending',
524 + dropdownLabel: mainBtn.dataset.titleDraft || 'Save Draft',
525 + dropdownClass: 'cb-btn-darft',
526 + dropdownStatus: 'draft',
527 + dropdownIcon: 'dashicons-media-default',
528 + },
529 + };
530 +
531 + const config = statusConfig[ newStatus ] || statusConfig.draft;
532 +
533 + // Update main button
534 + mainBtn.className = `${ config.mainClass } cb-btn-primary cb-btn-main-action`;
535 + mainBtn.dataset.status = config.mainStatus;
536 + mainBtn.textContent = config.mainLabel;
537 +
538 + // Update dropdown item (first item, excluding trash)
539 + const dropdownItems = dropdownMenu.querySelectorAll( '.cb-dropdown-item:not(.cb-btn-trash)' );
540 + if ( dropdownItems.length > 0 ) {
541 + const firstItem = dropdownItems[ 0 ];
542 + firstItem.className = `cb-dropdown-item ${ config.dropdownClass }`;
543 + firstItem.dataset.status = config.dropdownStatus;
544 + firstItem.innerHTML = `<span class="dashicons ${ config.dropdownIcon }"></span>${ config.dropdownLabel }`;
545 + }
546 +
547 + // Update dropdown data-current-status
548 + dropdown.dataset.currentStatus = newStatus;
549 + }
550 +
551 + /**
552 + * Check if we're in question edit context
553 + * @return {boolean}
554 + */
555 + isQuestionContext() {
556 + return !! document.querySelector( BuilderEditQuestion.selectors.elDataQuestion );
557 + }
558 +
559 + getQuestionDataForUpdate() {
560 + const data = {};
561 +
562 + const wrapperEl = document.querySelector( BuilderEditQuestion.selectors.elDataQuestion );
563 +
564 + data.question_id = wrapperEl ? parseInt( wrapperEl.dataset.questionId ) || 0 : 0;
565 +
566 + const titleInput = document.getElementById( BuilderEditQuestion.selectors.idTitle );
567 + data.question_title = titleInput ? titleInput.value : '';
568 +
569 + const descEditor = document.getElementById( BuilderEditQuestion.selectors.idDescEditor );
570 + data.question_description = descEditor ? descEditor.value : '';
571 +
572 + if ( typeof tinymce !== 'undefined' ) {
573 + const editor = tinymce.get( BuilderEditQuestion.selectors.idDescEditor );
574 + if ( editor ) {
575 + data.question_description = editor.getContent();
576 + }
577 + }
578 +
579 + const permalinkInput = document.querySelector(
580 + BuilderEditQuestion.selectors.elPermalinkSlugInput
581 + );
582 + if ( permalinkInput && permalinkInput.value ) {
583 + data.question_permalink = permalinkInput.value;
584 + }
585 +
586 + const elFormSetting = document.querySelector( BuilderEditQuestion.selectors.elFormSetting );
587 +
588 + if ( elFormSetting ) {
589 + data.question_settings = true;
590 + const formElements = elFormSetting.querySelectorAll( 'input, select, textarea' );
591 +
592 + formElements.forEach( ( element ) => {
593 + const name = element.name || element.id;
594 +
595 + if ( ! name ) {
596 + return;
597 + }
598 +
599 + if ( name === 'learnpress_meta_box_nonce' || name === '_wp_http_referer' ) {
600 + return;
601 + }
602 +
603 + if ( element.type === 'checkbox' ) {
604 + const fieldName = name.replace( '[]', '' );
605 + if ( ! data.hasOwnProperty( fieldName ) ) {
606 + data[ fieldName ] = element.checked ? 'yes' : 'no';
607 + }
608 + } else if ( element.type === 'radio' ) {
609 + if ( element.checked ) {
610 + const fieldName = name.replace( '[]', '' );
611 + data[ fieldName ] = element.value;
612 + }
613 + } else if ( element.type === 'file' ) {
614 + const fieldName = name.replace( '[]', '' );
615 + if ( element.files && element.files.length > 0 ) {
616 + data[ fieldName ] = element.files;
617 + }
618 + } else {
619 + const fieldName = name.replace( '[]', '' );
620 +
621 + if ( name.endsWith( '[]' ) ) {
622 + if ( ! data.hasOwnProperty( fieldName ) ) {
623 + data[ fieldName ] = [];
624 + }
625 +
626 + if ( Array.isArray( data[ fieldName ] ) ) {
627 + data[ fieldName ].push( element.value );
628 + }
629 + } else {
630 + if ( ! data.hasOwnProperty( fieldName ) ) {
631 + data[ fieldName ] = element.value;
632 + }
633 + }
634 + }
635 + } );
636 +
637 + Object.keys( data ).forEach( ( key ) => {
638 + if ( Array.isArray( data[ key ] ) ) {
639 + data[ key ] = data[ key ].join( ',' );
640 + }
641 + } );
642 + }
643 +
644 + return data;
645 + }
646 +
647 + getStatusFromPublishPanel( fallbackStatus = 'publish' ) {
648 + const statusSelect = document.querySelector(
649 + BuilderEditQuestion.selectors.elPublishStatusSelect
650 + );
651 + if ( ! statusSelect ) {
652 + return fallbackStatus;
653 + }
654 +
655 + const selectedStatus = statusSelect.value;
656 + if ( selectedStatus === 'publish' || selectedStatus === 'draft' ) {
657 + return selectedStatus;
658 + }
659 +
660 + return fallbackStatus;
661 + }
662 +
663 + hasPublishDrivenSingleAction() {
664 + if ( ! document.querySelector( BuilderEditQuestion.selectors.elPublishStatusSelect ) ) {
665 + return false;
666 + }
667 +
668 + const wrapperEl = document.querySelector( BuilderEditQuestion.selectors.elDataQuestion );
669 + const questionId = wrapperEl ? parseInt( wrapperEl.dataset.questionId ) || 0 : 0;
670 +
671 + return questionId > 0;
672 + }
673 +
674 + syncHeaderActionWithPublishPanel() {
675 + if ( ! this.hasPublishDrivenSingleAction() ) {
676 + return;
677 + }
678 +
679 + const dropdown = document.querySelector(
680 + BuilderEditQuestion.selectors.elHeaderActionsDropdown
681 + );
682 + if ( ! dropdown ) {
683 + return;
684 + }
685 +
686 + const mainBtn = dropdown.querySelector( '.cb-btn-main-action' );
687 + if ( ! mainBtn ) {
688 + return;
689 + }
690 +
691 + const toggleBtn = dropdown.querySelector( BuilderEditQuestion.selectors.elDropdownToggle );
692 + const dropdownMenu = dropdown.querySelector( BuilderEditQuestion.selectors.elDropdownMenu );
693 + const status = this.getStatusFromPublishPanel( mainBtn.dataset.status || 'publish' );
694 +
695 + mainBtn.classList.remove( 'cb-btn-darft', 'cb-btn-publish', 'cb-btn-pending' );
696 + mainBtn.classList.add( 'cb-btn-update', 'cb-btn-primary', 'cb-btn-main-action' );
697 + mainBtn.textContent = mainBtn.dataset.titleUpdate || 'Update';
698 + mainBtn.dataset.status = status;
699 + dropdown.dataset.currentStatus = status;
700 + dropdown.classList.add( 'cb-header-actions-dropdown--single' );
701 +
702 + if ( toggleBtn ) {
703 + toggleBtn.style.display = 'none';
704 + }
705 + if ( dropdownMenu ) {
706 + dropdownMenu.style.display = 'none';
707 + }
708 + }
709 +
710 + syncPublishPanelStatus( status ) {
711 + const statusSelect = document.querySelector(
712 + BuilderEditQuestion.selectors.elPublishStatusSelect
713 + );
714 + if ( ! statusSelect ) {
715 + return;
716 + }
717 +
718 + statusSelect.value = status === 'publish' ? 'publish' : 'draft';
719 + this.syncHeaderActionWithPublishPanel();
720 + }
721 +
722 + updatePermalinkUIAfterSave( data = {} ) {
723 + const slugInput = document.querySelector( BuilderEditQuestion.selectors.elPermalinkSlugInput );
724 + const urlLink = document.querySelector( BuilderEditQuestion.selectors.elPermalinkUrl );
725 + const baseUrlInput = document.querySelector( BuilderEditQuestion.selectors.elPermalinkBaseUrl );
726 + const permalinkDisplayUrl = this.buildPermalinkDisplayUrl(
727 + baseUrlInput ? baseUrlInput.value : '',
728 + data?.question_slug,
729 + data?.question_permalink
730 + );
731 +
732 + if ( slugInput && data?.question_slug ) {
733 + slugInput.value = data.question_slug;
734 + slugInput.dataset.originalValue = data.question_slug;
735 + }
736 +
737 + if ( urlLink && data?.question_permalink ) {
738 + urlLink.href = data.question_permalink;
739 + urlLink.textContent = permalinkDisplayUrl || data.question_permalink;
740 + } else if ( urlLink && permalinkDisplayUrl ) {
741 + urlLink.textContent = permalinkDisplayUrl;
742 + }
743 + }
744 +
745 + slugify( str ) {
746 + return ( str || '' )
747 + .toString()
748 + .normalize( 'NFD' )
749 + .replace( /[\u0300-\u036f]/g, '' )
750 + .replace( /đ/g, 'd' )
751 + .replace( /Đ/g, 'D' )
752 + .toLowerCase()
753 + .replace( /\s+/g, '-' )
754 + .replace( /[^\w-]+/g, '' )
755 + .replace( /--+/g, '-' )
756 + .replace( /^-+/, '' )
757 + .replace( /-+$/, '' );
758 + }
759 +
760 + buildPermalinkDisplayUrl( baseUrl = '', slug = '', fallbackUrl = '' ) {
761 + const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl : '';
762 + const normalizedSlug = typeof slug === 'string' ? slug.trim() : '';
763 +
764 + if ( normalizedBaseUrl && normalizedSlug ) {
765 + return `${ normalizedBaseUrl }${ normalizedSlug }`;
766 + }
767 +
768 + return typeof fallbackUrl === 'string' ? fallbackUrl : '';
769 + }
770 +
771 + handlePermalinkEdit( args ) {
772 + if ( ! this.isQuestionContext() ) {
773 + return;
774 + }
775 +
776 + const { e } = args;
777 + if ( e ) e.preventDefault();
778 +
779 + const display = document.querySelector( BuilderEditQuestion.selectors.elPermalinkDisplay );
780 + const editor = document.querySelector( BuilderEditQuestion.selectors.elPermalinkEditor );
781 + const input = document.querySelector( BuilderEditQuestion.selectors.elPermalinkSlugInput );
782 +
783 + if ( ! display || ! editor || ! input ) return;
784 +
785 + input.dataset.originalValue = input.value;
786 + display.classList.add( 'lp-hidden' );
787 + editor.classList.remove( 'lp-hidden' );
788 + input.focus();
789 + input.select();
790 + }
791 +
792 + handlePermalinkOk( args ) {
793 + if ( ! this.isQuestionContext() ) {
794 + return;
795 + }
796 +
797 + const { e } = args;
798 + if ( e ) e.preventDefault();
799 +
800 + const display = document.querySelector( BuilderEditQuestion.selectors.elPermalinkDisplay );
801 + const editor = document.querySelector( BuilderEditQuestion.selectors.elPermalinkEditor );
802 + const input = document.querySelector( BuilderEditQuestion.selectors.elPermalinkSlugInput );
803 + const urlLink = document.querySelector( BuilderEditQuestion.selectors.elPermalinkUrl );
804 + const baseUrlInput = document.querySelector( BuilderEditQuestion.selectors.elPermalinkBaseUrl );
805 +
806 + if ( ! display || ! editor || ! input || ! urlLink ) return;
807 +
808 + let newSlug = this.slugify( input.value.trim() );
809 + if ( ! newSlug ) {
810 + newSlug = input.dataset.originalValue || 'question';
811 + }
812 +
813 + input.value = newSlug;
814 + const baseUrl = baseUrlInput ? baseUrlInput.value : '';
815 + const newUrl = this.buildPermalinkDisplayUrl( baseUrl, newSlug, urlLink.textContent || '' );
816 +
817 + // Keep href as the current saved link and only update display text.
818 + urlLink.textContent = newUrl;
819 + editor.classList.add( 'lp-hidden' );
820 + display.classList.remove( 'lp-hidden' );
821 +
822 + if ( newSlug !== input.dataset.originalValue ) {
823 + getFormState().markAsChanged();
824 + }
825 + }
826 +
827 + handlePermalinkCancel( args ) {
828 + if ( ! this.isQuestionContext() ) {
829 + return;
830 + }
831 +
832 + const { e } = args;
833 + if ( e ) e.preventDefault();
834 +
835 + const display = document.querySelector( BuilderEditQuestion.selectors.elPermalinkDisplay );
836 + const editor = document.querySelector( BuilderEditQuestion.selectors.elPermalinkEditor );
837 + const input = document.querySelector( BuilderEditQuestion.selectors.elPermalinkSlugInput );
838 +
839 + if ( ! display || ! editor || ! input ) return;
840 +
841 + input.value = input.dataset.originalValue || '';
842 + editor.classList.add( 'lp-hidden' );
843 + display.classList.remove( 'lp-hidden' );
844 + }
845 +
846 + async updateQuestion( args ) {
847 + // Context check: only handle if on question edit page
848 + if ( ! this.isQuestionContext() ) {
849 + return;
850 + }
851 +
852 + const { target } = args;
853 + const elBtnMainAction = target.closest( BuilderEditQuestion.selectors.elBtnMainAction );
854 +
855 + if ( ! elBtnMainAction ) {
856 + return;
857 + }
858 +
859 + // Validate title before update
860 + if ( ! this.validateTitleBeforeUpdate() ) {
861 + return;
862 + }
863 +
864 + // Resolve status from Overview publish panel first, fallback to button data-status.
865 + const targetStatus = this.getStatusFromPublishPanel(
866 + elBtnMainAction.dataset.status || 'publish'
867 + );
868 + const canContinue = await this.confirmUnpublishIfNeeded( targetStatus, elBtnMainAction );
869 + if ( ! canContinue ) {
870 + return;
871 + }
872 +
873 + lpUtils.lpSetLoadingEl( elBtnMainAction, 1 );
874 +
875 + const questionData = this.getQuestionDataForUpdate();
876 +
877 + const dataSend = {
878 + ...questionData,
879 + action: 'builder_update_question',
880 + args: {
881 + id_url: 'builder-update-question',
882 + },
883 + question_status: targetStatus,
884 + };
885 +
886 + if ( typeof lpQuestionBuilder !== 'undefined' && lpQuestionBuilder.nonce ) {
887 + dataSend.nonce = lpQuestionBuilder.nonce;
888 + }
889 +
890 + const callBack = {
891 + success: ( response ) => {
892 + const { status, message, data } = response;
893 + lpToastify.show( message, status );
894 +
895 + if ( status === 'success' ) {
896 + // Update action button text with actual status from server
897 + this.updateActionButtons( data?.status || targetStatus );
898 + this.syncPublishPanelStatus( data?.status || targetStatus );
899 + this.updatePermalinkUIAfterSave( data );
900 +
901 + // Reset form state before potential redirect.
902 + document.dispatchEvent( new CustomEvent( 'lp-course-builder-saved' ) );
903 +
904 + if ( data?.redirect_url ) {
905 + window.location.href = data.redirect_url;
906 + }
907 +
908 + if ( data?.status ) {
909 + const elStatus = document.querySelector(
910 + BuilderEditQuestion.selectors.elQuestionStatus
911 + );
912 + if ( elStatus ) {
913 + elStatus.className = 'question-status ' + data.status;
914 + elStatus.textContent = data.status;
915 + }
916 +
917 + if ( data.status === 'trash' || data.status === 'draft' ) {
918 + const curriculumItem = document.querySelector(
919 + `.question-item[data-question-id="${ questionData.question_id }"]`
920 + );
921 + if ( curriculumItem ) {
922 + const elCurriculumQuestion = curriculumItem.closest( '.question' );
923 + if ( elCurriculumQuestion ) {
924 + elCurriculumQuestion.remove();
925 + }
926 + }
927 + }
928 + }
929 + }
930 + },
931 + error: ( error ) => {
932 + lpToastify.show( error.message || error, 'error' );
933 + },
934 + completed: () => {
935 + lpUtils.lpSetLoadingEl( elBtnMainAction, 0 );
936 + },
937 + };
938 +
939 + window.lpAJAXG.fetchAJAX( dataSend, callBack );
940 + }
941 +
942 + async confirmUnpublishIfNeeded( targetStatus, triggerEl ) {
943 + if ( targetStatus !== 'draft' ) {
944 + return true;
945 + }
946 +
947 + const statusEl = document.querySelector( BuilderEditQuestion.selectors.elQuestionStatus );
948 + const isPublished = statusEl && statusEl.classList.contains( 'publish' );
949 + if ( ! isPublished ) {
950 + return true;
951 + }
952 +
953 + const confirmMsg =
954 + triggerEl?.dataset?.confirmUnpublish ||
955 + 'Saving as draft will unpublish this item. Are you sure?';
956 + const result = await SweetAlert.fire( {
957 + title: confirmMsg,
958 + iconHtml: SWAL_ICON_TRASH_DRAFT,
959 + customClass: { icon: 'lp-cb-swal-icon-html' },
960 + showCloseButton: true,
961 + showCancelButton: true,
962 + cancelButtonText: lpData.i18n.cancel,
963 + confirmButtonText: lpData.i18n.yes,
964 + reverseButtons: true,
965 + } );
966 +
967 + return !! result.isConfirmed;
968 + }
969 +
970 + async saveDraftQuestion( args ) {
971 + // Context check: only handle if on question edit page
972 + if ( ! this.isQuestionContext() ) {
973 + return;
974 + }
975 +
976 + const { target } = args;
977 + const elBtnDraftQuestion = target.closest( BuilderEditQuestion.selectors.elBtnDraftQuestion );
978 +
979 + if ( ! elBtnDraftQuestion ) {
980 + return;
981 + }
982 +
983 + // Check if published to show confirm unpublish modal
984 + const statusEl = document.querySelector( BuilderEditQuestion.selectors.elQuestionStatus );
985 + const isPublished = statusEl && statusEl.classList.contains( 'publish' );
986 + if ( isPublished ) {
987 + const confirmMsg =
988 + elBtnDraftQuestion.dataset.confirmUnpublish ||
989 + 'Saving as draft will unpublish this item. Are you sure?';
990 + const result = await SweetAlert.fire( {
991 + title: confirmMsg,
992 + iconHtml: SWAL_ICON_TRASH_DRAFT,
993 + customClass: { icon: 'lp-cb-swal-icon-html' },
994 + showCloseButton: true,
995 + showCancelButton: true,
996 + cancelButtonText: lpData.i18n.cancel,
997 + confirmButtonText: lpData.i18n.yes,
998 + reverseButtons: true,
999 + } );
1000 +
1001 + if ( ! result.isConfirmed ) {
1002 + return;
1003 + }
1004 + }
1005 +
1006 + // Validate title before saving draft
1007 + if ( ! this.validateTitleBeforeUpdate() ) {
1008 + return;
1009 + }
1010 +
1011 + const elActionExpanded = document.querySelector(
1012 + BuilderEditQuestion.selectors.elQuestionActionExpanded
1013 + );
1014 +
1015 + lpUtils.lpSetLoadingEl( elActionExpanded, 1 );
1016 +
1017 + const questionData = this.getQuestionDataForUpdate();
1018 +
1019 + const dataSend = {
1020 + ...questionData,
1021 + action: 'builder_update_question',
1022 + args: {
1023 + id_url: 'builder-update-question',
1024 + },
1025 + question_status: 'draft',
1026 + };
1027 +
1028 + if ( typeof lpQuestionBuilder !== 'undefined' && lpQuestionBuilder.nonce ) {
1029 + dataSend.nonce = lpQuestionBuilder.nonce;
1030 + }
1031 +
1032 + const callBack = {
1033 + success: ( response ) => {
1034 + const { status, message, data } = response;
1035 + lpToastify.show( message, status );
1036 +
1037 + if ( status === 'success' ) {
1038 + // Update action button text
1039 + this.updateActionButtons( 'draft' );
1040 + this.syncPublishPanelStatus( data?.status || 'draft' );
1041 + this.updatePermalinkUIAfterSave( data );
1042 +
1043 + // Reset form state before potential redirect.
1044 + document.dispatchEvent( new CustomEvent( 'lp-course-builder-saved' ) );
1045 +
1046 + if ( data?.redirect_url ) {
1047 + window.location.href = data.redirect_url;
1048 + }
1049 +
1050 + if ( data?.status ) {
1051 + const elStatus = document.querySelector(
1052 + BuilderEditQuestion.selectors.elQuestionStatus
1053 + );
1054 + if ( elStatus ) {
1055 + elStatus.className = 'question-status ' + data.status;
1056 + elStatus.textContent = data.status;
1057 + }
1058 +
1059 + if ( data.status === 'trash' || data.status === 'draft' ) {
1060 + const curriculumItem = document.querySelector(
1061 + `.question-item[data-question-id="${ questionData.question_id }"]`
1062 + );
1063 + if ( curriculumItem ) {
1064 + const elCurriculumQuestion = curriculumItem.closest( '.question' );
1065 + if ( elCurriculumQuestion ) {
1066 + elCurriculumQuestion.remove();
1067 + }
1068 + }
1069 + }
1070 + }
1071 + }
1072 + },
1073 + error: ( error ) => {
1074 + lpToastify.show( error.message || error, 'error' );
1075 + },
1076 + completed: () => {
1077 + lpUtils.lpSetLoadingEl( elActionExpanded, 0 );
1078 + },
1079 + };
1080 +
1081 + window.lpAJAXG.fetchAJAX( dataSend, callBack );
1082 + }
1083 +
1084 + async trashQuestion( args ) {
1085 + // Context check: only handle if on question edit page
1086 + if ( ! this.isQuestionContext() ) {
1087 + return;
1088 + }
1089 +
1090 + const { target } = args;
1091 + const elBtnTrashQuestion = target.closest( BuilderEditQuestion.selectors.elBtnTrashQuestion );
1092 + if ( ! elBtnTrashQuestion ) {
1093 + return;
1094 + }
1095 +
1096 + const result = await SweetAlert.fire( {
1097 + title: 'Are you sure you want to trash this question?',
1098 + iconHtml: SWAL_ICON_TRASH_DRAFT,
1099 + customClass: { icon: 'lp-cb-swal-icon-html' },
1100 + showCloseButton: true,
1101 + showCancelButton: true,
1102 + cancelButtonText: lpData.i18n.cancel,
1103 + confirmButtonText: lpData.i18n.yes,
1104 + reverseButtons: true,
1105 + } );
1106 +
1107 + if ( ! result.isConfirmed ) {
1108 + return;
1109 + }
1110 +
1111 + const elActionExpanded = document.querySelector(
1112 + BuilderEditQuestion.selectors.elQuestionActionExpanded
1113 + );
1114 + lpUtils.lpSetLoadingEl( elActionExpanded, 1 );
1115 +
1116 + const questionData = this.getQuestionDataForUpdate();
1117 + const dataSend = {
1118 + action: 'move_trash_question',
1119 + args: {
1120 + id_url: 'move-trash-question',
1121 + },
1122 + question_id: questionData.question_id || 0,
1123 + };
1124 +
1125 + const callBack = {
1126 + success: ( response ) => {
1127 + const { status, message, data } = response;
1128 + lpToastify.show( message, status );
1129 +
1130 + if ( status === 'success' ) {
1131 + if ( data?.redirect_url ) {
1132 + window.location.href = data.redirect_url;
1133 + }
1134 +
1135 + if ( data?.status ) {
1136 + const elStatus = document.querySelector(
1137 + BuilderEditQuestion.selectors.elQuestionStatus
1138 + );
1139 + if ( elStatus ) {
1140 + elStatus.className = 'question-status ' + data.status;
1141 + elStatus.textContent = data.status;
1142 + }
1143 + if ( data.status === 'trash' || data.status === 'draft' ) {
1144 + const curriculumItem = document.querySelector(
1145 + `.question-item[data-question-id="${ questionData.question_id }"]`
1146 + );
1147 + if ( curriculumItem ) {
1148 + const elCurriculumQuestion = curriculumItem.closest( '.question' );
1149 + if ( elCurriculumQuestion ) {
1150 + elCurriculumQuestion.remove();
1151 + }
1152 + }
1153 + }
1154 + }
1155 + }
1156 + },
1157 + error: ( error ) => {
1158 + lpToastify.show( error.message || error, 'error' );
1159 + },
1160 + completed: () => {
1161 + lpUtils.lpSetLoadingEl( elActionExpanded, 0 );
1162 + },
1163 + };
1164 +
1165 + window.lpAJAXG.fetchAJAX( dataSend, callBack );
1166 + }
1167 +
1168 + async duplicateQuestion( args ) {
1169 + // Context check: only handle if on question edit page
1170 + if ( ! this.isQuestionContext() ) {
1171 + return;
1172 + }
1173 +
1174 + const { target } = args;
1175 + const elBtnDuplicateQuestion = target.closest(
1176 + BuilderEditQuestion.selectors.elBtnDuplicateQuestion
1177 + );
1178 + if ( ! elBtnDuplicateQuestion ) {
1179 + return;
1180 + }
1181 +
1182 + const questionData = this.getQuestionDataForUpdate();
1183 + const questionId = parseInt( questionData?.question_id, 10 ) || 0;
1184 + if ( ! questionId ) {
1185 + return;
1186 + }
1187 +
1188 + const result = await SweetAlert.fire( {
1189 + title: elBtnDuplicateQuestion.dataset.title || 'Are you sure?',
1190 + text:
1191 + elBtnDuplicateQuestion.dataset.content ||
1192 + 'Are you sure you want to duplicate this question?',
1193 + iconHtml: SWAL_ICON_DUPLICATE,
1194 + customClass: { icon: 'lp-cb-swal-icon-html' },
1195 + showCloseButton: true,
1196 + showCancelButton: true,
1197 + cancelButtonText: lpData.i18n.cancel,
1198 + confirmButtonText: lpData.i18n.yes,
1199 + reverseButtons: true,
1200 + } );
1201 +
1202 + if ( ! result.isConfirmed ) {
1203 + return;
1204 + }
1205 +
1206 + const elActionExpanded = document.querySelector(
1207 + BuilderEditQuestion.selectors.elQuestionActionExpanded
1208 + );
1209 +
1210 + lpUtils.lpSetLoadingEl( elActionExpanded, 1 );
1211 +
1212 + const dataSend = {
1213 + action: 'duplicate_question',
1214 + args: {
1215 + id_url: 'duplicate-question',
1216 + },
1217 + question_id: questionId,
1218 + };
1219 +
1220 + const callBack = {
1221 + success: ( response ) => {
1222 + const { status, message, data } = response;
1223 + lpToastify.show( message || 'Duplicated successfully!', status );
1224 +
1225 + if ( status === 'success' && data?.redirect_url ) {
1226 + window.location.href = data.redirect_url;
1227 + }
1228 + },
1229 + error: ( error ) => {
1230 + lpToastify.show( error.message || error, 'error' );
1231 + },
1232 + completed: () => {
1233 + lpUtils.lpSetLoadingEl( elActionExpanded, 0 );
1234 + },
1235 + };
1236 +
1237 + window.lpAJAXG.fetchAJAX( dataSend, callBack );
1238 + }
1239 +}