1/** cm-alpha/ui-modals/js/scripts.js 2 * CM UI Modals JavaScript 3 */ 4 5jQuery(document).ready(function($) { 6 window.cmActiveModals = new Set(); 7 8 // Track custom modal containers 9 window.cmModalContainers = {}; 10 11 // Global modal close callback system 12 window.cmModalCloseCallbacks = []; 13 window.cmModalSpecificCallbacks = {}; 14 15 // Global variables for YouTube size tracking 16 let hasCustomYouTubeSize = false; 17 let youtubeCustomDimensions = null; 18 19 // Function to process include files (JS, CSS, PHP) 20 function processIncludeFiles(modalId, settings) { 21 const includeFiles = settings.include_files; 22 if (!includeFiles) return; 23 24 const $contentContainer = $(`#${modalId}-container .cm-modal-content`); 25 26 // Parse comma-separated list of files 27 const files = includeFiles.split(',').map(file => file.trim()); 28 29 // Track loaded resources 30 const loadedResources = { 31 css: [], 32 js: [], 33 php: [] 34 }; 35 36 // Process each file based on its extension 37 files.forEach(file => { 38 const extension = file.split('.').pop().toLowerCase(); 39 40 switch (extension) { 41 case 'css': 42 // Load CSS file 43 if (!loadedResources.css.includes(file)) { 44 const linkElement = document.createElement('link'); 45 linkElement.rel = 'stylesheet'; 46 linkElement.href = file.startsWith('/') ? file : ajaxurl.replace('admin-ajax.php', '') + file; 47 linkElement.className = `cm-modal-${modalId}-css`; 48 document.head.appendChild(linkElement); 49 loadedResources.css.push(file); 50 } 51 break; 52 53 case 'js': 54 // Load JavaScript file 55 if (!loadedResources.js.includes(file)) { 56 const scriptElement = document.createElement('script'); 57 scriptElement.src = file.startsWith('/') ? file : ajaxurl.replace('admin-ajax.php', '') + file; 58 scriptElement.className = `cm-modal-${modalId}-js`; 59 document.body.appendChild(scriptElement); 60 loadedResources.js.push(file); 61 } 62 break; 63 64 case 'php': 65 // Load PHP file via AJAX 66 if (!loadedResources.php.includes(file)) { 67 loadedResources.php.push(file); 68 69 // Make AJAX request to load PHP content 70 $.ajax({ 71 url: typeof cmModalsConfig !== 'undefined' ? cmModalsConfig.ajaxurl : ajaxurl, 72 type: 'POST', 73 data: { 74 action: 'cm_load_modal_include', 75 nonce: typeof cmModalsConfig !== 'undefined' ? cmModalsConfig.nonce : '', 76 file: file, 77 modal_id: modalId, 78 settings: JSON.stringify(settings) 79 }, 80 success: function(response) { 81 // Replace loading indicator in the included content section 82 $contentContainer.find('.cm-modal-included-content').html(response); 83 84 // Trigger an event so other scripts can initialize 85 $(document).trigger('cm_modal_content_loaded', [modalId, file, settings]); 86 87 // Handle specific case for edit-media-dialog.php 88 if (file.includes('edit-media-dialog.php')) { 89 // Re-initialize UI controls if CMColorPicker exists 90 if (typeof window.CMColorPicker !== 'undefined' && typeof window.CMColorPicker.init === 'function') { 91 window.CMColorPicker.init('.cm-color-picker'); 92 } 93 } 94 }, 95 error: function(xhr, status, error) { 96 console.error(`Failed to load include file: ${file}`, error); 97 $contentContainer.find('.cm-modal-included-content').html(`<div class="cm-modal-error">Error loading content: ${error}</div>`); 98 99 // If this was the default include file, fall back to direct content 100 // No need for special default handling anymore since content is already displayed 101 } 102 }); 103 } 104 break; 105 106 default: 107 // Assume PHP file if no extension or unknown 108 if (!loadedResources.php.includes(file)) { 109 loadedResources.php.push(file); 110 111 // Make AJAX request to load PHP content (assuming it's a PHP file) 112 $.ajax({ 113 url: typeof cmModalsConfig !== 'undefined' ? cmModalsConfig.ajaxurl : ajaxurl, 114 type: 'POST', 115 data: { 116 action: 'cm_load_modal_include', 117 nonce: typeof cmModalsConfig !== 'undefined' ? cmModalsConfig.nonce : '', 118 file: file, 119 modal_id: modalId, 120 settings: JSON.stringify(settings) 121 }, 122 success: function(response) { 123 // Replace loading indicator in the included content section 124 $contentContainer.find('.cm-modal-included-content').html(response); 125 126 // Trigger an event so other scripts can initialize 127 $(document).trigger('cm_modal_content_loaded', [modalId, file, settings]); 128 129 // Handle specific case for edit-media-dialog.php 130 if (file.includes('edit-media-dialog.php')) { 131 // Re-initialize UI controls if CMColorPicker exists 132 if (typeof window.CMColorPicker !== 'undefined' && typeof window.CMColorPicker.init === 'function') { 133 window.CMColorPicker.init('.cm-color-picker'); 134 } 135 } 136 }, 137 error: function(xhr, status, error) { 138 console.error(`Failed to load include file: ${file}`, error); 139 $contentContainer.find('.cm-modal-included-content').html(`<div class="cm-modal-error">Error loading content: ${error}</div>`); 140 141 // Fall back to direct content 142 $contentContainer.find('.cm-modal-included-content').html(`<div class="cm-modal-fallback">Error loading include file</div>`); 143 } 144 }); 145 } 146 break; 147 } 148 }); 149 } 150 151 // Modal display function 152 // Helper function to create a close button for modals 153 function createCloseButton(modalId, showCloseButton) { 154 if (!showCloseButton) { 155 return ''; 156 } 157 return `<div class="cm-modal-close" data-modal-id="${modalId}">Ã</div>`; 158 } 159 160 // Create or get the modal container 161 function getModalContainer(modalId) { 162 // If a modalId is provided, create/get a dedicated container 163 if (modalId) { 164 // Create a container ID based on the modal ID 165 // If modalId already starts with 'cm-modal-', use just the numeric part 166 const baseId = modalId.startsWith('cm-modal-') ? modalId.substring(9) : modalId; 167 const containerId = `cm-modal-wrapper-${baseId}`; 168 169 // Check if this container already exists 170 let $container = $(`#${containerId}`); 171 172 if ($container.length === 0) { 173 $('body').append(`<div id="${containerId}" class="cm-modal-container-wrapper"></div>`); 174 $container = $(`#${containerId}`); 175 176 // Store in tracking object with the original modalId 177 window.cmModalContainers[modalId] = { 178 element: $container, 179 containerId: containerId, 180 baseId: baseId 181 }; 182 183 // Check if default container exists and is empty - clean it up if so 184 const $defaultContainer = $('#cm-modal-dynamic-container'); 185 if ($defaultContainer.length > 0 && $defaultContainer.children().length === 0) { 186 $defaultContainer.remove(); 187 } 188 } 189 190 return $container; 191 } 192 193 // We should never reach here with our new approach, but keep as fallback
194 console.warn('No modalId provided for container creation - this should not happen'); 195 let $container = $('#cm-modal-dynamic-container'); 196 197 if ($container.length === 0) { 198 $('body').append('<div id="cm-modal-dynamic-container" class="cm-modal-container-wrapper"></div>'); 199 $container = $('#cm-modal-dynamic-container'); 200 } 201 202 return $container; 203 } 204 205 // Helper function to extract YouTube video ID from various URL formats 206 function getYouTubeVideoId(url) { 207 208 if (!url) return null; 209 210 // Clean URL if it contains any HTML tags 211 let cleanUrl = url; 212 if (cleanUrl.includes('<') && cleanUrl.includes('>')) { 213 // Try to extract just the URL part 214 const tempDiv = document.createElement('div'); 215 tempDiv.innerHTML = cleanUrl; 216 cleanUrl = tempDiv.textContent.trim(); 217 } 218 219 // Try multiple extraction methods 220 221 // Method 1: Using regex to match various YouTube URL formats 222 const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; 223 const match = cleanUrl.match(regExp); 224 225 if (match && match[2] && match[2].length === 11) { 226 return match[2]; 227 } 228 229 // Method 2: Direct string manipulation for common formats 230 if (cleanUrl.includes('youtube.com/watch?v=')) { 231 const id = cleanUrl.split('youtube.com/watch?v=')[1].split(/[&#?]/)[0]; 232 if (id && id.length === 11) { 233 return id; 234 } 235 } 236 237 if (cleanUrl.includes('youtu.be/')) { 238 const id = cleanUrl.split('youtu.be/')[1].split(/[&#?]/)[0]; 239 if (id && id.length === 11) { 240 return id; 241 } 242 } 243 244 if (cleanUrl.includes('youtube.com/embed/')) { 245 const id = cleanUrl.split('youtube.com/embed/')[1].split(/[&#?]/)[0]; 246 if (id && id.length === 11) { 247 return id; 248 } 249 } 250 251 // Method 3: Check if the string itself is just an 11-character YouTube ID 252 if (cleanUrl.length === 11 && /^[a-zA-Z0-9_-]{11}$/.test(cleanUrl)) { 253 return cleanUrl; 254 } 255 256 return null; 257 } 258 259 // Function to extract custom dimensions from YouTube content 260 function extractYouTubeDimensions(content) { 261 let customDimensions = null; 262 let videoUrl = content; 263 264 // Handle HTML content first - extract the actual URL from HTML if needed 265 if (typeof content === 'string' && (content.includes('<div') || content.includes('<span'))) { 266 // Create a temporary element to parse the HTML 267 const tempDiv = document.createElement('div'); 268 tempDiv.innerHTML = content; 269 270 // Try to find the URL in the inner text of the div 271 const innerText = tempDiv.textContent.trim(); 272 if (innerText.includes('youtube.com') || innerText.includes('youtu.be')) { 273 videoUrl = innerText; 274 } 275 276 // Also try to find any divs with style attributes for dimensions 277 const styledDiv = tempDiv.querySelector('div[style]'); 278 if (styledDiv) { 279 const styleAttr = styledDiv.getAttribute('style'); 280 const innerContent = styledDiv.textContent.trim(); 281 282 // If the inner content is a YouTube URL, use that 283 if (innerContent.includes('youtube.com') || innerContent.includes('youtu.be')) { 284 videoUrl = innerContent; 285 } 286 287 // Extract width and height from style attribute 288 const widthMatch = styleAttr ? styleAttr.match(/width\s*:\s*([^;]+)/i) : null; 289 const heightMatch = styleAttr ? styleAttr.match(/height\s*:\s*([^;]+)/i) : null; 290 291 if (widthMatch) { 292 customDimensions = customDimensions || {}; 293 customDimensions.width = widthMatch[1].trim(); 294 295 // Store numeric width if possible (for calculations) 296 const numericWidth = parseFloat(customDimensions.width); 297 if (!isNaN(numericWidth)) { 298 customDimensions.numericWidth = numericWidth; 299 customDimensions.unit = customDimensions.width.replace(/[\d.]+/, ''); 300 } 301 } 302 303 if (heightMatch) { 304 customDimensions = customDimensions || {}; 305 customDimensions.height = heightMatch[1].trim(); 306 307 // If height is 'auto', calculate based on 16:9 aspect ratio 308 if (heightMatch[1].trim() === 'auto' && customDimensions.numericWidth) { 309 const heightValue = customDimensions.numericWidth * 0.5625; // 16:9 aspect ratio 310 customDimensions.height = heightValue + customDimensions.unit; 311 } 312 } 313 } 314 } else if (typeof content === 'string') { 315 // If content is already a string but not HTML, just use it directly 316 videoUrl = content.trim(); 317 } 318 319 // As a fallback, also try a regex match for a div without using DOM parsing 320 if (typeof content === 'string' && content.includes('<div') && videoUrl === content) { 321 const divRegex = /<div[^>]*>(.*?)<\/div>/is; 322 const match = content.match(divRegex); 323 if (match && match[1] && (match[1].includes('youtube.com') || match[1].includes('youtu.be'))) { 324 videoUrl = match[1].trim(); 325 } 326 } 327 328 return { 329 videoUrl: videoUrl, 330 customDimensions: customDimensions 331 }; 332 } 333 334 window.cmDisplayModal = function(options) { 335 336 // Default options 337 // include_files: allows a comma separated list of files to be included in the modal 338 const defaults = { 339 type_modal: 'html', 340 type_alert: '', 341 heading: '', 342 heading_align: 'center', 343 heading_background_color: '#0000007A', 344 heading_color: '#d8d8d8', 345 heading_border: '', 346 heading_padding: '.2em .5em .2em .5em', 347 heading_text_align: 'center', 348 heading_styles: null, 349 content: '', 350 content_styles: null, 351 include_files: '', 352 php_include_file: '', // For backward compatibility 353 content_below_included_files: true, // Controls content position relative to included files 354 fadeInDuration: 0.5, 355 faderColor: 'rgba(0, 0, 0, 0.3)', 356 closeButton: true, 357 custom_id: null,
358 container_classes: null, 359 container_styles: null, 360 auto_play: null, 361 data_attributes: {} 362 }; 363 364 // Merges the options supplied over any existing defaults 365 const settings = $.extend({}, defaults, options); 366 367 // Validate type_modal against content and show error messages if mismatched 368 if (settings.type_modal && settings.content) { 369 const contentStr = String(settings.content).trim(); 370 const isYouTubeUrl = getYouTubeVideoId(contentStr) !== null; 371 372 if (settings.type_modal === 'html' && isYouTubeUrl) { 373 settings.content = `<span style="color:red;">You have provided modal_type=html but provided a youtube url ${contentStr}</span>`; 374 } else if (settings.type_modal === 'youtube' && !isYouTubeUrl) { 375 settings.content = '<span style="color:red;">You provided modal_type=youtube but then you did not provide a youtube url</span>'; 376 } 377 } 378 379 // Generate or use custom modal ID 380 let modalId; 381 if (settings.custom_id) { 382 modalId = settings.custom_id; 383 // Check if modal with this ID already exists 384 if ($(`#${modalId}`).length > 0 || window.cmActiveModals.has(modalId)) { 385 console.warn(`Modal with ID ${modalId} already exists. Will replace it.`); 386 closeModal(modalId); // Close existing modal with this ID 387 } 388 } else { 389 // Generate a random ID with the same format as toast alerts for consistency 390 modalId = 'cm-modal-' + Math.floor(Math.random() * 1000000); 391 } 392 393 // Create the data attributes HTML 394 let dataAttributesHtml = ''; 395 if (settings.data_attributes && typeof settings.data_attributes === 'object') { 396 Object.keys(settings.data_attributes).forEach(key => { 397 dataAttributesHtml += ` data-${key}="${settings.data_attributes[key]}"`; 398 }); 399 } 400 401 // Handle backward compatibility with php_include_file 402 if (settings.php_include_file && !settings.include_files) { 403 settings.include_files = settings.php_include_file; 404 } 405 406 // No default include file - if include_files is empty, we'll just use the content directly 407 408 // Find the highest z-index of existing modal faders 409 let highestZIndex = 9999; // Start with the CSS default 410 $('.cm-modal-fader').each(function() { 411 const zIndex = parseInt($(this).css('z-index'), 10); 412 if (!isNaN(zIndex) && zIndex > highestZIndex) { 413 highestZIndex = zIndex; 414 } 415 }); 416 417 // Increment for the new modal 418 const newZIndex = highestZIndex + 1; 419 420 // Create the modal HTML structure with the new z-index applied 421 const modalHTML = ` 422 <div class="cm-modal-fader" id="${modalId}-fader" style="background-color: ${settings.faderColor}; z-index: ${newZIndex};"> 423 <div class="cm-modal-container ${settings.container_
423classes || ''}" 424 id="${modalId}-container" 425 style="${settings.container_styles || ''}; z-index: ${newZIndex + 1}" 426 ${dataAttributesHtml}> 427 ${createCloseButton(modalId, settings.closeButton)} 428 <div class="cm-modal-header" style="background-color: ${settings.heading_background_color}; color: ${settings.heading_color}; border: ${settings.heading_border}; padding: ${settings.heading_padding}; text-align: ${settings.heading_align}; ${settings.heading_styles || ''}"> 429 ${settings.heading} 430 </div> 431 <div class="cm-modal-content" data-include-files="${settings.include_files}" data-modal-id="${modalId}"> 432 ${!settings.include_files || settings.content_below_included_files === false ? 433 `<div class="cm-modal-direct-content" style="${settings.content_styles || ''}">${settings.content}</div>` : ''} 434 <div class="cm-modal-included-content"> 435 ${settings.include_files ? '<div class="cm-modal-loading">Loading...</div>' : ''} 436 </div> 437 ${settings.include_files && settings.content_below_included_files === true && settings.content ? 438 `<div class="cm-modal-direct-content" style="${settings.content_styles || ''}">${settings.content}</div>` : ''} 439 </div> 440 </div> 441 </div> 442 `; 443 444 // Use the wrapper ID for the container 445 const wrapperAlertId = settings.custom_id || modalId; 446 const container = getModalContainer(wrapperAlertId); 447 448 // Append modal to container 449 container.append(modalHTML); 450 451 // Process include files if specified 452 if (settings.include_files) { 453 processIncludeFiles(modalId, settings); 454 } 455 456 // Get the modal elements 457 const $fader = $(`#${modalId}-fader`); 458 const $container = $(`#${modalId}-container`); 459 460 // Show the modal with optional fade 461 if (settings.fadeInDuration > 0) { 462 $fader.css('animation-duration', settings.fadeInDuration + 's'); 463 $container.css('animation-duration', settings.fadeInDuration + 's'); 464 465 $fader.addClass('cm-modal-fade-in').css('display', 'block'); 466 $container.addClass('cm-modal-fade-in').css('display', 'block'); 467 } else { 468 $fader.css('display', 'block'); 469 $container.css('display', 'block'); 470 } 471 472 // Add to active modals set for tracking 473 window.cmActiveModals.add(modalId); 474 475 // Set up event handlers for closing 476 $fader.on('click', function(e) { 477 // Only close if the fader itself was clicked (not bubbled from container) 478 if (e.target === this) { 479 closeModal(modalId); 480 } 481 }); 482 483 $(document).on('click', `.cm-modal-close[data-modal-id="${modalId}"]`, function() { 484 closeModal(modalId); 485 }); 486 487 // Return the modal ID for potential future reference 488 return modalId; 489 }; 490 491 // Function to close a specific modal 492 function closeModal(modalId) { 493 // Get the fader element for this modal 494 const $fader = $(`#${modalId}-fader`); 495 if ($fader.length === 0) { 496 console.warn(`Fader for modal ${modalId} not found`); 497 window.cmActiveModals.delete(modalId); 498 return; 499 } 500 501 // Keep track of the parent container before removing the modal 502 const $parentContainer = $fader.parent(); 503 const containerId = $parentContainer.attr('id'); 504 505 // Remove from active modals set 506 window.cmActiveModals.delete(modalId); 507 508 // Execute modal close callbacks BEFORE removing the modal 509 // This allows callbacks to access modal elements if needed 510 511 // Execute global callbacks 512 window.cmModalCloseCallbacks.forEach(callback => { 513 try { 514 callback(modalId); 515 } catch (e) { 516 console.error('Global modal close callback failed:', e); 517 } 518 }); 519 520 // Execute modal-specific callbacks 521 if (window.cmModalSpecificCallbacks[modalId]) {
522 window.cmModalSpecificCallbacks[modalId].forEach(callback => { 523 try { 524 callback(modalId); 525 } catch (e) { 526 console.error('Modal-specific close callback failed:', e); 527 } 528 }); 529 530 // Clean up specific callbacks for this modal 531 delete window.cmModalSpecificCallbacks[modalId]; 532 } 533 534 // Fire custom jQuery event for other systems to listen to 535 $(document).trigger('cm_modal_closed', [modalId]); 536 537 // Remove the fader and its contents 538 $fader.remove(); 539 540 // Find the entry in our tracking object for this modal 541 if (window.cmModalContainers[modalId]) { 542 // Container info is now stored in the tracking object 543 const containerInfo = window.cmModalContainers[modalId]; 544 545 // Check if the container is now empty 546 if ($parentContainer.children().length === 0) { 547 $parentContainer.remove(); 548 549 // Clean up tracking object 550 delete window.cmModalContainers[modalId]; 551 } 552 } else { 553 // If container is empty, remove it anyway 554 if (containerId && containerId.startsWith('cm-modal-wrapper-') && $parentContainer.children().length === 0) { 555 $parentContainer.remove(); 556 } 557 } 558 } 559 560 // Make closeModal available globally for other components 561 window.closeModal = closeModal; 562 563 /** 564 * Convenience function to close all active modals or a specific modal 565 * @param {string} modalId - Optional specific modal ID to close. If omitted, closes all active modals. 566 */ 567 window.cmCloseModal = function(modalId) { 568 if (modalId) { 569 // Close specific modal 570 closeModal(modalId); 571 } else { 572 // Close all active modals (most recent first) 573 const activeModals = Array.from(window.cmActiveModals); 574 activeModals.reverse().forEach(id => closeModal(id)); 575 } 576 }; 577 578 /** 579 * Register a global callback that fires when ANY modal closes 580 * @param {function} callback - Function to call when modal closes. Receives modalId as parameter. 581 */ 582 window.registerGlobalModalCloseCallback = function(callback) { 583 if (typeof callback === 'function') { 584 window.cmModalCloseCallbacks.push(callback); 585 } else { 586 console.error('registerGlobalModalCloseCallback: callback must be a function'); 587 } 588 }; 589 590 /** 591 * Register a callback for a specific modal ID 592 * @param {string} modalId - The modal ID to listen for 593 * @param {function} callback - Function to call when this specific modal closes 594 */ 595 window.registerModalCloseCallback = function(modalId, callback) { 596 if (typeof callback === 'function') { 597 if (!window.cmModalSpecificCallbacks[modalId]) { 598 window.cmModalSpecificCallbacks[modalId] = []; 599 } 600 window.cmModalSpecificCallbacks[modalId].push(callback); 601 } else { 602 console.error('registerModalCloseCallback: callback must be a function'); 603 } 604 }; 605 606 /** 607 * Remove all callbacks for a specific modal ID 608 * @param {string} modalId - The modal ID to remove callbacks for 609 */ 610 window.unregisterModalCloseCallbacks = function(modalId) { 611 if (window.cmModalSpecificCallbacks[modalId]) { 612 delete window.cmModalSpecificCallbacks[modalId]; 613 } 614 }; 615 616 /** 617 * JavaScript wrapper for cmDisplayModal 618 * Can be called directly as a regular function from other JS code 619 * 620 * @param {string} type_modal - Type of modal ('html', 'youtube') 621 * @param {string} heading - Modal heading 622 * @param {string|HTMLElement} content - Modal content (HTML string or YouTube URL) 623 * @param {Object} additionalOptions - Optional additional parameters 624 * @returns {string} - The ID of the created modal 625 */ 626 window.cmOpenModal = function(type_modal, heading, content, additionalOptions = {}) { 627 628 // Create options object with required parameters 629 const options = { 630 type_modal: type_modal || 'html', 631 heading: heading || '', 632 content: content || '' 633 }; 634 635 // Merge with additional options 636 if (additionalOptions && typeof additionalOptions === 'object') { 637 Object.assign(options, additionalOptions); 638 } 639 640 // Call the main function 641 return window.cmDisplayModal(options); 642 }; 643 644});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.