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

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

371 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Course builder JS handler.
3 *
4 * @since 4.3.6
5 * @version 1.0.0
6 */
7 import * as lpUtils from 'lpAssetsJsPath/utils.js';
8 import { BuilderTabCourse } from './course-builder/builder-course/builder-tab-course.js';
9 import { BuilderEditCourse } from './course-builder/builder-course/builder-edit-course.js';
10 import { CreateCourseViaAI } from '../admin/courses/generate-with-ai.js';
11 import { GenerateWithOpenai } from '../admin/generate-with-openai.js';
12 import { BuilderTabLesson } from './course-builder/builder-lesson/builder-tab-lesson.js';
13 import { BuilderEditLesson } from './course-builder/builder-lesson/builder-edit-lesson.js';
14 import { BuilderTabQuiz } from './course-builder/builder-quiz/builder-tab-quiz.js';
15 import { BuilderStandaloneQuiz } from './course-builder/builder-quiz/builder-standalone-quiz.js';
16 import { BuilderTabQuestion } from './course-builder/builder-question/builder-tab-question.js';
17 import { BuilderEditQuestion } from './course-builder/builder-question/builder-edit-question.js';
18 import { BuilderPopup } from './course-builder/builder-popup.js';
19 import { BuilderMaterial } from './course-builder/builder-lesson/builder-material.js';
20 import { BuilderDashboard } from './course-builder/builder-dashboard.js';
21 import { BuilderSettings } from './course-builder/builder-settings.js';
22 import { getFormState } from './course-builder/builder-form-state.js';
23 import { EditCurriculumAi } from '../admin/edit-course/edit-curriculum/edit-curriculum-ai.js';
24 import { initElsTomSelect } from 'lpAssetsJsPath/admin/init-tom-select.js';
25 import { Utils } from 'lpAssetsJsPath/admin/utils-admin.js';
26 import * as lpToastify from 'lpAssetsJsPath/lpToastify';
27
28 // Initialize all builder components
29 const initBuilderComponents = () => {
30 try {
31 new BuilderTabCourse();
32 new BuilderEditCourse();
33
34 const hasCreateCourseAiButton = document.querySelector(
35 '.lp-btn-generate-course-with-ai, .lp-btn-warning-enable-ai'
36 );
37 if ( hasCreateCourseAiButton ) {
38 new CreateCourseViaAI( {
39 autoInsertButton: false,
40 isCourseBuilder: true,
41 redirectDelayMs: 1200,
42 } );
43 }
44
45 const hasEditCourseAiButton = document.querySelector(
46 '.cb-course-edit-ai-btn.lp-btn-generate-with-ai'
47 );
48 if ( hasEditCourseAiButton ) {
49 new GenerateWithOpenai( {
50 autoInsertButtons: false,
51 isCourseBuilder: true,
52 } );
53 }
54
55 new BuilderTabLesson();
56 new BuilderEditLesson();
57 new BuilderTabQuiz();
58 new BuilderStandaloneQuiz();
59 new BuilderTabQuestion();
60 new BuilderEditQuestion();
61 new BuilderPopup();
62 new BuilderDashboard();
63 new BuilderSettings();
64 new EditCurriculumAi( { isCourseBuilder: true } );
65
66 // Initialize form state management for ClassPress-style UX
67 getFormState();
68
69 // Initialize sidebar toggle
70 initSidebarToggle();
71 initSidebarSubMenus();
72 initHeaderMoreActions();
73 } catch ( e ) {
74 console.error( 'Error initializing builder components:', e );
75 }
76 };
77
78 /**
79 * Initialize sidebar collapse/expand toggle
80 * Persists state in localStorage
81 */
82 const initSidebarToggle = () => {
83 const sidebar = document.getElementById( 'lp-course-builder-sidebar' );
84 const toggleBtn = document.querySelector( '.lp-cb-sidebar__toggle' );
85 const wrapper = document.getElementById( 'lp-course-builder' );
86 const storageKey = 'lp_cb_sidebar_collapsed';
87
88 if ( ! sidebar || ! toggleBtn ) {
89 return;
90 }
91
92 // Restore saved state
93 const isCollapsed = localStorage.getItem( storageKey ) === 'true';
94 if ( isCollapsed ) {
95 sidebar.classList.add( 'is-collapsed' );
96 if ( wrapper ) {
97 wrapper.classList.add( 'has-collapsed-sidebar' );
98 }
99 }
100
101 // Handle toggle click
102 toggleBtn.addEventListener( 'click', () => {
103 const willCollapse = ! sidebar.classList.contains( 'is-collapsed' );
104
105 sidebar.classList.toggle( 'is-collapsed' );
106 if ( wrapper ) {
107 wrapper.classList.toggle( 'has-collapsed-sidebar' );
108 }
109
110 // Save state
111 localStorage.setItem( storageKey, willCollapse ? 'true' : 'false' );
112 } );
113 };
114
115 /**
116 * Initialize nested sidebar submenu toggles.
117 */
118 const initSidebarSubMenus = () => {
119 const sidebar = document.getElementById( 'lp-course-builder-sidebar' );
120
121 if ( ! sidebar || sidebar.dataset.subMenusInitialized === 'true' ) {
122 return;
123 }
124
125 sidebar.dataset.subMenusInitialized = 'true';
126
127 sidebar.addEventListener( 'click', ( e ) => {
128 const toggleBtn = e.target.closest( '.lp-cb-sidebar__sub-menu-toggle' );
129
130 if ( ! toggleBtn || ! sidebar.contains( toggleBtn ) ) {
131 return;
132 }
133
134 const item = toggleBtn.closest( '.lp-cb-sidebar__item.has-sub-menu' );
135 if ( ! item ) {
136 return;
137 }
138
139 e.preventDefault();
140
141 const isExpanded = item.classList.toggle( 'is-expanded' );
142 toggleBtn.setAttribute( 'aria-expanded', isExpanded ? 'true' : 'false' );
143 } );
144 };
145
146 /**
147 * Initialize "More actions" menu in edit header (duplicate/trash).
148 * Uses explicit open state to avoid focus-only behavior issues across browsers.
149 */
150 const initHeaderMoreActions = () => {
151 const wrapSelector = '.cb-header-action-expanded';
152 const toggleSelector = '.course-action-expanded';
153 const openClass = 'is-open';
154
155 const closeAll = ( exclude = null ) => {
156 document.querySelectorAll( `${ wrapSelector }.${ openClass }` ).forEach( ( wrap ) => {
157 if ( exclude && wrap === exclude ) {
158 return;
159 }
160
161 wrap.classList.remove( openClass );
162 const btn = wrap.querySelector( toggleSelector );
163 if ( btn ) {
164 btn.setAttribute( 'aria-expanded', 'false' );
165 }
166 } );
167 };
168
169 document.addEventListener( 'click', ( e ) => {
170 const target = e.target;
171 const toggleBtn = target.closest( toggleSelector );
172
173 if ( toggleBtn ) {
174 e.preventDefault();
175 e.stopPropagation();
176
177 const wrap = toggleBtn.closest( wrapSelector );
178 if ( ! wrap ) {
179 return;
180 }
181
182 const willOpen = ! wrap.classList.contains( openClass );
183 closeAll( wrap );
184 wrap.classList.toggle( openClass, willOpen );
185 toggleBtn.setAttribute(
186 'aria-expanded',
187 willOpen ? 'true' : 'false',
188 );
189 return;
190 }
191
192 if ( ! target.closest( wrapSelector ) ) {
193 closeAll();
194 }
195
196 // New expand item
197 const elActionExpands = document.querySelectorAll(
198 '.lp-cb-item-action-expand',
199 );
200 if ( target.closest( '.lp-cb-item-action-expand-toggle' ) ) {
201 const elParentAction = target.closest( '.lp-cb-item-action-wrap' );
202 const elActionExpand = elParentAction.querySelector(
203 '.lp-cb-item-action-expand',
204 );
205
206 elActionExpand.classList.toggle( lpUtils.lpClassName.hidden );
207
208 // Close all
209 elActionExpands.forEach( ( elActionExpandCheck ) => {
210 if ( elActionExpandCheck !== elActionExpand ) {
211 elActionExpandCheck.classList.add( lpUtils.lpClassName.hidden );
212 }
213 } );
214 }
215
216 if ( ! target.closest( '.lp-cb-item-action-expand-toggle' ) ) {
217 elActionExpands.forEach( ( elActionExpandCheck ) => {
218 elActionExpandCheck.classList.add(
219 lpUtils.lpClassName.hidden,
220 );
221 } );
222 }
223 // End new expand item
224
225 // Item action
226 if ( target.closest( '.lp-cb-item-action' ) ) {
227 const elAction = target.closest( '.lp-cb-item-action' );
228 const elParentActionWrap = elAction.closest(
229 '.lp-cb-item-action-wrap',
230 );
231 const elParentActionExpand = elParentActionWrap.querySelector(
232 '.lp-cb-item-action-expand-toggle',
233 );
234 const dataSend = JSON.parse( elAction.dataset.send );
235
236 lpUtils.lpSetLoadingEl( elParentActionExpand, 1 );
237 const svg = elParentActionExpand.querySelector( 'svg' );
238 //lpUtils.lpShowHideEl( svg, 0 );
239
240 // Ajax to generate prompt
241 const callBack = {
242 success: ( response ) => {
243 const { message, status, data } = response;
244
245 lpToastify.show( message, status );
246
247 if ( status === 'success' ) {
248 if ( data.redirect_url ) {
249 window.location.href = data.redirect_url;
250 } else if ( data.html ) {
251 const elParentLi = elAction.closest( 'li.course' );
252 elParentLi.outerHTML = data.html;
253 } else if ( dataSend.action_type === 'delete' ) {
254 const elParentLi = elAction.closest( 'li.course' );
255 elParentLi.remove();
256 }
257 }
258 },
259 error: ( error ) => {
260 lpToastify.show( error, 'error' );
261 },
262 completed: () => {
263 lpUtils.lpSetLoadingEl( elParentActionExpand, 0 );
264 },
265 };
266
267 window.lpAJAXG.fetchAJAX( dataSend, callBack );
268 }
269 // End item action
270 } );
271
272 document.addEventListener( 'keydown', ( e ) => {
273 if ( e.key === 'Escape' ) {
274 closeAll();
275 }
276 } );
277 };
278
279 // Add loading state to search buttons on form submit (page reload)
280 document.addEventListener( 'submit', ( e ) => {
281 const form = e.target.closest( '.cb-search-form' );
282 if ( ! form ) {
283 return;
284 }
285
286 const searchBtn = form.querySelector( '.cb-search-btn' );
287 if ( searchBtn ) {
288 searchBtn.classList.add( 'loading' );
289 searchBtn.disabled = true;
290 }
291 } );
292
293 // Initialize components
294 initBuilderComponents();
295
296 // Events
297 document.addEventListener( 'click', ( e ) => {
298 try {
299 initElsTomSelect();
300 } catch ( e ) {
301 console.warn( 'Error initializing TomSelect:', e );
302 }
303 } );
304
305 document.addEventListener( 'DOMContentLoaded', () => {
306 // Sure that the TomSelect is loaded if listener can't find elements.
307 try {
308 initElsTomSelect();
309 } catch ( e ) {
310 console.warn( 'Error initializing TomSelect on DOMContentLoaded:', e );
311 }
312
313 // Initialize BuilderMaterial for Course Builder Settings tab Material
314 try {
315 initBuilderMaterialForCourseSettings();
316 } catch ( e ) {
317 console.error( 'Error initializing BuilderMaterial:', e );
318 }
319 } );
320
321 // Use lpOnElementReady safely
322 if ( Utils?.lpOnElementReady ) {
323 Utils.lpOnElementReady( 'select.lp-tom-select', () => {
324 try {
325 initElsTomSelect();
326 } catch ( e ) {
327 console.warn( 'Error initializing TomSelect:', e );
328 }
329 } );
330 }
331
332 window.lpFindTomSelect = initElsTomSelect;
333
334 /**
335 * Initialize BuilderMaterial for Course Builder Settings tab Material
336 */
337 function initBuilderMaterialForCourseSettings() {
338 const initializedContainers = new WeakSet();
339
340 // Listen for tab clicks in Course Settings using event delegation
341 document.addEventListener( 'click', ( e ) => {
342 const target = e.target.closest( 'ul.lp-meta-box__course-tab__tabs a' );
343
344 if ( ! target ) {
345 return;
346 }
347
348 const targetPanel = target.getAttribute( 'href' );
349
350 // Check if Material tab is clicked
351 if ( targetPanel && targetPanel.includes( 'material' ) ) {
352 // Wait for DOM to update
353 setTimeout( () => {
354 try {
355 const materialContainer = document.querySelector(
356 targetPanel + ' #lp-material-container'
357 );
358 if ( materialContainer && ! initializedContainers.has( materialContainer ) ) {
359 // Mark as initialized to prevent multiple instances
360 initializedContainers.add( materialContainer );
361 // Initialize BuilderMaterial
362 new BuilderMaterial( materialContainer );
363 }
364 } catch ( e ) {
365 console.error( 'Error initializing BuilderMaterial:', e );
366 }
367 }, 100 );
368 }
369 } );
370 }
371