PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.10
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.10
4.4.10 4.4.9.1 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 All 142 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.10, at assets/src/js/frontend/course-builder/builder-popup.js

1,869 lines 51.6 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 nextListItem = currentItem.nextElementSibling;
464 currentItem.insertAdjacentHTML( 'afterend', data.list_item_html.trim() );
465 const newListItem = currentItem.nextElementSibling;
466 if ( newListItem && newListItem !== nextListItem ) {
467 currentItem.remove();
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 item.tagName === 'TR'
628 ) {
629 foundItems.add( item );
630 } else {
631 // If it's a wrapper, like a div in Content Bank
632 if ( item.closest( 'ul' ) ) {
633 foundItems.add( item );
634 }
635 }
636 }
637 }
638 }
639
640 return Array.from( foundItems );
641 }
642
643 /**
644 * Insert a newly created list item into the current tab list.
645 */
646 insertNewListItem( type, id, listItemHtml ) {
647 if ( ! listItemHtml ) {
648 return null;
649 }
650
651 const existingListItems = this.findListItems( type, id );
652 if ( existingListItems && existingListItems.length > 0 ) {
653 return existingListItems[ 0 ];
654 }
655
656 const listContainer = this.findListContainer( type );
657 if ( ! listContainer ) {
658 return null;
659 }
660
661 const emptyRow = listContainer.querySelector( '.cb-list-empty-row' );
662 const firstListItem = listContainer.firstElementChild;
663 listContainer.insertAdjacentHTML( 'afterbegin', listItemHtml.trim() );
664 const newListItem = listContainer.firstElementChild;
665
666 if ( ! newListItem || newListItem === firstListItem ) {
667 return null;
668 }
669
670 if ( emptyRow ) {
671 emptyRow.remove();
672 }
673
674 const highlightClassByType = {
675 lesson: 'highlight-new-lesson',
676 quiz: 'highlight-new-quiz',
677 question: 'highlight-new-question',
678 };
679 const highlightClass = highlightClassByType[ type ];
680
681 if ( highlightClass ) {
682 newListItem.classList.add( highlightClass );
683 newListItem.scrollIntoView( {
684 behavior: 'smooth',
685 block: 'nearest',
686 } );
687
688 setTimeout( () => {
689 newListItem.classList.remove( highlightClass );
690 }, 1500 );
691 }
692
693 const finalListItems = this.findListItems( type, id );
694 return finalListItems && finalListItems.length > 0 ? finalListItems[ 0 ] : newListItem;
695 }
696
697 /**
698 * Find the table body for the requested list type.
699 */
700 findListContainer( type ) {
701 const listSelectorByType = {
702 lesson: '.cb-list-lesson tbody',
703 quiz: '.cb-list-quiz tbody',
704 question: '.cb-list-question tbody',
705 };
706
707 const listSelector = listSelectorByType[ type ] || `[data-builder-list="${ type }"]`;
708 if ( ! listSelector ) {
709 return null;
710 }
711
712 const existingList = document.querySelector( listSelector );
713 if ( existingList ) {
714 return existingList;
715 }
716
717 return null;
718 }
719
720 /**
721 * Update element text (input value or textContent)
722 */
723 updateElementText( parent, selectors, newText ) {
724 for ( const selector of selectors ) {
725 const el = parent.querySelector( selector );
726 if ( el ) {
727 if ( el.tagName === 'INPUT' ) {
728 el.value = newText;
729 } else {
730 el.textContent = newText;
731 }
732 return true;
733 }
734 }
735 return false;
736 }
737
738 /**
739 * Update element class
740 */
741 updateElementClass( parent, selectors, newClass ) {
742 for ( const selector of selectors ) {
743 const el = parent.querySelector( selector );
744 if ( el ) {
745 const baseClass = selector.replace( '.', '' );
746 el.className = el.className.replace( /\b(publish|draft|pending|trash)\b/g, '' ).trim();
747 el.classList.add( baseClass, newClass );
748 el.textContent = newClass;
749 return true;
750 }
751 }
752 return false;
753 }
754
755 /**
756 * Update duration meta
757 */
758 updateDuration( listItem, duration ) {
759 const durationStr = this.formatDuration( duration );
760 const updated = this.updateElementText(
761 listItem,
762 [ '.item-meta.duration', '.duration', '.course-item-duration', '.meta-duration' ],
763 durationStr
764 );
765
766 if ( ! updated && durationStr ) {
767 const metaContainer = listItem.querySelector(
768 '.course-item__right, .item-meta-container, .course-item-meta'
769 );
770 if ( metaContainer ) {
771 let durationEl = metaContainer.querySelector( '.duration' );
772 if ( ! durationEl ) {
773 durationEl = document.createElement( 'span' );
774 durationEl.className = 'duration';
775 metaContainer.insertBefore( durationEl, metaContainer.firstChild );
776 }
777 durationEl.textContent = durationStr;
778 }
779 }
780 }
781
782 /**
783 * Format duration value
784 */
785 formatDuration( duration ) {
786 if ( ! duration ) {
787 return '';
788 }
789
790 if ( typeof duration === 'string' && duration.match( /\d+\s+\w+/ ) ) {
791 return duration;
792 }
793
794 const parts = String( duration ).trim().split( /\s+/ );
795 if ( parts.length >= 2 ) {
796 const value = parseInt( parts[ 0 ] ) || 0;
797 const unit = parts[ 1 ].toLowerCase();
798
799 if ( value === 0 ) {
800 return '';
801 }
802
803 const unitMap = {
804 minute: value === 1 ? 'Minute' : 'Minutes',
805 hour: value === 1 ? 'Hour' : 'Hours',
806 day: value === 1 ? 'Day' : 'Days',
807 week: value === 1 ? 'Week' : 'Weeks',
808 };
809
810 return `${ value } ${ unitMap[ unit ] || unit }`;
811 }
812
813 const numValue = parseInt( duration ) || 0;
814 return numValue > 0 ? `${ numValue } ${ numValue === 1 ? 'Minute' : 'Minutes' }` : '';
815 }
816
817 /**
818 * Check if popup is open
819 */
820 isPopupOpen() {
821 return this.popupContainer?.classList.contains( 'active' );
822 }
823
824 /**
825 * Switch tab with dynamic asset loading
826 */
827 switchTab( args ) {
828 const tabEl = args?.target ? args.target.closest( BuilderPopup.selectors.tab ) : args;
829 if ( ! tabEl ) {
830 return;
831 }
832
833 const tabName = tabEl.dataset.tab;
834 const popup = tabEl.closest( BuilderPopup.selectors.popup );
835
836 if ( ! popup || ! tabName ) {
837 return;
838 }
839
840 // Sync TinyMCE before switching
841 this.syncAllTinyMCE();
842
843 // Update tab states
844 popup.querySelectorAll( BuilderPopup.selectors.tab ).forEach( ( tab ) => {
845 tab.classList.remove( 'active' );
846 } );
847 tabEl.classList.add( 'active' );
848
849 // Update pane states
850 popup.querySelectorAll( BuilderPopup.selectors.tabPane ).forEach( ( pane ) => {
851 pane.classList.remove( 'active' );
852 } );
853
854 const targetPane = popup.querySelector(
855 `${ BuilderPopup.selectors.tabPane }[data-tab="${ tabName }"]`
856 );
857
858 if ( ! targetPane ) {
859 return;
860 }
861
862 targetPane.classList.add( 'active' );
863 this.loadTabAssets( tabName, targetPane );
864 const tabKey = `${ this.currentType }-${ tabName }`;
865
866 if ( ! this.initializedTabs.has( tabKey ) ) {
867 if ( tabName === 'overview' ) {
868 setTimeout( () => this.initTinyMCE(), 100 );
869 this.initializedTabs.set( tabKey, true );
870 } else if ( tabName === 'questions' && this.currentType === 'quiz' ) {
871 this.triggerAjaxLoadForTab( targetPane );
872 if ( this.builderEditQuiz ) {
873 setTimeout( () => {
874 this.builderEditQuiz.reinit( this.popupContainer );
875 this.initializedTabs.set( tabKey, true );
876 }, 100 );
877 }
878 } else if ( tabName === 'settings' && this.currentType === 'question' ) {
879 this.triggerAjaxLoadForTab( targetPane );
880 if ( this.builderEditQuestion ) {
881 setTimeout( () => {
882 this.builderEditQuestion.reinit( this.popupContainer );
883 this.initializedTabs.set( tabKey, true );
884 }, 100 );
885 }
886 } else if ( tabName === 'settings' && this.currentType === 'lesson' ) {
887 this.triggerAjaxLoadForTab( targetPane );
888 if ( this.builderMaterial ) {
889 setTimeout( () => {
890 this.builderMaterial.reinit( this.popupContainer );
891 this.initializedTabs.set( tabKey, true );
892 }, 100 );
893 }
894 }
895 }
896
897 document.dispatchEvent(
898 new CustomEvent( 'lp-builder-tab-switched', {
899 detail: { tabName, type: this.currentType, id: this.currentId },
900 } )
901 );
902 }
903
904 /**
905 * Trigger AJAX loading for tab elements
906 */
907 triggerAjaxLoadForTab( tabPane ) {
908 if ( ! tabPane || ! window.lpAJAXG ) {
909 return;
910 }
911
912 const ajaxElements = tabPane.querySelectorAll( '.lp-load-ajax-element:not(.loaded)' );
913
914 if ( ajaxElements.length > 0 ) {
915 ajaxElements.forEach( ( el ) => el.classList.remove( 'loaded' ) );
916 window.lpAJAXG.getElements();
917 }
918 }
919
920 /**
921 * Initialize TinyMCE for current popup type
922 */
923 initTinyMCE() {
924 const editorId = `${ this.currentType }_description_editor`;
925 const textarea = document.getElementById( editorId );
926
927 if ( ! textarea || typeof tinymce === 'undefined' ) {
928 return;
929 }
930
931 this.destroyTinyMCE( editorId );
932
933 if ( typeof wp !== 'undefined' && wp.editor?.initialize ) {
934 wp.editor.initialize( editorId, {
935 tinymce: {
936 wpautop: true,
937 content_style:
938 "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; }",
939 plugins:
940 'charmap colorpicker compat3x directionality fullscreen hr image lists media paste tabfocus textcolor wordpress wpautoresize wplink wptextpattern',
941 toolbar1:
942 'formatselect,bold,italic,underline,bullist,numlist,blockquote,alignleft,aligncenter,alignright,link,unlink,spellchecker,wp_adv',
943 toolbar2:
944 'strikethrough,hr,forecolor,pastetext,removeformat,charmap,outdent,indent,undo,redo,wp_help',
945 wordpress_adv_hidden: true,
946 },
947 quicktags: { buttons: 'strong,em,link,block,del,ins,img,ul,ol,li,code,more,close' },
948 mediaButtons: true,
949 } );
950 } else {
951 tinymce.init( {
952 selector: '#' + editorId,
953 height: 300,
954 menubar: false,
955 content_style:
956 "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; }",
957 plugins: [
958 'advlist autolink lists link image charmap print preview anchor',
959 'searchreplace visualblocks code fullscreen',
960 'insertdatetime media table paste code help wordcount',
961 ],
962 toolbar:
963 'undo redo | formatselect | bold italic backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help',
964 } );
965 }
966 }
967
968 /**
969 * Sync all TinyMCE instances
970 */
971 syncAllTinyMCE() {
972 if ( typeof tinymce === 'undefined' || ! this.currentType ) {
973 return;
974 }
975
976 const editorId = `${ this.currentType }_description_editor`;
977 const editor = tinymce.get( editorId );
978
979 if ( editor && ! this.isEditorInCodeMode( editorId ) ) {
980 editor.save();
981 }
982
983 // Sync additional editors
984 tinymce.editors.forEach( ( ed ) => {
985 if ( ed.id?.includes( this.currentType ) && ! this.isEditorInCodeMode( ed.id ) ) {
986 ed.save();
987 }
988 } );
989 }
990
991 isEditorInCodeMode( editorId ) {
992 const wrapper =
993 document.getElementById( `wp-${ editorId }-wrap` ) ||
994 document.getElementById( `${ editorId }-wrap` );
995 if ( wrapper?.classList.contains( 'html-active' ) ) {
996 return true;
997 }
998
999 const editor = typeof tinymce !== 'undefined' ? tinymce.get( editorId ) : null;
1000
1001 return !! ( editor?.isHidden && editor.isHidden() );
1002 }
1003
1004 getEditorContent( editorId, root = document ) {
1005 const textarea =
1006 root?.querySelector?.( `#${ editorId }` ) || document.getElementById( editorId );
1007
1008 if ( this.isEditorInCodeMode( editorId ) ) {
1009 return textarea ? textarea.value : '';
1010 }
1011
1012 if ( typeof tinymce !== 'undefined' ) {
1013 const editor = tinymce.get( editorId );
1014 if ( editor ) {
1015 return editor.getContent();
1016 }
1017 }
1018
1019 return textarea ? textarea.value : '';
1020 }
1021
1022 /**
1023 * Destroy specific TinyMCE instance
1024 */
1025 destroyTinyMCE( editorId ) {
1026 if ( typeof tinymce !== 'undefined' ) {
1027 const editor = tinymce.get( editorId );
1028 if ( editor ) {
1029 editor.remove();
1030 }
1031 }
1032
1033 if ( typeof wp !== 'undefined' && wp.editor?.remove ) {
1034 wp.editor.remove( editorId );
1035 }
1036 }
1037
1038 /**
1039 * Destroy all TinyMCE editors in popup
1040 */
1041 destroyAllTinyMCE() {
1042 if ( ! this.currentType || typeof tinymce === 'undefined' ) {
1043 return;
1044 }
1045
1046 const editorId = `${ this.currentType }_description_editor`;
1047 this.destroyTinyMCE( editorId );
1048
1049 const editorsToRemove = [];
1050 tinymce.editors.forEach( ( ed ) => {
1051 if ( ed.id && this.popupContainer?.querySelector( `#${ ed.id }` ) ) {
1052 editorsToRemove.push( ed.id );
1053 }
1054 } );
1055 editorsToRemove.forEach( ( id ) => this.destroyTinyMCE( id ) );
1056 }
1057
1058 getStatusFromPublishPanel( fallbackStatus = 'publish' ) {
1059 if ( ! this.currentType || ! this.popupContainer ) {
1060 return fallbackStatus;
1061 }
1062
1063 const statusSelect = this.popupContainer.querySelector(
1064 `#cb-${ this.currentType }-publish-status`
1065 );
1066 if ( ! statusSelect ) {
1067 return fallbackStatus;
1068 }
1069
1070 const selectedStatus = statusSelect.value;
1071 if ( selectedStatus === 'publish' || selectedStatus === 'draft' ) {
1072 return selectedStatus;
1073 }
1074
1075 return fallbackStatus;
1076 }
1077
1078 syncPublishPanelStatus( status ) {
1079 if ( ! this.currentType || ! this.popupContainer ) {
1080 return;
1081 }
1082
1083 const statusSelect = this.popupContainer.querySelector(
1084 `#cb-${ this.currentType }-publish-status`
1085 );
1086 if ( ! statusSelect ) {
1087 return;
1088 }
1089
1090 statusSelect.value = status === 'publish' ? 'publish' : 'draft';
1091 }
1092
1093 /**
1094 * Handle save action
1095 */
1096 handleSave( args ) {
1097 const saveBtn = args?.target ? args.target.closest( BuilderPopup.selectors.saveBtn ) : args;
1098 if ( ! saveBtn ) {
1099 return;
1100 }
1101
1102 if ( ! this.currentType ) {
1103 return;
1104 }
1105
1106 const publishLabel = ( saveBtn?.dataset?.titlePublish || '' ).toString().trim().toLowerCase();
1107 const currentLabel = ( saveBtn?.textContent || '' ).toString().trim().toLowerCase();
1108 const forcePublish = !! publishLabel && currentLabel === publishLabel;
1109 const targetStatus = forcePublish ? 'publish' : this.getStatusFromPublishPanel( 'publish' );
1110 if ( forcePublish ) {
1111 this.syncPublishPanelStatus( 'publish' );
1112 }
1113 this.syncAllTinyMCE();
1114
1115 const formData = this.getFormData();
1116 const validation = this.validateFormData( formData );
1117
1118 if ( ! validation.valid ) {
1119 lpToastify.show( validation.errors.join( '. ' ), 'error' );
1120 return;
1121 }
1122
1123 lpUtils.lpSetLoadingEl( saveBtn, 1 );
1124
1125 const actionMap = {
1126 lesson: 'builder_update_lesson',
1127 quiz: 'builder_update_quiz',
1128 question: 'builder_update_question',
1129 };
1130
1131 const wasNewItem = this.isNewItem;
1132
1133 const dataSend = {
1134 ...formData,
1135 action: actionMap[ this.currentType ] || `builder_update_${ this.currentType }`,
1136 args: { id_url: `builder-update-${ this.currentType }` },
1137 [ `${ this.currentType }_status` ]: targetStatus,
1138 return_html: 'yes',
1139 };
1140
1141 const callBack = {
1142 success: ( response ) => {
1143 const { status, message, data } = response;
1144
1145 lpToastify.show( message, status );
1146
1147 if ( status === 'success' ) {
1148 this.handleSaveSuccess( data, formData, wasNewItem );
1149 }
1150 },
1151 error: ( error ) => {
1152 lpToastify.show( error.message || 'Save failed', 'error' );
1153 },
1154 completed: () => {
1155 lpUtils.lpSetLoadingEl( saveBtn, 0 );
1156 },
1157 };
1158
1159 window.lpAJAXG.fetchAJAX( dataSend, callBack );
1160 }
1161
1162 /**
1163 * Handle save as draft action
1164 */
1165 async handleDraft( args ) {
1166 const draftBtn = args?.target ? args.target.closest( BuilderPopup.selectors.draftBtn ) : args;
1167 if ( ! draftBtn ) {
1168 return;
1169 }
1170
1171 if ( ! this.currentType ) {
1172 return;
1173 }
1174
1175 // Check if published to show confirm unpublish modal
1176 const statusEl = this.popupContainer.querySelector( `.${ this.currentType }-status` );
1177 const isPublished = statusEl && statusEl.classList.contains( 'publish' );
1178 if ( isPublished ) {
1179 const confirmMsg =
1180 draftBtn.dataset.confirmUnpublish ||
1181 'Saving as draft will unpublish this item from the course.';
1182 const result = await SweetAlert.fire( {
1183 title: 'Are you sure?',
1184 text: confirmMsg,
1185 iconHtml: SWAL_ICON_TRASH_DRAFT,
1186 customClass: { icon: 'lp-cb-swal-icon-html' },
1187 showCloseButton: true,
1188 showCancelButton: true,
1189 cancelButtonText: lpData.i18n.cancel,
1190 confirmButtonText: lpData.i18n.yes,
1191 reverseButtons: true,
1192 } );
1193
1194 if ( ! result.isConfirmed ) {
1195 return;
1196 }
1197 }
1198
1199 this.syncAllTinyMCE();
1200
1201 const formData = this.getFormData();
1202 const validation = this.validateFormData( formData );
1203
1204 if ( ! validation.valid ) {
1205 lpToastify.show( validation.errors.join( '. ' ), 'error' );
1206 return;
1207 }
1208
1209 lpUtils.lpSetLoadingEl( draftBtn, 1 );
1210
1211 const actionMap = {
1212 lesson: 'builder_update_lesson',
1213 quiz: 'builder_update_quiz',
1214 question: 'builder_update_question',
1215 };
1216
1217 const wasNewItem = this.isNewItem;
1218
1219 const dataSend = {
1220 ...formData,
1221 action: actionMap[ this.currentType ] || `builder_update_${ this.currentType }`,
1222 args: { id_url: `builder-update-${ this.currentType }` },
1223 [ `${ this.currentType }_status` ]: 'draft',
1224 return_html: 'yes',
1225 };
1226
1227 const callBack = {
1228 success: ( response ) => {
1229 const { status, message, data } = response;
1230
1231 lpToastify.show( message, status );
1232
1233 if ( status === 'success' ) {
1234 this.handleSaveSuccess( data, formData, wasNewItem );
1235 }
1236 },
1237 error: ( error ) => {
1238 lpToastify.show( error.message || 'Save draft failed', 'error' );
1239 },
1240 completed: () => {
1241 lpUtils.lpSetLoadingEl( draftBtn, 0 );
1242 },
1243 };
1244
1245 window.lpAJAXG.fetchAJAX( dataSend, callBack );
1246 }
1247
1248 /**
1249 * Handle save success
1250 */
1251 handleSaveSuccess( data, formData, wasNewItem ) {
1252 if ( data?.button_title ) {
1253 const primarySaveBtn = this.popupContainer.querySelector( BuilderPopup.selectors.saveBtn );
1254 if ( primarySaveBtn ) {
1255 primarySaveBtn.textContent = data.button_title;
1256 }
1257 }
1258
1259 // Update status
1260 if ( data?.status ) {
1261 this.syncPublishPanelStatus( data.status );
1262
1263 const statusEl = this.popupContainer.querySelector( `.${ this.currentType }-status` );
1264 if ( statusEl ) {
1265 statusEl.className = `${ this.currentType }-status ${ data.status }`;
1266 statusEl.textContent = data.status;
1267 }
1268
1269 if ( this.shouldRemoveFromCurriculum( data.status ) ) {
1270 this.removeItemFromCurriculum( this.currentId );
1271 }
1272
1273 if ( this.shouldRemoveQuestionFromAssignedQuiz( data.status ) ) {
1274 this.removeQuestionFromAssignedQuiz( this.currentId );
1275 }
1276 }
1277
1278 this.updatePermalinkUIAfterSave( data );
1279
1280 // Handle new item
1281 const newIdKey = `${ this.currentType }_id_new`;
1282 if ( data?.[ newIdKey ] ) {
1283 const newId = data[ newIdKey ];
1284 this.currentId = newId;
1285 this.isNewItem = false;
1286
1287 const wrapper = this.popupContainer.querySelector( `[data-${ this.currentType }-id]` );
1288 if ( wrapper ) {
1289 wrapper.dataset[ `${ this.currentType }Id` ] = newId;
1290 }
1291
1292 const popup = this.popupContainer.querySelector( BuilderPopup.selectors.popup );
1293 if ( popup ) {
1294 popup.dataset[ `${ this.currentType }Id` ] = newId;
1295 }
1296 }
1297
1298 // Store saved data
1299 this.savedData = { formData, data, wasNewItem };
1300
1301 // Update the list item immediately
1302 this.updateListItem( this.currentType, this.currentId, this.savedData );
1303
1304 // Handle new item creation
1305 if ( wasNewItem && this.currentId ) {
1306 document.dispatchEvent(
1307 new CustomEvent( 'lp-builder-popup-saved', {
1308 detail: {
1309 type: this.currentType,
1310 id: this.currentId,
1311 data,
1312 formData,
1313 wasNewItem,
1314 listItemHtml: data?.list_item_html || null,
1315 },
1316 } )
1317 );
1318
1319 // Reload popup to show all tabs
1320 setTimeout( () => {
1321 this.destroyAllTinyMCE();
1322 this.reloadCurrentPopup();
1323 }, 300 );
1324 } else {
1325 document.dispatchEvent(
1326 new CustomEvent( 'lp-builder-popup-saved', {
1327 detail: { type: this.currentType, id: this.currentId, data, formData, wasNewItem: false },
1328 } )
1329 );
1330 }
1331 }
1332
1333 /**
1334 * Handle trash action
1335 */
1336 async handleTrash( args ) {
1337 const trashBtn = args?.target ? args.target.closest( BuilderPopup.selectors.trashBtn ) : args;
1338 if ( ! trashBtn ) {
1339 return;
1340 }
1341
1342 if ( ! this.currentType || ! this.currentId ) {
1343 return;
1344 }
1345
1346 const confirmMsg =
1347 trashBtn.dataset.confirmTrash ||
1348 'Moving it to the trash will cause this item to be removed from the course.';
1349 const result = await SweetAlert.fire( {
1350 title: 'Are you sure?',
1351 text: confirmMsg,
1352 iconHtml: SWAL_ICON_TRASH_DRAFT,
1353 customClass: { icon: 'lp-cb-swal-icon-html' },
1354 showCloseButton: true,
1355 showCancelButton: true,
1356 cancelButtonText: lpData.i18n.cancel,
1357 confirmButtonText: lpData.i18n.yes,
1358 reverseButtons: true,
1359 } );
1360
1361 if ( ! result.isConfirmed ) {
1362 return;
1363 }
1364
1365 lpUtils.lpSetLoadingEl( trashBtn, 1 );
1366
1367 const actionMap = {
1368 lesson: 'move_trash_lesson',
1369 quiz: 'move_trash_quiz',
1370 question: 'move_trash_question',
1371 };
1372
1373 const dataSend = {
1374 action: actionMap[ this.currentType ] || `move_trash_${ this.currentType }`,
1375 args: { id_url: `move-trash-${ this.currentType }` },
1376 [ `${ this.currentType }_id` ]: this.currentId,
1377 };
1378 if (
1379 !! this.openContext?.isCurriculum &&
1380 ( parseInt( this.openContext?.courseId ) || 0 ) > 0
1381 ) {
1382 dataSend.course_id = parseInt( this.openContext.courseId ) || 0;
1383 }
1384
1385 const callBack = {
1386 success: ( response ) => {
1387 const { status, message, data } = response;
1388 lpToastify.show( message, status );
1389
1390 if ( status === 'success' ) {
1391 if ( data?.button_title ) {
1392 const saveBtn = this.popupContainer.querySelector( BuilderPopup.selectors.saveBtn );
1393 if ( saveBtn ) {
1394 saveBtn.textContent = data.button_title;
1395 }
1396 }
1397
1398 if ( data?.status ) {
1399 const statusEl = this.popupContainer.querySelector( `.${ this.currentType }-status` );
1400 if ( statusEl ) {
1401 statusEl.className = `${ this.currentType }-status ${ data.status }`;
1402 statusEl.textContent = data.status;
1403 }
1404
1405 if ( this.shouldRemoveFromCurriculum( data.status ) ) {
1406 this.removeItemFromCurriculum( this.currentId );
1407 }
1408
1409 if ( this.shouldRemoveQuestionFromAssignedQuiz( data.status ) ) {
1410 this.removeQuestionFromAssignedQuiz( this.currentId );
1411 }
1412 }
1413
1414 this.updatePermalinkUIAfterSave( data );
1415
1416 this.savedData = { formData: this.getFormData(), data, wasNewItem: false };
1417
1418 document.dispatchEvent(
1419 new CustomEvent( 'lp-builder-popup-trashed', {
1420 detail: { type: this.currentType, id: this.currentId, data },
1421 } )
1422 );
1423 }
1424 },
1425 error: ( error ) => {
1426 lpToastify.show( error.message || 'Trash failed', 'error' );
1427 },
1428 completed: () => {
1429 lpUtils.lpSetLoadingEl( trashBtn, 0 );
1430 },
1431 };
1432
1433 window.lpAJAXG.fetchAJAX( dataSend, callBack );
1434 }
1435
1436 shouldRemoveFromCurriculum( status ) {
1437 const normalizedStatus = ( status || '' ).toString().toLowerCase();
1438 const removableStatuses = [ 'draft', 'trash' ];
1439
1440 return (
1441 !! this.openContext?.isCurriculum &&
1442 removableStatuses.includes( normalizedStatus ) &&
1443 ( parseInt( this.currentId ) || 0 ) > 0
1444 );
1445 }
1446
1447 shouldRemoveQuestionFromAssignedQuiz( status ) {
1448 const normalizedStatus = ( status || '' ).toString().toLowerCase();
1449 return (
1450 this.currentType === 'question' &&
1451 [ 'draft', 'trash' ].includes( normalizedStatus ) &&
1452 ( parseInt( this.currentId ) || 0 ) > 0
1453 );
1454 }
1455
1456 removeItemFromCurriculum( itemId ) {
1457 const parsedItemId = parseInt( itemId ) || 0;
1458 if ( parsedItemId <= 0 ) {
1459 return;
1460 }
1461
1462 const curriculumRoot =
1463 document.querySelector( '#lp-course-edit-curriculum' ) ||
1464 document.querySelector( '.lp-edit-curriculum-wrap' );
1465 if ( ! curriculumRoot ) {
1466 return;
1467 }
1468
1469 const items = curriculumRoot.querySelectorAll(
1470 `.section-item[data-item-id="${ parsedItemId }"]`
1471 );
1472 if ( ! items.length ) {
1473 return;
1474 }
1475
1476 const sectionsToUpdate = new Set();
1477
1478 items.forEach( ( item ) => {
1479 const section = item.closest( '.section' );
1480 if ( section ) {
1481 sectionsToUpdate.add( section );
1482 }
1483
1484 item.remove();
1485 } );
1486
1487 this.syncCurriculumCounters( curriculumRoot, sectionsToUpdate );
1488 }
1489
1490 removeQuestionFromAssignedQuiz( questionId ) {
1491 const parsedQuestionId = parseInt( questionId ) || 0;
1492 if ( parsedQuestionId <= 0 ) {
1493 return;
1494 }
1495
1496 const questionItems = document.querySelectorAll(
1497 `.lp-question-item[data-question-id="${ parsedQuestionId }"]`
1498 );
1499
1500 questionItems.forEach( ( item ) => item.remove() );
1501 }
1502
1503 syncCurriculumCounters( curriculumRoot, sectionsToUpdate = new Set() ) {
1504 if ( ! curriculumRoot ) {
1505 return;
1506 }
1507
1508 const allItems = curriculumRoot.querySelectorAll( '.section-item:not(.clone)' );
1509 const totalItemsCount = allItems.length;
1510 const totalItemsEl = curriculumRoot.querySelector( '.total-items' );
1511
1512 if ( totalItemsEl ) {
1513 totalItemsEl.dataset.count = totalItemsCount;
1514
1515 const totalItemsCountEl = totalItemsEl.querySelector( '.count' );
1516 if ( totalItemsCountEl ) {
1517 totalItemsCountEl.textContent = totalItemsCount;
1518 }
1519 }
1520
1521 const sections =
1522 sectionsToUpdate.size > 0
1523 ? Array.from( sectionsToUpdate )
1524 : Array.from( curriculumRoot.querySelectorAll( '.section' ) );
1525
1526 sections.forEach( ( section ) => {
1527 const sectionItemsCountEl = section.querySelector( '.section-items-counts' );
1528 if ( ! sectionItemsCountEl ) {
1529 return;
1530 }
1531
1532 const sectionItemsCount = section.querySelectorAll( '.section-item:not(.clone)' ).length;
1533 sectionItemsCountEl.dataset.count = sectionItemsCount;
1534
1535 const countEl = sectionItemsCountEl.querySelector( '.count' );
1536 if ( countEl ) {
1537 countEl.textContent = sectionItemsCount;
1538 }
1539 } );
1540 }
1541
1542 /**
1543 * Validate form data
1544 */
1545 validateFormData( formData ) {
1546 const errors = [];
1547 const titleKey = `${ this.currentType }_title`;
1548 const title = formData[ titleKey ] || '';
1549
1550 if ( ! title.trim() ) {
1551 errors.push(
1552 `${
1553 this.currentType.charAt( 0 ).toUpperCase() + this.currentType.slice( 1 )
1554 } title is required`
1555 );
1556 }
1557
1558 if ( title.length > 200 ) {
1559 errors.push( 'Title must be less than 200 characters' );
1560 }
1561
1562 return { valid: errors.length === 0, errors };
1563 }
1564
1565 /**
1566 * Get form data from popup
1567 */
1568 getFormData() {
1569 const data = {};
1570 const popup = this.popupContainer.querySelector( BuilderPopup.selectors.popup );
1571
1572 if ( ! popup ) {
1573 return data;
1574 }
1575
1576 const idKey = `${ this.currentType }_id`;
1577 data[ idKey ] = this.currentId || 0;
1578 if (
1579 !! this.openContext?.isCurriculum &&
1580 ( parseInt( this.openContext?.courseId ) || 0 ) > 0
1581 ) {
1582 data.course_id = parseInt( this.openContext.courseId ) || 0;
1583 }
1584
1585 // Get title
1586 const titleInput = popup.querySelector(
1587 'input[name$="_title"], #title, #' + this.currentType + '_title'
1588 );
1589 if ( titleInput ) {
1590 data[ `${ this.currentType }_title` ] = titleInput.value;
1591 }
1592
1593 // Get description
1594 const editorId = `${ this.currentType }_description_editor`;
1595 const descContent = this.getEditorContent( editorId, popup );
1596
1597 data[ `${ this.currentType }_description` ] = descContent;
1598
1599 // Get form settings
1600 const formSettings = popup.querySelector( `.lp-form-setting-${ this.currentType }` );
1601 if ( formSettings ) {
1602 data[ `${ this.currentType }_settings` ] = true;
1603 this.collectFormData( formSettings, data );
1604 }
1605
1606 // Capture permalink slug in overview tab (quiz/question popup).
1607 const permalinkInput = popup.querySelector(
1608 `input[name="${ this.currentType }_permalink"], #${ this.currentType }_permalink, ${ BuilderPopup.selectors.permalinkSlugInput }`
1609 );
1610 if ( permalinkInput && permalinkInput.value ) {
1611 data[ `${ this.currentType }_permalink` ] = permalinkInput.value;
1612 }
1613
1614 return data;
1615 }
1616
1617 updatePermalinkUIAfterSave( data = {} ) {
1618 if ( ! this.currentType || ! this.popupContainer ) {
1619 return;
1620 }
1621
1622 const popup = this.popupContainer.querySelector( BuilderPopup.selectors.popup );
1623 if ( ! popup ) {
1624 return;
1625 }
1626
1627 const slugInput = popup.querySelector(
1628 `input[name="${ this.currentType }_permalink"], #${ this.currentType }_permalink, ${ BuilderPopup.selectors.permalinkSlugInput }`
1629 );
1630 const permalinkRoot = popup.querySelector( BuilderPopup.selectors.permalinkRoot );
1631 const permalinkPlaceholder = permalinkRoot?.querySelector(
1632 BuilderPopup.selectors.permalinkPlaceholder
1633 );
1634
1635 const responseSlug = data?.[ `${ this.currentType }_slug` ];
1636 if ( slugInput && responseSlug ) {
1637 slugInput.value = responseSlug;
1638 slugInput.dataset.originalValue = responseSlug;
1639 }
1640
1641 const responsePermalink = data?.[ `${ this.currentType }_permalink` ];
1642 const isCourseItem = [ 'lesson', 'quiz' ].includes( this.currentType );
1643 const shouldShowUnavailable =
1644 data?.permalink_available === false ||
1645 ( isCourseItem &&
1646 ( data?.status === 'draft' || data?.status === 'trash' || ! responsePermalink ) );
1647
1648 if ( shouldShowUnavailable ) {
1649 if ( ! permalinkRoot ) {
1650 return;
1651 }
1652
1653 const permalinkDisplay = permalinkRoot.querySelector(
1654 BuilderPopup.selectors.permalinkDisplay
1655 );
1656 const label =
1657 permalinkRoot.querySelector( '.cb-item-edit-permalink__label' ) ||
1658 permalinkRoot.querySelector( '.cb-permalink-label' );
1659 const editor = permalinkRoot.querySelector( BuilderPopup.selectors.permalinkEditor );
1660 let placeholder = permalinkPlaceholder;
1661
1662 if ( ! placeholder ) {
1663 placeholder = document.createElement( 'span' );
1664 placeholder.className = 'cb-item-edit-permalink__placeholder';
1665
1666 if ( label ) {
1667 label.insertAdjacentElement( 'afterend', placeholder );
1668 } else {
1669 permalinkRoot.prepend( placeholder );
1670 }
1671 }
1672
1673 placeholder.textContent =
1674 data?.permalink_notice ||
1675 'Permalink is only available if the item is already assigned to a course.';
1676 placeholder.classList.remove( 'lp-hidden' );
1677
1678 if ( permalinkDisplay ) {
1679 permalinkDisplay.classList.add( 'lp-hidden' );
1680 }
1681
1682 if ( editor ) {
1683 editor.classList.add( 'lp-hidden' );
1684 }
1685
1686 return;
1687 }
1688
1689 const urlLink = popup.querySelector( BuilderPopup.selectors.permalinkUrl );
1690 const permalinkDisplay = permalinkRoot?.querySelector(
1691 BuilderPopup.selectors.permalinkDisplay
1692 );
1693 const baseUrlInput = popup.querySelector( BuilderPopup.selectors.permalinkBaseUrl );
1694 const normalizedBaseUrl = typeof baseUrlInput?.value === 'string' ? baseUrlInput.value : '';
1695 const normalizedSlug = typeof responseSlug === 'string' ? responseSlug.trim() : '';
1696 let permalinkDisplayUrl = '';
1697
1698 if ( normalizedBaseUrl && normalizedSlug ) {
1699 permalinkDisplayUrl = `${ normalizedBaseUrl }${ normalizedSlug }`;
1700 } else if ( typeof responsePermalink === 'string' ) {
1701 permalinkDisplayUrl = responsePermalink;
1702 }
1703
1704 if ( permalinkPlaceholder ) {
1705 permalinkPlaceholder.classList.add( 'lp-hidden' );
1706 }
1707
1708 if ( permalinkDisplay ) {
1709 permalinkDisplay.classList.remove( 'lp-hidden' );
1710 }
1711
1712 if ( urlLink && responsePermalink ) {
1713 urlLink.href = responsePermalink;
1714 urlLink.textContent = permalinkDisplayUrl || responsePermalink;
1715 } else if ( urlLink && permalinkDisplayUrl ) {
1716 urlLink.textContent = permalinkDisplayUrl;
1717 }
1718 }
1719
1720 /**
1721 * Collect form data from form element
1722 */
1723 collectFormData( form, data ) {
1724 const formElements = form.querySelectorAll( 'input, select, textarea' );
1725
1726 formElements.forEach( ( element ) => {
1727 const name = element.name || element.id;
1728
1729 if ( ! name || name === 'learnpress_meta_box_nonce' || name === '_wp_http_referer' ) {
1730 return;
1731 }
1732
1733 const fieldName = name.replace( '[]', '' );
1734
1735 if ( element.type === 'checkbox' ) {
1736 if ( ! data.hasOwnProperty( fieldName ) ) {
1737 data[ fieldName ] = element.checked ? 'yes' : 'no';
1738 }
1739 } else if ( element.type === 'radio' ) {
1740 if ( element.checked ) {
1741 data[ fieldName ] = element.value;
1742 }
1743 } else if ( element.type === 'file' ) {
1744 if ( element.files?.length > 0 ) {
1745 data[ fieldName ] = element.files;
1746 }
1747 } else if ( name.endsWith( '[]' ) ) {
1748 if ( ! data.hasOwnProperty( fieldName ) ) {
1749 data[ fieldName ] = [];
1750 }
1751 if ( Array.isArray( data[ fieldName ] ) ) {
1752 data[ fieldName ].push( element.value );
1753 }
1754 } else if ( ! data.hasOwnProperty( fieldName ) ) {
1755 data[ fieldName ] = element.value;
1756 }
1757 } );
1758
1759 // Convert arrays to comma-separated strings
1760 Object.keys( data ).forEach( ( key ) => {
1761 if ( Array.isArray( data[ key ] ) ) {
1762 data[ key ] = data[ key ].join( ',' );
1763 }
1764 } );
1765 }
1766
1767 /**
1768 * Load tab-specific assets (CSS/JS)
1769 */
1770 loadTabAssets( tabName, tabPane ) {
1771 const tabKey = `${ this.currentType }-${ tabName }`;
1772
1773 if ( this.loadedTabAssets.has( tabKey ) ) {
1774 return;
1775 }
1776
1777 const assetsData = tabPane.dataset.tabAssets;
1778 if ( ! assetsData ) {
1779 this.loadedTabAssets.add( tabKey );
1780 return;
1781 }
1782
1783 try {
1784 const assets = JSON.parse( assetsData );
1785
1786 // Load CSS
1787 if ( assets.css && Array.isArray( assets.css ) ) {
1788 assets.css.forEach( ( cssUrl ) => {
1789 if ( ! document.querySelector( `link[href="${ cssUrl }"]` ) ) {
1790 const link = document.createElement( 'link' );
1791 link.rel = 'stylesheet';
1792 link.href = cssUrl;
1793 link.dataset.tabAsset = tabKey;
1794 document.head.appendChild( link );
1795 }
1796 } );
1797 }
1798
1799 // Load JS
1800 if ( assets.js && Array.isArray( assets.js ) ) {
1801 assets.js.forEach( ( jsUrl ) => {
1802 if ( ! document.querySelector( `script[src="${ jsUrl }"]` ) ) {
1803 const script = document.createElement( 'script' );
1804 script.src = jsUrl;
1805 script.dataset.tabAsset = tabKey;
1806 document.head.appendChild( script );
1807 }
1808 } );
1809 }
1810
1811 this.loadedTabAssets.add( tabKey );
1812 } catch ( e ) {
1813 console.warn( `Failed to load assets for tab "${ tabName }":`, e );
1814 this.loadedTabAssets.add( tabKey );
1815 }
1816 }
1817
1818 /**
1819 * Static method to open popup programmatically
1820 */
1821 static open( type, id = 0 ) {
1822 if ( ! BuilderPopup._instance ) {
1823 BuilderPopup._instance = new BuilderPopup();
1824 }
1825
1826 const selectors = {
1827 lesson: id ? `[data-popup-lesson="${ id }"]` : BuilderPopup.selectors.addNewLesson,
1828 quiz: id ? `[data-popup-quiz="${ id }"]` : '',
1829 question: id ? `[data-popup-question="${ id }"]` : '',
1830 };
1831 const triggerSelector =
1832 selectors[ type ] ||
1833 ( id
1834 ? `[data-popup-type="${ type }"][data-popup-id="${ id }"]`
1835 : `[data-popup-type="${ type }"][data-template]` );
1836 if ( ! triggerSelector ) {
1837 return;
1838 }
1839
1840 const triggerEl = document.querySelector( triggerSelector );
1841 if ( ! triggerEl ) {
1842 return;
1843 }
1844
1845 BuilderPopup._instance.showPopup(
1846 triggerEl,
1847 type,
1848 id,
1849 BuilderPopup._instance.resolveOpenContext( triggerEl )
1850 );
1851 }
1852
1853 /**
1854 * Static method to close popup programmatically
1855 */
1856 static close() {
1857 if ( BuilderPopup._instance ) {
1858 BuilderPopup._instance.closePopup();
1859 }
1860 }
1861 }
1862
1863 // Auto-initialize
1864 document.addEventListener( 'DOMContentLoaded', () => {
1865 BuilderPopup._instance = new BuilderPopup();
1866 } );
1867
1868 export default BuilderPopup;
1869