PluginProbe
Echo Knowledge Base – Documentation, FAQs, Chat & Smart Search / trunk
Echo Knowledge Base – Documentation, FAQs, Chat & Smart Search vtrunk
17.214.0 17.213.1 17.212.0 17.211.0 17.210.0 17.200.0 11.0.0 12.0.0 13.10.0 14.0.0 15.0.0 16.011.0 16.20.0 17.0.1 17.0.4 17.1.0 17.1.2 17.111.0 17.3.0 17.4.0 trunk
echo-knowledge-base / js / admin-setup-steps.js

admin-setup-steps.js in Echo Knowledge Base – Documentation, FAQs, Chat & Smart Search trunk, at js/admin-setup-steps.js

486 lines 14.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * KB Setup Steps - JavaScript for Help Resources page
3 */
4 jQuery( document ).ready( function( $ ) {
5 'use strict';
6
7 const setupSteps = {
8
9 init: function() {
10 this.bindEvents();
11 },
12
13 bindEvents: function() {
14 // Done button click
15 $( document ).on( 'click', '.epkb-setup-steps__btn--done', this.handleDoneClick.bind( this ) );
16
17 // Later button click
18 $( document ).on( 'click', '.epkb-setup-steps__btn--later', this.handleLaterClick.bind( this ) );
19
20 // Complete later step from hidden list
21 $( document ).on( 'click', '.epkb-setup-steps__btn--complete-later', this.handleCompleteLaterClick.bind( this ) );
22
23 // Restore button click
24 $( document ).on( 'click', '.epkb-setup-steps__btn--restore', this.handleRestoreClick.bind( this ) );
25
26 // Toggle completed steps
27 $( document ).on( 'click', '.epkb-setup-steps__toggle-btn', this.toggleCompletedSteps.bind( this ) );
28
29 // Reset button click
30 $( document ).on( 'click', '.epkb-setup-steps__btn--reset', this.handleResetClick.bind( this ) );
31
32 // Inline pointer button click
33 $( document ).on( 'click', '.epkb-setup-steps__btn--inline-pointer', this.handleInlinePointerClick.bind( this ) );
34
35 // Learn More dialog button click
36 $( document ).on( 'click', 'button.epkb-setup-steps__btn--learn-more', this.handleLearnMoreClick.bind( this ) );
37
38 // Learn More dialog close handlers
39 $( document ).on( 'click', '.epkb-setup-steps-dialog__close, .epkb-setup-steps-dialog-overlay', this.closeLearnMoreDialog.bind( this ) );
40 $( document ).on( 'keydown', this.handleDialogKeydown.bind( this ) );
41
42 // Show celebration fireworks if all complete
43 this.checkForCelebration();
44 },
45
46 handleDoneClick: function( e ) {
47 e.preventDefault();
48 this.submitStepAction( $( e.currentTarget ), 'done' );
49 },
50
51 handleLaterClick: function( e ) {
52 e.preventDefault();
53 this.submitStepAction( $( e.currentTarget ), 'later' );
54 },
55
56 handleCompleteLaterClick: function( e ) {
57 e.preventDefault();
58 this.submitStepAction( $( e.currentTarget ), 'done' );
59 },
60
61 submitStepAction: function( $btn, mode ) {
62 const $item = $btn.closest( '.epkb-setup-steps__item' );
63 const $container = $btn.closest( '.epkb-setup-steps' );
64 const stepKey = $btn.data( 'step-key' );
65 const kbId = parseInt( $container.data( 'kb-id' ), 10 );
66 const actionMode = mode === 'later' ? 'later' : 'done';
67
68 if ( ! stepKey ) {
69 return;
70 }
71
72 const $actionButtons = $item.find( '.epkb-setup-steps__btn--done, .epkb-setup-steps__btn--later, .epkb-setup-steps__btn--complete-later' );
73 const $restoreBtn = $item.find( '.epkb-setup-steps__btn--restore' );
74
75 $actionButtons.prop( 'disabled', true );
76 $restoreBtn.prop( 'disabled', true );
77 $btn.addClass( 'epkb-loading' );
78
79 const data = {
80 action: 'epkb_mark_setup_step_done',
81 step_key: stepKey,
82 mode: actionMode,
83 _wpnonce_epkb_ajax_action: epkb_vars.nonce
84 };
85
86 if ( ! isNaN( kbId ) && kbId > 0 ) {
87 data.kb_id = kbId;
88 }
89
90 $.ajax( {
91 url: ajaxurl,
92 type: 'POST',
93 data: data,
94 success: function( response ) {
95 if ( response.success ) {
96 location.reload();
97 return;
98 }
99
100 $actionButtons.prop( 'disabled', false );
101 $restoreBtn.prop( 'disabled', false );
102 $btn.removeClass( 'epkb-loading' );
103 console.error( 'Error marking step:', response.data );
104 },
105 error: function( xhr, status, error ) {
106 $actionButtons.prop( 'disabled', false );
107 $restoreBtn.prop( 'disabled', false );
108 $btn.removeClass( 'epkb-loading' );
109 console.error( 'AJAX error:', error );
110 }
111 } );
112 },
113
114 checkForCelebration: function() {
115 const $celebration = $( '.epkb-setup-steps__celebration' );
116 if ( $celebration.length ) {
117 // Trigger celebration fireworks
118 this.showCelebrationFireworks();
119 }
120 },
121
122 showCelebrationFireworks: function() {
123 const $celebration = $( '.epkb-setup-steps__celebration' );
124 if ( ! $celebration.length ) return;
125
126 const offset = $celebration.offset();
127 const width = $celebration.outerWidth();
128 const height = $celebration.outerHeight();
129 const colors = [ '#fbbf24', '#f59e0b', '#d97706', '#22c55e', '#3b82f6', '#8b5cf6', '#ec4899' ];
130
131 // Create container
132 let $container = $( '.epkb-fireworks-container' );
133 if ( ! $container.length ) {
134 $container = $( '<div class="epkb-fireworks-container"></div>' ).appendTo( 'body' );
135 }
136
137 // Launch fireworks in waves
138 const launchWave = function( delay ) {
139 setTimeout( function() {
140 for ( let i = 0; i < 25; i++ ) {
141 const color = colors[ Math.floor( Math.random() * colors.length ) ];
142 const size = Math.random() * 10 + 6;
143 const left = offset.left + Math.random() * width;
144 const top = offset.top + ( height / 2 );
145 const spread = ( Math.random() - 0.5 ) * 400;
146 const particleDelay = Math.random() * 0.2;
147 const duration = Math.random() * 0.5 + 1.2;
148
149 const $particle = $( '<div class="epkb-firework"></div>' )
150 .css( {
151 left: left + 'px',
152 top: top + 'px',
153 width: size + 'px',
154 height: size + 'px',
155 backgroundColor: color,
156 animationDelay: particleDelay + 's',
157 animationDuration: duration + 's',
158 '--spread': spread + 'px'
159 } );
160
161 $container.append( $particle );
162
163 setTimeout( function() {
164 $particle.remove();
165 }, ( particleDelay + duration ) * 1000 + 100 );
166 }
167 }, delay );
168 };
169
170 // 3 waves of fireworks
171 launchWave( 0 );
172 launchWave( 400 );
173 launchWave( 800 );
174
175 // Clean up container
176 setTimeout( function() {
177 if ( $container.children().length === 0 ) {
178 $container.remove();
179 }
180 }, 3000 );
181 },
182
183 handleRestoreClick: function( e ) {
184 e.preventDefault();
185
186 const $btn = $( e.currentTarget );
187 const $container = $btn.closest( '.epkb-setup-steps' );
188 const stepKey = $btn.data( 'step-key' );
189 const kbId = parseInt( $container.data( 'kb-id' ), 10 );
190
191 if ( ! stepKey ) {
192 return;
193 }
194
195 $btn.prop( 'disabled', true ).addClass( 'epkb-loading' );
196
197 $.ajax( {
198 url: ajaxurl,
199 type: 'POST',
200 data: Object.assign( {
201 action: 'epkb_restore_setup_step',
202 step_key: stepKey,
203 _wpnonce_epkb_ajax_action: epkb_vars.nonce
204 }, ( ! isNaN( kbId ) && kbId > 0 ) ? { kb_id: kbId } : {} ),
205 success: function( response ) {
206 if ( response.success ) {
207 // Reload the page to refresh the steps
208 location.reload();
209 } else {
210 $btn.prop( 'disabled', false ).removeClass( 'epkb-loading' );
211 console.error( 'Error restoring step:', response.data );
212 }
213 },
214 error: function( xhr, status, error ) {
215 $btn.prop( 'disabled', false ).removeClass( 'epkb-loading' );
216 console.error( 'AJAX error:', error );
217 }
218 } );
219 },
220
221 toggleCompletedSteps: function( e ) {
222 e.preventDefault();
223
224 const $btn = $( e.currentTarget );
225 const $list = $( '.epkb-setup-steps__completed-list' );
226 const completedCount = $list.find( '.epkb-setup-steps__item' ).length;
227
228 // Use toggle instead of slideToggle to avoid issues with column-count layout
229 if ( $list.is( ':visible' ) ) {
230 $list.hide();
231 $btn.html(
232 '<span class="epkbfa epkbfa-chevron-down"></span> ' +
233 ( epkb_vars.show_completed_text || 'Show Completed Steps' ) +
234 ' (' + completedCount + ')'
235 );
236 } else {
237 $list.show();
238 $btn.html(
239 '<span class="epkbfa epkbfa-chevron-up"></span> ' +
240 ( epkb_vars.hide_completed_text || 'Hide Completed Steps' ) +
241 ' (' + completedCount + ')'
242 );
243 }
244 },
245
246 handleResetClick: function( e ) {
247 e.preventDefault();
248
249 const $btn = $( e.currentTarget );
250
251 $btn.prop( 'disabled', true ).addClass( 'epkb-loading' );
252
253 $.ajax( {
254 url: ajaxurl,
255 type: 'POST',
256 data: {
257 action: 'epkb_reset_setup_steps',
258 _wpnonce_epkb_ajax_action: epkb_vars.nonce
259 },
260 success: function( response ) {
261 if ( response.success ) {
262 // Reload page to show fresh state
263 location.reload();
264 } else {
265 $btn.prop( 'disabled', false ).removeClass( 'epkb-loading' );
266 console.error( 'Error resetting steps:', response.data );
267 }
268 },
269 error: function( xhr, status, error ) {
270 $btn.prop( 'disabled', false ).removeClass( 'epkb-loading' );
271 console.error( 'AJAX error:', error );
272 }
273 } );
274 },
275
276 handleInlinePointerClick: function( e ) {
277 e.preventDefault();
278
279 const $btn = $( e.currentTarget );
280 const targetElement = $btn.data( 'target-element' );
281 const pointerTitle = $btn.data( 'pointer-title' );
282 const pointerContent = $btn.data( 'pointer-content' );
283
284 if ( ! targetElement ) {
285 return;
286 }
287
288 // Find the target element
289 const $target = this.findTarget( targetElement );
290 if ( ! $target || ! $target.length ) {
291 console.warn( 'Target element not found:', targetElement );
292 return;
293 }
294
295 // Check if this is an admin menu item
296 const isAdminMenuItem = $target.closest( '#adminmenu' ).length > 0;
297
298 // For admin menu items, expand the submenu to ensure visibility
299 if ( isAdminMenuItem ) {
300 const $menuItem = $target.closest( 'li.menu-top' );
301 if ( $menuItem.length && ! $menuItem.hasClass( 'wp-has-current-submenu' ) ) {
302 $menuItem.addClass( 'opensub' ).find( '.wp-submenu' ).show();
303 }
304 }
305
306 // Close any existing pointers
307 $( '.wp-pointer' ).remove();
308
309 // Show the pointer
310 const content = '<h3>' + pointerTitle + '</h3><p>' + pointerContent + '</p>';
311 $target.pointer( {
312 content: content,
313 position: {
314 edge: isAdminMenuItem ? 'left' : 'top',
315 align: 'center'
316 },
317 pointerClass: 'epkb-setup-pointer',
318 buttons: function( event, t ) {
319 const $closeBtn = $( '<button type="button" class="button button-primary epkb-pointer-next">' + ( epkb_vars.got_it_text || 'Got it!' ) + '</button>' );
320 $closeBtn.on( 'click', function() {
321 t.element.pointer( 'close' );
322 } );
323 return $closeBtn;
324 }
325 } ).pointer( 'open' );
326 },
327
328 findTarget: function( selector ) {
329 // Handle comma-separated selectors (find first match)
330 const selectors = selector.split( ',' );
331 for ( let i = 0; i < selectors.length; i++ ) {
332 const sel = selectors[i].trim();
333 const $el = $( sel );
334 if ( $el.length ) {
335 const $visible = $el.filter( ':visible' );
336 if ( $visible.length ) {
337 return $visible.first();
338 }
339 return $el.first();
340 }
341 }
342 return null;
343 },
344
345 handleLearnMoreClick: function( e ) {
346 e.preventDefault();
347
348 const $btn = $( e.currentTarget );
349 const title = $btn.data( 'title' );
350 const description = $btn.data( 'description' );
351 const docUrl = $btn.data( 'doc-url' );
352 const videoUrl = $btn.data( 'video-url' );
353 const askAi = $btn.data( 'ask-ai' );
354 const actionUrl = $btn.data( 'action-url' );
355 const actionText = $btn.data( 'action-text' );
356
357 const $dialog = $( '.epkb-setup-steps-dialog' );
358 const $overlay = $( '.epkb-setup-steps-dialog-overlay' );
359
360 // Set dialog content
361 $dialog.find( '.epkb-setup-steps-dialog__title' ).text( title );
362 $dialog.find( '.epkb-setup-steps-dialog__description' ).html( this.formatDescription( description ) );
363
364 // Show/hide footer buttons - simple display toggle, no animations
365 const $footer = $dialog.find( '.epkb-setup-steps-dialog__footer' );
366 const $actionBtn = $dialog.find( '.epkb-setup-steps-dialog__btn--action' );
367 const $docBtn = $dialog.find( '.epkb-setup-steps-dialog__btn--doc' );
368 const $videoBtn = $dialog.find( '.epkb-setup-steps-dialog__btn--video' );
369 const $askAiBtn = $dialog.find( '.epkb-setup-steps-dialog__btn--ask-ai' );
370
371 // Reset all buttons to hidden
372 $actionBtn[0].style.display = 'none';
373 $docBtn[0].style.display = 'none';
374 $videoBtn[0].style.display = 'none';
375 $askAiBtn[0].style.display = 'none';
376
377 // Show only needed buttons
378 if ( actionUrl && actionText ) {
379 $actionBtn.attr( 'href', actionUrl ).find( '.epkb-setup-steps-dialog__btn-text' ).text( actionText );
380 $actionBtn[0].style.display = 'inline-flex';
381 }
382
383 if ( docUrl ) {
384 $docBtn.attr( 'href', docUrl );
385 $docBtn[0].style.display = 'inline-flex';
386 }
387
388 if ( videoUrl ) {
389 $videoBtn.attr( 'href', videoUrl );
390 $videoBtn[0].style.display = 'inline-flex';
391 }
392
393 if ( askAi ) {
394 $askAiBtn[0].style.display = 'inline-flex';
395 }
396
397 // Show/hide footer
398 const hasButtons = actionUrl || docUrl || videoUrl || askAi;
399 $footer[0].style.display = hasButtons ? 'flex' : 'none';
400
401 // Show dialog
402 $overlay.addClass( 'epkb-setup-steps-dialog-overlay--active' );
403 $dialog.addClass( 'epkb-setup-steps-dialog--active' );
404
405 // Focus close button for accessibility
406 $dialog.find( '.epkb-setup-steps-dialog__close' ).focus();
407 },
408
409 closeLearnMoreDialog: function( e ) {
410 if ( e ) {
411 e.preventDefault();
412 }
413 $( '.epkb-setup-steps-dialog-overlay' ).removeClass( 'epkb-setup-steps-dialog-overlay--active' );
414 $( '.epkb-setup-steps-dialog' ).removeClass( 'epkb-setup-steps-dialog--active' );
415 },
416
417 handleDialogKeydown: function( e ) {
418 // Close dialog on Escape key
419 if ( e.key === 'Escape' && $( '.epkb-setup-steps-dialog--active' ).length ) {
420 this.closeLearnMoreDialog();
421 }
422 },
423
424 /**
425 * Format description text to HTML with proper lists
426 * Converts: "Text: • item1 • item2" to "<p>Text:</p><ul><li>item1</li><li>item2</li></ul>"
427 * Converts: "Text: 1) step1 2) step2" to "<p>Text:</p><ol><li>step1</li><li>step2</li></ol>"
428 */
429 formatDescription: function( text ) {
430 if ( ! text ) {
431 return '';
432 }
433
434 // Escape HTML to prevent XSS
435 const escapeHtml = function( str ) {
436 const div = document.createElement( 'div' );
437 div.textContent = str;
438 return div.innerHTML;
439 };
440
441 text = escapeHtml( text );
442
443 // Check for numbered steps pattern: "1) ... 2) ... 3) ..."
444 const numberedPattern = /(\d+)\)\s+/g;
445 if ( numberedPattern.test( text ) ) {
446 // Split by numbered pattern
447 const parts = text.split( /\d+\)\s+/ );
448 const intro = parts[0].trim();
449 const items = parts.slice( 1 ).map( item => item.trim() ).filter( item => item );
450
451 if ( items.length > 0 ) {
452 let html = intro ? '<p>' + intro + '</p>' : '';
453 html += '<ol>';
454 items.forEach( function( item ) {
455 html += '<li>' + item + '</li>';
456 } );
457 html += '</ol>';
458 return html;
459 }
460 }
461
462 // Check for bullet pattern: "• item1 • item2"
463 if ( text.includes( '' ) ) {
464 const parts = text.split( '' );
465 const intro = parts[0].trim();
466 const items = parts.slice( 1 ).map( item => item.trim() ).filter( item => item );
467
468 if ( items.length > 0 ) {
469 let html = intro ? '<p>' + intro + '</p>' : '';
470 html += '<ul>';
471 items.forEach( function( item ) {
472 html += '<li>' + item + '</li>';
473 } );
474 html += '</ul>';
475 return html;
476 }
477 }
478
479 // No special formatting, return as paragraph
480 return '<p>' + text + '</p>';
481 }
482 };
483
484 setupSteps.init();
485 } );
486