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