1/** 2 * NextGEN Gallery TikTok Video Player 3 * @package NextGEN Gallery 4 */ 5 6(function($, window) { 7 'use strict'; 8 9 window.NggTikTokVideo = window.NggTikTokVideo || {}; 10 11 let linkHandlersInitialized = false; 12 // Cache for gallery ID lookups to avoid repeated DOM traversals 13 const galleryIdCache = new Map(); 14 15 // Constants 16 const DEFAULT_DIMENSIONS = { width: 480, height: 854 }; 17 const ASPECT_RATIO = 9 / 16; 18 const MAX_DEPTH = 15; 19 const INIT_DELAY = 100; 20 21 /** 22 * Get TikTok settings for a gallery 23 * @param {string|number|null} galleryId - Gallery ID 24 * @returns {Object} Settings object with link and link_target 25 */ 26 NggTikTokVideo.getTikTokSettings = (galleryId) => { 27 if (!window.ngg_tiktok_gallery_settings) { 28 window.ngg_tiktok_gallery_settings = { 29 global: { 30 link: '0', 31 link_target: '0' 32 } 33 }; 34 } 35 36 const settings = window.ngg_tiktok_gallery_settings; 37 const galleryIdStr = galleryId ? String(galleryId) : null; 38 39 if (galleryIdStr && settings[`gallery_${galleryIdStr}`]) { 40 const gallerySettings = settings[`gallery_${galleryIdStr}`]; 41 if (gallerySettings && (gallerySettings.link !== undefined || gallerySettings.link_target !== undefined)) { 42 return { 43 link: String(gallerySettings.link ?? '0'), 44 link_target: String(gallerySettings.link_target ?? '0') 45 }; 46 } 47 } 48 49 const globalSettings = settings.global || {}; 50 return { 51 link: String(globalSettings.link || '0'), 52 link_target: String(globalSettings.link_target || '0') 53 }; 54 }; 55 56 /** 57 * Find gallery ID from anchor element 58 * @param {jQuery} $anchor - jQuery anchor element 59 * @param {string|null} imageId - Image ID if available 60 * @returns {Object|null} Object with galleryId and $galleryContainer 61 */ 62 const findGalleryId = ($anchor, imageId) => { 63 const cacheKey = imageId || $anchor[0]; 64 const cachedResult = galleryIdCache.get(cacheKey); 65 66 if (cachedResult) { 67 return cachedResult; 68 } 69 70 let $galleryContainer = null; 71 let galleryId = null; 72 73 // Try closest data-gallery-id first (most common case) 74 $galleryContainer = $anchor.closest('[data-gallery-id]'); 75 if ($galleryContainer.length) { 76 galleryId = $galleryContainer.attr('data-gallery-id') || $galleryContainer.data('gallery-id'); 77 } 78 79 // Try common gallery container classes 80 if (!galleryId) { 81 $galleryContainer = $anchor.closest('.ngg-galleryoverview, .ngg-imagebrowser, .ngg-slideshow'); 82 if ($galleryContainer.length) { 83 galleryId = $galleryContainer.attr('data-gallery-id') || 84 $galleryContainer.data('gallery-id') || 85 $galleryContainer.attr('data-nextgen-gallery-id') || 86 $galleryContainer.data('nextgen-gallery-id'); 87 } 88 } 89 90 // Traverse up the DOM tree 91 if (!galleryId) { 92 let $current = $anchor; 93 let depth = 0; 94 95 while (depth < MAX_DEPTH && $current.length) { 96 if ($current.is('[data-gallery-id]')) { 97 galleryId = $current.attr('data-gallery-id') || $current.data('gallery-id'); 98 $galleryContainer = $current; 99 break; 100 } 101 102 const $sibling = $current.siblings('[data-gallery-id]').first(); 103 if ($sibling.length) { 104 galleryId = $sibling.attr('data-gallery-id') || $sibling.data('gallery-id'); 105 $galleryContainer = $sibling; 106 break; 107 } 108 109 $current = $current.parent(); 110 depth++; 111 } 112 } 113 114 // Last resort: search all containers 115 if (!galleryId && imageId) { 116 const $allContainers = $('[data-gallery-id]'); 117 $allContainers.each(function() { 118 const $container = $(this); 119 if ($container.find($anchor).length > 0) { 120 galleryId = $container.attr('data-gallery-id') || $container.data('gallery-id'); 121 $galleryContainer = $container; 122 return false;
123 } 124 }); 125 } 126 127 const result = { 128 galleryId: galleryId ? String(galleryId) : null, 129 $galleryContainer 130 }; 131 132 // Cache the result (even if null) to avoid repeated traversals 133 galleryIdCache.set(cacheKey, result); 134 return result; 135 }; 136 137 /** 138 * Get TikTok data from anchor element 139 * @param {jQuery} $anchor - jQuery anchor element 140 * @param {string|null} imageId - Image ID if available 141 * @returns {Object|null} TikTok data object 142 */ 143 const getTikTokDataFromAnchor = ($anchor, imageId) => { 144 if (imageId && window.ngg_tiktok_images?.[imageId]) { 145 return window.ngg_tiktok_images[imageId]; 146 } 147 148 const playUrl = $anchor.attr('data-tiktok-play-url'); 149 const shareUrl = $anchor.attr('data-tiktok-share-url'); 150 const embedUrl = $anchor.attr('data-tiktok-embed-url'); 151 152 if (playUrl || shareUrl || embedUrl) { 153 return { 154 playUrl: playUrl || '', 155 shareUrl: shareUrl || '', 156 embedUrl: embedUrl || '' 157 }; 158 } 159 160 return null; 161 }; 162 163 /** 164 * Handle click events on TikTok image links 165 * @param {Event} e - Click event 166 */ 167 const handleTikTokLinkClick = (e) => { 168 const anchor = e.target.closest('a[data-image-id]'); 169 if (!anchor) { 170 return; 171 } 172 173 const $anchor = $(anchor); 174 const imageId = $anchor.attr('data-image-id'); 175 const tiktokData = getTikTokDataFromAnchor($anchor, imageId); 176 177 if (!tiktokData) { 178 return; 179 } 180 181 const { galleryId } = findGalleryId($anchor, imageId); 182 const tiktokSettings = NggTikTokVideo.getTikTokSettings(galleryId); 183 const linkSetting = String(tiktokSettings.link || '0'); 184 const linkTarget = String(tiktokSettings.link_target || '0'); 185 186 if (linkSetting !== '1' && linkSetting !== '2') { 187 return; 188 } 189 190 if (linkSetting === '1' && !tiktokData.shareUrl) { 191 return; 192 } 193 194 if (linkSetting === '2' && !tiktokData.embedUrl && !tiktokData.shareUrl) { 195 return; 196 } 197 198 e.preventDefault(); 199 e.stopPropagation(); 200 201 const openNewTab = linkTarget === '1' || linkTarget === '_blank'; 202 let targetUrl = ''; 203 204 if (linkSetting === '1') { 205 targetUrl = tiktokData.shareUrl || ''; 206 } else if (linkSetting === '2') { 207 targetUrl = tiktokData.embedUrl || tiktokData.shareUrl || ''; 208 } 209 210 if (targetUrl) { 211 if (openNewTab) { 212 window.open(targetUrl, '_blank', 'noopener,noreferrer'); 213 } else { 214 window.location.href = targetUrl; 215 } 216 } 217 }; 218 219 /** 220 * Initialize link handlers for TikTok images 221 */ 222 NggTikTokVideo.initLinkHandlers = () => { 223 if (linkHandlersInitialized) { 224 return; 225 } 226 linkHandlersInitialized = true; 227 228 // Use capture phase to run BEFORE lightbox handlers 229 document.addEventListener('click', handleTikTokLinkClick, true); 230 }; 231 232 /** 233 * Check if element is a TikTok image 234 * @param {Element|jQuery} element - Element to check 235 * @returns {boolean} True if element is a TikTok image 236 */ 237 NggTikTokVideo.isTikTokImage = (element) => { 238 const $el = $(element); 239 const $anchor = $el.is('a') ? $el : $el.closest('a'); 240 241 if ($anchor.attr('data-ngg-tiktok-source') === 'true') { 242 return true; 243 } 244 245 const imageId = $anchor.attr('data-image-id'); 246 if (imageId && window.ngg_tiktok_images?.[imageId]) { 247 const $galleryContainer = $anchor.closest('[data-gallery-id]'); 248 const galleryId = $galleryContainer.length ? $galleryContainer.attr('data-gallery-id') : null; 249 const settings = NggTikTokVideo.getTikTokSettings(galleryId); 250 return String(settings.link || '0') === '0'; 251 } 252 253 return false; 254 }; 255 256 /** 257 * Get TikTok data from element 258 * @param {Element|jQuery} element - Element to get data from 259 * @returns {Object|null} TikTok data object 260 */ 261 NggTikTokVideo.getTikTokData = (element) => { 262 const $el = $(element); 263 const $anchor = $el.is('a') ? $el : $el.closest('a'); 264 265 const playUrl = $anchor.attr('data-ngg-tiktok-play-url'); 266 const shareUrl = $anchor.attr('data-ngg-tiktok-share-url'); 267 const embedUrl = $anchor.attr('data-ngg-tiktok-embed-url'); 268 const tiktokId = $anchor.attr('data-ngg-tiktok-id'); 269 270 if (playUrl || shareUrl || embedUrl) { 271 return { 272 tiktokId: tiktokId || '', 273 playUrl: playUrl || '', 274 shareUrl: shareUrl || '', 275 embedUrl: embedUrl || '', 276 linkSetting: '0' 277 }; 278 } 279 280 const imageId = $anchor.attr('data-image-id'); 281 if (imageId && window.ngg_tiktok_images?.[imageId]) { 282 return window.ngg_tiktok_images[imageId]; 283 } 284 285 return null; 286 }; 287 288 /** 289 * Get video URL from TikTok data 290 * @param {Object|null} tiktokData - TikTok data object 291 * @returns {string} Video URL 292 */ 293 NggTikTokVideo.getVideoUrl = (tiktokData) => { 294 if (!tiktokData) { 295 return ''; 296 } 297 298 if (tiktokData.playUrl?.length > 0) { 299 return tiktokData.playUrl; 300 } 301 302 if (tiktokData.shareUrl?.length > 0) { 303 return tiktokData.shareUrl; 304 } 305 306 return ''; 307 }; 308 309 /** 310 * Get embed URL from TikTok data 311 * @param {Object|null} tiktokData - TikTok data object 312 * @returns {string} Embed URL 313 */ 314 NggTikTokVideo.getEmbedUrl = (tiktokData) => { 315 if (!tiktokData) { 316 return ''; 317 } 318 319 if (tiktokData.embedUrl) { 320 return tiktokData.embedUrl; 321 } 322 323 if (tiktokData.shareUrl) { 324 const match = tiktokData.shareUrl.match(/video\/(\d+)/); 325 if (match?.[1]) { 326 return `https://www.tiktok.com/embed/v2/${match[1]}`; 327 } 328 } 329 330 return ''; 331 }; 332 333 /** 334 * Create error message element 335 * @param {string} message - Error message 336 * @returns {jQuery} Error message jQuery element 337 */ 338 const createErrorMessage = (message) => { 339 return $('<div class="ngg-tiktok-error"></div>') 340 .text(message) 341 .css({ 342 color: '#fff', 343 textAlign: 'center', 344 padding: '20px' 345 }); 346 }; 347 348 /** 349 * Create video player element 350 * @param {string} videoUrl - Video URL 351 * @param {boolean} autoplay - Autoplay setting 352 * @param {Function} onReady - Ready callback 353 * @param {Function} onError - Error callback 354 * @returns {jQuery} Video jQuery element 355 */ 356 const createVideoElement = (videoUrl, autoplay, onReady, onError) => { 357 const $video = $('<video></video>').attr({ 358 src: videoUrl, 359 controls: true, 360 autoplay: autoplay, 361 playsinline: true, 362 preload: 'auto' 363 }).css({ 364 width: '100%', 365 height: '100%', 366 objectFit: 'contain' 367 }); 368 369 $video.on('loadeddata', () => { 370 onReady($video[0]); 371 }); 372 373 $video.on('error', () => { 374 $video.remove(); 375 onError(new Error('Video failed to load')); 376 }); 377 378 return $video; 379 }; 380 381 /** 382 * Create player container 383 * @param {Object} options - Player options 384 * @returns {jQuery} Container jQuery element 385 */ 386 NggTikTokVideo.createPlayer = (options = {}) => { 387 const { 388 tiktokData, 389 width = DEFAULT_DIMENSIONS.width, 390 height = DEFAULT_DIMENSIONS.height, 391 autoplay = true, 392 onReady = () => {}, 393 onError = () => {} 394 } = options; 395
396 const $container = $('<div class="ngg-tiktok-video-container"></div>').css({ 397 position: 'relative', 398 width, 399 height, 400 maxWidth: '100%', 401 maxHeight: '100%', 402 display: 'flex', 403 alignItems: 'center', 404 justifyContent: 'center' 405 }); 406 407 const videoUrl = NggTikTokVideo.getVideoUrl(tiktokData); 408 409 if (videoUrl && videoUrl.includes('tiktokcdn.com')) { 410 const $video = createVideoElement(videoUrl, autoplay, onReady, (error) => { 411 $container.append(createErrorMessage('Video failed to load')); 412 onError(error); 413 }); 414 $container.append($video); 415 } else { 416 $container.append(createErrorMessage('Video not available')); 417 onError(new Error('No video URL available')); 418 } 419 420 return $container; 421 }; 422 423 /** 424 * Create embed player 425 * @param {jQuery} $container - Container element 426 * @param {string} embedUrl - Embed URL 427 * @param {number} width - Width 428 * @param {number} height - Height 429 * @param {boolean} autoplay - Autoplay setting 430 * @param {Function} onReady - Ready callback 431 * @param {Function} onError - Error callback 432 */ 433 NggTikTokVideo._createEmbedPlayer = ($container, embedUrl, width, height, autoplay, onReady, onError) => { 434 $container.empty(); 435 436 if (!embedUrl) { 437 $container.append(createErrorMessage('Video not available')); 438 onError(new Error('No embed URL available')); 439 return; 440 } 441 442 const url = autoplay 443 ? `${embedUrl}${embedUrl.includes('?') ? '&' : '?'}autoplay=1` 444 : embedUrl; 445 446 const $iframe = $('<iframe></iframe>').attr({ 447 src: url, 448 frameborder: '0', 449 allowfullscreen: true, 450 allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' 451 }).css({ 452 width: '100%', 453 height: '100%', 454 border: 'none' 455 }); 456 457 $iframe.on('load', () => { 458 onReady($iframe[0]); 459 }); 460 461 $iframe.on('error', () => { 462 onError(new Error('Failed to load TikTok embed')); 463 }); 464 465 $container.append($iframe); 466 }; 467 468 /** 469 * Replace image with video player 470 * @param {Element|jQuery} imageElement - Image element 471 * @param {Object} options - Player options 472 * @returns {jQuery|null} Player jQuery element or null 473 */ 474 NggTikTokVideo.replaceImageWithVideo = (imageElement, options = {}) => { 475 const $image = $(imageElement); 476 const $anchor = $image.closest('a'); 477 const tiktokData = options.tiktokData || NggTikTokVideo.getTikTokData($anchor); 478 479 if (!tiktokData) { 480 return null; 481 } 482 483 const width = options.width || $image.width() || DEFAULT_DIMENSIONS.width; 484 const height = options.height || $image.height() || DEFAULT_DIMENSIONS.height; 485 486 const playerOptions = { 487 ...options, 488 tiktokData, 489 width, 490 height 491 }; 492 493 const $player = NggTikTokVideo.createPlayer(playerOptions); 494 $image.replaceWith($player); 495 return $player; 496 }; 497 498 /** 499 * Create lightbox player 500 * @param {Object} tiktokData - TikTok data object 501 * @param {Object} lightboxOptions - Lightbox options 502 * @returns {jQuery} Player jQuery element 503 */ 504 NggTikTokVideo.createLightboxPlayer = (tiktokData, lightboxOptions = {}) => { 505 const { 506 maxWidth = window.innerWidth * 0.8, 507 maxHeight = window.innerHeight * 0.9, 508 onReady, 509 onError 510 } = lightboxOptions; 511 512 let width, height; 513 if (maxHeight * ASPECT_RATIO <= maxWidth) { 514 height = maxHeight; 515 width = height * ASPECT_RATIO; 516 } else { 517 width = maxWidth; 518 height = width / ASPECT_RATIO; 519 } 520 521 return NggTikTokVideo.createPlayer({ 522 tiktokData, 523 width: Math.round(width), 524 height: Math.round(height),
525 autoplay: true, 526 onReady, 527 onError 528 }); 529 }; 530 531 /** 532 * Destroy player and clean up resources 533 * @param {jQuery} $container - Container element 534 */ 535 NggTikTokVideo.destroyPlayer = ($container) => { 536 if (!$container?.length) { 537 return; 538 } 539 540 const $video = $container.find('video'); 541 if ($video.length) { 542 $video[0].pause(); 543 $video.attr('src', ''); 544 } 545 546 const $iframe = $container.find('iframe'); 547 if ($iframe.length) { 548 $iframe.attr('src', ''); 549 } 550 551 $container.remove(); 552 }; 553 554 // Initialize on DOM ready 555 $(() => { 556 setTimeout(() => { 557 NggTikTokVideo.initLinkHandlers(); 558 }, INIT_DELAY); 559 }); 560 561})(jQuery, window);
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.