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-lesson/builder-material.js +757 -757 4.3.8 → 4.4.9 View file →
@@ -1,757 +1,757 @@
1 -/**
2 - * Builder Material Handler
3 - * Handles material upload and management for lesson popup in course builder.
4 - * Pure JavaScript implementation with optimizations
5 - *
6 - * @since 4.3.0
7 - * @version 1.0.0
8 - */
9 -
10 -export class BuilderMaterial {
11 - constructor( container = null ) {
12 - this.container = container;
13 - this.initialized = false;
14 - this.eventsBound = false;
15 - this.sortable = null;
16 -
17 - // Store references for cleanup
18 - this.boundHandlers = {
19 - handleAddMaterial: null,
20 - handleChange: null,
21 - handleClick: null,
22 - handleSaveAll: null,
23 - handleDelete: null,
24 - handleDragStart: null,
25 - handleDragOver: null,
26 - handleDrop: null,
27 - handleDragEnd: null,
28 - };
29 -
30 - // Cache DOM elements
31 - this.elements = {};
32 -
33 - if ( this.container ) {
34 - this.init();
35 - }
36 - }
37 -
38 - /**
39 - * Reinitialize with new container (called from BuilderPopup)
40 - */
41 - reinit( container ) {
42 - if ( this.initialized ) {
43 - this.destroy();
44 - }
45 -
46 - this.container = container;
47 -
48 - if ( this.container ) {
49 - this.init();
50 - }
51 - }
52 -
53 - /**
54 - * Cache all DOM elements for better performance
55 - */
56 - cacheElements() {
57 - const el = this.elements;
58 -
59 - el.postId = this.container.querySelector( '#current-material-post-id' );
60 - el.maxFileSize = this.container.querySelector( '#material-max-file-size' );
61 - el.uploadField = this.container.querySelector( '.lp-material--field-upload' );
62 - el.canUpload = this.container.querySelector( '#available-to-upload' );
63 - el.addBtn = this.container.querySelector( '#btn-lp--add-material' );
64 - el.groupTemplate = this.container.querySelector( '#lp-material--add-material-template' );
65 - el.groupContainer = this.container.querySelector( '#lp-material--group-container' );
66 - el.materialTab = this.container.querySelector( '#lp-material-container' ) || this.container;
67 - el.saveBtn = this.container.querySelector( '#btn-lp--save-material' );
68 - el.uploadTemplate = this.container.querySelector( '#lp-material--upload-field-template' );
69 - el.externalTemplate = this.container.querySelector( '#lp-material--external-field-template' );
70 - el.deleteText = this.container.querySelector( '#delete-material-row-text' );
71 - el.deleteMessage = this.container.querySelector( '#delete-material-message' );
72 - el.materialTable = this.container.querySelector( '.lp-material--table' );
73 - el.tbody = el.materialTable?.querySelector( 'tbody' );
74 - el.thead = el.materialTable?.querySelector( 'thead' );
75 - }
76 -
77 - init() {
78 - if ( ! this.container ) return;
79 -
80 - // Cache all elements
81 - this.cacheElements();
82 -
83 - const { postId, materialTab } = this.elements;
84 -
85 - // Validate required elements
86 - if ( ! postId || ! materialTab ) return;
87 -
88 - // Store config values
89 - this.postID = postId.value;
90 - this.maxFileSize = this.elements.maxFileSize?.value || 10;
91 - this.acceptFile = this.elements.uploadField
92 - ? this.elements.uploadField
93 - .getAttribute( 'accept' )
94 - ?.split( ',' )
95 - .map( ( s ) => s.trim() ) || []
96 - : [];
97 -
98 - // Load existing materials
99 - this.loadMaterials();
100 -
101 - // Bind events
102 - this.bindEvents();
103 -
104 - // Initialize native drag & drop sortable
105 - this.initSortable();
106 -
107 - this.initialized = true;
108 - }
109 -
110 - /**
111 - * Load materials from API with better error handling
112 - */
113 - async loadMaterials() {
114 - const { materialTab, tbody } = this.elements;
115 -
116 - if ( ! materialTab || ! this.postID || ! tbody ) return;
117 -
118 - try {
119 - const restUrl = this.getRestUrl();
120 - const url = `${ restUrl }lp/v1/material/item-materials/${ this.postID }`;
121 -
122 - const response = await fetch( url, {
123 - method: 'GET',
124 - headers: {
125 - 'X-WP-Nonce': this.getNonce(),
126 - 'Content-Type': 'application/json',
127 - },
128 - } );
129 -
130 - if ( ! response.ok ) {
131 - throw new Error( `HTTP error! status: ${ response.status }` );
132 - }
133 -
134 - const result = await response.json();
135 - const { data, status } = result;
136 -
137 - if ( status !== 'success' ) {
138 - console.error( result.message );
139 - return;
140 - }
141 -
142 - if ( data?.items?.length > 0 ) {
143 - // Remove skeleton loader
144 - const skeleton = materialTab.querySelector( '.lp-skeleton-animation' );
145 - skeleton?.remove();
146 -
147 - // Use DocumentFragment for better performance
148 - const fragment = document.createDocumentFragment();
149 - data.items.forEach( ( item ) => {
150 - const row = this.createRow( item );
151 - fragment.appendChild( row );
152 - } );
153 - tbody.appendChild( fragment );
154 -
155 - // Reinit sortable after loading data
156 - this.initSortable();
157 - }
158 - } catch ( error ) {
159 - console.error( 'Load materials error:', error.message );
160 - }
161 - }
162 -
163 - /**
164 - * Create a table row element (more efficient than insertAdjacentHTML)
165 - */
166 - createRow( data ) {
167 - const tr = document.createElement( 'tr' );
168 - tr.dataset.id = data.file_id;
169 - tr.dataset.sort = data.orders;
170 - tr.draggable = true;
171 -
172 - const deleteBtnText = this.elements.deleteText?.value || 'Delete';
173 -
174 - tr.innerHTML = `
175 - <td class="sort">
176 - <span class="dashicons dashicons-menu"></span> ${ this.escapeHtml( data.file_name ) }
177 - </td>
178 - <td>${ this.capitalizeFirstChar( data.method ) }</td>
179 - <td>
180 - <a href="javascript:void(0)" class="delete-material-row" data-id="${ data.file_id }">
181 - ${ deleteBtnText }
182 - </a>
183 - </td>
184 - `;
185 -
186 - return tr;
187 - }
188 -
189 - /**
190 - * Escape HTML to prevent XSS
191 - */
192 - escapeHtml( text ) {
193 - const div = document.createElement( 'div' );
194 - div.textContent = text;
195 - return div.innerHTML;
196 - }
197 -
198 - capitalizeFirstChar( str ) {
199 - return str.charAt( 0 ).toUpperCase() + str.substring( 1 );
200 - }
201 -
202 - /**
203 - * Bind all events with delegation for better performance
204 - */
205 - bindEvents() {
206 - if ( this.eventsBound ) return;
207 -
208 - const { addBtn, materialTab, saveBtn } = this.elements;
209 -
210 - // Create bound handlers for later removal
211 - this.boundHandlers.handleAddMaterial = () => this.handleAddMaterial();
212 - this.boundHandlers.handleChange = ( e ) => this.handleChange( e );
213 - this.boundHandlers.handleClick = ( e ) => this.handleClick( e );
214 - this.boundHandlers.handleSaveAll = () => this.handleSaveAll();
215 - this.boundHandlers.handleDelete = ( e ) => this.handleDelete( e );
216 -
217 - // Add material button
218 - addBtn?.addEventListener( 'click', this.boundHandlers.handleAddMaterial );
219 -
220 - // Use event delegation on materialTab
221 - if ( materialTab ) {
222 - materialTab.addEventListener( 'change', this.boundHandlers.handleChange );
223 - materialTab.addEventListener( 'click', this.boundHandlers.handleClick );
224 - }
225 -
226 - // Save all button
227 - saveBtn?.addEventListener( 'click', this.boundHandlers.handleSaveAll );
228 -
229 - // Delete material (use event delegation on container)
230 - this.container.addEventListener( 'click', this.boundHandlers.handleDelete );
231 -
232 - this.eventsBound = true;
233 - }
234 -
235 - /**
236 - * Handle add material button click
237 - */
238 - handleAddMaterial() {
239 - const { addBtn, groupContainer, groupTemplate } = this.elements;
240 -
241 - if ( ! addBtn || ! groupContainer || ! groupTemplate ) return;
242 -
243 - const canUploadData = parseInt( addBtn.getAttribute( 'can-upload' ) ) || 0;
244 - const groups = groupContainer.querySelectorAll( '.lp-material--group' ).length;
245 -
246 - if ( groups >= canUploadData ) return;
247 -
248 - groupContainer.insertAdjacentHTML( 'afterbegin', groupTemplate.innerHTML );
249 - }
250 -
251 - /**
252 - * Handle change events with delegation
253 - */
254 - handleChange( event ) {
255 - const target = event.target;
256 -
257 - // Switch between upload and external
258 - if ( target.classList.contains( 'lp-material--field-method' ) ) {
259 - this.handleMethodSwitch( target );
260 - }
261 -
262 - // File validation
263 - if ( target.classList.contains( 'lp-material--field-upload' ) ) {
264 - this.validateFile( target );
265 - }
266 - }
267 -
268 - /**
269 - * Handle method switch (upload/external)
270 - */
271 - handleMethodSwitch( target ) {
272 - const method = target.value;
273 - const { uploadTemplate, externalTemplate } = this.elements;
274 -
275 - if ( ! uploadTemplate || ! externalTemplate ) return;
276 -
277 - const group = target.closest( '.lp-material--group' );
278 - if ( ! group ) return;
279 -
280 - switch ( method ) {
281 - case 'upload':
282 - target.parentNode.insertAdjacentHTML( 'afterend', uploadTemplate.innerHTML );
283 - group.querySelector( '.lp-material--external-wrap' )?.remove();
284 - break;
285 - case 'external':
286 - target.parentNode.insertAdjacentHTML( 'afterend', externalTemplate.innerHTML );
287 - group.querySelector( '.lp-material--upload-wrap' )?.remove();
288 - break;
289 - }
290 - }
291 -
292 - /**
293 - * Validate uploaded file
294 - */
295 - validateFile( target ) {
296 - if ( ! target.value || ! target.files?.length ) {
297 - this.resetUploadLabel( target );
298 - return;
299 - }
300 -
301 - const file = target.files[ 0 ];
302 -
303 - if ( this.acceptFile.length > 0 && ! this.acceptFile.includes( file.type ) ) {
304 - alert( 'This file is not allowed! Please choose another file!' );
305 - target.value = '';
306 - this.resetUploadLabel( target );
307 - return;
308 - }
309 -
310 - if ( file.size > this.maxFileSize * 1024 * 1024 ) {
311 - alert(
312 - `This file size is greater than ${ this.maxFileSize }MB! Please choose another file!`
313 - );
314 - target.value = '';
315 - this.resetUploadLabel( target );
316 - return;
317 - }
318 -
319 - this.updateUploadLabel( target, file.name );
320 - }
321 -
322 - /**
323 - * Update upload label to show selected file name
324 - */
325 - updateUploadLabel( target, fileName ) {
326 - if ( ! fileName ) return;
327 -
328 - const uploadLabel = target
329 - .closest( '.lp-material--upload-wrap' )
330 - ?.querySelector( 'label' );
331 -
332 - if ( ! uploadLabel ) return;
333 -
334 - uploadLabel.classList.add( 'has-selected-file' );
335 - uploadLabel.setAttribute( 'data-file-name', fileName );
336 - uploadLabel.setAttribute( 'title', fileName );
337 - }
338 -
339 - /**
340 - * Reset upload label state
341 - */
342 - resetUploadLabel( target ) {
343 - const uploadLabel = target
344 - ?.closest( '.lp-material--upload-wrap' )
345 - ?.querySelector( 'label' );
346 -
347 - if ( ! uploadLabel ) return;
348 -
349 - uploadLabel.classList.remove( 'has-selected-file' );
350 - uploadLabel.removeAttribute( 'data-file-name' );
351 - uploadLabel.removeAttribute( 'title' );
352 - }
353 -
354 - /**
355 - * Handle click events with delegation
356 - */
357 - handleClick( event ) {
358 - const target = event.target;
359 -
360 - // Delete group
361 - if ( target.classList.contains( 'lp-material--delete' ) && target.nodeName === 'BUTTON' ) {
362 - target.closest( '.lp-material--group' )?.remove();
363 - return;
364 - }
365 -
366 - // Save single material
367 - if ( target.classList.contains( 'lp-material-save-field' ) ) {
368 - const material = target.closest( '.lp-material--group' );
369 - if ( material ) {
370 - this.saveMaterial( [ material ], true, target );
371 - }
372 - }
373 - }
374 -
375 - /**
376 - * Handle save all button
377 - */
378 - handleSaveAll() {
379 - const { groupContainer, saveBtn } = this.elements;
380 -
381 - if ( ! groupContainer ) return;
382 -
383 - const materials = Array.from( groupContainer.querySelectorAll( '.lp-material--group' ) );
384 - if ( materials.length > 0 ) {
385 - this.saveMaterial( materials, false, saveBtn );
386 - }
387 - }
388 -
389 - /**
390 - * Save material(s) with improved validation
391 - */
392 - async saveMaterial( materials, isSingle = false, targetBtn ) {
393 - if ( ! materials.length ) return;
394 -
395 - const materialData = [];
396 - const formData = new FormData();
397 - let isValid = true;
398 -
399 - for ( const ele of materials ) {
400 - const label = ele.querySelector( '.lp-material--field-title' )?.value;
401 - const method = ele.querySelector( '.lp-material--field-method' )?.value;
402 - const externalField = ele.querySelector( '.lp-material--field-external-link' );
403 - const uploadField = ele.querySelector( '.lp-material--field-upload' );
404 -
405 - if ( ! label ) {
406 - isValid = false;
407 - break;
408 - }
409 -
410 - let file = '';
411 - let link = '';
412 -
413 - switch ( method ) {
414 - case 'upload':
415 - if ( uploadField?.value && uploadField.files?.length > 0 ) {
416 - file = uploadField.files[ 0 ].name;
417 - formData.append( 'file[]', uploadField.files[ 0 ] );
418 - } else {
419 - isValid = false;
420 - }
421 - break;
422 - case 'external':
423 - link = externalField?.value || '';
424 - if ( ! link ) {
425 - isValid = false;
426 - }
427 - break;
428 - }
429 -
430 - if ( ! isValid ) break;
431 -
432 - materialData.push( { label, method, file, link } );
433 - }
434 -
435 - if ( ! isValid ) {
436 - alert( 'Enter file title, choose file or enter file link!' );
437 - return;
438 - }
439 -
440 - formData.append( 'data', JSON.stringify( materialData ) );
441 - targetBtn?.classList.add( 'loading' );
442 -
443 - try {
444 - const restUrl = this.getRestUrl();
445 - const url = `${ restUrl }lp/v1/material/item-materials/${ this.postID }`;
446 -
447 - const response = await fetch( url, {
448 - method: 'POST',
449 - headers: {
450 - 'X-WP-Nonce': this.getNonce(),
451 - },
452 - body: formData,
453 - } );
454 -
455 - if ( ! response.ok ) {
456 - throw new Error( `HTTP error! status: ${ response.status }` );
457 - }
458 -
459 - const text = await response.text();
460 - let res;
461 -
462 - try {
463 - res = JSON.parse( text );
464 - } catch ( e ) {
465 - console.error( 'Response is not valid JSON:', text.substring( 0, 200 ) );
466 - throw new Error( 'Server returned invalid response. Check console for details.' );
467 - }
468 -
469 - // Clear or remove materials
470 - if ( ! isSingle ) {
471 - materials.forEach( ( ele ) => {
472 - ele.querySelector( '.lp-material--field-title' ).value = '';
473 - const uploadField = ele.querySelector( '.lp-material--field-upload' );
474 - if ( uploadField ) {
475 - uploadField.value = '';
476 - this.resetUploadLabel( uploadField );
477 - }
478 - const externalField = ele.querySelector( '.lp-material--field-external-link' );
479 - if ( externalField ) externalField.value = '';
480 - } );
481 - } else {
482 - materials[ 0 ].remove();
483 - }
484 -
485 - const { message, data, status } = res;
486 - // console.log( message );
487 -
488 - if ( status === 'success' && data?.length > 0 ) {
489 - const { thead, tbody } = this.elements;
490 -
491 - thead?.classList.remove( 'hidden' );
492 -
493 - if ( tbody ) {
494 - const fragment = document.createDocumentFragment();
495 - data.forEach( ( row ) => {
496 - fragment.appendChild( this.createRow( row ) );
497 - } );
498 - tbody.appendChild( fragment );
499 - }
500 -
501 - this.updateCanUploadCount( -data.length );
502 - this.initSortable();
503 - }
504 - } catch ( err ) {
505 - console.error( 'Save material error:', err );
506 - alert( 'Error saving material: ' + err.message );
507 - } finally {
508 - targetBtn?.classList.remove( 'loading' );
509 - }
510 - }
511 -
512 - /**
513 - * Handle delete material
514 - */
515 - async handleDelete( e ) {
516 - const target = e.target;
517 -
518 - if ( ! target.classList.contains( 'delete-material-row' ) || target.nodeName !== 'A' ) {
519 - return;
520 - }
521 -
522 - e.preventDefault();
523 -
524 - const rowID = target.dataset.id;
525 - const message =
526 - this.elements.deleteMessage?.value || 'Are you sure you want to delete this material?';
527 -
528 - if ( ! confirm( message ) ) return;
529 -
530 - try {
531 - const restUrl = this.getRestUrl();
532 - const url = `${ restUrl }lp/v1/material/${ rowID }`;
533 -
534 - const response = await fetch( url, {
535 - method: 'DELETE',
536 - headers: {
537 - 'X-WP-Nonce': this.getNonce(),
538 - 'Content-Type': 'application/json',
539 - },
540 - body: JSON.stringify( {
541 - item_id: this.postID,
542 - } ),
543 - } );
544 -
545 - const res = await response.json();
546 -
547 - if ( res.status !== 200 || ! res.delete ) {
548 - alert( res.message );
549 - } else {
550 - target.closest( 'tr' )?.remove();
551 - this.updateCanUploadCount( 1 );
552 - }
553 - } catch ( err ) {
554 - console.error( 'Delete material error:', err );
555 - alert( 'Error deleting material: ' + err.message );
556 - }
557 - }
558 -
559 - /**
560 - * Update can upload count
561 - */
562 - updateCanUploadCount( delta ) {
563 - const { canUpload, addBtn } = this.elements;
564 -
565 - if ( canUpload && addBtn ) {
566 - const newCount = parseInt( canUpload.textContent ) + delta;
567 - canUpload.textContent = newCount;
568 - addBtn.setAttribute( 'can-upload', newCount );
569 - }
570 - }
571 -
572 - /**
573 - * Initialize native drag & drop sortable (no jQuery)
574 - */
575 - initSortable() {
576 - const { tbody } = this.elements;
577 -
578 - if ( ! tbody ) return;
579 -
580 - // Remove existing listeners
581 - this.destroySortable();
582 -
583 - const rows = tbody.querySelectorAll( 'tr' );
584 -
585 - // Create bound handlers
586 - this.boundHandlers.handleDragStart = ( e ) => this.handleDragStart( e );
587 - this.boundHandlers.handleDragOver = ( e ) => this.handleDragOver( e );
588 - this.boundHandlers.handleDrop = ( e ) => this.handleDrop( e );
589 - this.boundHandlers.handleDragEnd = () => this.handleDragEnd();
590 -
591 - rows.forEach( ( row ) => {
592 - row.draggable = true;
593 - row.addEventListener( 'dragstart', this.boundHandlers.handleDragStart );
594 - row.addEventListener( 'dragover', this.boundHandlers.handleDragOver );
595 - row.addEventListener( 'drop', this.boundHandlers.handleDrop );
596 - row.addEventListener( 'dragend', this.boundHandlers.handleDragEnd );
597 - } );
598 -
599 - this.sortable = { tbody, rows };
600 - }
601 -
602 - handleDragStart( e ) {
603 - this.draggedElement = e.currentTarget;
604 - e.currentTarget.style.opacity = '0.4';
605 - e.dataTransfer.effectAllowed = 'move';
606 - e.dataTransfer.setData( 'text/html', e.currentTarget.innerHTML );
607 - }
608 -
609 - handleDragOver( e ) {
610 - if ( e.preventDefault ) {
611 - e.preventDefault();
612 - }
613 - e.dataTransfer.dropEffect = 'move';
614 -
615 - const target = e.currentTarget;
616 - if ( this.draggedElement !== target ) {
617 - const rect = target.getBoundingClientRect();
618 - const next = ( e.clientY - rect.top ) / ( rect.bottom - rect.top ) > 0.5;
619 - target.parentNode.insertBefore( this.draggedElement, next ? target.nextSibling : target );
620 - }
621 -
622 - return false;
623 - }
624 -
625 - handleDrop( e ) {
626 - if ( e.stopPropagation ) {
627 - e.stopPropagation();
628 - }
629 - return false;
630 - }
631 -
632 - handleDragEnd() {
633 - this.draggedElement.style.opacity = '1';
634 - this.draggedElement = null;
635 -
636 - // Update sort order after drag ends
637 - this.updateSort();
638 - }
639 -
640 - /**
641 - * Update sort order
642 - */
643 - async updateSort() {
644 - const { tbody } = this.elements;
645 - if ( ! tbody ) return;
646 -
647 - const items = tbody.querySelectorAll( 'tr' );
648 - const data = Array.from( items ).map( ( item, index ) => {
649 - item.dataset.sort = index + 1;
650 - return {
651 - file_id: parseInt( item.dataset.id ),
652 - orders: index + 1,
653 - };
654 - } );
655 -
656 - try {
657 - const restUrl = this.getRestUrl();
658 - const url = `${ restUrl }lp/v1/material/item-materials/${ this.postID }`;
659 -
660 - const response = await fetch( url, {
661 - method: 'PUT',
662 - headers: {
663 - 'X-WP-Nonce': this.getNonce(),
664 - 'Content-Type': 'application/json',
665 - },
666 - body: JSON.stringify( {
667 - sort_arr: JSON.stringify( data ),
668 - } ),
669 - } );
670 -
671 - const res = await response.json();
672 -
673 - if ( res.status !== 200 ) {
674 - console.error( 'Sort table fail.' );
675 - }
676 - } catch ( err ) {
677 - console.error( 'Update sort error:', err );
678 - }
679 - }
680 -
681 - /**
682 - * Helper methods for REST URL and nonce
683 - */
684 - getRestUrl() {
685 - return window.lpGlobalSettings?.rest || window.lpData?.lp_rest_url || '/wp-json/';
686 - }
687 -
688 - getNonce() {
689 - return window.lpGlobalSettings?.nonce || window.lpData?.nonce || '';
690 - }
691 -
692 - /**
693 - * Destroy sortable
694 - */
695 - destroySortable() {
696 - if ( ! this.sortable ) return;
697 -
698 - const { rows } = this.sortable;
699 - rows?.forEach( ( row ) => {
700 - row.removeEventListener( 'dragstart', this.boundHandlers.handleDragStart );
701 - row.removeEventListener( 'dragover', this.boundHandlers.handleDragOver );
702 - row.removeEventListener( 'drop', this.boundHandlers.handleDrop );
703 - row.removeEventListener( 'dragend', this.boundHandlers.handleDragEnd );
704 - } );
705 -
706 - this.sortable = null;
707 - }
708 -
709 - /**
710 - * Destroy instance and cleanup
711 - */
712 - destroy() {
713 - const { addBtn, materialTab, saveBtn } = this.elements;
714 -
715 - // Remove event listeners
716 - if ( addBtn ) {
717 - addBtn.removeEventListener( 'click', this.boundHandlers.handleAddMaterial );
718 - }
719 -
720 - if ( materialTab ) {
721 - materialTab.removeEventListener( 'change', this.boundHandlers.handleChange );
722 - materialTab.removeEventListener( 'click', this.boundHandlers.handleClick );
723 - }
724 -
725 - if ( saveBtn ) {
726 - saveBtn.removeEventListener( 'click', this.boundHandlers.handleSaveAll );
727 - }
728 -
729 - if ( this.container ) {
730 - this.container.removeEventListener( 'click', this.boundHandlers.handleDelete );
731 - }
732 -
733 - // Destroy sortable
734 - this.destroySortable();
735 -
736 - // Reset bound handlers
737 - this.boundHandlers = {
738 - handleAddMaterial: null,
739 - handleChange: null,
740 - handleClick: null,
741 - handleSaveAll: null,
742 - handleDelete: null,
743 - handleDragStart: null,
744 - handleDragOver: null,
745 - handleDrop: null,
746 - handleDragEnd: null,
747 - };
748 -
749 - // Clear cached elements
750 - this.elements = {};
751 -
752 - this.initialized = false;
753 - this.eventsBound = false;
754 - }
755 -}
756 -
757 -export default BuilderMaterial;
1 +/**
2 + * Builder Material Handler
3 + * Handles material upload and management for lesson popup in course builder.
4 + * Pure JavaScript implementation with optimizations
5 + *
6 + * @since 4.3.0
7 + * @version 1.0.0
8 + */
9 +
10 +export class BuilderMaterial {
11 + constructor( container = null ) {
12 + this.container = container;
13 + this.initialized = false;
14 + this.eventsBound = false;
15 + this.sortable = null;
16 +
17 + // Store references for cleanup
18 + this.boundHandlers = {
19 + handleAddMaterial: null,
20 + handleChange: null,
21 + handleClick: null,
22 + handleSaveAll: null,
23 + handleDelete: null,
24 + handleDragStart: null,
25 + handleDragOver: null,
26 + handleDrop: null,
27 + handleDragEnd: null,
28 + };
29 +
30 + // Cache DOM elements
31 + this.elements = {};
32 +
33 + if ( this.container ) {
34 + this.init();
35 + }
36 + }
37 +
38 + /**
39 + * Reinitialize with new container (called from BuilderPopup)
40 + */
41 + reinit( container ) {
42 + if ( this.initialized ) {
43 + this.destroy();
44 + }
45 +
46 + this.container = container;
47 +
48 + if ( this.container ) {
49 + this.init();
50 + }
51 + }
52 +
53 + /**
54 + * Cache all DOM elements for better performance
55 + */
56 + cacheElements() {
57 + const el = this.elements;
58 +
59 + el.postId = this.container.querySelector( '#current-material-post-id' );
60 + el.maxFileSize = this.container.querySelector( '#material-max-file-size' );
61 + el.uploadField = this.container.querySelector( '.lp-material--field-upload' );
62 + el.canUpload = this.container.querySelector( '#available-to-upload' );
63 + el.addBtn = this.container.querySelector( '#btn-lp--add-material' );
64 + el.groupTemplate = this.container.querySelector( '#lp-material--add-material-template' );
65 + el.groupContainer = this.container.querySelector( '#lp-material--group-container' );
66 + el.materialTab = this.container.querySelector( '#lp-material-container' ) || this.container;
67 + el.saveBtn = this.container.querySelector( '#btn-lp--save-material' );
68 + el.uploadTemplate = this.container.querySelector( '#lp-material--upload-field-template' );
69 + el.externalTemplate = this.container.querySelector( '#lp-material--external-field-template' );
70 + el.deleteText = this.container.querySelector( '#delete-material-row-text' );
71 + el.deleteMessage = this.container.querySelector( '#delete-material-message' );
72 + el.materialTable = this.container.querySelector( '.lp-material--table' );
73 + el.tbody = el.materialTable?.querySelector( 'tbody' );
74 + el.thead = el.materialTable?.querySelector( 'thead' );
75 + }
76 +
77 + init() {
78 + if ( ! this.container ) return;
79 +
80 + // Cache all elements
81 + this.cacheElements();
82 +
83 + const { postId, materialTab } = this.elements;
84 +
85 + // Validate required elements
86 + if ( ! postId || ! materialTab ) return;
87 +
88 + // Store config values
89 + this.postID = postId.value;
90 + this.maxFileSize = this.elements.maxFileSize?.value || 10;
91 + this.acceptFile = this.elements.uploadField
92 + ? this.elements.uploadField
93 + .getAttribute( 'accept' )
94 + ?.split( ',' )
95 + .map( ( s ) => s.trim() ) || []
96 + : [];
97 +
98 + // Load existing materials
99 + this.loadMaterials();
100 +
101 + // Bind events
102 + this.bindEvents();
103 +
104 + // Initialize native drag & drop sortable
105 + this.initSortable();
106 +
107 + this.initialized = true;
108 + }
109 +
110 + /**
111 + * Load materials from API with better error handling
112 + */
113 + async loadMaterials() {
114 + const { materialTab, tbody } = this.elements;
115 +
116 + if ( ! materialTab || ! this.postID || ! tbody ) return;
117 +
118 + try {
119 + const restUrl = this.getRestUrl();
120 + const url = `${ restUrl }lp/v1/material/item-materials/${ this.postID }`;
121 +
122 + const response = await fetch( url, {
123 + method: 'GET',
124 + headers: {
125 + 'X-WP-Nonce': this.getNonce(),
126 + 'Content-Type': 'application/json',
127 + },
128 + } );
129 +
130 + if ( ! response.ok ) {
131 + throw new Error( `HTTP error! status: ${ response.status }` );
132 + }
133 +
134 + const result = await response.json();
135 + const { data, status } = result;
136 +
137 + if ( status !== 'success' ) {
138 + console.error( result.message );
139 + return;
140 + }
141 +
142 + if ( data?.items?.length > 0 ) {
143 + // Remove skeleton loader
144 + const skeleton = materialTab.querySelector( '.lp-skeleton-animation' );
145 + skeleton?.remove();
146 +
147 + // Use DocumentFragment for better performance
148 + const fragment = document.createDocumentFragment();
149 + data.items.forEach( ( item ) => {
150 + const row = this.createRow( item );
151 + fragment.appendChild( row );
152 + } );
153 + tbody.appendChild( fragment );
154 +
155 + // Reinit sortable after loading data
156 + this.initSortable();
157 + }
158 + } catch ( error ) {
159 + console.error( 'Load materials error:', error.message );
160 + }
161 + }
162 +
163 + /**
164 + * Create a table row element (more efficient than insertAdjacentHTML)
165 + */
166 + createRow( data ) {
167 + const tr = document.createElement( 'tr' );
168 + tr.dataset.id = data.file_id;
169 + tr.dataset.sort = data.orders;
170 + tr.draggable = true;
171 +
172 + const deleteBtnText = this.elements.deleteText?.value || 'Delete';
173 +
174 + tr.innerHTML = `
175 + <td class="sort">
176 + <span class="dashicons dashicons-menu"></span> ${ this.escapeHtml( data.file_name ) }
177 + </td>
178 + <td>${ this.capitalizeFirstChar( data.method ) }</td>
179 + <td>
180 + <a href="javascript:void(0)" class="delete-material-row" data-id="${ data.file_id }">
181 + ${ deleteBtnText }
182 + </a>
183 + </td>
184 + `;
185 +
186 + return tr;
187 + }
188 +
189 + /**
190 + * Escape HTML to prevent XSS
191 + */
192 + escapeHtml( text ) {
193 + const div = document.createElement( 'div' );
194 + div.textContent = text;
195 + return div.innerHTML;
196 + }
197 +
198 + capitalizeFirstChar( str ) {
199 + return str.charAt( 0 ).toUpperCase() + str.substring( 1 );
200 + }
201 +
202 + /**
203 + * Bind all events with delegation for better performance
204 + */
205 + bindEvents() {
206 + if ( this.eventsBound ) return;
207 +
208 + const { addBtn, materialTab, saveBtn } = this.elements;
209 +
210 + // Create bound handlers for later removal
211 + this.boundHandlers.handleAddMaterial = () => this.handleAddMaterial();
212 + this.boundHandlers.handleChange = ( e ) => this.handleChange( e );
213 + this.boundHandlers.handleClick = ( e ) => this.handleClick( e );
214 + this.boundHandlers.handleSaveAll = () => this.handleSaveAll();
215 + this.boundHandlers.handleDelete = ( e ) => this.handleDelete( e );
216 +
217 + // Add material button
218 + addBtn?.addEventListener( 'click', this.boundHandlers.handleAddMaterial );
219 +
220 + // Use event delegation on materialTab
221 + if ( materialTab ) {
222 + materialTab.addEventListener( 'change', this.boundHandlers.handleChange );
223 + materialTab.addEventListener( 'click', this.boundHandlers.handleClick );
224 + }
225 +
226 + // Save all button
227 + saveBtn?.addEventListener( 'click', this.boundHandlers.handleSaveAll );
228 +
229 + // Delete material (use event delegation on container)
230 + this.container.addEventListener( 'click', this.boundHandlers.handleDelete );
231 +
232 + this.eventsBound = true;
233 + }
234 +
235 + /**
236 + * Handle add material button click
237 + */
238 + handleAddMaterial() {
239 + const { addBtn, groupContainer, groupTemplate } = this.elements;
240 +
241 + if ( ! addBtn || ! groupContainer || ! groupTemplate ) return;
242 +
243 + const canUploadData = parseInt( addBtn.getAttribute( 'can-upload' ) ) || 0;
244 + const groups = groupContainer.querySelectorAll( '.lp-material--group' ).length;
245 +
246 + if ( groups >= canUploadData ) return;
247 +
248 + groupContainer.insertAdjacentHTML( 'afterbegin', groupTemplate.innerHTML );
249 + }
250 +
251 + /**
252 + * Handle change events with delegation
253 + */
254 + handleChange( event ) {
255 + const target = event.target;
256 +
257 + // Switch between upload and external
258 + if ( target.classList.contains( 'lp-material--field-method' ) ) {
259 + this.handleMethodSwitch( target );
260 + }
261 +
262 + // File validation
263 + if ( target.classList.contains( 'lp-material--field-upload' ) ) {
264 + this.validateFile( target );
265 + }
266 + }
267 +
268 + /**
269 + * Handle method switch (upload/external)
270 + */
271 + handleMethodSwitch( target ) {
272 + const method = target.value;
273 + const { uploadTemplate, externalTemplate } = this.elements;
274 +
275 + if ( ! uploadTemplate || ! externalTemplate ) return;
276 +
277 + const group = target.closest( '.lp-material--group' );
278 + if ( ! group ) return;
279 +
280 + switch ( method ) {
281 + case 'upload':
282 + target.parentNode.insertAdjacentHTML( 'afterend', uploadTemplate.innerHTML );
283 + group.querySelector( '.lp-material--external-wrap' )?.remove();
284 + break;
285 + case 'external':
286 + target.parentNode.insertAdjacentHTML( 'afterend', externalTemplate.innerHTML );
287 + group.querySelector( '.lp-material--upload-wrap' )?.remove();
288 + break;
289 + }
290 + }
291 +
292 + /**
293 + * Validate uploaded file
294 + */
295 + validateFile( target ) {
296 + if ( ! target.value || ! target.files?.length ) {
297 + this.resetUploadLabel( target );
298 + return;
299 + }
300 +
301 + const file = target.files[ 0 ];
302 +
303 + if ( this.acceptFile.length > 0 && ! this.acceptFile.includes( file.type ) ) {
304 + alert( 'This file is not allowed! Please choose another file!' );
305 + target.value = '';
306 + this.resetUploadLabel( target );
307 + return;
308 + }
309 +
310 + if ( file.size > this.maxFileSize * 1024 * 1024 ) {
311 + alert(
312 + `This file size is greater than ${ this.maxFileSize }MB! Please choose another file!`
313 + );
314 + target.value = '';
315 + this.resetUploadLabel( target );
316 + return;
317 + }
318 +
319 + this.updateUploadLabel( target, file.name );
320 + }
321 +
322 + /**
323 + * Update upload label to show selected file name
324 + */
325 + updateUploadLabel( target, fileName ) {
326 + if ( ! fileName ) return;
327 +
328 + const uploadLabel = target
329 + .closest( '.lp-material--upload-wrap' )
330 + ?.querySelector( 'label' );
331 +
332 + if ( ! uploadLabel ) return;
333 +
334 + uploadLabel.classList.add( 'has-selected-file' );
335 + uploadLabel.setAttribute( 'data-file-name', fileName );
336 + uploadLabel.setAttribute( 'title', fileName );
337 + }
338 +
339 + /**
340 + * Reset upload label state
341 + */
342 + resetUploadLabel( target ) {
343 + const uploadLabel = target
344 + ?.closest( '.lp-material--upload-wrap' )
345 + ?.querySelector( 'label' );
346 +
347 + if ( ! uploadLabel ) return;
348 +
349 + uploadLabel.classList.remove( 'has-selected-file' );
350 + uploadLabel.removeAttribute( 'data-file-name' );
351 + uploadLabel.removeAttribute( 'title' );
352 + }
353 +
354 + /**
355 + * Handle click events with delegation
356 + */
357 + handleClick( event ) {
358 + const target = event.target;
359 +
360 + // Delete group
361 + if ( target.classList.contains( 'lp-material--delete' ) && target.nodeName === 'BUTTON' ) {
362 + target.closest( '.lp-material--group' )?.remove();
363 + return;
364 + }
365 +
366 + // Save single material
367 + if ( target.classList.contains( 'lp-material-save-field' ) ) {
368 + const material = target.closest( '.lp-material--group' );
369 + if ( material ) {
370 + this.saveMaterial( [ material ], true, target );
371 + }
372 + }
373 + }
374 +
375 + /**
376 + * Handle save all button
377 + */
378 + handleSaveAll() {
379 + const { groupContainer, saveBtn } = this.elements;
380 +
381 + if ( ! groupContainer ) return;
382 +
383 + const materials = Array.from( groupContainer.querySelectorAll( '.lp-material--group' ) );
384 + if ( materials.length > 0 ) {
385 + this.saveMaterial( materials, false, saveBtn );
386 + }
387 + }
388 +
389 + /**
390 + * Save material(s) with improved validation
391 + */
392 + async saveMaterial( materials, isSingle = false, targetBtn ) {
393 + if ( ! materials.length ) return;
394 +
395 + const materialData = [];
396 + const formData = new FormData();
397 + let isValid = true;
398 +
399 + for ( const ele of materials ) {
400 + const label = ele.querySelector( '.lp-material--field-title' )?.value;
401 + const method = ele.querySelector( '.lp-material--field-method' )?.value;
402 + const externalField = ele.querySelector( '.lp-material--field-external-link' );
403 + const uploadField = ele.querySelector( '.lp-material--field-upload' );
404 +
405 + if ( ! label ) {
406 + isValid = false;
407 + break;
408 + }
409 +
410 + let file = '';
411 + let link = '';
412 +
413 + switch ( method ) {
414 + case 'upload':
415 + if ( uploadField?.value && uploadField.files?.length > 0 ) {
416 + file = uploadField.files[ 0 ].name;
417 + formData.append( 'file[]', uploadField.files[ 0 ] );
418 + } else {
419 + isValid = false;
420 + }
421 + break;
422 + case 'external':
423 + link = externalField?.value || '';
424 + if ( ! link ) {
425 + isValid = false;
426 + }
427 + break;
428 + }
429 +
430 + if ( ! isValid ) break;
431 +
432 + materialData.push( { label, method, file, link } );
433 + }
434 +
435 + if ( ! isValid ) {
436 + alert( 'Enter file title, choose file or enter file link!' );
437 + return;
438 + }
439 +
440 + formData.append( 'data', JSON.stringify( materialData ) );
441 + targetBtn?.classList.add( 'loading' );
442 +
443 + try {
444 + const restUrl = this.getRestUrl();
445 + const url = `${ restUrl }lp/v1/material/item-materials/${ this.postID }`;
446 +
447 + const response = await fetch( url, {
448 + method: 'POST',
449 + headers: {
450 + 'X-WP-Nonce': this.getNonce(),
451 + },
452 + body: formData,
453 + } );
454 +
455 + if ( ! response.ok ) {
456 + throw new Error( `HTTP error! status: ${ response.status }` );
457 + }
458 +
459 + const text = await response.text();
460 + let res;
461 +
462 + try {
463 + res = JSON.parse( text );
464 + } catch ( e ) {
465 + console.error( 'Response is not valid JSON:', text.substring( 0, 200 ) );
466 + throw new Error( 'Server returned invalid response. Check console for details.' );
467 + }
468 +
469 + // Clear or remove materials
470 + if ( ! isSingle ) {
471 + materials.forEach( ( ele ) => {
472 + ele.querySelector( '.lp-material--field-title' ).value = '';
473 + const uploadField = ele.querySelector( '.lp-material--field-upload' );
474 + if ( uploadField ) {
475 + uploadField.value = '';
476 + this.resetUploadLabel( uploadField );
477 + }
478 + const externalField = ele.querySelector( '.lp-material--field-external-link' );
479 + if ( externalField ) externalField.value = '';
480 + } );
481 + } else {
482 + materials[ 0 ].remove();
483 + }
484 +
485 + const { message, data, status } = res;
486 + // console.log( message );
487 +
488 + if ( status === 'success' && data?.length > 0 ) {
489 + const { thead, tbody } = this.elements;
490 +
491 + thead?.classList.remove( 'hidden' );
492 +
493 + if ( tbody ) {
494 + const fragment = document.createDocumentFragment();
495 + data.forEach( ( row ) => {
496 + fragment.appendChild( this.createRow( row ) );
497 + } );
498 + tbody.appendChild( fragment );
499 + }
500 +
501 + this.updateCanUploadCount( -data.length );
502 + this.initSortable();
503 + }
504 + } catch ( err ) {
505 + console.error( 'Save material error:', err );
506 + alert( 'Error saving material: ' + err.message );
507 + } finally {
508 + targetBtn?.classList.remove( 'loading' );
509 + }
510 + }
511 +
512 + /**
513 + * Handle delete material
514 + */
515 + async handleDelete( e ) {
516 + const target = e.target;
517 +
518 + if ( ! target.classList.contains( 'delete-material-row' ) || target.nodeName !== 'A' ) {
519 + return;
520 + }
521 +
522 + e.preventDefault();
523 +
524 + const rowID = target.dataset.id;
525 + const message =
526 + this.elements.deleteMessage?.value || 'Are you sure you want to delete this material?';
527 +
528 + if ( ! confirm( message ) ) return;
529 +
530 + try {
531 + const restUrl = this.getRestUrl();
532 + const url = `${ restUrl }lp/v1/material/${ rowID }`;
533 +
534 + const response = await fetch( url, {
535 + method: 'DELETE',
536 + headers: {
537 + 'X-WP-Nonce': this.getNonce(),
538 + 'Content-Type': 'application/json',
539 + },
540 + body: JSON.stringify( {
541 + item_id: this.postID,
542 + } ),
543 + } );
544 +
545 + const res = await response.json();
546 +
547 + if ( res.status !== 200 || ! res.delete ) {
548 + alert( res.message );
549 + } else {
550 + target.closest( 'tr' )?.remove();
551 + this.updateCanUploadCount( 1 );
552 + }
553 + } catch ( err ) {
554 + console.error( 'Delete material error:', err );
555 + alert( 'Error deleting material: ' + err.message );
556 + }
557 + }
558 +
559 + /**
560 + * Update can upload count
561 + */
562 + updateCanUploadCount( delta ) {
563 + const { canUpload, addBtn } = this.elements;
564 +
565 + if ( canUpload && addBtn ) {
566 + const newCount = parseInt( canUpload.textContent ) + delta;
567 + canUpload.textContent = newCount;
568 + addBtn.setAttribute( 'can-upload', newCount );
569 + }
570 + }
571 +
572 + /**
573 + * Initialize native drag & drop sortable (no jQuery)
574 + */
575 + initSortable() {
576 + const { tbody } = this.elements;
577 +
578 + if ( ! tbody ) return;
579 +
580 + // Remove existing listeners
581 + this.destroySortable();
582 +
583 + const rows = tbody.querySelectorAll( 'tr' );
584 +
585 + // Create bound handlers
586 + this.boundHandlers.handleDragStart = ( e ) => this.handleDragStart( e );
587 + this.boundHandlers.handleDragOver = ( e ) => this.handleDragOver( e );
588 + this.boundHandlers.handleDrop = ( e ) => this.handleDrop( e );
589 + this.boundHandlers.handleDragEnd = () => this.handleDragEnd();
590 +
591 + rows.forEach( ( row ) => {
592 + row.draggable = true;
593 + row.addEventListener( 'dragstart', this.boundHandlers.handleDragStart );
594 + row.addEventListener( 'dragover', this.boundHandlers.handleDragOver );
595 + row.addEventListener( 'drop', this.boundHandlers.handleDrop );
596 + row.addEventListener( 'dragend', this.boundHandlers.handleDragEnd );
597 + } );
598 +
599 + this.sortable = { tbody, rows };
600 + }
601 +
602 + handleDragStart( e ) {
603 + this.draggedElement = e.currentTarget;
604 + e.currentTarget.style.opacity = '0.4';
605 + e.dataTransfer.effectAllowed = 'move';
606 + e.dataTransfer.setData( 'text/html', e.currentTarget.innerHTML );
607 + }
608 +
609 + handleDragOver( e ) {
610 + if ( e.preventDefault ) {
611 + e.preventDefault();
612 + }
613 + e.dataTransfer.dropEffect = 'move';
614 +
615 + const target = e.currentTarget;
616 + if ( this.draggedElement !== target ) {
617 + const rect = target.getBoundingClientRect();
618 + const next = ( e.clientY - rect.top ) / ( rect.bottom - rect.top ) > 0.5;
619 + target.parentNode.insertBefore( this.draggedElement, next ? target.nextSibling : target );
620 + }
621 +
622 + return false;
623 + }
624 +
625 + handleDrop( e ) {
626 + if ( e.stopPropagation ) {
627 + e.stopPropagation();
628 + }
629 + return false;
630 + }
631 +
632 + handleDragEnd() {
633 + this.draggedElement.style.opacity = '1';
634 + this.draggedElement = null;
635 +
636 + // Update sort order after drag ends
637 + this.updateSort();
638 + }
639 +
640 + /**
641 + * Update sort order
642 + */
643 + async updateSort() {
644 + const { tbody } = this.elements;
645 + if ( ! tbody ) return;
646 +
647 + const items = tbody.querySelectorAll( 'tr' );
648 + const data = Array.from( items ).map( ( item, index ) => {
649 + item.dataset.sort = index + 1;
650 + return {
651 + file_id: parseInt( item.dataset.id ),
652 + orders: index + 1,
653 + };
654 + } );
655 +
656 + try {
657 + const restUrl = this.getRestUrl();
658 + const url = `${ restUrl }lp/v1/material/item-materials/${ this.postID }`;
659 +
660 + const response = await fetch( url, {
661 + method: 'PUT',
662 + headers: {
663 + 'X-WP-Nonce': this.getNonce(),
664 + 'Content-Type': 'application/json',
665 + },
666 + body: JSON.stringify( {
667 + sort_arr: JSON.stringify( data ),
668 + } ),
669 + } );
670 +
671 + const res = await response.json();
672 +
673 + if ( res.status !== 200 ) {
674 + console.error( 'Sort table fail.' );
675 + }
676 + } catch ( err ) {
677 + console.error( 'Update sort error:', err );
678 + }
679 + }
680 +
681 + /**
682 + * Helper methods for REST URL and nonce
683 + */
684 + getRestUrl() {
685 + return window.lpGlobalSettings?.rest || window.lpData?.lp_rest_url || '/wp-json/';
686 + }
687 +
688 + getNonce() {
689 + return window.lpGlobalSettings?.nonce || window.lpData?.nonce || '';
690 + }
691 +
692 + /**
693 + * Destroy sortable
694 + */
695 + destroySortable() {
696 + if ( ! this.sortable ) return;
697 +
698 + const { rows } = this.sortable;
699 + rows?.forEach( ( row ) => {
700 + row.removeEventListener( 'dragstart', this.boundHandlers.handleDragStart );
701 + row.removeEventListener( 'dragover', this.boundHandlers.handleDragOver );
702 + row.removeEventListener( 'drop', this.boundHandlers.handleDrop );
703 + row.removeEventListener( 'dragend', this.boundHandlers.handleDragEnd );
704 + } );
705 +
706 + this.sortable = null;
707 + }
708 +
709 + /**
710 + * Destroy instance and cleanup
711 + */
712 + destroy() {
713 + const { addBtn, materialTab, saveBtn } = this.elements;
714 +
715 + // Remove event listeners
716 + if ( addBtn ) {
717 + addBtn.removeEventListener( 'click', this.boundHandlers.handleAddMaterial );
718 + }
719 +
720 + if ( materialTab ) {
721 + materialTab.removeEventListener( 'change', this.boundHandlers.handleChange );
722 + materialTab.removeEventListener( 'click', this.boundHandlers.handleClick );
723 + }
724 +
725 + if ( saveBtn ) {
726 + saveBtn.removeEventListener( 'click', this.boundHandlers.handleSaveAll );
727 + }
728 +
729 + if ( this.container ) {
730 + this.container.removeEventListener( 'click', this.boundHandlers.handleDelete );
731 + }
732 +
733 + // Destroy sortable
734 + this.destroySortable();
735 +
736 + // Reset bound handlers
737 + this.boundHandlers = {
738 + handleAddMaterial: null,
739 + handleChange: null,
740 + handleClick: null,
741 + handleSaveAll: null,
742 + handleDelete: null,
743 + handleDragStart: null,
744 + handleDragOver: null,
745 + handleDrop: null,
746 + handleDragEnd: null,
747 + };
748 +
749 + // Clear cached elements
750 + this.elements = {};
751 +
752 + this.initialized = false;
753 + this.eventsBound = false;
754 + }
755 +}
756 +
757 +export default BuilderMaterial;