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

1,873 lines 51.7 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 ) {
1006 editor.save();
1007 }
1008
1009 // Sync additional editors
1010 tinymce.editors.forEach( ( ed ) => {
1011 if ( ed.id?.includes( this.currentType ) ) {
1012 ed.save();
1013 }
1014 } );
1015 }
1016
1017 /**
1018 * Destroy specific TinyMCE instance
1019 */
1020 destroyTinyMCE( editorId ) {
1021 if ( typeof tinymce !== 'undefined' ) {
1022 const editor = tinymce.get( editorId );
1023 if ( editor ) {
1024 editor.remove();
1025 }
1026 }
1027
1028 if ( typeof wp !== 'undefined' && wp.editor?.remove ) {
1029 wp.editor.remove( editorId );
1030 }
1031 }
1032
1033 /**
1034 * Destroy all TinyMCE editors in popup
1035 */
1036 destroyAllTinyMCE() {
1037 if ( ! this.currentType || typeof tinymce === 'undefined' ) {
1038 return;
1039 }
1040
1041 const editorId = `${ this.currentType }_description_editor`;
1042 this.destroyTinyMCE( editorId );
1043
1044 const editorsToRemove = [];
1045 tinymce.editors.forEach( ( ed ) => {
1046 if ( ed.id && this.popupContainer?.querySelector( `#${ ed.id }` ) ) {
1047 editorsToRemove.push( ed.id );
1048 }
1049 } );
1050 editorsToRemove.forEach( ( id ) => this.destroyTinyMCE( id ) );
1051 }
1052
1053 getStatusFromPublishPanel( fallbackStatus = 'publish' ) {
1054 if ( ! this.currentType || ! this.popupContainer ) {
1055 return fallbackStatus;
1056 }
1057
1058 const statusSelect = this.popupContainer.querySelector(
1059 `#cb-${ this.currentType }-publish-status`
1060 );
1061 if ( ! statusSelect ) {
1062 return fallbackStatus;
1063 }
1064
1065 const selectedStatus = statusSelect.value;
1066 if ( selectedStatus === 'publish' || selectedStatus === 'draft' ) {
1067 return selectedStatus;
1068 }
1069
1070 return fallbackStatus;
1071 }
1072
1073 syncPublishPanelStatus( status ) {
1074 if ( ! this.currentType || ! this.popupContainer ) {
1075 return;
1076 }
1077
1078 const statusSelect = this.popupContainer.querySelector(
1079 `#cb-${ this.currentType }-publish-status`
1080 );
1081 if ( ! statusSelect ) {
1082 return;
1083 }
1084
1085 statusSelect.value = status === 'publish' ? 'publish' : 'draft';
1086 }
1087
1088 /**
1089 * Handle save action
1090 */
1091 handleSave( args ) {
1092 const saveBtn = args?.target ? args.target.closest( BuilderPopup.selectors.saveBtn ) : args;
1093 if ( ! saveBtn ) {
1094 return;
1095 }
1096
1097 if ( ! this.currentType ) {
1098 return;
1099 }
1100
1101 const publishLabel = ( saveBtn?.dataset?.titlePublish || '' ).toString().trim().toLowerCase();
1102 const currentLabel = ( saveBtn?.textContent || '' ).toString().trim().toLowerCase();
1103 const forcePublish = !! publishLabel && currentLabel === publishLabel;
1104 const targetStatus = forcePublish ? 'publish' : this.getStatusFromPublishPanel( 'publish' );
1105 if ( forcePublish ) {
1106 this.syncPublishPanelStatus( 'publish' );
1107 }
1108 this.syncAllTinyMCE();
1109
1110 const formData = this.getFormData();
1111 const validation = this.validateFormData( formData );
1112
1113 if ( ! validation.valid ) {
1114 lpToastify.show( validation.errors.join( '. ' ), 'error' );
1115 return;
1116 }
1117
1118 lpUtils.lpSetLoadingEl( saveBtn, 1 );
1119
1120 const actionMap = {
1121 lesson: 'builder_update_lesson',
1122 quiz: 'builder_update_quiz',
1123 question: 'builder_update_question',
1124 };
1125
1126 const wasNewItem = this.isNewItem;
1127
1128 const dataSend = {
1129 ...formData,
1130 action: actionMap[ this.currentType ] || `builder_update_${ this.currentType }`,
1131 args: { id_url: `builder-update-${ this.currentType }` },
1132 [ `${ this.currentType }_status` ]: targetStatus,
1133 return_html: 'yes',
1134 };
1135
1136 const callBack = {
1137 success: ( response ) => {
1138 const { status, message, data } = response;
1139
1140 lpToastify.show( message, status );
1141
1142 if ( status === 'success' ) {
1143 this.handleSaveSuccess( data, formData, wasNewItem );
1144 }
1145 },
1146 error: ( error ) => {
1147 lpToastify.show( error.message || 'Save failed', 'error' );
1148 },
1149 completed: () => {
1150 lpUtils.lpSetLoadingEl( saveBtn, 0 );
1151 },
1152 };
1153
1154 window.lpAJAXG.fetchAJAX( dataSend, callBack );
1155 }
1156
1157 /**
1158 * Handle save as draft action
1159 */
1160 async handleDraft( args ) {
1161 const draftBtn = args?.target ? args.target.closest( BuilderPopup.selectors.draftBtn ) : args;
1162 if ( ! draftBtn ) {
1163 return;
1164 }
1165
1166 if ( ! this.currentType ) {
1167 return;
1168 }
1169
1170 // Check if published to show confirm unpublish modal
1171 const statusEl = this.popupContainer.querySelector( `.${ this.currentType }-status` );
1172 const isPublished = statusEl && statusEl.classList.contains( 'publish' );
1173 if ( isPublished ) {
1174 const confirmMsg =
1175 draftBtn.dataset.confirmUnpublish ||
1176 'Saving as draft will unpublish this item from the course.';
1177 const result = await SweetAlert.fire( {
1178 title: 'Are you sure?',
1179 text: confirmMsg,
1180 iconHtml: SWAL_ICON_TRASH_DRAFT,
1181 customClass: { icon: 'lp-cb-swal-icon-html' },
1182 showCloseButton: true,
1183 showCancelButton: true,
1184 cancelButtonText: lpData.i18n.cancel,
1185 confirmButtonText: lpData.i18n.yes,
1186 reverseButtons: true,
1187 } );
1188
1189 if ( ! result.isConfirmed ) {
1190 return;
1191 }
1192 }
1193
1194 this.syncAllTinyMCE();
1195
1196 const formData = this.getFormData();
1197 const validation = this.validateFormData( formData );
1198
1199 if ( ! validation.valid ) {
1200 lpToastify.show( validation.errors.join( '. ' ), 'error' );
1201 return;
1202 }
1203
1204 lpUtils.lpSetLoadingEl( draftBtn, 1 );
1205
1206 const actionMap = {
1207 lesson: 'builder_update_lesson',
1208 quiz: 'builder_update_quiz',
1209 question: 'builder_update_question',
1210 };
1211
1212 const wasNewItem = this.isNewItem;
1213
1214 const dataSend = {
1215 ...formData,
1216 action: actionMap[ this.currentType ] || `builder_update_${ this.currentType }`,
1217 args: { id_url: `builder-update-${ this.currentType }` },
1218 [ `${ this.currentType }_status` ]: 'draft',
1219 return_html: 'yes',
1220 };
1221
1222 const callBack = {
1223 success: ( response ) => {
1224 const { status, message, data } = response;
1225
1226 lpToastify.show( message, status );
1227
1228 if ( status === 'success' ) {
1229 this.handleSaveSuccess( data, formData, wasNewItem );
1230 }
1231 },
1232 error: ( error ) => {
1233 lpToastify.show( error.message || 'Save draft failed', 'error' );
1234 },
1235 completed: () => {
1236 lpUtils.lpSetLoadingEl( draftBtn, 0 );
1237 },
1238 };
1239
1240 window.lpAJAXG.fetchAJAX( dataSend, callBack );
1241 }
1242
1243 /**
1244 * Handle save success
1245 */
1246 handleSaveSuccess( data, formData, wasNewItem ) {
1247 if ( data?.button_title ) {
1248 const primarySaveBtn = this.popupContainer.querySelector( BuilderPopup.selectors.saveBtn );
1249 if ( primarySaveBtn ) {
1250 primarySaveBtn.textContent = data.button_title;
1251 }
1252 }
1253
1254 // Update status
1255 if ( data?.status ) {
1256 this.syncPublishPanelStatus( data.status );
1257
1258 const statusEl = this.popupContainer.querySelector( `.${ this.currentType }-status` );
1259 if ( statusEl ) {
1260 statusEl.className = `${ this.currentType }-status ${ data.status }`;
1261 statusEl.textContent = data.status;
1262 }
1263
1264 if ( this.shouldRemoveFromCurriculum( data.status ) ) {
1265 this.removeItemFromCurriculum( this.currentId );
1266 }
1267
1268 if ( this.shouldRemoveQuestionFromAssignedQuiz( data.status ) ) {
1269 this.removeQuestionFromAssignedQuiz( this.currentId );
1270 }
1271 }
1272
1273 this.updatePermalinkUIAfterSave( data );
1274
1275 // Handle new item
1276 const newIdKey = `${ this.currentType }_id_new`;
1277 if ( data?.[ newIdKey ] ) {
1278 const newId = data[ newIdKey ];
1279 this.currentId = newId;
1280 this.isNewItem = false;
1281
1282 const wrapper = this.popupContainer.querySelector( `[data-${ this.currentType }-id]` );
1283 if ( wrapper ) {
1284 wrapper.dataset[ `${ this.currentType }Id` ] = newId;
1285 }
1286
1287 const popup = this.popupContainer.querySelector( BuilderPopup.selectors.popup );
1288 if ( popup ) {
1289 popup.dataset[ `${ this.currentType }Id` ] = newId;
1290 }
1291 }
1292
1293 // Store saved data
1294 this.savedData = { formData, data, wasNewItem };
1295
1296 // Update the list item immediately
1297 this.updateListItem( this.currentType, this.currentId, this.savedData );
1298
1299 // Handle new item creation
1300 if ( wasNewItem && this.currentId ) {
1301 document.dispatchEvent(
1302 new CustomEvent( 'lp-builder-popup-saved', {
1303 detail: {
1304 type: this.currentType,
1305 id: this.currentId,
1306 data,
1307 formData,
1308 wasNewItem,
1309 listItemHtml: data?.list_item_html || null,
1310 },
1311 } )
1312 );
1313
1314 // Reload popup to show all tabs
1315 setTimeout( () => {
1316 this.destroyAllTinyMCE();
1317 this.reloadCurrentPopup();
1318 }, 300 );
1319 } else {
1320 document.dispatchEvent(
1321 new CustomEvent( 'lp-builder-popup-saved', {
1322 detail: { type: this.currentType, id: this.currentId, data, formData, wasNewItem: false },
1323 } )
1324 );
1325 }
1326 }
1327
1328 /**
1329 * Handle trash action
1330 */
1331 async handleTrash( args ) {
1332 const trashBtn = args?.target ? args.target.closest( BuilderPopup.selectors.trashBtn ) : args;
1333 if ( ! trashBtn ) {
1334 return;
1335 }
1336
1337 if ( ! this.currentType || ! this.currentId ) {
1338 return;
1339 }
1340
1341 const confirmMsg =
1342 trashBtn.dataset.confirmTrash ||
1343 'Moving it to the trash will cause this item to be removed from the course.';
1344 const result = await SweetAlert.fire( {
1345 title: 'Are you sure?',
1346 text: confirmMsg,
1347 iconHtml: SWAL_ICON_TRASH_DRAFT,
1348 customClass: { icon: 'lp-cb-swal-icon-html' },
1349 showCloseButton: true,
1350 showCancelButton: true,
1351 cancelButtonText: lpData.i18n.cancel,
1352 confirmButtonText: lpData.i18n.yes,
1353 reverseButtons: true,
1354 } );
1355
1356 if ( ! result.isConfirmed ) {
1357 return;
1358 }
1359
1360 lpUtils.lpSetLoadingEl( trashBtn, 1 );
1361
1362 const actionMap = {
1363 lesson: 'move_trash_lesson',
1364 quiz: 'move_trash_quiz',
1365 question: 'move_trash_question',
1366 };
1367
1368 const dataSend = {
1369 action: actionMap[ this.currentType ] || `move_trash_${ this.currentType }`,
1370 args: { id_url: `move-trash-${ this.currentType }` },
1371 [ `${ this.currentType }_id` ]: this.currentId,
1372 };
1373 if (
1374 !! this.openContext?.isCurriculum &&
1375 ( parseInt( this.openContext?.courseId ) || 0 ) > 0
1376 ) {
1377 dataSend.course_id = parseInt( this.openContext.courseId ) || 0;
1378 }
1379
1380 const callBack = {
1381 success: ( response ) => {
1382 const { status, message, data } = response;
1383 lpToastify.show( message, status );
1384
1385 if ( status === 'success' ) {
1386 if ( data?.button_title ) {
1387 const saveBtn = this.popupContainer.querySelector( BuilderPopup.selectors.saveBtn );
1388 if ( saveBtn ) {
1389 saveBtn.textContent = data.button_title;
1390 }
1391 }
1392
1393 if ( data?.status ) {
1394 const statusEl = this.popupContainer.querySelector( `.${ this.currentType }-status` );
1395 if ( statusEl ) {
1396 statusEl.className = `${ this.currentType }-status ${ data.status }`;
1397 statusEl.textContent = data.status;
1398 }
1399
1400 if ( this.shouldRemoveFromCurriculum( data.status ) ) {
1401 this.removeItemFromCurriculum( this.currentId );
1402 }
1403
1404 if ( this.shouldRemoveQuestionFromAssignedQuiz( data.status ) ) {
1405 this.removeQuestionFromAssignedQuiz( this.currentId );
1406 }
1407 }
1408
1409 this.updatePermalinkUIAfterSave( data );
1410
1411 this.savedData = { formData: this.getFormData(), data, wasNewItem: false };
1412
1413 document.dispatchEvent(
1414 new CustomEvent( 'lp-builder-popup-trashed', {
1415 detail: { type: this.currentType, id: this.currentId, data },
1416 } )
1417 );
1418 }
1419 },
1420 error: ( error ) => {
1421 lpToastify.show( error.message || 'Trash failed', 'error' );
1422 },
1423 completed: () => {
1424 lpUtils.lpSetLoadingEl( trashBtn, 0 );
1425 },
1426 };
1427
1428 window.lpAJAXG.fetchAJAX( dataSend, callBack );
1429 }
1430
1431 shouldRemoveFromCurriculum( status ) {
1432 const normalizedStatus = ( status || '' ).toString().toLowerCase();
1433 const removableStatuses = [ 'draft', 'trash' ];
1434
1435 return (
1436 !! this.openContext?.isCurriculum &&
1437 removableStatuses.includes( normalizedStatus ) &&
1438 ( parseInt( this.currentId ) || 0 ) > 0
1439 );
1440 }
1441
1442 shouldRemoveQuestionFromAssignedQuiz( status ) {
1443 const normalizedStatus = ( status || '' ).toString().toLowerCase();
1444 return (
1445 this.currentType === 'question' &&
1446 [ 'draft', 'trash' ].includes( normalizedStatus ) &&
1447 ( parseInt( this.currentId ) || 0 ) > 0
1448 );
1449 }
1450
1451 removeItemFromCurriculum( itemId ) {
1452 const parsedItemId = parseInt( itemId ) || 0;
1453 if ( parsedItemId <= 0 ) {
1454 return;
1455 }
1456
1457 const curriculumRoot =
1458 document.querySelector( '#lp-course-edit-curriculum' ) ||
1459 document.querySelector( '.lp-edit-curriculum-wrap' );
1460 if ( ! curriculumRoot ) {
1461 return;
1462 }
1463
1464 const items = curriculumRoot.querySelectorAll(
1465 `.section-item[data-item-id="${ parsedItemId }"]`
1466 );
1467 if ( ! items.length ) {
1468 return;
1469 }
1470
1471 const sectionsToUpdate = new Set();
1472
1473 items.forEach( ( item ) => {
1474 const section = item.closest( '.section' );
1475 if ( section ) {
1476 sectionsToUpdate.add( section );
1477 }
1478
1479 item.remove();
1480 } );
1481
1482 this.syncCurriculumCounters( curriculumRoot, sectionsToUpdate );
1483 }
1484
1485 removeQuestionFromAssignedQuiz( questionId ) {
1486 const parsedQuestionId = parseInt( questionId ) || 0;
1487 if ( parsedQuestionId <= 0 ) {
1488 return;
1489 }
1490
1491 const questionItems = document.querySelectorAll(
1492 `.lp-question-item[data-question-id="${ parsedQuestionId }"]`
1493 );
1494
1495 questionItems.forEach( ( item ) => item.remove() );
1496 }
1497
1498 syncCurriculumCounters( curriculumRoot, sectionsToUpdate = new Set() ) {
1499 if ( ! curriculumRoot ) {
1500 return;
1501 }
1502
1503 const allItems = curriculumRoot.querySelectorAll( '.section-item:not(.clone)' );
1504 const totalItemsCount = allItems.length;
1505 const totalItemsEl = curriculumRoot.querySelector( '.total-items' );
1506
1507 if ( totalItemsEl ) {
1508 totalItemsEl.dataset.count = totalItemsCount;
1509
1510 const totalItemsCountEl = totalItemsEl.querySelector( '.count' );
1511 if ( totalItemsCountEl ) {
1512 totalItemsCountEl.textContent = totalItemsCount;
1513 }
1514 }
1515
1516 const sections =
1517 sectionsToUpdate.size > 0
1518 ? Array.from( sectionsToUpdate )
1519 : Array.from( curriculumRoot.querySelectorAll( '.section' ) );
1520
1521 sections.forEach( ( section ) => {
1522 const sectionItemsCountEl = section.querySelector( '.section-items-counts' );
1523 if ( ! sectionItemsCountEl ) {
1524 return;
1525 }
1526
1527 const sectionItemsCount = section.querySelectorAll( '.section-item:not(.clone)' ).length;
1528 sectionItemsCountEl.dataset.count = sectionItemsCount;
1529
1530 const countEl = sectionItemsCountEl.querySelector( '.count' );
1531 if ( countEl ) {
1532 countEl.textContent = sectionItemsCount;
1533 }
1534 } );
1535 }
1536
1537 /**
1538 * Validate form data
1539 */
1540 validateFormData( formData ) {
1541 const errors = [];
1542 const titleKey = `${ this.currentType }_title`;
1543 const title = formData[ titleKey ] || '';
1544
1545 if ( ! title.trim() ) {
1546 errors.push(
1547 `${
1548 this.currentType.charAt( 0 ).toUpperCase() + this.currentType.slice( 1 )
1549 } title is required`
1550 );
1551 }
1552
1553 if ( title.length > 200 ) {
1554 errors.push( 'Title must be less than 200 characters' );
1555 }
1556
1557 return { valid: errors.length === 0, errors };
1558 }
1559
1560 /**
1561 * Get form data from popup
1562 */
1563 getFormData() {
1564 const data = {};
1565 const popup = this.popupContainer.querySelector( BuilderPopup.selectors.popup );
1566
1567 if ( ! popup ) {
1568 return data;
1569 }
1570
1571 const idKey = `${ this.currentType }_id`;
1572 data[ idKey ] = this.currentId || 0;
1573 if (
1574 !! this.openContext?.isCurriculum &&
1575 ( parseInt( this.openContext?.courseId ) || 0 ) > 0
1576 ) {
1577 data.course_id = parseInt( this.openContext.courseId ) || 0;
1578 }
1579
1580 // Get title
1581 const titleInput = popup.querySelector(
1582 'input[name$="_title"], #title, #' + this.currentType + '_title'
1583 );
1584 if ( titleInput ) {
1585 data[ `${ this.currentType }_title` ] = titleInput.value;
1586 }
1587
1588 // Get description
1589 const editorId = `${ this.currentType }_description_editor`;
1590 let descContent = '';
1591
1592 if ( typeof tinymce !== 'undefined' && tinymce.get( editorId ) ) {
1593 descContent = tinymce.get( editorId ).getContent();
1594 } else {
1595 const descTextarea = popup.querySelector( `#${ editorId }` );
1596 if ( descTextarea ) {
1597 descContent = descTextarea.value;
1598 }
1599 }
1600
1601 data[ `${ this.currentType }_description` ] = descContent;
1602
1603 // Get form settings
1604 const formSettings = popup.querySelector( `.lp-form-setting-${ this.currentType }` );
1605 if ( formSettings ) {
1606 data[ `${ this.currentType }_settings` ] = true;
1607 this.collectFormData( formSettings, data );
1608 }
1609
1610 // Capture permalink slug in overview tab (quiz/question popup).
1611 const permalinkInput = popup.querySelector(
1612 `input[name="${ this.currentType }_permalink"], #${ this.currentType }_permalink, ${ BuilderPopup.selectors.permalinkSlugInput }`
1613 );
1614 if ( permalinkInput && permalinkInput.value ) {
1615 data[ `${ this.currentType }_permalink` ] = permalinkInput.value;
1616 }
1617
1618 return data;
1619 }
1620
1621 updatePermalinkUIAfterSave( data = {} ) {
1622 if ( ! this.currentType || ! this.popupContainer ) {
1623 return;
1624 }
1625
1626 const popup = this.popupContainer.querySelector( BuilderPopup.selectors.popup );
1627 if ( ! popup ) {
1628 return;
1629 }
1630
1631 const slugInput = popup.querySelector(
1632 `input[name="${ this.currentType }_permalink"], #${ this.currentType }_permalink, ${ BuilderPopup.selectors.permalinkSlugInput }`
1633 );
1634 const permalinkRoot = popup.querySelector( BuilderPopup.selectors.permalinkRoot );
1635 const permalinkPlaceholder = permalinkRoot?.querySelector(
1636 BuilderPopup.selectors.permalinkPlaceholder
1637 );
1638
1639 const responseSlug = data?.[ `${ this.currentType }_slug` ];
1640 if ( slugInput && responseSlug ) {
1641 slugInput.value = responseSlug;
1642 slugInput.dataset.originalValue = responseSlug;
1643 }
1644
1645 const responsePermalink = data?.[ `${ this.currentType }_permalink` ];
1646 const isCourseItem = [ 'lesson', 'quiz' ].includes( this.currentType );
1647 const shouldShowUnavailable =
1648 data?.permalink_available === false ||
1649 ( isCourseItem &&
1650 ( data?.status === 'draft' || data?.status === 'trash' || ! responsePermalink ) );
1651
1652 if ( shouldShowUnavailable ) {
1653 if ( ! permalinkRoot ) {
1654 return;
1655 }
1656
1657 const permalinkDisplay = permalinkRoot.querySelector(
1658 BuilderPopup.selectors.permalinkDisplay
1659 );
1660 const label =
1661 permalinkRoot.querySelector( '.cb-item-edit-permalink__label' ) ||
1662 permalinkRoot.querySelector( '.cb-permalink-label' );
1663 const editor = permalinkRoot.querySelector( BuilderPopup.selectors.permalinkEditor );
1664 let placeholder = permalinkPlaceholder;
1665
1666 if ( ! placeholder ) {
1667 placeholder = document.createElement( 'span' );
1668 placeholder.className = 'cb-item-edit-permalink__placeholder';
1669
1670 if ( label ) {
1671 label.insertAdjacentElement( 'afterend', placeholder );
1672 } else {
1673 permalinkRoot.prepend( placeholder );
1674 }
1675 }
1676
1677 placeholder.textContent =
1678 data?.permalink_notice ||
1679 'Permalink is only available if the item is already assigned to a course.';
1680 placeholder.classList.remove( 'lp-hidden' );
1681
1682 if ( permalinkDisplay ) {
1683 permalinkDisplay.classList.add( 'lp-hidden' );
1684 }
1685
1686 if ( editor ) {
1687 editor.classList.add( 'lp-hidden' );
1688 }
1689
1690 return;
1691 }
1692
1693 const urlLink = popup.querySelector( BuilderPopup.selectors.permalinkUrl );
1694 const permalinkDisplay = permalinkRoot?.querySelector(
1695 BuilderPopup.selectors.permalinkDisplay
1696 );
1697 const baseUrlInput = popup.querySelector( BuilderPopup.selectors.permalinkBaseUrl );
1698 const normalizedBaseUrl = typeof baseUrlInput?.value === 'string' ? baseUrlInput.value : '';
1699 const normalizedSlug = typeof responseSlug === 'string' ? responseSlug.trim() : '';
1700 let permalinkDisplayUrl = '';
1701
1702 if ( normalizedBaseUrl && normalizedSlug ) {
1703 permalinkDisplayUrl = `${ normalizedBaseUrl }${ normalizedSlug }`;
1704 } else if ( typeof responsePermalink === 'string' ) {
1705 permalinkDisplayUrl = responsePermalink;
1706 }
1707
1708 if ( permalinkPlaceholder ) {
1709 permalinkPlaceholder.classList.add( 'lp-hidden' );
1710 }
1711
1712 if ( permalinkDisplay ) {
1713 permalinkDisplay.classList.remove( 'lp-hidden' );
1714 }
1715
1716 if ( urlLink && responsePermalink ) {
1717 urlLink.href = responsePermalink;
1718 urlLink.textContent = permalinkDisplayUrl || responsePermalink;
1719 } else if ( urlLink && permalinkDisplayUrl ) {
1720 urlLink.textContent = permalinkDisplayUrl;
1721 }
1722 }
1723
1724 /**
1725 * Collect form data from form element
1726 */
1727 collectFormData( form, data ) {
1728 const formElements = form.querySelectorAll( 'input, select, textarea' );
1729
1730 formElements.forEach( ( element ) => {
1731 const name = element.name || element.id;
1732
1733 if ( ! name || name === 'learnpress_meta_box_nonce' || name === '_wp_http_referer' ) {
1734 return;
1735 }
1736
1737 const fieldName = name.replace( '[]', '' );
1738
1739 if ( element.type === 'checkbox' ) {
1740 if ( ! data.hasOwnProperty( fieldName ) ) {
1741 data[ fieldName ] = element.checked ? 'yes' : 'no';
1742 }
1743 } else if ( element.type === 'radio' ) {
1744 if ( element.checked ) {
1745 data[ fieldName ] = element.value;
1746 }
1747 } else if ( element.type === 'file' ) {
1748 if ( element.files?.length > 0 ) {
1749 data[ fieldName ] = element.files;
1750 }
1751 } else if ( name.endsWith( '[]' ) ) {
1752 if ( ! data.hasOwnProperty( fieldName ) ) {
1753 data[ fieldName ] = [];
1754 }
1755 if ( Array.isArray( data[ fieldName ] ) ) {
1756 data[ fieldName ].push( element.value );
1757 }
1758 } else if ( ! data.hasOwnProperty( fieldName ) ) {
1759 data[ fieldName ] = element.value;
1760 }
1761 } );
1762
1763 // Convert arrays to comma-separated strings
1764 Object.keys( data ).forEach( ( key ) => {
1765 if ( Array.isArray( data[ key ] ) ) {
1766 data[ key ] = data[ key ].join( ',' );
1767 }
1768 } );
1769 }
1770
1771 /**
1772 * Load tab-specific assets (CSS/JS)
1773 */
1774 loadTabAssets( tabName, tabPane ) {
1775 const tabKey = `${ this.currentType }-${ tabName }`;
1776
1777 if ( this.loadedTabAssets.has( tabKey ) ) {
1778 return;
1779 }
1780
1781 const assetsData = tabPane.dataset.tabAssets;
1782 if ( ! assetsData ) {
1783 this.loadedTabAssets.add( tabKey );
1784 return;
1785 }
1786
1787 try {
1788 const assets = JSON.parse( assetsData );
1789
1790 // Load CSS
1791 if ( assets.css && Array.isArray( assets.css ) ) {
1792 assets.css.forEach( ( cssUrl ) => {
1793 if ( ! document.querySelector( `link[href="${ cssUrl }"]` ) ) {
1794 const link = document.createElement( 'link' );
1795 link.rel = 'stylesheet';
1796 link.href = cssUrl;
1797 link.dataset.tabAsset = tabKey;
1798 document.head.appendChild( link );
1799 }
1800 } );
1801 }
1802
1803 // Load JS
1804 if ( assets.js && Array.isArray( assets.js ) ) {
1805 assets.js.forEach( ( jsUrl ) => {
1806 if ( ! document.querySelector( `script[src="${ jsUrl }"]` ) ) {
1807 const script = document.createElement( 'script' );
1808 script.src = jsUrl;
1809 script.dataset.tabAsset = tabKey;
1810 document.head.appendChild( script );
1811 }
1812 } );
1813 }
1814
1815 this.loadedTabAssets.add( tabKey );
1816 } catch ( e ) {
1817 console.warn( `Failed to load assets for tab "${ tabName }":`, e );
1818 this.loadedTabAssets.add( tabKey );
1819 }
1820 }
1821
1822 /**
1823 * Static method to open popup programmatically
1824 */
1825 static open( type, id = 0 ) {
1826 if ( ! BuilderPopup._instance ) {
1827 BuilderPopup._instance = new BuilderPopup();
1828 }
1829
1830 const selectors = {
1831 lesson: id ? `[data-popup-lesson="${ id }"]` : BuilderPopup.selectors.addNewLesson,
1832 quiz: id ? `[data-popup-quiz="${ id }"]` : '',
1833 question: id ? `[data-popup-question="${ id }"]` : '',
1834 };
1835 const triggerSelector =
1836 selectors[ type ] ||
1837 ( id
1838 ? `[data-popup-type="${ type }"][data-popup-id="${ id }"]`
1839 : `[data-popup-type="${ type }"][data-template]` );
1840 if ( ! triggerSelector ) {
1841 return;
1842 }
1843
1844 const triggerEl = document.querySelector( triggerSelector );
1845 if ( ! triggerEl ) {
1846 return;
1847 }
1848
1849 BuilderPopup._instance.showPopup(
1850 triggerEl,
1851 type,
1852 id,
1853 BuilderPopup._instance.resolveOpenContext( triggerEl )
1854 );
1855 }
1856
1857 /**
1858 * Static method to close popup programmatically
1859 */
1860 static close() {
1861 if ( BuilderPopup._instance ) {
1862 BuilderPopup._instance.closePopup();
1863 }
1864 }
1865 }
1866
1867 // Auto-initialize
1868 document.addEventListener( 'DOMContentLoaded', () => {
1869 BuilderPopup._instance = new BuilderPopup();
1870 } );
1871
1872 export default BuilderPopup;
1873