PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.2
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.2
4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 All 139 releases
learnpress / assets / src / js / frontend / course-builder / builder-popup.js

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

1,895 lines 52.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Builder Popup Handler
3 * Handles AJAX popup loading for lesson, quiz, and question builders.
4 *
5 * @since 4.3.0
6 * @version 1.0.1
7 */
8
9 import * as lpUtils from 'lpAssetsJsPath/utils.js';
10 import * as lpToastify from 'lpAssetsJsPath/lpToastify.js';
11 import SweetAlert from 'sweetalert2';
12 import { SWAL_ICON_TRASH_DRAFT } from './swal-icons.js';
13 import { BuilderEditQuiz } from './builder-quiz/builder-edit-quiz.js';
14 import { BuilderEditQuestion } from './builder-question/builder-edit-question.js';
15 import { BuilderMaterial } from './builder-lesson/builder-material.js';
16
17 export class BuilderPopup {
18 constructor() {
19 this.popupContainer = null;
20 this.currentType = null;
21 this.currentId = null;
22 this.currentTemplate = '';
23 this.isNewItem = false;
24 this.savedData = null;
25 this.openContext = this.getDefaultOpenContext();
26 this.builderEditQuiz = null;
27 this.builderEditQuestion = null;
28 this.builderMaterial = null;
29 this.loadedTabAssets = new Set();
30 this.initializedTabs = new Map();
31 this.init();
32 }
33
34 static selectors = {
35 popupContainer: '#lp-builder-popup-container',
36 popupOverlay: '.lp-builder-popup-overlay',
37 popup: '.lp-builder-popup',
38 closeBtn: '.lp-builder-popup__close',
39 resizeBtn: '.lp-builder-popup__resize',
40 cancelBtn: '.lp-builder-popup__btn--cancel',
41 saveBtn: '.lp-builder-popup__btn--save',
42 draftBtn: '.lp-builder-popup__btn--draft',
43 trashBtn: '.lp-builder-popup__btn--trash',
44 tabs: '.lp-builder-popup__tabs',
45 tab: '.lp-builder-popup__tab',
46 tabPane: '.lp-builder-popup__tab-pane',
47 permalinkSlugInput: '.cb-permalink-slug-input',
48 permalinkUrl: '.cb-permalink-url',
49 permalinkBaseUrl: '#cb-permalink-base-url',
50 permalinkDisplay: '.cb-permalink-display',
51 permalinkEditor: '.cb-permalink-editor',
52 permalinkRoot: '.cb-item-edit-permalink, .cb-course-edit-permalink',
53 permalinkPlaceholder: '.cb-item-edit-permalink__placeholder',
54 // Trigger buttons
55 popupTrigger:
56 '[data-popup-lesson], [data-popup-quiz], [data-popup-question], [data-add-new-lesson], [data-template][data-popup-type]',
57 triggerLesson: '[data-popup-lesson]',
58 triggerQuiz: '[data-popup-quiz]',
59 triggerQuestion: '[data-popup-question]',
60 // Add new buttons
61 addNewLesson: '[data-add-new-lesson]',
62 };
63
64 init() {
65 let popupContainer = document.querySelector( BuilderPopup.selectors.popupContainer );
66
67 if ( ! popupContainer ) {
68 popupContainer = document.createElement( 'div' );
69 popupContainer.id = 'lp-builder-popup-container';
70 document.body.appendChild( popupContainer );
71 }
72
73 this.popupContainer = popupContainer;
74 this.events();
75 }
76
77 events() {
78 if ( BuilderPopup._loadedEvents ) {
79 return;
80 }
81 BuilderPopup._loadedEvents = true;
82
83 lpUtils.eventHandlers( 'click', [
84 {
85 selector: BuilderPopup.selectors.popupTrigger,
86 class: this,
87 callBack: this.openPopup.name,
88 },
89 {
90 selector: `${ BuilderPopup.selectors.closeBtn }, ${ BuilderPopup.selectors.cancelBtn }, ${ BuilderPopup.selectors.popupOverlay }`,
91 class: this,
92 callBack: this.closePopup.name,
93 conditionBeforeCallBack: () => this.isPopupOpen(),
94 },
95 {
96 selector: BuilderPopup.selectors.resizeBtn,
97 class: this,
98 callBack: this.toggleFullscreen.name,
99 conditionBeforeCallBack: () => this.isPopupOpen(),
100 },
101 {
102 selector: BuilderPopup.selectors.tab,
103 class: this,
104 callBack: this.switchTab.name,
105 conditionBeforeCallBack: () => this.isPopupOpen(),
106 },
107 {
108 selector: BuilderPopup.selectors.saveBtn,
109 class: this,
110 callBack: this.handleSave.name,
111 conditionBeforeCallBack: () => this.isPopupOpen(),
112 },
113 {
114 selector: BuilderPopup.selectors.draftBtn,
115 class: this,
116 callBack: this.handleDraft.name,
117 conditionBeforeCallBack: () => this.isPopupOpen(),
118 },
119 {
120 selector: BuilderPopup.selectors.trashBtn,
121 class: this,
122 callBack: this.handleTrash.name,
123 conditionBeforeCallBack: () => this.isPopupOpen(),
124 },
125 ] );
126
127 lpUtils.eventHandlers( 'keydown', [
128 {
129 selector: 'body',
130 class: this,
131 callBack: this.closePopup.name,
132 conditionBeforeCallBack: ( args ) => args.e.key === 'Escape' && this.isPopupOpen(),
133 },
134 ] );
135 }
136
137 /**
138 * Toggle fullscreen mode for popup
139 */
140 toggleFullscreen() {
141 const popup = this.popupContainer.querySelector( BuilderPopup.selectors.popup );
142 if ( ! popup ) {
143 return;
144 }
145
146 popup.classList.toggle( 'lp-builder-popup--fullscreen' );
147
148 // Update resize button icon
149 const resizeBtn = popup.querySelector( BuilderPopup.selectors.resizeBtn );
150 if ( resizeBtn ) {
151 const icon = resizeBtn.querySelector( 'i' );
152 if ( icon ) {
153 const isFullscreen = popup.classList.contains( 'lp-builder-popup--fullscreen' );
154 icon.classList.toggle( 'lp-icon-expand', ! isFullscreen );
155 icon.classList.toggle( 'lp-icon-compress', isFullscreen );
156 }
157 }
158
159 document.dispatchEvent(
160 new CustomEvent( 'lp-builder-popup-fullscreen-toggled', {
161 detail: {
162 isFullscreen: popup.classList.contains( 'lp-builder-popup--fullscreen' ),
163 type: this.currentType,
164 id: this.currentId,
165 },
166 } )
167 );
168 }
169
170 openPopup( args ) {
171 const { target } = args;
172 const triggerEl = target.closest( BuilderPopup.selectors.popupTrigger );
173 if ( ! triggerEl ) {
174 return;
175 }
176
177 let type = '';
178 let id = 0;
179
180 if ( triggerEl.matches( BuilderPopup.selectors.addNewLesson ) ) {
181 type = 'lesson';
182 } else if ( triggerEl.dataset.popupType ) {
183 type = triggerEl.dataset.popupType;
184 id = parseInt( triggerEl.dataset.popupId ) || 0;
185 } else if ( triggerEl.dataset.popupLesson !== undefined ) {
186 type = 'lesson';
187 id = parseInt( triggerEl.dataset.popupLesson ) || 0;
188 } else if ( triggerEl.dataset.popupQuiz !== undefined ) {
189 type = 'quiz';
190 id = parseInt( triggerEl.dataset.popupQuiz ) || 0;
191 } else if ( triggerEl.dataset.popupQuestion !== undefined ) {
192 type = 'question';
193 id = parseInt( triggerEl.dataset.popupQuestion ) || 0;
194 }
195
196 if ( ! type ) {
197 return;
198 }
199
200 this.showPopup( triggerEl, type, id, this.resolveOpenContext( triggerEl ) );
201 }
202
203 getDefaultOpenContext() {
204 return {
205 isCurriculum: false,
206 courseId: 0,
207 };
208 }
209
210 resolveOpenContext( triggerEl ) {
211 const isCurriculumContainer =
212 !! triggerEl?.closest( '#lp-course-edit-curriculum' ) ||
213 !! triggerEl?.closest( '.lp-edit-curriculum-wrap' );
214 const courseId = parseInt( triggerEl?.dataset?.courseId ) || 0;
215 const isCurriculum = isCurriculumContainer && courseId > 0;
216
217 return {
218 isCurriculum,
219 courseId,
220 };
221 }
222
223 showPopup( triggerEl, type, id, openContext = null ) {
224 const templateId = triggerEl?.dataset?.template || '';
225 const templateEl = document.querySelector( templateId );
226 if ( ! templateId || ! templateEl ) {
227 return;
228 }
229
230 this.currentType = type;
231 this.currentId = id;
232 this.currentTemplate = templateId;
233 this.isNewItem = id === 0;
234 this.openContext = openContext ? { ...openContext } : this.getDefaultOpenContext();
235
236 if ( ! this.popupContainer ) {
237 return;
238 }
239
240 this.popupContainer.innerHTML = templateEl.innerHTML;
241 this.popupContainer.classList.add( 'active' );
242 document.body.classList.add( 'lp-popup-open' );
243
244 const elLPTarget = this.popupContainer.querySelector( '.lp-target' );
245 if ( ! elLPTarget || ! window.lpAJAXG ) {
246 return;
247 }
248
249 const dataSend = window.lpAJAXG.getDataSetCurrent( elLPTarget );
250 dataSend.args = dataSend.args || {};
251 dataSend.args[ `${ type }_id` ] = id;
252 window.lpAJAXG.setDataSetCurrent( elLPTarget, dataSend );
253
254 this.requestPopupContent( dataSend );
255 }
256
257 reloadCurrentPopup() {
258 if ( ! this.currentTemplate || ! this.currentType ) {
259 return;
260 }
261
262 const templateEl = document.querySelector( this.currentTemplate );
263 if ( ! templateEl ) {
264 return;
265 }
266
267 if ( ! this.popupContainer ) {
268 return;
269 }
270
271 this.popupContainer.innerHTML = templateEl.innerHTML;
272 this.popupContainer.classList.add( 'active' );
273 document.body.classList.add( 'lp-popup-open' );
274
275 const elLPTarget = this.popupContainer.querySelector( '.lp-target' );
276 if ( ! elLPTarget || ! window.lpAJAXG ) {
277 return;
278 }
279
280 const dataSend = window.lpAJAXG.getDataSetCurrent( elLPTarget );
281 dataSend.args = dataSend.args || {};
282 dataSend.args[ `${ this.currentType }_id` ] = this.currentId;
283 window.lpAJAXG.setDataSetCurrent( elLPTarget, dataSend );
284
285 this.requestPopupContent( dataSend );
286 }
287
288 requestPopupContent( dataSend ) {
289 if ( ! this.popupContainer || ! window.lpAJAXG ) {
290 return;
291 }
292
293 const callBack = {
294 success: ( response ) => {
295 const { status, data } = response;
296 if ( status === 'success' && data?.content ) {
297 this.popupContainer.innerHTML = data.content;
298 this.popupContainer.classList.add( 'active' );
299 document.body.classList.add( 'lp-popup-open' );
300
301 this.loadedTabAssets.clear();
302 this.initializedTabs.clear(); // Clear initialized tabs cache
303 const ajaxElements = this.popupContainer.querySelectorAll(
304 '.lp-load-ajax-element.loaded'
305 );
306 ajaxElements.forEach( ( el ) => el.classList.remove( 'loaded' ) );
307
308 setTimeout( () => window.lpAJAXG.getElements(), 50 );
309
310 const popup = this.popupContainer.querySelector( BuilderPopup.selectors.popup );
311 const activeTab = popup?.querySelector( `${ BuilderPopup.selectors.tab }.active` );
312 const activeTabName = activeTab?.dataset.tab || 'overview';
313 const activePane = popup?.querySelector(
314 `${ BuilderPopup.selectors.tabPane }[data-tab="${ activeTabName }"]`
315 );
316
317 if ( activePane ) {
318 this.loadTabAssets( activeTabName, activePane );
319 }
320
321 if ( activeTabName === 'overview' ) {
322 setTimeout( () => this.initTinyMCE(), 50 );
323 }
324
325 if ( this.currentType === 'quiz' ) {
326 if ( ! this.builderEditQuiz ) {
327 this.builderEditQuiz = new BuilderEditQuiz();
328 }
329
330 if ( activeTabName === 'questions' ) {
331 const tabKey = `${ this.currentType }-${ activeTabName }`;
332 setTimeout( () => {
333 this.triggerAjaxLoadForTab( activePane );
334 this.builderEditQuiz.reinit( this.popupContainer );
335 this.initializedTabs.set( tabKey, true );
336 }, 100 );
337 }
338 } else if ( this.currentType === 'question' ) {
339 if ( ! this.builderEditQuestion ) {
340 this.builderEditQuestion = new BuilderEditQuestion();
341 }
342
343 if ( activeTabName === 'settings' ) {
344 const tabKey = `${ this.currentType }-${ activeTabName }`;
345 setTimeout( () => {
346 this.triggerAjaxLoadForTab( activePane );
347 this.builderEditQuestion.reinit( this.popupContainer );
348 this.initializedTabs.set( tabKey, true );
349 }, 100 );
350 }
351 } else if ( this.currentType === 'lesson' ) {
352 if ( ! this.builderMaterial ) {
353 this.builderMaterial = new BuilderMaterial();
354 }
355
356 if ( activeTabName === 'settings' ) {
357 const tabKey = `${ this.currentType }-${ activeTabName }`;
358 setTimeout( () => {
359 this.triggerAjaxLoadForTab( activePane );
360 this.builderMaterial.reinit( this.popupContainer );
361 this.initializedTabs.set( tabKey, true );
362 }, 100 );
363 }
364 }
365
366 document.dispatchEvent(
367 new CustomEvent( 'lp-builder-popup-opened', {
368 detail: { type: this.currentType, id: this.currentId, isNew: this.isNewItem },
369 } )
370 );
371 } else {
372 lpToastify.show( response.message || 'Failed to load popup', 'error' );
373 this.popupContainer.innerHTML = '';
374 this.popupContainer.classList.remove( 'active' );
375 document.body.classList.remove( 'lp-popup-open' );
376 }
377 },
378 error: ( error ) => {
379 lpToastify.show( error.message || 'Failed to load popup', 'error' );
380 this.popupContainer.innerHTML = '';
381 this.popupContainer.classList.remove( 'active' );
382 document.body.classList.remove( 'lp-popup-open' );
383 },
384 completed: () => {
385 // Loading hidden in success/error
386 },
387 };
388
389 window.lpAJAXG.fetchAJAX( dataSend, callBack );
390 }
391
392 /**
393 * Close popup
394 */
395 closePopup() {
396 const closedType = this.currentType;
397 const closedId = this.currentId;
398 const savedData = this.savedData;
399
400 this.destroyAllTinyMCE();
401
402 this.popupContainer.innerHTML = '';
403 this.popupContainer.classList.remove( 'active' );
404 document.body.classList.remove( 'lp-popup-open' );
405
406 this.loadedTabAssets.clear();
407 this.initializedTabs.clear(); // Clear initialized tabs cache
408
409 if ( savedData && closedId ) {
410 this.updateListItem( closedType, closedId, savedData );
411 }
412
413 document.dispatchEvent(
414 new CustomEvent( 'lp-builder-popup-closed', {
415 detail: { type: closedType, id: closedId, savedData },
416 } )
417 );
418
419 this.currentType = null;
420 this.currentId = null;
421 this.currentTemplate = '';
422 this.isNewItem = false;
423 this.savedData = null;
424 this.openContext = this.getDefaultOpenContext();
425 }
426
427 /**
428 * Update list item in the background list
429 */
430 updateListItem( type, id, savedData ) {
431 if ( ! type || ! id || ! savedData ) {
432 return;
433 }
434
435 const { formData, data, wasNewItem } = savedData;
436 let listItems = this.findListItems( type, id );
437
438 // New items created from popup need to be inserted into the list first.
439 if ( ( ! listItems || listItems.length === 0 ) && wasNewItem ) {
440 const newItem = this.insertNewListItem( type, id, data?.list_item_html );
441 if ( newItem ) {
442 listItems = [ newItem ];
443 }
444 }
445
446 if ( ! listItems || listItems.length === 0 ) {
447 return;
448 }
449
450 listItems.forEach( ( listItem ) => {
451 let currentItem = listItem;
452
453 // Replace the entire item HTML if returned from the server
454 if ( data?.section_item_html && currentItem.classList.contains( 'section-item' ) ) {
455 const template = document.createElement( 'template' );
456 template.innerHTML = data.section_item_html.trim();
457 const newListItem = template.content.firstElementChild;
458 if ( newListItem ) {
459 currentItem.replaceWith( newListItem );
460 currentItem = newListItem;
461 }
462 } else if ( data?.list_item_html && ! currentItem.classList.contains( 'section-item' ) ) {
463 const template = document.createElement( 'template' );
464 template.innerHTML = data.list_item_html.trim();
465 const newListItem = template.content.firstElementChild;
466 if ( newListItem ) {
467 currentItem.replaceWith( newListItem );
468 currentItem = newListItem;
469 }
470 } else {
471 // Fallback: manually update elements if HTML replacement isn't used
472 // Update title
473 const newTitle = formData[ `${ type }_title` ];
474 if ( newTitle ) {
475 this.updateElementText(
476 currentItem,
477 [
478 '.item-title',
479 '.lp-item-title',
480 `.lp-${ type }-title`,
481 '.curriculum-item-title',
482 '.item-name',
483 'span.title',
484 '.lp-question-title-input',
485 '.section-item-title input',
486 '.section-item-title span',
487 '.lp-item-title-input',
488 ],
489 newTitle
490 );
491 }
492
493 // Update status
494 if ( data?.status ) {
495 this.updateElementClass(
496 currentItem,
497 [ `.${ type }-status`, '.item-status', '.post-status' ],
498 data.status
499 );
500 }
501
502 if ( type === 'lesson' ) {
503 const duration = formData._lp_duration || data?.duration;
504 if ( duration ) {
505 this.updateDuration( currentItem, duration );
506 }
507
508 const preview = formData._lp_preview || data?.preview;
509 const isPreview = preview === 'yes' || preview === true || preview === '1';
510 const previewEl = currentItem.querySelector(
511 '.lp-btn-set-preview-item a, .course-item-preview'
512 );
513 if ( previewEl ) {
514 if ( isPreview ) {
515 previewEl.classList.remove( 'lp-icon-eye-slash' );
516 previewEl.classList.add( 'lp-icon-eye' );
517 } else {
518 previewEl.classList.remove( 'lp-icon-eye' );
519 previewEl.classList.add( 'lp-icon-eye-slash' );
520 }
521 }
522
523 const checkbox = currentItem.querySelector( 'input[type="checkbox"].preview-checkbox' );
524 if ( checkbox ) {
525 checkbox.checked = isPreview;
526 }
527
528 currentItem.classList.toggle( 'is-preview', isPreview );
529 currentItem.classList.toggle( 'preview-item', isPreview );
530 } else if ( type === 'quiz' ) {
531 const duration = formData._lp_duration || data?.duration;
532 if ( duration ) {
533 this.updateDuration( currentItem, duration );
534 }
535
536 const questionCount = data?.question_count || data?.questions_count;
537 if ( questionCount !== null && questionCount !== undefined ) {
538 const questionCountEl = currentItem.querySelector( '.question-count' );
539 if ( questionCountEl ) {
540 questionCountEl.textContent = `${ questionCount } ${
541 questionCount === 1 ? 'Question' : 'Questions'
542 }`;
543 }
544 }
545
546 const passingGrade = formData._lp_passing_grade || data?.passing_grade;
547 if ( passingGrade ) {
548 const passingGradeEl = currentItem.querySelector( '.passing-grade' );
549 if ( passingGradeEl ) {
550 passingGradeEl.textContent = `${ passingGrade }%`;
551 }
552 }
553 } else if ( type === 'question' ) {
554 const questionType = formData._lp_type || data?.type;
555 if ( questionType ) {
556 const typeMap = {
557 true_or_false: 'True or False',
558 single_choice: 'Single Choice',
559 multi_choice: 'Multi Choice',
560 fill_in_blanks: 'Fill in Blanks',
561 };
562
563 this.updateElementText(
564 currentItem,
565 [ '.question-type', '.item-type' ],
566 typeMap[ questionType ] || questionType
567 );
568
569 const typeClasses = [
570 'true_or_false',
571 'single_choice',
572 'multi_choice',
573 'fill_in_blanks',
574 ];
575 typeClasses.forEach( ( cls ) => currentItem.classList.remove( cls ) );
576 currentItem.classList.add( questionType );
577 }
578
579 const mark = formData._lp_mark || data?.mark;
580 if ( mark ) {
581 const questionMarkEl = currentItem.querySelector( '.question-mark' );
582 if ( questionMarkEl ) {
583 questionMarkEl.textContent = mark;
584 }
585 }
586 }
587 }
588 } );
589
590 document.dispatchEvent(
591 new CustomEvent( 'lp-builder-list-item-updated', {
592 detail: { type, id, formData, data },
593 } )
594 );
595 }
596
597 /**
598 * Find all instances of a list item by type and ID
599 */
600 findListItems( type, id ) {
601 const selectors = [
602 `[data-${ type }-id="${ id }"]`,
603 `[data-id="${ id }"]`,
604 `[data-popup-${ type }="${ id }"]`,
605 `[data-item-id="${ id }"]`,
606 `.section-item[data-item-id="${ id }"]`,
607 `.lp-${ type }-item[data-id="${ id }"]`,
608 ];
609
610 const foundItems = new Set();
611
612 for ( const selector of selectors ) {
613 const items = document.querySelectorAll( selector );
614 for ( const item of items ) {
615 // Ensure we don't select elements inside the popup itself
616 if ( ! item.closest( '#lp-builder-popup-container' ) ) {
617 // Exclude elements that are merely trigger buttons but not the list item container itself
618 // If it's just a generic button to open a popup, it might not be the actual item container.
619 // However, some UI lists use the trigger button as the container. We rely on the DOM structure.
620 // We can assume `.section-item` and `.lp-lesson-item` and `.cb-list-item` are containers.
621 if (
622 item.classList.contains( 'section-item' ) ||
623 item.classList.contains( `lp-${ type }-item` ) ||
624 item.classList.contains( 'list-item' ) ||
625 item.classList.contains( 'cb-list-item' ) ||
626 item.tagName === 'LI'
627 ) {
628 foundItems.add( item );
629 } else {
630 // If it's a wrapper, like a div in Content Bank
631 if ( item.closest( 'ul' ) ) {
632 foundItems.add( item );
633 }
634 }
635 }
636 }
637 }
638
639 return Array.from( foundItems );
640 }
641
642 /**
643 * Insert a newly created list item into the current tab list.
644 */
645 insertNewListItem( type, id, listItemHtml ) {
646 if ( ! listItemHtml ) {
647 return null;
648 }
649
650 const existingListItems = this.findListItems( type, id );
651 if ( existingListItems && existingListItems.length > 0 ) {
652 return existingListItems[ 0 ];
653 }
654
655 const listContainer = this.findListContainer( type );
656 if ( ! listContainer ) {
657 return null;
658 }
659
660 const template = document.createElement( 'template' );
661 template.innerHTML = listItemHtml.trim();
662 const newListItem = template.content.firstElementChild;
663
664 if ( ! newListItem ) {
665 return null;
666 }
667
668 listContainer.prepend( newListItem );
669 const highlightClassByType = {
670 lesson: 'highlight-new-lesson',
671 quiz: 'highlight-new-quiz',
672 question: 'highlight-new-question',
673 };
674 const highlightClass = highlightClassByType[ type ];
675
676 if ( highlightClass ) {
677 newListItem.classList.add( highlightClass );
678 newListItem.scrollIntoView( {
679 behavior: 'smooth',
680 block: 'nearest',
681 } );
682
683 setTimeout( () => {
684 newListItem.classList.remove( highlightClass );
685 }, 1500 );
686 }
687
688 const finalListItems = this.findListItems( type, id );
689 return finalListItems && finalListItems.length > 0 ? finalListItems[ 0 ] : newListItem;
690 }
691
692 /**
693 * Find list container for type; create one if tab currently shows empty message.
694 */
695 findListContainer( type ) {
696 const listSelectorByType = {
697 lesson: '.cb-list-lesson',
698 quiz: '.cb-list-quiz',
699 question: '.cb-list-question',
700 };
701
702 const tabSelectorByType = {
703 lesson: '.courses-builder__lesson-tab',
704 quiz: '.courses-builder__quiz-tab',
705 question: '.courses-builder__question-tab',
706 };
707
708 const listClassByType = {
709 lesson: 'cb-list-lesson',
710 quiz: 'cb-list-quiz',
711 question: 'cb-list-question',
712 };
713
714 const listSelector =
715 listSelectorByType[ type ] || `.cb-list-${ type }, [data-builder-list="${ type }"]`;
716 if ( ! listSelector ) {
717 return null;
718 }
719
720 const existingList = document.querySelector( listSelector );
721 if ( existingList ) {
722 return existingList;
723 }
724
725 const tabContainer = document.querySelector(
726 tabSelectorByType[ type ] || `.courses-builder__${ type }-tab, [data-builder-tab="${ type }"]`
727 );
728 const listClass = listClassByType[ type ] || `cb-list-${ type }`;
729
730 if ( ! tabContainer || ! listClass ) {
731 return null;
732 }
733
734 const emptyMessage = tabContainer.querySelector( '.learn-press-message' );
735 if ( emptyMessage ) {
736 emptyMessage.remove();
737 }
738
739 const listContainer = document.createElement( 'ul' );
740 listContainer.className = listClass;
741 tabContainer.appendChild( listContainer );
742
743 return listContainer;
744 }
745
746 /**
747 * Update element text (input value or textContent)
748 */
749 updateElementText( parent, selectors, newText ) {
750 for ( const selector of selectors ) {
751 const el = parent.querySelector( selector );
752 if ( el ) {
753 if ( el.tagName === 'INPUT' ) {
754 el.value = newText;
755 } else {
756 el.textContent = newText;
757 }
758 return true;
759 }
760 }
761 return false;
762 }
763
764 /**
765 * Update element class
766 */
767 updateElementClass( parent, selectors, newClass ) {
768 for ( const selector of selectors ) {
769 const el = parent.querySelector( selector );
770 if ( el ) {
771 const baseClass = selector.replace( '.', '' );
772 el.className = el.className.replace( /\b(publish|draft|pending|trash)\b/g, '' ).trim();
773 el.classList.add( baseClass, newClass );
774 el.textContent = newClass;
775 return true;
776 }
777 }
778 return false;
779 }
780
781 /**
782 * Update duration meta
783 */
784 updateDuration( listItem, duration ) {
785 const durationStr = this.formatDuration( duration );
786 const updated = this.updateElementText(
787 listItem,
788 [ '.item-meta.duration', '.duration', '.course-item-duration', '.meta-duration' ],
789 durationStr
790 );
791
792 if ( ! updated && durationStr ) {
793 const metaContainer = listItem.querySelector(
794 '.course-item__right, .item-meta-container, .course-item-meta'
795 );
796 if ( metaContainer ) {
797 let durationEl = metaContainer.querySelector( '.duration' );
798 if ( ! durationEl ) {
799 durationEl = document.createElement( 'span' );
800 durationEl.className = 'duration';
801 metaContainer.insertBefore( durationEl, metaContainer.firstChild );
802 }
803 durationEl.textContent = durationStr;
804 }
805 }
806 }
807
808 /**
809 * Format duration value
810 */
811 formatDuration( duration ) {
812 if ( ! duration ) {
813 return '';
814 }
815
816 if ( typeof duration === 'string' && duration.match( /\d+\s+\w+/ ) ) {
817 return duration;
818 }
819
820 const parts = String( duration ).trim().split( /\s+/ );
821 if ( parts.length >= 2 ) {
822 const value = parseInt( parts[ 0 ] ) || 0;
823 const unit = parts[ 1 ].toLowerCase();
824
825 if ( value === 0 ) {
826 return '';
827 }
828
829 const unitMap = {
830 minute: value === 1 ? 'Minute' : 'Minutes',
831 hour: value === 1 ? 'Hour' : 'Hours',
832 day: value === 1 ? 'Day' : 'Days',
833 week: value === 1 ? 'Week' : 'Weeks',
834 };
835
836 return `${ value } ${ unitMap[ unit ] || unit }`;
837 }
838
839 const numValue = parseInt( duration ) || 0;
840 return numValue > 0 ? `${ numValue } ${ numValue === 1 ? 'Minute' : 'Minutes' }` : '';
841 }
842
843 /**
844 * Check if popup is open
845 */
846 isPopupOpen() {
847 return this.popupContainer?.classList.contains( 'active' );
848 }
849
850 /**
851 * Switch tab with dynamic asset loading
852 */
853 switchTab( args ) {
854 const tabEl = args?.target ? args.target.closest( BuilderPopup.selectors.tab ) : args;
855 if ( ! tabEl ) {
856 return;
857 }
858
859 const tabName = tabEl.dataset.tab;
860 const popup = tabEl.closest( BuilderPopup.selectors.popup );
861
862 if ( ! popup || ! tabName ) {
863 return;
864 }
865
866 // Sync TinyMCE before switching
867 this.syncAllTinyMCE();
868
869 // Update tab states
870 popup.querySelectorAll( BuilderPopup.selectors.tab ).forEach( ( tab ) => {
871 tab.classList.remove( 'active' );
872 } );
873 tabEl.classList.add( 'active' );
874
875 // Update pane states
876 popup.querySelectorAll( BuilderPopup.selectors.tabPane ).forEach( ( pane ) => {
877 pane.classList.remove( 'active' );
878 } );
879
880 const targetPane = popup.querySelector(
881 `${ BuilderPopup.selectors.tabPane }[data-tab="${ tabName }"]`
882 );
883
884 if ( ! targetPane ) {
885 return;
886 }
887
888 targetPane.classList.add( 'active' );
889 this.loadTabAssets( tabName, targetPane );
890 const tabKey = `${ this.currentType }-${ tabName }`;
891
892 if ( ! this.initializedTabs.has( tabKey ) ) {
893 if ( tabName === 'overview' ) {
894 setTimeout( () => this.initTinyMCE(), 100 );
895 this.initializedTabs.set( tabKey, true );
896 } else if ( tabName === 'questions' && this.currentType === 'quiz' ) {
897 this.triggerAjaxLoadForTab( targetPane );
898 if ( this.builderEditQuiz ) {
899 setTimeout( () => {
900 this.builderEditQuiz.reinit( this.popupContainer );
901 this.initializedTabs.set( tabKey, true );
902 }, 100 );
903 }
904 } else if ( tabName === 'settings' && this.currentType === 'question' ) {
905 this.triggerAjaxLoadForTab( targetPane );
906 if ( this.builderEditQuestion ) {
907 setTimeout( () => {
908 this.builderEditQuestion.reinit( this.popupContainer );
909 this.initializedTabs.set( tabKey, true );
910 }, 100 );
911 }
912 } else if ( tabName === 'settings' && this.currentType === 'lesson' ) {
913 this.triggerAjaxLoadForTab( targetPane );
914 if ( this.builderMaterial ) {
915 setTimeout( () => {
916 this.builderMaterial.reinit( this.popupContainer );
917 this.initializedTabs.set( tabKey, true );
918 }, 100 );
919 }
920 }
921 }
922
923 document.dispatchEvent(
924 new CustomEvent( 'lp-builder-tab-switched', {
925 detail: { tabName, type: this.currentType, id: this.currentId },
926 } )
927 );
928 }
929
930 /**
931 * Trigger AJAX loading for tab elements
932 */
933 triggerAjaxLoadForTab( tabPane ) {
934 if ( ! tabPane || ! window.lpAJAXG ) {
935 return;
936 }
937
938 const ajaxElements = tabPane.querySelectorAll( '.lp-load-ajax-element:not(.loaded)' );
939
940 if ( ajaxElements.length > 0 ) {
941 ajaxElements.forEach( ( el ) => el.classList.remove( 'loaded' ) );
942 window.lpAJAXG.getElements();
943 }
944 }
945
946 /**
947 * Initialize TinyMCE for current popup type
948 */
949 initTinyMCE() {
950 const editorId = `${ this.currentType }_description_editor`;
951 const textarea = document.getElementById( editorId );
952
953 if ( ! textarea || typeof tinymce === 'undefined' ) {
954 return;
955 }
956
957 this.destroyTinyMCE( editorId );
958
959 if ( typeof wp !== 'undefined' && wp.editor?.initialize ) {
960 wp.editor.initialize( editorId, {
961 tinymce: {
962 wpautop: true,
963 content_style:
964 "body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; font-size: 14px; line-height: 1.6; color: #1e1e1e; }",
965 plugins:
966 'charmap colorpicker compat3x directionality fullscreen hr image lists media paste tabfocus textcolor wordpress wpautoresize wplink wptextpattern',
967 toolbar1:
968 'formatselect,bold,italic,underline,bullist,numlist,blockquote,alignleft,aligncenter,alignright,link,unlink,spellchecker,wp_adv',
969 toolbar2:
970 'strikethrough,hr,forecolor,pastetext,removeformat,charmap,outdent,indent,undo,redo,wp_help',
971 wordpress_adv_hidden: true,
972 },
973 quicktags: { buttons: 'strong,em,link,block,del,ins,img,ul,ol,li,code,more,close' },
974 mediaButtons: true,
975 } );
976 } else {
977 tinymce.init( {
978 selector: '#' + editorId,
979 height: 300,
980 menubar: false,
981 content_style:
982 "body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; font-size: 14px; line-height: 1.6; color: #1e1e1e; }",
983 plugins: [
984 'advlist autolink lists link image charmap print preview anchor',
985 'searchreplace visualblocks code fullscreen',
986 'insertdatetime media table paste code help wordcount',
987 ],
988 toolbar:
989 'undo redo | formatselect | bold italic backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help',
990 } );
991 }
992 }
993
994 /**
995 * Sync all TinyMCE instances
996 */
997 syncAllTinyMCE() {
998 if ( typeof tinymce === 'undefined' || ! this.currentType ) {
999 return;
1000 }
1001
1002 const editorId = `${ this.currentType }_description_editor`;
1003 const editor = tinymce.get( editorId );
1004
1005 if ( editor && ! this.isEditorInCodeMode( editorId ) ) {
1006 editor.save();
1007 }
1008
1009 // Sync additional editors
1010 tinymce.editors.forEach( ( ed ) => {
1011 if ( ed.id?.includes( this.currentType ) && ! this.isEditorInCodeMode( ed.id ) ) {
1012 ed.save();
1013 }
1014 } );
1015 }
1016
1017 isEditorInCodeMode( editorId ) {
1018 const wrapper =
1019 document.getElementById( `wp-${ editorId }-wrap` ) ||
1020 document.getElementById( `${ editorId }-wrap` );
1021 if ( wrapper?.classList.contains( 'html-active' ) ) {
1022 return true;
1023 }
1024
1025 const editor = typeof tinymce !== 'undefined' ? tinymce.get( editorId ) : null;
1026
1027 return !! ( editor?.isHidden && editor.isHidden() );
1028 }
1029
1030 getEditorContent( editorId, root = document ) {
1031 const textarea =
1032 root?.querySelector?.( `#${ editorId }` ) || document.getElementById( editorId );
1033
1034 if ( this.isEditorInCodeMode( editorId ) ) {
1035 return textarea ? textarea.value : '';
1036 }
1037
1038 if ( typeof tinymce !== 'undefined' ) {
1039 const editor = tinymce.get( editorId );
1040 if ( editor ) {
1041 return editor.getContent();
1042 }
1043 }
1044
1045 return textarea ? textarea.value : '';
1046 }
1047
1048 /**
1049 * Destroy specific TinyMCE instance
1050 */
1051 destroyTinyMCE( editorId ) {
1052 if ( typeof tinymce !== 'undefined' ) {
1053 const editor = tinymce.get( editorId );
1054 if ( editor ) {
1055 editor.remove();
1056 }
1057 }
1058
1059 if ( typeof wp !== 'undefined' && wp.editor?.remove ) {
1060 wp.editor.remove( editorId );
1061 }
1062 }
1063
1064 /**
1065 * Destroy all TinyMCE editors in popup
1066 */
1067 destroyAllTinyMCE() {
1068 if ( ! this.currentType || typeof tinymce === 'undefined' ) {
1069 return;
1070 }
1071
1072 const editorId = `${ this.currentType }_description_editor`;
1073 this.destroyTinyMCE( editorId );
1074
1075 const editorsToRemove = [];
1076 tinymce.editors.forEach( ( ed ) => {
1077 if ( ed.id && this.popupContainer?.querySelector( `#${ ed.id }` ) ) {
1078 editorsToRemove.push( ed.id );
1079 }
1080 } );
1081 editorsToRemove.forEach( ( id ) => this.destroyTinyMCE( id ) );
1082 }
1083
1084 getStatusFromPublishPanel( fallbackStatus = 'publish' ) {
1085 if ( ! this.currentType || ! this.popupContainer ) {
1086 return fallbackStatus;
1087 }
1088
1089 const statusSelect = this.popupContainer.querySelector(
1090 `#cb-${ this.currentType }-publish-status`
1091 );
1092 if ( ! statusSelect ) {
1093 return fallbackStatus;
1094 }
1095
1096 const selectedStatus = statusSelect.value;
1097 if ( selectedStatus === 'publish' || selectedStatus === 'draft' ) {
1098 return selectedStatus;
1099 }
1100
1101 return fallbackStatus;
1102 }
1103
1104 syncPublishPanelStatus( status ) {
1105 if ( ! this.currentType || ! this.popupContainer ) {
1106 return;
1107 }
1108
1109 const statusSelect = this.popupContainer.querySelector(
1110 `#cb-${ this.currentType }-publish-status`
1111 );
1112 if ( ! statusSelect ) {
1113 return;
1114 }
1115
1116 statusSelect.value = status === 'publish' ? 'publish' : 'draft';
1117 }
1118
1119 /**
1120 * Handle save action
1121 */
1122 handleSave( args ) {
1123 const saveBtn = args?.target ? args.target.closest( BuilderPopup.selectors.saveBtn ) : args;
1124 if ( ! saveBtn ) {
1125 return;
1126 }
1127
1128 if ( ! this.currentType ) {
1129 return;
1130 }
1131
1132 const publishLabel = ( saveBtn?.dataset?.titlePublish || '' ).toString().trim().toLowerCase();
1133 const currentLabel = ( saveBtn?.textContent || '' ).toString().trim().toLowerCase();
1134 const forcePublish = !! publishLabel && currentLabel === publishLabel;
1135 const targetStatus = forcePublish ? 'publish' : this.getStatusFromPublishPanel( 'publish' );
1136 if ( forcePublish ) {
1137 this.syncPublishPanelStatus( 'publish' );
1138 }
1139 this.syncAllTinyMCE();
1140
1141 const formData = this.getFormData();
1142 const validation = this.validateFormData( formData );
1143
1144 if ( ! validation.valid ) {
1145 lpToastify.show( validation.errors.join( '. ' ), 'error' );
1146 return;
1147 }
1148
1149 lpUtils.lpSetLoadingEl( saveBtn, 1 );
1150
1151 const actionMap = {
1152 lesson: 'builder_update_lesson',
1153 quiz: 'builder_update_quiz',
1154 question: 'builder_update_question',
1155 };
1156
1157 const wasNewItem = this.isNewItem;
1158
1159 const dataSend = {
1160 ...formData,
1161 action: actionMap[ this.currentType ] || `builder_update_${ this.currentType }`,
1162 args: { id_url: `builder-update-${ this.currentType }` },
1163 [ `${ this.currentType }_status` ]: targetStatus,
1164 return_html: 'yes',
1165 };
1166
1167 const callBack = {
1168 success: ( response ) => {
1169 const { status, message, data } = response;
1170
1171 lpToastify.show( message, status );
1172
1173 if ( status === 'success' ) {
1174 this.handleSaveSuccess( data, formData, wasNewItem );
1175 }
1176 },
1177 error: ( error ) => {
1178 lpToastify.show( error.message || 'Save failed', 'error' );
1179 },
1180 completed: () => {
1181 lpUtils.lpSetLoadingEl( saveBtn, 0 );
1182 },
1183 };
1184
1185 window.lpAJAXG.fetchAJAX( dataSend, callBack );
1186 }
1187
1188 /**
1189 * Handle save as draft action
1190 */
1191 async handleDraft( args ) {
1192 const draftBtn = args?.target ? args.target.closest( BuilderPopup.selectors.draftBtn ) : args;
1193 if ( ! draftBtn ) {
1194 return;
1195 }
1196
1197 if ( ! this.currentType ) {
1198 return;
1199 }
1200
1201 // Check if published to show confirm unpublish modal
1202 const statusEl = this.popupContainer.querySelector( `.${ this.currentType }-status` );
1203 const isPublished = statusEl && statusEl.classList.contains( 'publish' );
1204 if ( isPublished ) {
1205 const confirmMsg =
1206 draftBtn.dataset.confirmUnpublish ||
1207 'Saving as draft will unpublish this item from the course.';
1208 const result = await SweetAlert.fire( {
1209 title: 'Are you sure?',
1210 text: confirmMsg,
1211 iconHtml: SWAL_ICON_TRASH_DRAFT,
1212 customClass: { icon: 'lp-cb-swal-icon-html' },
1213 showCloseButton: true,
1214 showCancelButton: true,
1215 cancelButtonText: lpData.i18n.cancel,
1216 confirmButtonText: lpData.i18n.yes,
1217 reverseButtons: true,
1218 } );
1219
1220 if ( ! result.isConfirmed ) {
1221 return;
1222 }
1223 }
1224
1225 this.syncAllTinyMCE();
1226
1227 const formData = this.getFormData();
1228 const validation = this.validateFormData( formData );
1229
1230 if ( ! validation.valid ) {
1231 lpToastify.show( validation.errors.join( '. ' ), 'error' );
1232 return;
1233 }
1234
1235 lpUtils.lpSetLoadingEl( draftBtn, 1 );
1236
1237 const actionMap = {
1238 lesson: 'builder_update_lesson',
1239 quiz: 'builder_update_quiz',
1240 question: 'builder_update_question',
1241 };
1242
1243 const wasNewItem = this.isNewItem;
1244
1245 const dataSend = {
1246 ...formData,
1247 action: actionMap[ this.currentType ] || `builder_update_${ this.currentType }`,
1248 args: { id_url: `builder-update-${ this.currentType }` },
1249 [ `${ this.currentType }_status` ]: 'draft',
1250 return_html: 'yes',
1251 };
1252
1253 const callBack = {
1254 success: ( response ) => {
1255 const { status, message, data } = response;
1256
1257 lpToastify.show( message, status );
1258
1259 if ( status === 'success' ) {
1260 this.handleSaveSuccess( data, formData, wasNewItem );
1261 }
1262 },
1263 error: ( error ) => {
1264 lpToastify.show( error.message || 'Save draft failed', 'error' );
1265 },
1266 completed: () => {
1267 lpUtils.lpSetLoadingEl( draftBtn, 0 );
1268 },
1269 };
1270
1271 window.lpAJAXG.fetchAJAX( dataSend, callBack );
1272 }
1273
1274 /**
1275 * Handle save success
1276 */
1277 handleSaveSuccess( data, formData, wasNewItem ) {
1278 if ( data?.button_title ) {
1279 const primarySaveBtn = this.popupContainer.querySelector( BuilderPopup.selectors.saveBtn );
1280 if ( primarySaveBtn ) {
1281 primarySaveBtn.textContent = data.button_title;
1282 }
1283 }
1284
1285 // Update status
1286 if ( data?.status ) {
1287 this.syncPublishPanelStatus( data.status );
1288
1289 const statusEl = this.popupContainer.querySelector( `.${ this.currentType }-status` );
1290 if ( statusEl ) {
1291 statusEl.className = `${ this.currentType }-status ${ data.status }`;
1292 statusEl.textContent = data.status;
1293 }
1294
1295 if ( this.shouldRemoveFromCurriculum( data.status ) ) {
1296 this.removeItemFromCurriculum( this.currentId );
1297 }
1298
1299 if ( this.shouldRemoveQuestionFromAssignedQuiz( data.status ) ) {
1300 this.removeQuestionFromAssignedQuiz( this.currentId );
1301 }
1302 }
1303
1304 this.updatePermalinkUIAfterSave( data );
1305
1306 // Handle new item
1307 const newIdKey = `${ this.currentType }_id_new`;
1308 if ( data?.[ newIdKey ] ) {
1309 const newId = data[ newIdKey ];
1310 this.currentId = newId;
1311 this.isNewItem = false;
1312
1313 const wrapper = this.popupContainer.querySelector( `[data-${ this.currentType }-id]` );
1314 if ( wrapper ) {
1315 wrapper.dataset[ `${ this.currentType }Id` ] = newId;
1316 }
1317
1318 const popup = this.popupContainer.querySelector( BuilderPopup.selectors.popup );
1319 if ( popup ) {
1320 popup.dataset[ `${ this.currentType }Id` ] = newId;
1321 }
1322 }
1323
1324 // Store saved data
1325 this.savedData = { formData, data, wasNewItem };
1326
1327 // Update the list item immediately
1328 this.updateListItem( this.currentType, this.currentId, this.savedData );
1329
1330 // Handle new item creation
1331 if ( wasNewItem && this.currentId ) {
1332 document.dispatchEvent(
1333 new CustomEvent( 'lp-builder-popup-saved', {
1334 detail: {
1335 type: this.currentType,
1336 id: this.currentId,
1337 data,
1338 formData,
1339 wasNewItem,
1340 listItemHtml: data?.list_item_html || null,
1341 },
1342 } )
1343 );
1344
1345 // Reload popup to show all tabs
1346 setTimeout( () => {
1347 this.destroyAllTinyMCE();
1348 this.reloadCurrentPopup();
1349 }, 300 );
1350 } else {
1351 document.dispatchEvent(
1352 new CustomEvent( 'lp-builder-popup-saved', {
1353 detail: { type: this.currentType, id: this.currentId, data, formData, wasNewItem: false },
1354 } )
1355 );
1356 }
1357 }
1358
1359 /**
1360 * Handle trash action
1361 */
1362 async handleTrash( args ) {
1363 const trashBtn = args?.target ? args.target.closest( BuilderPopup.selectors.trashBtn ) : args;
1364 if ( ! trashBtn ) {
1365 return;
1366 }
1367
1368 if ( ! this.currentType || ! this.currentId ) {
1369 return;
1370 }
1371
1372 const confirmMsg =
1373 trashBtn.dataset.confirmTrash ||
1374 'Moving it to the trash will cause this item to be removed from the course.';
1375 const result = await SweetAlert.fire( {
1376 title: 'Are you sure?',
1377 text: confirmMsg,
1378 iconHtml: SWAL_ICON_TRASH_DRAFT,
1379 customClass: { icon: 'lp-cb-swal-icon-html' },
1380 showCloseButton: true,
1381 showCancelButton: true,
1382 cancelButtonText: lpData.i18n.cancel,
1383 confirmButtonText: lpData.i18n.yes,
1384 reverseButtons: true,
1385 } );
1386
1387 if ( ! result.isConfirmed ) {
1388 return;
1389 }
1390
1391 lpUtils.lpSetLoadingEl( trashBtn, 1 );
1392
1393 const actionMap = {
1394 lesson: 'move_trash_lesson',
1395 quiz: 'move_trash_quiz',
1396 question: 'move_trash_question',
1397 };
1398
1399 const dataSend = {
1400 action: actionMap[ this.currentType ] || `move_trash_${ this.currentType }`,
1401 args: { id_url: `move-trash-${ this.currentType }` },
1402 [ `${ this.currentType }_id` ]: this.currentId,
1403 };
1404 if (
1405 !! this.openContext?.isCurriculum &&
1406 ( parseInt( this.openContext?.courseId ) || 0 ) > 0
1407 ) {
1408 dataSend.course_id = parseInt( this.openContext.courseId ) || 0;
1409 }
1410
1411 const callBack = {
1412 success: ( response ) => {
1413 const { status, message, data } = response;
1414 lpToastify.show( message, status );
1415
1416 if ( status === 'success' ) {
1417 if ( data?.button_title ) {
1418 const saveBtn = this.popupContainer.querySelector( BuilderPopup.selectors.saveBtn );
1419 if ( saveBtn ) {
1420 saveBtn.textContent = data.button_title;
1421 }
1422 }
1423
1424 if ( data?.status ) {
1425 const statusEl = this.popupContainer.querySelector( `.${ this.currentType }-status` );
1426 if ( statusEl ) {
1427 statusEl.className = `${ this.currentType }-status ${ data.status }`;
1428 statusEl.textContent = data.status;
1429 }
1430
1431 if ( this.shouldRemoveFromCurriculum( data.status ) ) {
1432 this.removeItemFromCurriculum( this.currentId );
1433 }
1434
1435 if ( this.shouldRemoveQuestionFromAssignedQuiz( data.status ) ) {
1436 this.removeQuestionFromAssignedQuiz( this.currentId );
1437 }
1438 }
1439
1440 this.updatePermalinkUIAfterSave( data );
1441
1442 this.savedData = { formData: this.getFormData(), data, wasNewItem: false };
1443
1444 document.dispatchEvent(
1445 new CustomEvent( 'lp-builder-popup-trashed', {
1446 detail: { type: this.currentType, id: this.currentId, data },
1447 } )
1448 );
1449 }
1450 },
1451 error: ( error ) => {
1452 lpToastify.show( error.message || 'Trash failed', 'error' );
1453 },
1454 completed: () => {
1455 lpUtils.lpSetLoadingEl( trashBtn, 0 );
1456 },
1457 };
1458
1459 window.lpAJAXG.fetchAJAX( dataSend, callBack );
1460 }
1461
1462 shouldRemoveFromCurriculum( status ) {
1463 const normalizedStatus = ( status || '' ).toString().toLowerCase();
1464 const removableStatuses = [ 'draft', 'trash' ];
1465
1466 return (
1467 !! this.openContext?.isCurriculum &&
1468 removableStatuses.includes( normalizedStatus ) &&
1469 ( parseInt( this.currentId ) || 0 ) > 0
1470 );
1471 }
1472
1473 shouldRemoveQuestionFromAssignedQuiz( status ) {
1474 const normalizedStatus = ( status || '' ).toString().toLowerCase();
1475 return (
1476 this.currentType === 'question' &&
1477 [ 'draft', 'trash' ].includes( normalizedStatus ) &&
1478 ( parseInt( this.currentId ) || 0 ) > 0
1479 );
1480 }
1481
1482 removeItemFromCurriculum( itemId ) {
1483 const parsedItemId = parseInt( itemId ) || 0;
1484 if ( parsedItemId <= 0 ) {
1485 return;
1486 }
1487
1488 const curriculumRoot =
1489 document.querySelector( '#lp-course-edit-curriculum' ) ||
1490 document.querySelector( '.lp-edit-curriculum-wrap' );
1491 if ( ! curriculumRoot ) {
1492 return;
1493 }
1494
1495 const items = curriculumRoot.querySelectorAll(
1496 `.section-item[data-item-id="${ parsedItemId }"]`
1497 );
1498 if ( ! items.length ) {
1499 return;
1500 }
1501
1502 const sectionsToUpdate = new Set();
1503
1504 items.forEach( ( item ) => {
1505 const section = item.closest( '.section' );
1506 if ( section ) {
1507 sectionsToUpdate.add( section );
1508 }
1509
1510 item.remove();
1511 } );
1512
1513 this.syncCurriculumCounters( curriculumRoot, sectionsToUpdate );
1514 }
1515
1516 removeQuestionFromAssignedQuiz( questionId ) {
1517 const parsedQuestionId = parseInt( questionId ) || 0;
1518 if ( parsedQuestionId <= 0 ) {
1519 return;
1520 }
1521
1522 const questionItems = document.querySelectorAll(
1523 `.lp-question-item[data-question-id="${ parsedQuestionId }"]`
1524 );
1525
1526 questionItems.forEach( ( item ) => item.remove() );
1527 }
1528
1529 syncCurriculumCounters( curriculumRoot, sectionsToUpdate = new Set() ) {
1530 if ( ! curriculumRoot ) {
1531 return;
1532 }
1533
1534 const allItems = curriculumRoot.querySelectorAll( '.section-item:not(.clone)' );
1535 const totalItemsCount = allItems.length;
1536 const totalItemsEl = curriculumRoot.querySelector( '.total-items' );
1537
1538 if ( totalItemsEl ) {
1539 totalItemsEl.dataset.count = totalItemsCount;
1540
1541 const totalItemsCountEl = totalItemsEl.querySelector( '.count' );
1542 if ( totalItemsCountEl ) {
1543 totalItemsCountEl.textContent = totalItemsCount;
1544 }
1545 }
1546
1547 const sections =
1548 sectionsToUpdate.size > 0
1549 ? Array.from( sectionsToUpdate )
1550 : Array.from( curriculumRoot.querySelectorAll( '.section' ) );
1551
1552 sections.forEach( ( section ) => {
1553 const sectionItemsCountEl = section.querySelector( '.section-items-counts' );
1554 if ( ! sectionItemsCountEl ) {
1555 return;
1556 }
1557
1558 const sectionItemsCount = section.querySelectorAll( '.section-item:not(.clone)' ).length;
1559 sectionItemsCountEl.dataset.count = sectionItemsCount;
1560
1561 const countEl = sectionItemsCountEl.querySelector( '.count' );
1562 if ( countEl ) {
1563 countEl.textContent = sectionItemsCount;
1564 }
1565 } );
1566 }
1567
1568 /**
1569 * Validate form data
1570 */
1571 validateFormData( formData ) {
1572 const errors = [];
1573 const titleKey = `${ this.currentType }_title`;
1574 const title = formData[ titleKey ] || '';
1575
1576 if ( ! title.trim() ) {
1577 errors.push(
1578 `${
1579 this.currentType.charAt( 0 ).toUpperCase() + this.currentType.slice( 1 )
1580 } title is required`
1581 );
1582 }
1583
1584 if ( title.length > 200 ) {
1585 errors.push( 'Title must be less than 200 characters' );
1586 }
1587
1588 return { valid: errors.length === 0, errors };
1589 }
1590
1591 /**
1592 * Get form data from popup
1593 */
1594 getFormData() {
1595 const data = {};
1596 const popup = this.popupContainer.querySelector( BuilderPopup.selectors.popup );
1597
1598 if ( ! popup ) {
1599 return data;
1600 }
1601
1602 const idKey = `${ this.currentType }_id`;
1603 data[ idKey ] = this.currentId || 0;
1604 if (
1605 !! this.openContext?.isCurriculum &&
1606 ( parseInt( this.openContext?.courseId ) || 0 ) > 0
1607 ) {
1608 data.course_id = parseInt( this.openContext.courseId ) || 0;
1609 }
1610
1611 // Get title
1612 const titleInput = popup.querySelector(
1613 'input[name$="_title"], #title, #' + this.currentType + '_title'
1614 );
1615 if ( titleInput ) {
1616 data[ `${ this.currentType }_title` ] = titleInput.value;
1617 }
1618
1619 // Get description
1620 const editorId = `${ this.currentType }_description_editor`;
1621 const descContent = this.getEditorContent( editorId, popup );
1622
1623 data[ `${ this.currentType }_description` ] = descContent;
1624
1625 // Get form settings
1626 const formSettings = popup.querySelector( `.lp-form-setting-${ this.currentType }` );
1627 if ( formSettings ) {
1628 data[ `${ this.currentType }_settings` ] = true;
1629 this.collectFormData( formSettings, data );
1630 }
1631
1632 // Capture permalink slug in overview tab (quiz/question popup).
1633 const permalinkInput = popup.querySelector(
1634 `input[name="${ this.currentType }_permalink"], #${ this.currentType }_permalink, ${ BuilderPopup.selectors.permalinkSlugInput }`
1635 );
1636 if ( permalinkInput && permalinkInput.value ) {
1637 data[ `${ this.currentType }_permalink` ] = permalinkInput.value;
1638 }
1639
1640 return data;
1641 }
1642
1643 updatePermalinkUIAfterSave( data = {} ) {
1644 if ( ! this.currentType || ! this.popupContainer ) {
1645 return;
1646 }
1647
1648 const popup = this.popupContainer.querySelector( BuilderPopup.selectors.popup );
1649 if ( ! popup ) {
1650 return;
1651 }
1652
1653 const slugInput = popup.querySelector(
1654 `input[name="${ this.currentType }_permalink"], #${ this.currentType }_permalink, ${ BuilderPopup.selectors.permalinkSlugInput }`
1655 );
1656 const permalinkRoot = popup.querySelector( BuilderPopup.selectors.permalinkRoot );
1657 const permalinkPlaceholder = permalinkRoot?.querySelector(
1658 BuilderPopup.selectors.permalinkPlaceholder
1659 );
1660
1661 const responseSlug = data?.[ `${ this.currentType }_slug` ];
1662 if ( slugInput && responseSlug ) {
1663 slugInput.value = responseSlug;
1664 slugInput.dataset.originalValue = responseSlug;
1665 }
1666
1667 const responsePermalink = data?.[ `${ this.currentType }_permalink` ];
1668 const isCourseItem = [ 'lesson', 'quiz' ].includes( this.currentType );
1669 const shouldShowUnavailable =
1670 data?.permalink_available === false ||
1671 ( isCourseItem &&
1672 ( data?.status === 'draft' || data?.status === 'trash' || ! responsePermalink ) );
1673
1674 if ( shouldShowUnavailable ) {
1675 if ( ! permalinkRoot ) {
1676 return;
1677 }
1678
1679 const permalinkDisplay = permalinkRoot.querySelector(
1680 BuilderPopup.selectors.permalinkDisplay
1681 );
1682 const label =
1683 permalinkRoot.querySelector( '.cb-item-edit-permalink__label' ) ||
1684 permalinkRoot.querySelector( '.cb-permalink-label' );
1685 const editor = permalinkRoot.querySelector( BuilderPopup.selectors.permalinkEditor );
1686 let placeholder = permalinkPlaceholder;
1687
1688 if ( ! placeholder ) {
1689 placeholder = document.createElement( 'span' );
1690 placeholder.className = 'cb-item-edit-permalink__placeholder';
1691
1692 if ( label ) {
1693 label.insertAdjacentElement( 'afterend', placeholder );
1694 } else {
1695 permalinkRoot.prepend( placeholder );
1696 }
1697 }
1698
1699 placeholder.textContent =
1700 data?.permalink_notice ||
1701 'Permalink is only available if the item is already assigned to a course.';
1702 placeholder.classList.remove( 'lp-hidden' );
1703
1704 if ( permalinkDisplay ) {
1705 permalinkDisplay.classList.add( 'lp-hidden' );
1706 }
1707
1708 if ( editor ) {
1709 editor.classList.add( 'lp-hidden' );
1710 }
1711
1712 return;
1713 }
1714
1715 const urlLink = popup.querySelector( BuilderPopup.selectors.permalinkUrl );
1716 const permalinkDisplay = permalinkRoot?.querySelector(
1717 BuilderPopup.selectors.permalinkDisplay
1718 );
1719 const baseUrlInput = popup.querySelector( BuilderPopup.selectors.permalinkBaseUrl );
1720 const normalizedBaseUrl = typeof baseUrlInput?.value === 'string' ? baseUrlInput.value : '';
1721 const normalizedSlug = typeof responseSlug === 'string' ? responseSlug.trim() : '';
1722 let permalinkDisplayUrl = '';
1723
1724 if ( normalizedBaseUrl && normalizedSlug ) {
1725 permalinkDisplayUrl = `${ normalizedBaseUrl }${ normalizedSlug }`;
1726 } else if ( typeof responsePermalink === 'string' ) {
1727 permalinkDisplayUrl = responsePermalink;
1728 }
1729
1730 if ( permalinkPlaceholder ) {
1731 permalinkPlaceholder.classList.add( 'lp-hidden' );
1732 }
1733
1734 if ( permalinkDisplay ) {
1735 permalinkDisplay.classList.remove( 'lp-hidden' );
1736 }
1737
1738 if ( urlLink && responsePermalink ) {
1739 urlLink.href = responsePermalink;
1740 urlLink.textContent = permalinkDisplayUrl || responsePermalink;
1741 } else if ( urlLink && permalinkDisplayUrl ) {
1742 urlLink.textContent = permalinkDisplayUrl;
1743 }
1744 }
1745
1746 /**
1747 * Collect form data from form element
1748 */
1749 collectFormData( form, data ) {
1750 const formElements = form.querySelectorAll( 'input, select, textarea' );
1751
1752 formElements.forEach( ( element ) => {
1753 const name = element.name || element.id;
1754
1755 if ( ! name || name === 'learnpress_meta_box_nonce' || name === '_wp_http_referer' ) {
1756 return;
1757 }
1758
1759 const fieldName = name.replace( '[]', '' );
1760
1761 if ( element.type === 'checkbox' ) {
1762 if ( ! data.hasOwnProperty( fieldName ) ) {
1763 data[ fieldName ] = element.checked ? 'yes' : 'no';
1764 }
1765 } else if ( element.type === 'radio' ) {
1766 if ( element.checked ) {
1767 data[ fieldName ] = element.value;
1768 }
1769 } else if ( element.type === 'file' ) {
1770 if ( element.files?.length > 0 ) {
1771 data[ fieldName ] = element.files;
1772 }
1773 } else if ( name.endsWith( '[]' ) ) {
1774 if ( ! data.hasOwnProperty( fieldName ) ) {
1775 data[ fieldName ] = [];
1776 }
1777 if ( Array.isArray( data[ fieldName ] ) ) {
1778 data[ fieldName ].push( element.value );
1779 }
1780 } else if ( ! data.hasOwnProperty( fieldName ) ) {
1781 data[ fieldName ] = element.value;
1782 }
1783 } );
1784
1785 // Convert arrays to comma-separated strings
1786 Object.keys( data ).forEach( ( key ) => {
1787 if ( Array.isArray( data[ key ] ) ) {
1788 data[ key ] = data[ key ].join( ',' );
1789 }
1790 } );
1791 }
1792
1793 /**
1794 * Load tab-specific assets (CSS/JS)
1795 */
1796 loadTabAssets( tabName, tabPane ) {
1797 const tabKey = `${ this.currentType }-${ tabName }`;
1798
1799 if ( this.loadedTabAssets.has( tabKey ) ) {
1800 return;
1801 }
1802
1803 const assetsData = tabPane.dataset.tabAssets;
1804 if ( ! assetsData ) {
1805 this.loadedTabAssets.add( tabKey );
1806 return;
1807 }
1808
1809 try {
1810 const assets = JSON.parse( assetsData );
1811
1812 // Load CSS
1813 if ( assets.css && Array.isArray( assets.css ) ) {
1814 assets.css.forEach( ( cssUrl ) => {
1815 if ( ! document.querySelector( `link[href="${ cssUrl }"]` ) ) {
1816 const link = document.createElement( 'link' );
1817 link.rel = 'stylesheet';
1818 link.href = cssUrl;
1819 link.dataset.tabAsset = tabKey;
1820 document.head.appendChild( link );
1821 }
1822 } );
1823 }
1824
1825 // Load JS
1826 if ( assets.js && Array.isArray( assets.js ) ) {
1827 assets.js.forEach( ( jsUrl ) => {
1828 if ( ! document.querySelector( `script[src="${ jsUrl }"]` ) ) {
1829 const script = document.createElement( 'script' );
1830 script.src = jsUrl;
1831 script.dataset.tabAsset = tabKey;
1832 document.head.appendChild( script );
1833 }
1834 } );
1835 }
1836
1837 this.loadedTabAssets.add( tabKey );
1838 } catch ( e ) {
1839 console.warn( `Failed to load assets for tab "${ tabName }":`, e );
1840 this.loadedTabAssets.add( tabKey );
1841 }
1842 }
1843
1844 /**
1845 * Static method to open popup programmatically
1846 */
1847 static open( type, id = 0 ) {
1848 if ( ! BuilderPopup._instance ) {
1849 BuilderPopup._instance = new BuilderPopup();
1850 }
1851
1852 const selectors = {
1853 lesson: id ? `[data-popup-lesson="${ id }"]` : BuilderPopup.selectors.addNewLesson,
1854 quiz: id ? `[data-popup-quiz="${ id }"]` : '',
1855 question: id ? `[data-popup-question="${ id }"]` : '',
1856 };
1857 const triggerSelector =
1858 selectors[ type ] ||
1859 ( id
1860 ? `[data-popup-type="${ type }"][data-popup-id="${ id }"]`
1861 : `[data-popup-type="${ type }"][data-template]` );
1862 if ( ! triggerSelector ) {
1863 return;
1864 }
1865
1866 const triggerEl = document.querySelector( triggerSelector );
1867 if ( ! triggerEl ) {
1868 return;
1869 }
1870
1871 BuilderPopup._instance.showPopup(
1872 triggerEl,
1873 type,
1874 id,
1875 BuilderPopup._instance.resolveOpenContext( triggerEl )
1876 );
1877 }
1878
1879 /**
1880 * Static method to close popup programmatically
1881 */
1882 static close() {
1883 if ( BuilderPopup._instance ) {
1884 BuilderPopup._instance.closePopup();
1885 }
1886 }
1887 }
1888
1889 // Auto-initialize
1890 document.addEventListener( 'DOMContentLoaded', () => {
1891 BuilderPopup._instance = new BuilderPopup();
1892 } );
1893
1894 export default BuilderPopup;
1895