PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.10
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.10
4.4.10 4.4.9.1 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 All 142 releases
← All changes | assets/src/js/frontend/course-builder/builder-lesson/builder-edit-lesson.js +831 -831 4.3.8 → 4.4.10 View file →
@@ -1,831 +1,831 @@
1 -import * as lpUtils from 'lpAssetsJsPath/utils.js';
2 -import * as lpToastify from 'lpAssetsJsPath/lpToastify.js';
3 -
4 -export class BuilderEditLesson {
5 - constructor() {
6 - // Events use document-level event delegation, so always register them
7 - // The page context check happens in individual handlers via target.closest()
8 - this.init();
9 - }
10 -
11 - static selectors = {
12 - // Context selector - indicates we're on lesson edit page
13 - elDataLesson: '.cb-section__lesson-edit',
14 - // Shared header action buttons (generic selectors)
15 - elBtnMainAction: '.cb-btn-main-action',
16 - elBtnUpdate: '.cb-btn-update, .cb-btn-publish',
17 - elBtnDraft: '.cb-btn-darft, .cb-dropdown-item[data-status="draft"]',
18 - elBtnTrash: '.cb-btn-trash',
19 - // Status badge
20 - elLessonStatus: '.lesson-status',
21 - // Form fields
22 - idTitle: 'title',
23 - idDescEditor: 'lesson_description_editor',
24 - elFormSetting: '.lp-form-setting-lesson',
25 - // Permalink component
26 - elPermalinkDisplay: '.cb-permalink-display',
27 - elPermalinkEditor: '.cb-permalink-editor',
28 - elPermalinkEditBtn: '.cb-permalink-edit-btn',
29 - elPermalinkOkBtn: '.cb-permalink-ok-btn',
30 - elPermalinkCancelBtn: '.cb-permalink-cancel-btn',
31 - elPermalinkSlugInput: '.cb-permalink-slug-input',
32 - elPermalinkUrl: '.cb-permalink-url',
33 - elPermalinkBaseUrl: '#cb-permalink-base-url',
34 - elPermalinkRoot: '.cb-item-edit-permalink, .cb-course-edit-permalink',
35 - elPermalinkPlaceholder: '.cb-item-edit-permalink__placeholder',
36 - // Tab handling selectors
37 - elCBHorizontalTabs: '.lp-cb-tabs__item',
38 - elCBTabPanels: '.lp-cb-tab-panel',
39 - // Dropdown selectors
40 - elDropdownToggle: '.cb-btn-dropdown-toggle',
41 - elDropdownMenu: '.cb-dropdown-menu',
42 - elHeaderActionsDropdown: '.cb-header-actions-dropdown',
43 - };
44 -
45 - init() {
46 - this.initTabs();
47 - this.initHeaderActionsDropdown();
48 - this.events();
49 - }
50 -
51 - /**
52 - * Initialize header actions dropdown (toggle behavior)
53 - */
54 - initHeaderActionsDropdown() {
55 - // Close dropdown when clicking outside
56 - document.addEventListener( 'click', ( e ) => {
57 - const dropdown = document.querySelector( BuilderEditLesson.selectors.elHeaderActionsDropdown );
58 - if ( dropdown && ! dropdown.contains( e.target ) ) {
59 - const menu = dropdown.querySelector( BuilderEditLesson.selectors.elDropdownMenu );
60 - const toggle = dropdown.querySelector( BuilderEditLesson.selectors.elDropdownToggle );
61 - if ( menu ) {
62 - menu.classList.remove( 'is-open' );
63 - }
64 - if ( toggle ) {
65 - toggle.setAttribute( 'aria-expanded', 'false' );
66 - }
67 - }
68 - } );
69 - }
70 -
71 - /**
72 - * Handle dropdown toggle click
73 - */
74 - handleDropdownToggle( args ) {
75 - const { target } = args;
76 - const toggleBtn = target.closest( BuilderEditLesson.selectors.elDropdownToggle );
77 -
78 - if ( ! toggleBtn ) {
79 - return;
80 - }
81 -
82 - const dropdown = toggleBtn.closest( BuilderEditLesson.selectors.elHeaderActionsDropdown );
83 - if ( ! dropdown ) {
84 - return;
85 - }
86 -
87 - const menu = dropdown.querySelector( BuilderEditLesson.selectors.elDropdownMenu );
88 - if ( menu ) {
89 - menu.classList.toggle( 'is-open' );
90 - const isOpen = menu.classList.contains( 'is-open' );
91 - toggleBtn.setAttribute( 'aria-expanded', isOpen ? 'true' : 'false' );
92 - }
93 - }
94 -
95 - /**
96 - * Initialize horizontal tabs for client-side tab switching
97 - */
98 - initTabs() {
99 - const tabs = document.querySelectorAll( BuilderEditLesson.selectors.elCBHorizontalTabs );
100 - if ( tabs.length === 0 ) {
101 - return;
102 - }
103 -
104 - // Activate first tab by default if none is active
105 - const activeTab = document.querySelector( `${ BuilderEditLesson.selectors.elCBHorizontalTabs }.is-active` );
106 - if ( ! activeTab && tabs.length > 0 ) {
107 - tabs[0].classList.add( 'is-active' );
108 - const section = tabs[0].getAttribute( 'data-tab-section' );
109 - if ( section ) {
110 - const panel = document.querySelector( `${ BuilderEditLesson.selectors.elCBTabPanels }[data-section="${ section }"]` );
111 - if ( panel ) {
112 - panel.classList.remove( 'lp-hidden' );
113 - }
114 - }
115 - }
116 - }
117 -
118 - /**
119 - * Handle horizontal tab click for client-side tab switching
120 - */
121 - handleTabClick( args ) {
122 - const { e, target } = args;
123 - const tabLink = target.closest( BuilderEditLesson.selectors.elCBHorizontalTabs );
124 -
125 - if ( ! tabLink ) {
126 - return;
127 - }
128 -
129 - e.preventDefault();
130 -
131 - const section = tabLink.getAttribute( 'data-tab-section' );
132 - if ( ! section ) {
133 - return;
134 - }
135 -
136 - // Update active tab
137 - const allTabs = document.querySelectorAll( BuilderEditLesson.selectors.elCBHorizontalTabs );
138 - allTabs.forEach( tab => tab.classList.remove( 'is-active' ) );
139 - tabLink.classList.add( 'is-active' );
140 -
141 - // Show/hide panels
142 - const allPanels = document.querySelectorAll( BuilderEditLesson.selectors.elCBTabPanels );
143 - allPanels.forEach( panel => {
144 - if ( panel.getAttribute( 'data-section' ) === section ) {
145 - panel.classList.remove( 'lp-hidden' );
146 - } else {
147 - panel.classList.add( 'lp-hidden' );
148 - }
149 - } );
150 - }
151 -
152 - events() {
153 - if ( BuilderEditLesson._loadedEvents ) {
154 - return;
155 - }
156 - BuilderEditLesson._loadedEvents = true;
157 -
158 - lpUtils.eventHandlers( 'click', [
159 - {
160 - selector: BuilderEditLesson.selectors.elBtnMainAction,
161 - class: this,
162 - callBack: this.updateLesson.name,
163 - },
164 - {
165 - selector: BuilderEditLesson.selectors.elBtnDraft,
166 - class: this,
167 - callBack: this.saveDraftLesson.name,
168 - },
169 - {
170 - selector: BuilderEditLesson.selectors.elBtnTrash,
171 - class: this,
172 - callBack: this.trashLesson.name,
173 - },
174 - {
175 - selector: BuilderEditLesson.selectors.elCBHorizontalTabs,
176 - class: this,
177 - callBack: this.handleTabClick.name,
178 - },
179 - {
180 - selector: BuilderEditLesson.selectors.elDropdownToggle,
181 - class: this,
182 - callBack: this.handleDropdownToggle.name,
183 - },
184 - {
185 - selector: BuilderEditLesson.selectors.elPermalinkEditBtn,
186 - class: this,
187 - callBack: this.handlePermalinkEdit.name,
188 - },
189 - {
190 - selector: BuilderEditLesson.selectors.elPermalinkOkBtn,
191 - class: this,
192 - callBack: this.handlePermalinkOk.name,
193 - },
194 - {
195 - selector: BuilderEditLesson.selectors.elPermalinkCancelBtn,
196 - class: this,
197 - callBack: this.handlePermalinkCancel.name,
198 - },
199 - ] );
200 - }
201 -
202 - /**
203 - * Validate title is not empty before update
204 - * @return {boolean} True if valid, false if invalid
205 - */
206 - validateTitleBeforeUpdate() {
207 - const titleInput = document.getElementById( BuilderEditLesson.selectors.idTitle );
208 - if ( ! titleInput ) {
209 - return true;
210 - }
211 -
212 - const title = titleInput.value.trim();
213 - if ( ! title ) {
214 - lpToastify.show( 'Lesson title is required.', 'error' );
215 - titleInput.focus();
216 - return false;
217 - }
218 - return true;
219 - }
220 -
221 - /**
222 - * Update action button text after status change
223 - * @param {string} newStatus - The new lesson status
224 - */
225 - updateActionButtons( newStatus ) {
226 - const elBtnUpdate = document.querySelector( BuilderEditLesson.selectors.elBtnMainAction );
227 - if ( ! elBtnUpdate ) {
228 - return;
229 - }
230 -
231 - const titleUpdate = elBtnUpdate.getAttribute( 'data-title-update' ) || 'Update';
232 - const titlePublish = elBtnUpdate.getAttribute( 'data-title-publish' ) || 'Publish';
233 -
234 - if ( newStatus === 'publish' ) {
235 - elBtnUpdate.textContent = titleUpdate;
236 - } else {
237 - elBtnUpdate.textContent = titlePublish;
238 - }
239 - }
240 -
241 - /**
242 - * Check if we're in lesson edit context
243 - * @return {boolean}
244 - */
245 - isLessonContext() {
246 - return !! document.querySelector( BuilderEditLesson.selectors.elDataLesson );
247 - }
248 -
249 - slugify( str ) {
250 - return ( str || '' )
251 - .toString()
252 - .normalize( 'NFD' )
253 - .replace( /[\u0300-\u036f]/g, '' )
254 - .replace( /đ/g, 'd' )
255 - .replace( /Đ/g, 'D' )
256 - .toLowerCase()
257 - .replace( /\s+/g, '-' )
258 - .replace( /[^\w-]+/g, '' )
259 - .replace( /--+/g, '-' )
260 - .replace( /^-+/, '' )
261 - .replace( /-+$/, '' );
262 - }
263 -
264 - buildPermalinkDisplayUrl( baseUrl = '', slug = '', fallbackUrl = '' ) {
265 - const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl : '';
266 - const normalizedSlug = typeof slug === 'string' ? slug.trim() : '';
267 -
268 - if ( normalizedBaseUrl && normalizedSlug ) {
269 - return `${ normalizedBaseUrl }${ normalizedSlug }`;
270 - }
271 -
272 - return typeof fallbackUrl === 'string' ? fallbackUrl : '';
273 - }
274 -
275 - getPermalinkRoot( target = null ) {
276 - if ( target?.closest ) {
277 - const fromTarget = target.closest( BuilderEditLesson.selectors.elPermalinkRoot );
278 - if ( fromTarget ) {
279 - return fromTarget;
280 - }
281 - }
282 -
283 - return document.querySelector(
284 - `.cb-section__lesson-edit ${ BuilderEditLesson.selectors.elPermalinkRoot }`
285 - );
286 - }
287 -
288 - isDescriptionEditorInCodeMode() {
289 - const editorId = BuilderEditLesson.selectors.idDescEditor;
290 - const wrapper =
291 - document.getElementById( `wp-${ editorId }-wrap` ) ||
292 - document.getElementById( `${ editorId }-wrap` );
293 - if ( wrapper?.classList.contains( 'html-active' ) ) {
294 - return true;
295 - }
296 -
297 - const editor = typeof tinymce !== 'undefined' ? tinymce.get( editorId ) : null;
298 -
299 - return !! ( editor?.isHidden && editor.isHidden() );
300 - }
301 -
302 - getDescriptionContentForUpdate() {
303 - const editorId = BuilderEditLesson.selectors.idDescEditor;
304 - const descEditor = document.getElementById( editorId );
305 -
306 - if ( this.isDescriptionEditorInCodeMode() ) {
307 - return descEditor ? descEditor.value : '';
308 - }
309 -
310 - if ( typeof tinymce !== 'undefined' ) {
311 - const editor = tinymce.get( editorId );
312 - if ( editor ) {
313 - return editor.getContent();
314 - }
315 - }
316 -
317 - return descEditor ? descEditor.value : '';
318 - }
319 -
320 - showPermalinkUnavailable( permalinkRoot, message = '' ) {
321 - if ( ! permalinkRoot ) {
322 - return;
323 - }
324 -
325 - const label =
326 - permalinkRoot.querySelector( '.cb-item-edit-permalink__label' ) ||
327 - permalinkRoot.querySelector( '.cb-permalink-label' );
328 - const display = permalinkRoot.querySelector( BuilderEditLesson.selectors.elPermalinkDisplay );
329 - const editor = permalinkRoot.querySelector( BuilderEditLesson.selectors.elPermalinkEditor );
330 - let placeholder = permalinkRoot.querySelector(
331 - BuilderEditLesson.selectors.elPermalinkPlaceholder
332 - );
333 -
334 - if ( ! placeholder ) {
335 - placeholder = document.createElement( 'span' );
336 - placeholder.className = 'cb-item-edit-permalink__placeholder';
337 -
338 - if ( label ) {
339 - label.insertAdjacentElement( 'afterend', placeholder );
340 - } else {
341 - permalinkRoot.prepend( placeholder );
342 - }
343 - }
344 -
345 - placeholder.textContent =
346 - message || 'Permalink is only available if the item is already assigned to a course.';
347 - placeholder.classList.remove( 'lp-hidden' );
348 -
349 - if ( display ) {
350 - display.classList.add( 'lp-hidden' );
351 - }
352 -
353 - if ( editor ) {
354 - editor.classList.add( 'lp-hidden' );
355 - }
356 - }
357 -
358 - getLessonDataForUpdate() {
359 - const data = {};
360 -
361 - const wrapperEl = document.querySelector( BuilderEditLesson.selectors.elDataLesson );
362 - data.lesson_id = wrapperEl ? parseInt( wrapperEl.dataset.lessonId ) || 0 : 0;
363 -
364 - const titleInput = document.getElementById( BuilderEditLesson.selectors.idTitle );
365 - data.lesson_title = titleInput ? titleInput.value : '';
366 -
367 - data.lesson_description = this.getDescriptionContentForUpdate();
368 -
369 - const permalinkInput = document.querySelector(
370 - `input[name="lesson_permalink"], #lesson_permalink, ${ BuilderEditLesson.selectors.elPermalinkSlugInput }`
371 - );
372 - if ( permalinkInput && permalinkInput.value ) {
373 - data.lesson_permalink = permalinkInput.value;
374 - }
375 -
376 - const elFormSetting = document.querySelector( BuilderEditLesson.selectors.elFormSetting );
377 -
378 - if ( elFormSetting ) {
379 - data.lesson_settings = true;
380 - const formElements = elFormSetting.querySelectorAll( 'input, select, textarea' );
381 -
382 - formElements.forEach( ( element ) => {
383 - const name = element.name || element.id;
384 -
385 - if ( ! name || name === 'learnpress_meta_box_nonce' || name === '_wp_http_referer' ) {
386 - return;
387 - }
388 -
389 - if ( element.type === 'checkbox' ) {
390 - const fieldName = name.replace( '[]', '' );
391 - if ( ! data.hasOwnProperty( fieldName ) ) {
392 - data[ fieldName ] = element.checked ? 'yes' : 'no';
393 - }
394 - } else if ( element.type === 'radio' ) {
395 - if ( element.checked ) {
396 - const fieldName = name.replace( '[]', '' );
397 - data[ fieldName ] = element.value;
398 - }
399 - } else if ( element.type === 'file' ) {
400 - const fieldName = name.replace( '[]', '' );
401 - if ( element.files && element.files.length > 0 ) {
402 - data[ fieldName ] = element.files;
403 - }
404 - } else {
405 - const fieldName = name.replace( '[]', '' );
406 -
407 - if ( name.endsWith( '[]' ) ) {
408 - if ( ! data.hasOwnProperty( fieldName ) ) {
409 - data[ fieldName ] = [];
410 - }
411 -
412 - if ( Array.isArray( data[ fieldName ] ) ) {
413 - data[ fieldName ].push( element.value );
414 - }
415 - } else {
416 - if ( ! data.hasOwnProperty( fieldName ) ) {
417 - data[ fieldName ] = element.value;
418 - }
419 - }
420 - }
421 - } );
422 -
423 - Object.keys( data ).forEach( ( key ) => {
424 - if ( Array.isArray( data[ key ] ) ) {
425 - data[ key ] = data[ key ].join( ',' );
426 - }
427 - } );
428 - }
429 -
430 - return data;
431 - }
432 -
433 - updatePermalinkUIAfterSave( data = {} ) {
434 - const permalinkRoot = this.getPermalinkRoot();
435 - if ( ! permalinkRoot ) {
436 - return;
437 - }
438 -
439 - const slugInput = permalinkRoot.querySelector( BuilderEditLesson.selectors.elPermalinkSlugInput );
440 - const urlLink = permalinkRoot.querySelector( BuilderEditLesson.selectors.elPermalinkUrl );
441 - const baseUrlInput = permalinkRoot.querySelector( BuilderEditLesson.selectors.elPermalinkBaseUrl );
442 - const display = permalinkRoot.querySelector( BuilderEditLesson.selectors.elPermalinkDisplay );
443 - const placeholder = permalinkRoot.querySelector(
444 - BuilderEditLesson.selectors.elPermalinkPlaceholder
445 - );
446 - const permalinkDisplayUrl = this.buildPermalinkDisplayUrl(
447 - baseUrlInput ? baseUrlInput.value : '',
448 - data?.lesson_slug,
449 - data?.lesson_permalink
450 - );
451 -
452 - if ( slugInput && data?.lesson_slug ) {
453 - slugInput.value = data.lesson_slug;
454 - slugInput.dataset.originalValue = data.lesson_slug;
455 - }
456 -
457 - const shouldShowUnavailable =
458 - data?.permalink_available === false ||
459 - data?.status === 'draft' ||
460 - data?.status === 'trash' ||
461 - ! data?.lesson_permalink;
462 -
463 - if ( shouldShowUnavailable ) {
464 - this.showPermalinkUnavailable( permalinkRoot, data?.permalink_notice );
465 - return;
466 - }
467 -
468 - if ( placeholder ) {
469 - placeholder.classList.add( 'lp-hidden' );
470 - }
471 -
472 - if ( display ) {
473 - display.classList.remove( 'lp-hidden' );
474 - }
475 -
476 - if ( urlLink && data?.lesson_permalink ) {
477 - urlLink.href = data.lesson_permalink;
478 - urlLink.textContent = permalinkDisplayUrl || data.lesson_permalink;
479 - } else if ( urlLink && permalinkDisplayUrl ) {
480 - urlLink.textContent = permalinkDisplayUrl;
481 - }
482 - }
483 -
484 - handlePermalinkEdit( args ) {
485 - if ( ! this.isLessonContext() ) {
486 - return;
487 - }
488 -
489 - const { e, target } = args;
490 - if ( e ) e.preventDefault();
491 -
492 - const trigger = target?.closest( BuilderEditLesson.selectors.elPermalinkEditBtn );
493 - const lessonRoot = trigger?.closest( BuilderEditLesson.selectors.elDataLesson ) ||
494 - document.querySelector( BuilderEditLesson.selectors.elDataLesson );
495 -
496 - if ( ! lessonRoot ) {
497 - return;
498 - }
499 -
500 - const display = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkDisplay );
501 - const editor = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkEditor );
502 - const input = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkSlugInput );
503 -
504 - if ( ! display || ! editor || ! input ) {
505 - return;
506 - }
507 -
508 - input.dataset.originalValue = input.value;
509 - display.classList.add( 'lp-hidden' );
510 - editor.classList.remove( 'lp-hidden' );
511 - input.focus();
512 - input.select();
513 - }
514 -
515 - handlePermalinkOk( args ) {
516 - if ( ! this.isLessonContext() ) {
517 - return;
518 - }
519 -
520 - const { e, target } = args;
521 - if ( e ) e.preventDefault();
522 -
523 - const trigger = target?.closest( BuilderEditLesson.selectors.elPermalinkOkBtn );
524 - const lessonRoot = trigger?.closest( BuilderEditLesson.selectors.elDataLesson ) ||
525 - document.querySelector( BuilderEditLesson.selectors.elDataLesson );
526 -
527 - if ( ! lessonRoot ) {
528 - return;
529 - }
530 -
531 - const display = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkDisplay );
532 - const editor = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkEditor );
533 - const input = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkSlugInput );
534 - const urlLink = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkUrl );
535 - const baseUrlInput = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkBaseUrl );
536 -
537 - if ( ! display || ! editor || ! input || ! urlLink ) {
538 - return;
539 - }
540 -
541 - let newSlug = this.slugify( input.value.trim() );
542 - if ( ! newSlug ) {
543 - newSlug = input.dataset.originalValue || 'lesson';
544 - }
545 -
546 - input.value = newSlug;
547 - const baseUrl = baseUrlInput ? baseUrlInput.value : '';
548 - const newUrl = this.buildPermalinkDisplayUrl( baseUrl, newSlug, urlLink.textContent || '' );
549 -
550 - // Keep href as the current saved link and only update display text.
551 - urlLink.textContent = newUrl;
552 - editor.classList.add( 'lp-hidden' );
553 - display.classList.remove( 'lp-hidden' );
554 - }
555 -
556 - handlePermalinkCancel( args ) {
557 - if ( ! this.isLessonContext() ) {
558 - return;
559 - }
560 -
561 - const { e, target } = args;
562 - if ( e ) e.preventDefault();
563 -
564 - const trigger = target?.closest( BuilderEditLesson.selectors.elPermalinkCancelBtn );
565 - const lessonRoot = trigger?.closest( BuilderEditLesson.selectors.elDataLesson ) ||
566 - document.querySelector( BuilderEditLesson.selectors.elDataLesson );
567 -
568 - if ( ! lessonRoot ) {
569 - return;
570 - }
571 -
572 - const display = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkDisplay );
573 - const editor = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkEditor );
574 - const input = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkSlugInput );
575 -
576 - if ( ! display || ! editor || ! input ) {
577 - return;
578 - }
579 -
580 - input.value = input.dataset.originalValue || '';
581 - editor.classList.add( 'lp-hidden' );
582 - display.classList.remove( 'lp-hidden' );
583 - }
584 -
585 - updateLesson( args ) {
586 - // Context check: only handle if on lesson edit page
587 - if ( ! this.isLessonContext() ) {
588 - return;
589 - }
590 -
591 - const { target } = args;
592 - const elBtnUpdateLesson = target.closest( BuilderEditLesson.selectors.elBtnMainAction );
593 -
594 - if ( ! elBtnUpdateLesson ) {
595 - return;
596 - }
597 -
598 - // Validate title before update
599 - if ( ! this.validateTitleBeforeUpdate() ) {
600 - return;
601 - }
602 -
603 - lpUtils.lpSetLoadingEl( elBtnUpdateLesson, 1 );
604 -
605 - const lessonData = this.getLessonDataForUpdate();
606 -
607 - const targetStatus = elBtnUpdateLesson.dataset.status || 'publish';
608 -
609 - const dataSend = {
610 - ...lessonData,
611 - action: 'builder_update_lesson',
612 - args: {
613 - id_url: 'builder-update-lesson',
614 - },
615 - lesson_status: targetStatus,
616 - };
617 -
618 - if ( typeof lpLessonBuilder !== 'undefined' && lpLessonBuilder.nonce ) {
619 - dataSend.nonce = lpLessonBuilder.nonce;
620 - }
621 -
622 - const callBack = {
623 - success: ( response ) => {
624 - const { status, message, data } = response;
625 - lpToastify.show( message, status );
626 -
627 - if ( status === 'success' ) {
628 - // Update action button text
629 - this.updateActionButtons( targetStatus );
630 -
631 - if ( data?.lesson_id_new ) {
632 - const currentUrl = window.location.href;
633 - window.location.href = currentUrl.replace( /post-new\/?/, `${ data.lesson_id_new }/` );
634 - }
635 -
636 - if ( data?.status ) {
637 - const elStatus = document.querySelector( BuilderEditLesson.selectors.elLessonStatus );
638 - if ( elStatus ) {
639 - elStatus.className = 'lesson-status ' + data.status;
640 - elStatus.textContent = data.status;
641 - }
642 - if ( data.status === 'trash' || data.status === 'draft' ) {
643 - const curriculumItem = document.querySelector( `.lesson-item[data-lesson-id="${lessonData.lesson_id}"]` );
644 - if ( curriculumItem ) {
645 - const elCurriculumLesson = curriculumItem.closest( '.lesson' );
646 - if ( elCurriculumLesson ) {
647 - elCurriculumLesson.remove();
648 - }
649 - }
650 - }
651 - }
652 -
653 - this.updatePermalinkUIAfterSave( data );
654 -
655 - // Reset form state to prevent "leave site" warning
656 - document.dispatchEvent( new CustomEvent( 'lp-course-builder-saved' ) );
657 - }
658 - },
659 - error: ( error ) => {
660 - lpToastify.show( error.message || error, 'error' );
661 - },
662 - completed: () => {
663 - lpUtils.lpSetLoadingEl( elBtnUpdateLesson, 0 );
664 - },
665 - };
666 -
667 - window.lpAJAXG.fetchAJAX( dataSend, callBack );
668 - }
669 -
670 - saveDraftLesson( args ) {
671 - // Context check: only handle if on lesson edit page
672 - if ( ! this.isLessonContext() ) {
673 - return;
674 - }
675 -
676 - const { target } = args;
677 - const elBtnDraftLesson = target.closest( BuilderEditLesson.selectors.elBtnDraft );
678 -
679 - if ( ! elBtnDraftLesson ) {
680 - return;
681 - }
682 -
683 - // Check if published to show confirm unpublish modal
684 - const statusEl = document.querySelector( BuilderEditLesson.selectors.elLessonStatus );
685 - const isPublished = statusEl && statusEl.classList.contains( 'publish' );
686 - if ( isPublished ) {
687 - const confirmMsg = elBtnDraftLesson.dataset.confirmUnpublish || 'Saving as draft will unpublish this item. Are you sure?';
688 - if ( ! confirm( confirmMsg ) ) {
689 - return;
690 - }
691 - }
692 -
693 - // Validate title before saving draft
694 - if ( ! this.validateTitleBeforeUpdate() ) {
695 - return;
696 - }
697 -
698 - lpUtils.lpSetLoadingEl( elBtnDraftLesson, 1 );
699 -
700 - const lessonData = this.getLessonDataForUpdate();
701 -
702 - const dataSend = {
703 - ...lessonData,
704 - action: 'builder_update_lesson',
705 - args: {
706 - id_url: 'builder-update-lesson',
707 - },
708 - lesson_status: 'draft',
709 - };
710 -
711 - if ( typeof lpLessonBuilder !== 'undefined' && lpLessonBuilder.nonce ) {
712 - dataSend.nonce = lpLessonBuilder.nonce;
713 - }
714 -
715 - const callBack = {
716 - success: ( response ) => {
717 - const { status, message, data } = response;
718 - lpToastify.show( message, status );
719 -
720 - if ( status === 'success' ) {
721 - // Update action button text
722 - this.updateActionButtons( 'draft' );
723 -
724 - if ( data?.lesson_id_new ) {
725 - const currentUrl = window.location.href;
726 - window.location.href = currentUrl.replace( /post-new\/?/, `${ data.lesson_id_new }/` );
727 - }
728 -
729 - if ( data?.status ) {
730 - const elStatus = document.querySelector( BuilderEditLesson.selectors.elLessonStatus );
731 - if ( elStatus ) {
732 - elStatus.className = 'lesson-status ' + data.status;
733 - elStatus.textContent = data.status;
734 - }
735 - if ( data.status === 'trash' || data.status === 'draft' ) {
736 - const curriculumItem = document.querySelector( `.lesson-item[data-lesson-id="${lessonData.lesson_id}"]` );
737 - if ( curriculumItem ) {
738 - const elCurriculumLesson = curriculumItem.closest( '.lesson' );
739 - if ( elCurriculumLesson ) {
740 - elCurriculumLesson.remove();
741 - }
742 - }
743 - }
744 - }
745 -
746 - this.updatePermalinkUIAfterSave( data );
747 -
748 - // Reset form state to prevent "leave site" warning
749 - document.dispatchEvent( new CustomEvent( 'lp-course-builder-saved' ) );
750 - }
751 - },
752 - error: ( error ) => {
753 - lpToastify.show( error.message || error, 'error' );
754 - },
755 - completed: () => {
756 - lpUtils.lpSetLoadingEl( elBtnDraftLesson, 0 );
757 - },
758 - };
759 -
760 - window.lpAJAXG.fetchAJAX( dataSend, callBack );
761 - }
762 -
763 - trashLesson( args ) {
764 - // Context check: only handle if on lesson edit page
765 - if ( ! this.isLessonContext() ) {
766 - return;
767 - }
768 -
769 - const { target } = args;
770 - const elBtnTrashLesson = target.closest( BuilderEditLesson.selectors.elBtnTrash );
771 -
772 - if ( ! elBtnTrashLesson ) {
773 - return;
774 - }
775 -
776 - if ( ! confirm( 'Are you sure you want to trash this lesson?' ) ) {
777 - return;
778 - }
779 -
780 - lpUtils.lpSetLoadingEl( elBtnTrashLesson, 1 );
781 -
782 - const lessonData = this.getLessonDataForUpdate();
783 - const dataSend = {
784 - action: 'move_trash_lesson',
785 - args: {
786 - id_url: 'move-trash-lesson',
787 - },
788 - lesson_id: lessonData.lesson_id,
789 - };
790 -
791 - const callBack = {
792 - success: ( response ) => {
793 - const { status, message, data } = response;
794 - lpToastify.show( message, status );
795 -
796 - if ( status === 'success' ) {
797 - if ( data?.redirect_url ) {
798 - window.location.href = data.redirect_url;
799 - }
800 -
801 - if ( data?.status ) {
802 - const elStatus = document.querySelector( BuilderEditLesson.selectors.elLessonStatus );
803 - if ( elStatus ) {
804 - elStatus.className = 'lesson-status ' + data.status;
805 - elStatus.textContent = data.status;
806 - }
807 - if ( data.status === 'trash' || data.status === 'draft' ) {
808 - const curriculumItem = document.querySelector( `.lesson-item[data-lesson-id="${lessonData.lesson_id}"]` );
809 - if ( curriculumItem ) {
810 - const elCurriculumLesson = curriculumItem.closest( '.lesson' );
811 - if ( elCurriculumLesson ) {
812 - elCurriculumLesson.remove();
813 - }
814 - }
815 - }
816 - }
817 -
818 - this.updatePermalinkUIAfterSave( data );
819 - }
820 - },
821 - error: ( error ) => {
822 - lpToastify.show( error.message || error, 'error' );
823 - },
824 - completed: () => {
825 - lpUtils.lpSetLoadingEl( elBtnTrashLesson, 0 );
826 - },
827 - };
828 -
829 - window.lpAJAXG.fetchAJAX( dataSend, callBack );
830 - }
831 -}
1 +import * as lpUtils from 'lpAssetsJsPath/utils.js';
2 +import * as lpToastify from 'lpAssetsJsPath/lpToastify.js';
3 +
4 +export class BuilderEditLesson {
5 + constructor() {
6 + // Events use document-level event delegation, so always register them
7 + // The page context check happens in individual handlers via target.closest()
8 + this.init();
9 + }
10 +
11 + static selectors = {
12 + // Context selector - indicates we're on lesson edit page
13 + elDataLesson: '.cb-section__lesson-edit',
14 + // Shared header action buttons (generic selectors)
15 + elBtnMainAction: '.cb-btn-main-action',
16 + elBtnUpdate: '.cb-btn-update, .cb-btn-publish',
17 + elBtnDraft: '.cb-btn-darft, .cb-dropdown-item[data-status="draft"]',
18 + elBtnTrash: '.cb-btn-trash',
19 + // Status badge
20 + elLessonStatus: '.lesson-status',
21 + // Form fields
22 + idTitle: 'title',
23 + idDescEditor: 'lesson_description_editor',
24 + elFormSetting: '.lp-form-setting-lesson',
25 + // Permalink component
26 + elPermalinkDisplay: '.cb-permalink-display',
27 + elPermalinkEditor: '.cb-permalink-editor',
28 + elPermalinkEditBtn: '.cb-permalink-edit-btn',
29 + elPermalinkOkBtn: '.cb-permalink-ok-btn',
30 + elPermalinkCancelBtn: '.cb-permalink-cancel-btn',
31 + elPermalinkSlugInput: '.cb-permalink-slug-input',
32 + elPermalinkUrl: '.cb-permalink-url',
33 + elPermalinkBaseUrl: '#cb-permalink-base-url',
34 + elPermalinkRoot: '.cb-item-edit-permalink, .cb-course-edit-permalink',
35 + elPermalinkPlaceholder: '.cb-item-edit-permalink__placeholder',
36 + // Tab handling selectors
37 + elCBHorizontalTabs: '.lp-cb-tabs__item',
38 + elCBTabPanels: '.lp-cb-tab-panel',
39 + // Dropdown selectors
40 + elDropdownToggle: '.cb-btn-dropdown-toggle',
41 + elDropdownMenu: '.cb-dropdown-menu',
42 + elHeaderActionsDropdown: '.cb-header-actions-dropdown',
43 + };
44 +
45 + init() {
46 + this.initTabs();
47 + this.initHeaderActionsDropdown();
48 + this.events();
49 + }
50 +
51 + /**
52 + * Initialize header actions dropdown (toggle behavior)
53 + */
54 + initHeaderActionsDropdown() {
55 + // Close dropdown when clicking outside
56 + document.addEventListener( 'click', ( e ) => {
57 + const dropdown = document.querySelector( BuilderEditLesson.selectors.elHeaderActionsDropdown );
58 + if ( dropdown && ! dropdown.contains( e.target ) ) {
59 + const menu = dropdown.querySelector( BuilderEditLesson.selectors.elDropdownMenu );
60 + const toggle = dropdown.querySelector( BuilderEditLesson.selectors.elDropdownToggle );
61 + if ( menu ) {
62 + menu.classList.remove( 'is-open' );
63 + }
64 + if ( toggle ) {
65 + toggle.setAttribute( 'aria-expanded', 'false' );
66 + }
67 + }
68 + } );
69 + }
70 +
71 + /**
72 + * Handle dropdown toggle click
73 + */
74 + handleDropdownToggle( args ) {
75 + const { target } = args;
76 + const toggleBtn = target.closest( BuilderEditLesson.selectors.elDropdownToggle );
77 +
78 + if ( ! toggleBtn ) {
79 + return;
80 + }
81 +
82 + const dropdown = toggleBtn.closest( BuilderEditLesson.selectors.elHeaderActionsDropdown );
83 + if ( ! dropdown ) {
84 + return;
85 + }
86 +
87 + const menu = dropdown.querySelector( BuilderEditLesson.selectors.elDropdownMenu );
88 + if ( menu ) {
89 + menu.classList.toggle( 'is-open' );
90 + const isOpen = menu.classList.contains( 'is-open' );
91 + toggleBtn.setAttribute( 'aria-expanded', isOpen ? 'true' : 'false' );
92 + }
93 + }
94 +
95 + /**
96 + * Initialize horizontal tabs for client-side tab switching
97 + */
98 + initTabs() {
99 + const tabs = document.querySelectorAll( BuilderEditLesson.selectors.elCBHorizontalTabs );
100 + if ( tabs.length === 0 ) {
101 + return;
102 + }
103 +
104 + // Activate first tab by default if none is active
105 + const activeTab = document.querySelector( `${ BuilderEditLesson.selectors.elCBHorizontalTabs }.is-active` );
106 + if ( ! activeTab && tabs.length > 0 ) {
107 + tabs[0].classList.add( 'is-active' );
108 + const section = tabs[0].getAttribute( 'data-tab-section' );
109 + if ( section ) {
110 + const panel = document.querySelector( `${ BuilderEditLesson.selectors.elCBTabPanels }[data-section="${ section }"]` );
111 + if ( panel ) {
112 + panel.classList.remove( 'lp-hidden' );
113 + }
114 + }
115 + }
116 + }
117 +
118 + /**
119 + * Handle horizontal tab click for client-side tab switching
120 + */
121 + handleTabClick( args ) {
122 + const { e, target } = args;
123 + const tabLink = target.closest( BuilderEditLesson.selectors.elCBHorizontalTabs );
124 +
125 + if ( ! tabLink ) {
126 + return;
127 + }
128 +
129 + e.preventDefault();
130 +
131 + const section = tabLink.getAttribute( 'data-tab-section' );
132 + if ( ! section ) {
133 + return;
134 + }
135 +
136 + // Update active tab
137 + const allTabs = document.querySelectorAll( BuilderEditLesson.selectors.elCBHorizontalTabs );
138 + allTabs.forEach( tab => tab.classList.remove( 'is-active' ) );
139 + tabLink.classList.add( 'is-active' );
140 +
141 + // Show/hide panels
142 + const allPanels = document.querySelectorAll( BuilderEditLesson.selectors.elCBTabPanels );
143 + allPanels.forEach( panel => {
144 + if ( panel.getAttribute( 'data-section' ) === section ) {
145 + panel.classList.remove( 'lp-hidden' );
146 + } else {
147 + panel.classList.add( 'lp-hidden' );
148 + }
149 + } );
150 + }
151 +
152 + events() {
153 + if ( BuilderEditLesson._loadedEvents ) {
154 + return;
155 + }
156 + BuilderEditLesson._loadedEvents = true;
157 +
158 + lpUtils.eventHandlers( 'click', [
159 + {
160 + selector: BuilderEditLesson.selectors.elBtnMainAction,
161 + class: this,
162 + callBack: this.updateLesson.name,
163 + },
164 + {
165 + selector: BuilderEditLesson.selectors.elBtnDraft,
166 + class: this,
167 + callBack: this.saveDraftLesson.name,
168 + },
169 + {
170 + selector: BuilderEditLesson.selectors.elBtnTrash,
171 + class: this,
172 + callBack: this.trashLesson.name,
173 + },
174 + {
175 + selector: BuilderEditLesson.selectors.elCBHorizontalTabs,
176 + class: this,
177 + callBack: this.handleTabClick.name,
178 + },
179 + {
180 + selector: BuilderEditLesson.selectors.elDropdownToggle,
181 + class: this,
182 + callBack: this.handleDropdownToggle.name,
183 + },
184 + {
185 + selector: BuilderEditLesson.selectors.elPermalinkEditBtn,
186 + class: this,
187 + callBack: this.handlePermalinkEdit.name,
188 + },
189 + {
190 + selector: BuilderEditLesson.selectors.elPermalinkOkBtn,
191 + class: this,
192 + callBack: this.handlePermalinkOk.name,
193 + },
194 + {
195 + selector: BuilderEditLesson.selectors.elPermalinkCancelBtn,
196 + class: this,
197 + callBack: this.handlePermalinkCancel.name,
198 + },
199 + ] );
200 + }
201 +
202 + /**
203 + * Validate title is not empty before update
204 + * @return {boolean} True if valid, false if invalid
205 + */
206 + validateTitleBeforeUpdate() {
207 + const titleInput = document.getElementById( BuilderEditLesson.selectors.idTitle );
208 + if ( ! titleInput ) {
209 + return true;
210 + }
211 +
212 + const title = titleInput.value.trim();
213 + if ( ! title ) {
214 + lpToastify.show( 'Lesson title is required.', 'error' );
215 + titleInput.focus();
216 + return false;
217 + }
218 + return true;
219 + }
220 +
221 + /**
222 + * Update action button text after status change
223 + * @param {string} newStatus - The new lesson status
224 + */
225 + updateActionButtons( newStatus ) {
226 + const elBtnUpdate = document.querySelector( BuilderEditLesson.selectors.elBtnMainAction );
227 + if ( ! elBtnUpdate ) {
228 + return;
229 + }
230 +
231 + const titleUpdate = elBtnUpdate.getAttribute( 'data-title-update' ) || 'Update';
232 + const titlePublish = elBtnUpdate.getAttribute( 'data-title-publish' ) || 'Publish';
233 +
234 + if ( newStatus === 'publish' ) {
235 + elBtnUpdate.textContent = titleUpdate;
236 + } else {
237 + elBtnUpdate.textContent = titlePublish;
238 + }
239 + }
240 +
241 + /**
242 + * Check if we're in lesson edit context
243 + * @return {boolean}
244 + */
245 + isLessonContext() {
246 + return !! document.querySelector( BuilderEditLesson.selectors.elDataLesson );
247 + }
248 +
249 + slugify( str ) {
250 + return ( str || '' )
251 + .toString()
252 + .normalize( 'NFD' )
253 + .replace( /[\u0300-\u036f]/g, '' )
254 + .replace( /đ/g, 'd' )
255 + .replace( /Đ/g, 'D' )
256 + .toLowerCase()
257 + .replace( /\s+/g, '-' )
258 + .replace( /[^\w-]+/g, '' )
259 + .replace( /--+/g, '-' )
260 + .replace( /^-+/, '' )
261 + .replace( /-+$/, '' );
262 + }
263 +
264 + buildPermalinkDisplayUrl( baseUrl = '', slug = '', fallbackUrl = '' ) {
265 + const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl : '';
266 + const normalizedSlug = typeof slug === 'string' ? slug.trim() : '';
267 +
268 + if ( normalizedBaseUrl && normalizedSlug ) {
269 + return `${ normalizedBaseUrl }${ normalizedSlug }`;
270 + }
271 +
272 + return typeof fallbackUrl === 'string' ? fallbackUrl : '';
273 + }
274 +
275 + getPermalinkRoot( target = null ) {
276 + if ( target?.closest ) {
277 + const fromTarget = target.closest( BuilderEditLesson.selectors.elPermalinkRoot );
278 + if ( fromTarget ) {
279 + return fromTarget;
280 + }
281 + }
282 +
283 + return document.querySelector(
284 + `.cb-section__lesson-edit ${ BuilderEditLesson.selectors.elPermalinkRoot }`
285 + );
286 + }
287 +
288 + isDescriptionEditorInCodeMode() {
289 + const editorId = BuilderEditLesson.selectors.idDescEditor;
290 + const wrapper =
291 + document.getElementById( `wp-${ editorId }-wrap` ) ||
292 + document.getElementById( `${ editorId }-wrap` );
293 + if ( wrapper?.classList.contains( 'html-active' ) ) {
294 + return true;
295 + }
296 +
297 + const editor = typeof tinymce !== 'undefined' ? tinymce.get( editorId ) : null;
298 +
299 + return !! ( editor?.isHidden && editor.isHidden() );
300 + }
301 +
302 + getDescriptionContentForUpdate() {
303 + const editorId = BuilderEditLesson.selectors.idDescEditor;
304 + const descEditor = document.getElementById( editorId );
305 +
306 + if ( this.isDescriptionEditorInCodeMode() ) {
307 + return descEditor ? descEditor.value : '';
308 + }
309 +
310 + if ( typeof tinymce !== 'undefined' ) {
311 + const editor = tinymce.get( editorId );
312 + if ( editor ) {
313 + return editor.getContent();
314 + }
315 + }
316 +
317 + return descEditor ? descEditor.value : '';
318 + }
319 +
320 + showPermalinkUnavailable( permalinkRoot, message = '' ) {
321 + if ( ! permalinkRoot ) {
322 + return;
323 + }
324 +
325 + const label =
326 + permalinkRoot.querySelector( '.cb-item-edit-permalink__label' ) ||
327 + permalinkRoot.querySelector( '.cb-permalink-label' );
328 + const display = permalinkRoot.querySelector( BuilderEditLesson.selectors.elPermalinkDisplay );
329 + const editor = permalinkRoot.querySelector( BuilderEditLesson.selectors.elPermalinkEditor );
330 + let placeholder = permalinkRoot.querySelector(
331 + BuilderEditLesson.selectors.elPermalinkPlaceholder
332 + );
333 +
334 + if ( ! placeholder ) {
335 + placeholder = document.createElement( 'span' );
336 + placeholder.className = 'cb-item-edit-permalink__placeholder';
337 +
338 + if ( label ) {
339 + label.insertAdjacentElement( 'afterend', placeholder );
340 + } else {
341 + permalinkRoot.prepend( placeholder );
342 + }
343 + }
344 +
345 + placeholder.textContent =
346 + message || 'Permalink is only available if the item is already assigned to a course.';
347 + placeholder.classList.remove( 'lp-hidden' );
348 +
349 + if ( display ) {
350 + display.classList.add( 'lp-hidden' );
351 + }
352 +
353 + if ( editor ) {
354 + editor.classList.add( 'lp-hidden' );
355 + }
356 + }
357 +
358 + getLessonDataForUpdate() {
359 + const data = {};
360 +
361 + const wrapperEl = document.querySelector( BuilderEditLesson.selectors.elDataLesson );
362 + data.lesson_id = wrapperEl ? parseInt( wrapperEl.dataset.lessonId ) || 0 : 0;
363 +
364 + const titleInput = document.getElementById( BuilderEditLesson.selectors.idTitle );
365 + data.lesson_title = titleInput ? titleInput.value : '';
366 +
367 + data.lesson_description = this.getDescriptionContentForUpdate();
368 +
369 + const permalinkInput = document.querySelector(
370 + `input[name="lesson_permalink"], #lesson_permalink, ${ BuilderEditLesson.selectors.elPermalinkSlugInput }`
371 + );
372 + if ( permalinkInput && permalinkInput.value ) {
373 + data.lesson_permalink = permalinkInput.value;
374 + }
375 +
376 + const elFormSetting = document.querySelector( BuilderEditLesson.selectors.elFormSetting );
377 +
378 + if ( elFormSetting ) {
379 + data.lesson_settings = true;
380 + const formElements = elFormSetting.querySelectorAll( 'input, select, textarea' );
381 +
382 + formElements.forEach( ( element ) => {
383 + const name = element.name || element.id;
384 +
385 + if ( ! name || name === 'learnpress_meta_box_nonce' || name === '_wp_http_referer' ) {
386 + return;
387 + }
388 +
389 + if ( element.type === 'checkbox' ) {
390 + const fieldName = name.replace( '[]', '' );
391 + if ( ! data.hasOwnProperty( fieldName ) ) {
392 + data[ fieldName ] = element.checked ? 'yes' : 'no';
393 + }
394 + } else if ( element.type === 'radio' ) {
395 + if ( element.checked ) {
396 + const fieldName = name.replace( '[]', '' );
397 + data[ fieldName ] = element.value;
398 + }
399 + } else if ( element.type === 'file' ) {
400 + const fieldName = name.replace( '[]', '' );
401 + if ( element.files && element.files.length > 0 ) {
402 + data[ fieldName ] = element.files;
403 + }
404 + } else {
405 + const fieldName = name.replace( '[]', '' );
406 +
407 + if ( name.endsWith( '[]' ) ) {
408 + if ( ! data.hasOwnProperty( fieldName ) ) {
409 + data[ fieldName ] = [];
410 + }
411 +
412 + if ( Array.isArray( data[ fieldName ] ) ) {
413 + data[ fieldName ].push( element.value );
414 + }
415 + } else {
416 + if ( ! data.hasOwnProperty( fieldName ) ) {
417 + data[ fieldName ] = element.value;
418 + }
419 + }
420 + }
421 + } );
422 +
423 + Object.keys( data ).forEach( ( key ) => {
424 + if ( Array.isArray( data[ key ] ) ) {
425 + data[ key ] = data[ key ].join( ',' );
426 + }
427 + } );
428 + }
429 +
430 + return data;
431 + }
432 +
433 + updatePermalinkUIAfterSave( data = {} ) {
434 + const permalinkRoot = this.getPermalinkRoot();
435 + if ( ! permalinkRoot ) {
436 + return;
437 + }
438 +
439 + const slugInput = permalinkRoot.querySelector( BuilderEditLesson.selectors.elPermalinkSlugInput );
440 + const urlLink = permalinkRoot.querySelector( BuilderEditLesson.selectors.elPermalinkUrl );
441 + const baseUrlInput = permalinkRoot.querySelector( BuilderEditLesson.selectors.elPermalinkBaseUrl );
442 + const display = permalinkRoot.querySelector( BuilderEditLesson.selectors.elPermalinkDisplay );
443 + const placeholder = permalinkRoot.querySelector(
444 + BuilderEditLesson.selectors.elPermalinkPlaceholder
445 + );
446 + const permalinkDisplayUrl = this.buildPermalinkDisplayUrl(
447 + baseUrlInput ? baseUrlInput.value : '',
448 + data?.lesson_slug,
449 + data?.lesson_permalink
450 + );
451 +
452 + if ( slugInput && data?.lesson_slug ) {
453 + slugInput.value = data.lesson_slug;
454 + slugInput.dataset.originalValue = data.lesson_slug;
455 + }
456 +
457 + const shouldShowUnavailable =
458 + data?.permalink_available === false ||
459 + data?.status === 'draft' ||
460 + data?.status === 'trash' ||
461 + ! data?.lesson_permalink;
462 +
463 + if ( shouldShowUnavailable ) {
464 + this.showPermalinkUnavailable( permalinkRoot, data?.permalink_notice );
465 + return;
466 + }
467 +
468 + if ( placeholder ) {
469 + placeholder.classList.add( 'lp-hidden' );
470 + }
471 +
472 + if ( display ) {
473 + display.classList.remove( 'lp-hidden' );
474 + }
475 +
476 + if ( urlLink && data?.lesson_permalink ) {
477 + urlLink.href = data.lesson_permalink;
478 + urlLink.textContent = permalinkDisplayUrl || data.lesson_permalink;
479 + } else if ( urlLink && permalinkDisplayUrl ) {
480 + urlLink.textContent = permalinkDisplayUrl;
481 + }
482 + }
483 +
484 + handlePermalinkEdit( args ) {
485 + if ( ! this.isLessonContext() ) {
486 + return;
487 + }
488 +
489 + const { e, target } = args;
490 + if ( e ) e.preventDefault();
491 +
492 + const trigger = target?.closest( BuilderEditLesson.selectors.elPermalinkEditBtn );
493 + const lessonRoot = trigger?.closest( BuilderEditLesson.selectors.elDataLesson ) ||
494 + document.querySelector( BuilderEditLesson.selectors.elDataLesson );
495 +
496 + if ( ! lessonRoot ) {
497 + return;
498 + }
499 +
500 + const display = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkDisplay );
501 + const editor = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkEditor );
502 + const input = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkSlugInput );
503 +
504 + if ( ! display || ! editor || ! input ) {
505 + return;
506 + }
507 +
508 + input.dataset.originalValue = input.value;
509 + display.classList.add( 'lp-hidden' );
510 + editor.classList.remove( 'lp-hidden' );
511 + input.focus();
512 + input.select();
513 + }
514 +
515 + handlePermalinkOk( args ) {
516 + if ( ! this.isLessonContext() ) {
517 + return;
518 + }
519 +
520 + const { e, target } = args;
521 + if ( e ) e.preventDefault();
522 +
523 + const trigger = target?.closest( BuilderEditLesson.selectors.elPermalinkOkBtn );
524 + const lessonRoot = trigger?.closest( BuilderEditLesson.selectors.elDataLesson ) ||
525 + document.querySelector( BuilderEditLesson.selectors.elDataLesson );
526 +
527 + if ( ! lessonRoot ) {
528 + return;
529 + }
530 +
531 + const display = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkDisplay );
532 + const editor = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkEditor );
533 + const input = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkSlugInput );
534 + const urlLink = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkUrl );
535 + const baseUrlInput = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkBaseUrl );
536 +
537 + if ( ! display || ! editor || ! input || ! urlLink ) {
538 + return;
539 + }
540 +
541 + let newSlug = this.slugify( input.value.trim() );
542 + if ( ! newSlug ) {
543 + newSlug = input.dataset.originalValue || 'lesson';
544 + }
545 +
546 + input.value = newSlug;
547 + const baseUrl = baseUrlInput ? baseUrlInput.value : '';
548 + const newUrl = this.buildPermalinkDisplayUrl( baseUrl, newSlug, urlLink.textContent || '' );
549 +
550 + // Keep href as the current saved link and only update display text.
551 + urlLink.textContent = newUrl;
552 + editor.classList.add( 'lp-hidden' );
553 + display.classList.remove( 'lp-hidden' );
554 + }
555 +
556 + handlePermalinkCancel( args ) {
557 + if ( ! this.isLessonContext() ) {
558 + return;
559 + }
560 +
561 + const { e, target } = args;
562 + if ( e ) e.preventDefault();
563 +
564 + const trigger = target?.closest( BuilderEditLesson.selectors.elPermalinkCancelBtn );
565 + const lessonRoot = trigger?.closest( BuilderEditLesson.selectors.elDataLesson ) ||
566 + document.querySelector( BuilderEditLesson.selectors.elDataLesson );
567 +
568 + if ( ! lessonRoot ) {
569 + return;
570 + }
571 +
572 + const display = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkDisplay );
573 + const editor = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkEditor );
574 + const input = lessonRoot.querySelector( BuilderEditLesson.selectors.elPermalinkSlugInput );
575 +
576 + if ( ! display || ! editor || ! input ) {
577 + return;
578 + }
579 +
580 + input.value = input.dataset.originalValue || '';
581 + editor.classList.add( 'lp-hidden' );
582 + display.classList.remove( 'lp-hidden' );
583 + }
584 +
585 + updateLesson( args ) {
586 + // Context check: only handle if on lesson edit page
587 + if ( ! this.isLessonContext() ) {
588 + return;
589 + }
590 +
591 + const { target } = args;
592 + const elBtnUpdateLesson = target.closest( BuilderEditLesson.selectors.elBtnMainAction );
593 +
594 + if ( ! elBtnUpdateLesson ) {
595 + return;
596 + }
597 +
598 + // Validate title before update
599 + if ( ! this.validateTitleBeforeUpdate() ) {
600 + return;
601 + }
602 +
603 + lpUtils.lpSetLoadingEl( elBtnUpdateLesson, 1 );
604 +
605 + const lessonData = this.getLessonDataForUpdate();
606 +
607 + const targetStatus = elBtnUpdateLesson.dataset.status || 'publish';
608 +
609 + const dataSend = {
610 + ...lessonData,
611 + action: 'builder_update_lesson',
612 + args: {
613 + id_url: 'builder-update-lesson',
614 + },
615 + lesson_status: targetStatus,
616 + };
617 +
618 + if ( typeof lpLessonBuilder !== 'undefined' && lpLessonBuilder.nonce ) {
619 + dataSend.nonce = lpLessonBuilder.nonce;
620 + }
621 +
622 + const callBack = {
623 + success: ( response ) => {
624 + const { status, message, data } = response;
625 + lpToastify.show( message, status );
626 +
627 + if ( status === 'success' ) {
628 + // Update action button text
629 + this.updateActionButtons( targetStatus );
630 +
631 + if ( data?.lesson_id_new ) {
632 + const currentUrl = window.location.href;
633 + window.location.href = currentUrl.replace( /post-new\/?/, `${ data.lesson_id_new }/` );
634 + }
635 +
636 + if ( data?.status ) {
637 + const elStatus = document.querySelector( BuilderEditLesson.selectors.elLessonStatus );
638 + if ( elStatus ) {
639 + elStatus.className = 'lesson-status ' + data.status;
640 + elStatus.textContent = data.status;
641 + }
642 + if ( data.status === 'trash' || data.status === 'draft' ) {
643 + const curriculumItem = document.querySelector( `.lesson-item[data-lesson-id="${lessonData.lesson_id}"]` );
644 + if ( curriculumItem ) {
645 + const elCurriculumLesson = curriculumItem.closest( '.lesson' );
646 + if ( elCurriculumLesson ) {
647 + elCurriculumLesson.remove();
648 + }
649 + }
650 + }
651 + }
652 +
653 + this.updatePermalinkUIAfterSave( data );
654 +
655 + // Reset form state to prevent "leave site" warning
656 + document.dispatchEvent( new CustomEvent( 'lp-course-builder-saved' ) );
657 + }
658 + },
659 + error: ( error ) => {
660 + lpToastify.show( error.message || error, 'error' );
661 + },
662 + completed: () => {
663 + lpUtils.lpSetLoadingEl( elBtnUpdateLesson, 0 );
664 + },
665 + };
666 +
667 + window.lpAJAXG.fetchAJAX( dataSend, callBack );
668 + }
669 +
670 + saveDraftLesson( args ) {
671 + // Context check: only handle if on lesson edit page
672 + if ( ! this.isLessonContext() ) {
673 + return;
674 + }
675 +
676 + const { target } = args;
677 + const elBtnDraftLesson = target.closest( BuilderEditLesson.selectors.elBtnDraft );
678 +
679 + if ( ! elBtnDraftLesson ) {
680 + return;
681 + }
682 +
683 + // Check if published to show confirm unpublish modal
684 + const statusEl = document.querySelector( BuilderEditLesson.selectors.elLessonStatus );
685 + const isPublished = statusEl && statusEl.classList.contains( 'publish' );
686 + if ( isPublished ) {
687 + const confirmMsg = elBtnDraftLesson.dataset.confirmUnpublish || 'Saving as draft will unpublish this item. Are you sure?';
688 + if ( ! confirm( confirmMsg ) ) {
689 + return;
690 + }
691 + }
692 +
693 + // Validate title before saving draft
694 + if ( ! this.validateTitleBeforeUpdate() ) {
695 + return;
696 + }
697 +
698 + lpUtils.lpSetLoadingEl( elBtnDraftLesson, 1 );
699 +
700 + const lessonData = this.getLessonDataForUpdate();
701 +
702 + const dataSend = {
703 + ...lessonData,
704 + action: 'builder_update_lesson',
705 + args: {
706 + id_url: 'builder-update-lesson',
707 + },
708 + lesson_status: 'draft',
709 + };
710 +
711 + if ( typeof lpLessonBuilder !== 'undefined' && lpLessonBuilder.nonce ) {
712 + dataSend.nonce = lpLessonBuilder.nonce;
713 + }
714 +
715 + const callBack = {
716 + success: ( response ) => {
717 + const { status, message, data } = response;
718 + lpToastify.show( message, status );
719 +
720 + if ( status === 'success' ) {
721 + // Update action button text
722 + this.updateActionButtons( 'draft' );
723 +
724 + if ( data?.lesson_id_new ) {
725 + const currentUrl = window.location.href;
726 + window.location.href = currentUrl.replace( /post-new\/?/, `${ data.lesson_id_new }/` );
727 + }
728 +
729 + if ( data?.status ) {
730 + const elStatus = document.querySelector( BuilderEditLesson.selectors.elLessonStatus );
731 + if ( elStatus ) {
732 + elStatus.className = 'lesson-status ' + data.status;
733 + elStatus.textContent = data.status;
734 + }
735 + if ( data.status === 'trash' || data.status === 'draft' ) {
736 + const curriculumItem = document.querySelector( `.lesson-item[data-lesson-id="${lessonData.lesson_id}"]` );
737 + if ( curriculumItem ) {
738 + const elCurriculumLesson = curriculumItem.closest( '.lesson' );
739 + if ( elCurriculumLesson ) {
740 + elCurriculumLesson.remove();
741 + }
742 + }
743 + }
744 + }
745 +
746 + this.updatePermalinkUIAfterSave( data );
747 +
748 + // Reset form state to prevent "leave site" warning
749 + document.dispatchEvent( new CustomEvent( 'lp-course-builder-saved' ) );
750 + }
751 + },
752 + error: ( error ) => {
753 + lpToastify.show( error.message || error, 'error' );
754 + },
755 + completed: () => {
756 + lpUtils.lpSetLoadingEl( elBtnDraftLesson, 0 );
757 + },
758 + };
759 +
760 + window.lpAJAXG.fetchAJAX( dataSend, callBack );
761 + }
762 +
763 + trashLesson( args ) {
764 + // Context check: only handle if on lesson edit page
765 + if ( ! this.isLessonContext() ) {
766 + return;
767 + }
768 +
769 + const { target } = args;
770 + const elBtnTrashLesson = target.closest( BuilderEditLesson.selectors.elBtnTrash );
771 +
772 + if ( ! elBtnTrashLesson ) {
773 + return;
774 + }
775 +
776 + if ( ! confirm( 'Are you sure you want to trash this lesson?' ) ) {
777 + return;
778 + }
779 +
780 + lpUtils.lpSetLoadingEl( elBtnTrashLesson, 1 );
781 +
782 + const lessonData = this.getLessonDataForUpdate();
783 + const dataSend = {
784 + action: 'move_trash_lesson',
785 + args: {
786 + id_url: 'move-trash-lesson',
787 + },
788 + lesson_id: lessonData.lesson_id,
789 + };
790 +
791 + const callBack = {
792 + success: ( response ) => {
793 + const { status, message, data } = response;
794 + lpToastify.show( message, status );
795 +
796 + if ( status === 'success' ) {
797 + if ( data?.redirect_url ) {
798 + window.location.href = data.redirect_url;
799 + }
800 +
801 + if ( data?.status ) {
802 + const elStatus = document.querySelector( BuilderEditLesson.selectors.elLessonStatus );
803 + if ( elStatus ) {
804 + elStatus.className = 'lesson-status ' + data.status;
805 + elStatus.textContent = data.status;
806 + }
807 + if ( data.status === 'trash' || data.status === 'draft' ) {
808 + const curriculumItem = document.querySelector( `.lesson-item[data-lesson-id="${lessonData.lesson_id}"]` );
809 + if ( curriculumItem ) {
810 + const elCurriculumLesson = curriculumItem.closest( '.lesson' );
811 + if ( elCurriculumLesson ) {
812 + elCurriculumLesson.remove();
813 + }
814 + }
815 + }
816 + }
817 +
818 + this.updatePermalinkUIAfterSave( data );
819 + }
820 + },
821 + error: ( error ) => {
822 + lpToastify.show( error.message || error, 'error' );
823 + },
824 + completed: () => {
825 + lpUtils.lpSetLoadingEl( elBtnTrashLesson, 0 );
826 + },
827 + };
828 +
829 + window.lpAJAXG.fetchAJAX( dataSend, callBack );
830 + }
831 +}