PageSourceSearch

https://momentivesoftware.com/wp-content/themes/momentive/assets/js/momentive.js?ver=1790828014

js momentivesoftware.com collected 2026-10-01 12:31:40 UTC 16,898 bytes, 464 lines download raw bytes

1( function () {
2
3	// Swoop path definitions — add more shapes here as needed.
4	// Each path's `length` must match the actual SVG path length,
5	// or leave it null to have JS measure it automatically.
6	const SWOOPS = {
7		'swoop-default': {
8			viewBox: '0 0 500 150',
9			d: 'M7.7,145.6C109,125,299.9,116.2,401,121.3c42.1,2.2,87.6,11.8,87.3,25.7',
10		},
11		'swoop-straight': {
12			viewBox: '0 0 500 50',
13			d: 'M0,25 Q250,5 500,25',
14		},
15		'swoop-double': {
16			viewBox: '0 0 500 80',
17			d: 'M5,30 Q250,5 495,30 M5,55 Q250,30 495,55',
18		},
19	};
20
21	function getSwoopKey( el ) {
22		for ( const key of Object.keys( SWOOPS ) ) {
23			if ( el.classList.contains( key ) ) return key;
24		}
25		return 'swoop-default';
26	}
27
28	function buildSVG( swoop ) {
29		const svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
30		svg.setAttribute( 'viewBox', swoop.viewBox );
31		svg.setAttribute( 'preserveAspectRatio', 'none' );
32		svg.setAttribute( 'aria-hidden', 'true' );
33		svg.classList.add( 'swoop-svg' );
34
35		// Support multiple paths (e.g. swoop-double)
36		const paths = Array.isArray( swoop.d ) ? swoop.d : [ swoop.d ];
37		paths.forEach( d => {
38			const path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
39			path.setAttribute( 'd', d );
40			svg.appendChild( path );
41		} );
42
43		return svg;
44	}
45
46	function initSwoop( heading ) {
47		const strong = heading.querySelector( 'strong' );
48		if ( ! strong ) return;
49
50		const swoopKey = getSwoopKey( heading );
51		const swoop    = SWOOPS[ swoopKey ];
52		const svg      = buildSVG( swoop );
53
54		strong.style.position = 'relative';
55		strong.appendChild( svg );
56
57		// Wait for the browser to paint before measuring path length.
58		// Double-rAF ensures we're past the style recalc phase.
59		requestAnimationFrame( () => {
60			requestAnimationFrame( () => {
61				svg.querySelectorAll( 'path' ).forEach( path => {
62					const len = path.getTotalLength();
63
64					// Bail out if measurement still failed
65					if ( ! len ) {
66						path.style.strokeDasharray  = '';
67						path.style.strokeDashoffset = '';
68						return;
69					}
70//					path.style.setProperty( '--swoop-path-length', len );
71				} );
72
73				// Start observing only after the path is correctly hidden.
74				// This also prevents the "flash" on headings already in the viewport.
75				heading.classList.add( 'is-ready' );
76				observe( heading );
77			} );
78		} );
79	}
80
81	function observe( heading ) {
82		const io = new IntersectionObserver(
83			( entries ) => {
84				entries.forEach( entry => {
85					if ( entry.isIntersecting ) {
86						heading.classList.add( 'is-visible' );
87						io.unobserve( heading ); // fire once
88					}
89				} );
90			},
91			{ threshold: 0.3 }
92		);
93		io.observe( heading );
94	}
95
96	document.addEventListener( 'DOMContentLoaded', () => {
97		document.querySelectorAll( '.is-style-has-swoop' ).forEach( heading => {
98			initSwoop( heading );
99		} );
100
101		moveBlogPrefooter();
102		initMobileDrawer();
103	} );
104
105	// ── Megamenu ──────────────────────────────────────────────────────────────
106	
107	const navItems  = document.querySelectorAll( '.wp-block-navigation-item.has-megamenu' );
108	const panels    = document.querySelectorAll( '.megamenu-panel' );
109	const container = document.querySelector( '.megamenu-panels' );
110	
111	// Pending open/close timers — cancelled when the cursor reverses direction.
112	let openTimer = null;
113	let closeTimer = null;
114	
115	function openPanel( name ) {
116		if ( window.innerWidth < 1024 ) return;
117		closeAll();
118	
119		const panel   = document.querySelector( `.megamenu-panel[data-menu="${ name }"]` );
120		const trigger = document.querySelector(
121			`.wp-block-navigation-item.has-megamenu[data-menu="${ name }"], .megamenu--${ name }`
122		);
123		if ( ! panel ) return;
124	
125		panel.style.opacity = '0';
126		panel.style.display = 'block';
127		const targetHeight  = panel.scrollHeight;
128		panel.style.display = '';
129		panel.style.opacity = '';
130	
131		container.style.height = `${ targetHeight }px`;
132		panel.classList.add( 'is-open' );
133		trigger?.setAttribute( 'aria-expanded', 'true' );
134	}
135	
136	function scheduleOpen( name ) {
137		const alreadyOpen = container?.querySelector( '.megamenu-panel.is-open' );
138	
139		// No panel open: open immediately — deliberate hover, no diagonal-travel risk.
140		if ( ! alreadyOpen ) {
141			cancelOpen();
142			openPanel( name );
143			return;
144		}
145	
146		// Panel already open: delay to absorb accidental corner-brushing while the
147		// user moves their cursor diagonally from the nav toward the panel below.
148		cancelOpen();
149		openTimer = setTimeout( () => openPanel( name ), 160 );
150	}
151	
152	function cancelOpen() {
153		clearTimeout( openTimer );
154		openTimer = null;
155	}
156	
157	function scheduleClose() {
158		clearTimeout( closeTimer );
159		closeTimer = setTimeout( closeAll, 150 );
160	}
161
162	function cancelClose() {
163		clearTimeout( closeTimer );
164		closeTimer = null;
165	}
166
167	function closeAll() {
168		cancelOpen();  // prevent a delayed open from firing after close
169		cancelClose(); // prevent a re-entrant scheduled close
170		panels.forEach(   p => p.classList.remove( 'is-open' ) );
171		navItems.forEach( n => n.setAttribute( 'aria-expanded', 'false' ) );
172		if ( container ) container.style.height = '';
173	}
174	
175	navItems.forEach( item => {
176		const name = item.dataset.menu
177			?? [ ...item.classList ]
178				.find( c => c.startsWith( 'megamenu--' ) )
179				?.replace( 'megamenu--', '' );
180	
181		if ( ! name ) return;
182	
183		item.addEventListener( 'mouseenter', () => scheduleOpen( name ) );
184		item.addEventListener( 'mouseleave', cancelOpen ); // cursor left before delay fired
185	
186		item.querySelector( 'a' )?.addEventListener( 'keydown', e => {
187			if ( e.key !== 'Enter' && e.key !== ' ' ) return;
188			const isOpen = item.getAttribute( 'aria-expanded' ) === 'true';
189			if ( isOpen ) {
190				closeAll();
191			} else {
192				e.preventDefault();
193				openPanel( name );
194				document.querySelector( `.megamenu-panel[data-menu="${ name }"]` )
195					?.querySelector( 'a, button' )
196					?.focus();
197			}
198		} );
199	} );
200	
201	document.addEventListener( 'keydown', e => {
202		if ( e.key === 'Escape' ) closeAll();
203	} );
204	
205	document.addEventListener( 'click', e => {
206		if ( ! e.target.closest( '.site-header' ) ) closeAll();
207	} );
208	
209	// Delay close when the cursor leaves the header so brief exits don't collapse
210	// the panel (e.g. overshooting the nav bar slightly on the way to a panel link).
211	document.querySelector( '.site-header' )?.addEventListener( 'mouseleave', scheduleClose );
212	document.querySelector( '.site-header' )?.addEventListener( 'mouseenter', cancelClose );
213	
214	// Arrow keys within an open panel move between focusable items.
215	container?.addEventListener( 'keydown', e => {
216		if ( ! [ 'ArrowDown', 'ArrowUp' ].includes( e.key ) ) return;
217		const openPanel = container.querySelector( '.megamenu-panel.is-open' );
218		if ( ! openPanel ) return;
219	
220		const focusable = [ ...openPanel.querySelectorAll( 'a, button' ) ];
221		const index     = focusable.indexOf( document.activeElement );
222	
223		if ( e.key === 'ArrowDown' ) {
224			e.preventDefault();
225			focusable[ Math.min( index + 1, focusable.length - 1 ) ]?.focus();
226		} else if ( e.key === 'ArrowUp' ) {
227			e.preventDefault();
228			if ( index <= 0 ) {
229				const activeMenu = openPanel.dataset.menu;
230				closeAll();
231				document.querySelector(
232					`.wp-block-navigation-item.has-megamenu[data-menu="${ activeMenu }"] a,
233					 .megamenu--${ activeMenu } a`
234				)?.focus();
235			} else {
236				focusable[ index - 1 ]?.focus();
237			}
238		}
239	} );
240
241	// ── Prefooter gradient + blog-post relocation ────────────────────────────
242	//
243	// On non-blog pages the .prefooter block is already adjacent to .site-footer
244	// in the template HTML; updateFooterGradient() just sizes the CSS gradient.
245	//
246	// On single blog posts the block lives inside .wp-block-post-content (it's
247	// the last block in post_content). moveBlogPrefooter() lifts it out and
248	// inserts it immediately before .site-footer, producing the same visual
249	// result as non-blog pages. CSS hides it while it's still in the wrong
250	// position (see momentive.scss — .wp-block-post-content .prefooter).
251
252	function moveBlogPrefooter() {
253		const prefooter = document.querySelector( '.wp-block-post-content .prefooter' );
254		if ( ! prefooter ) return;
255		const footer = document.querySelector( '.site-footer' );
256		if ( ! footer ) return;
257
258		footer.parentNode.insertBefore( prefooter, footer );
259
260		// Remove the hide rule now that the element is in the right place,
261		// then recalculate the gradient with the correct height.
262		prefooter.style.removeProperty( 'display' );
263		updateFooterGradient();
264	}
265
266	function updateFooterGradient() {
267		const prefooter = document.querySelector( '.prefooter' );
268		const footer    = document.querySelector( '.site-footer' );
269		if ( ! footer ) return;
270
271		// Measure the actual distance from the footer's top edge to the
272		// prefooter's top edge — this captures both the prefooter's height
273		// and any gap between the two elements (e.g. a margin-top on the
274		// footer's block wrapper in the simple/landing footer variant).
275		let extraHeight = 0;
276		if ( prefooter ) {
277			const footerTop    = footer.getBoundingClientRect().top    + window.scrollY;
278			const prefooterTop = prefooter.getBoundingClientRect().top + window.scrollY;
279			extraHeight = Math.max( 0, footerTop - prefooterTop );
280		}
281
282		footer.style.setProperty( '--gradient-overshoot', `calc(100% + ${ extraHeight }px)` );
283	}
284
285	updateFooterGradient();
286	window.addEventListener( 'resize', updateFooterGradient );
287
288	// Re-run whenever the document height changes (e.g. a HubSpot form in the
289	// hero loads late and pushes content down after the initial measurement).
290	if ( typeof ResizeObserver !== 'undefined' ) {
291		new ResizeObserver( updateFooterGradient ).observe( document.body );
292	}
293
294	// ── Mobile drawer navigation ───────────────────────────────────────────────
295	//
296	// On tablet/mobile the responsive overlay shows top-level nav items.
297	// For items with a megamenu, clicking the row slides the primary list
298	// offscreen left and slides in a secondary panel (cloned from the matching
299	// .megamenu-panel) from the right. A "← Back" button returns to the list;
300	// a "View All [Section]" link at the bottom links to the section's top page.
301
302	function initMobileDrawer() {
303		const nav = document.querySelector( '.wp-block-navigation' );
304		if ( ! nav ) return;
305
306		const overlay = nav.querySelector( '.wp-block-navigation__responsive-container' );
307		if ( ! overlay ) return;
308
309		const content = overlay.querySelector( '.wp-block-navigation__responsive-container-content' );
310		if ( ! content ) return;
311
312		const primaryList = content.querySelector( 'ul.wp-block-navigation__container' );
313		if ( ! primaryList ) return;
314
315		// Build the drawer shell — two panels side-by-side inside a clipping wrapper.
316		const drawer    = document.createElement( 'div' );
317		drawer.className = 'mobile-drawer';
318
319		const primary   = document.createElement( 'div' );
320		primary.className = 'mobile-drawer__primary';
321
322		const secondary = document.createElement( 'div' );
323		secondary.className  = 'mobile-drawer__secondary';
324		secondary.setAttribute( 'aria-hidden', 'true' );
325
326		primaryList.parentNode.insertBefore( drawer, primaryList );
327		primary.appendChild( primaryList );
328		drawer.appendChild( primary );
329		drawer.appendChild( secondary );
330
331		// For each megamenu item, add a dedicated arrow toggle button.
332		// The link itself navigates normally; only the button opens the drawer.
333		nav.querySelectorAll( '.wp-block-navigation-item.has-megamenu' ).forEach( item => {
334			const link = item.querySelector( ':scope > .wp-block-navigation-item__content' );
335			if ( ! link ) return;
336
337			const name = item.dataset.menu
338				?? [ ...item.classList ]
339					.find( c => c.startsWith( 'megamenu--' ) )
340					?.replace( 'megamenu--', '' );
341			if ( ! name ) return;
342
343			const label = ( link.querySelector( '.wp-block-navigation-item__label' )?.textContent
344				?? link.textContent ).trim();
345
346			const toggle = document.createElement( 'button' );
347			toggle.className = 'mobile-drawer-toggle';
348			toggle.setAttribute( 'aria-label', `Open ${ label } submenu` );
349			item.appendChild( toggle );
350
351			toggle.addEventListener( 'click', e => {
352				e.stopPropagation();
353				openMobileSubpanel( drawer, secondary, name, toggle );
354			} );
355		} );
356
357		// Back button — delegated so it works after innerHTML is replaced.
358		secondary.addEventListener( 'click', e => {
359			const back = e.target.closest( '.mobile-drawer-back' );
360			if ( ! back ) return;
361			closeMobileSubpanel( drawer, secondary );
362			secondary._opener?.focus();
363		} );
364
365		// Reset the drawer whenever the overlay is closed.
366		overlay.querySelector( '.wp-block-navigation__responsive-container-close' )
367			?.addEventListener( 'click', () => closeMobileSubpanel( drawer, secondary, true ) );
368	}
369
370	function openMobileSubpanel( drawer, secondary, name, opener ) {
371		const panel = document.querySelector( `.megamenu-panel[data-menu="${ name }"]` );
372		if ( ! panel ) return;
373
374		secondary._opener = opener;
375
376		// Clone first so we can extract the .read-more link before injecting.
377		const cloned   = panel.cloneNode( true );
378		const readMore = cloned.querySelector( '.read-more a' );
379		const viewAll  = readMore
380			? `<a class="mobile-drawer__view-all" href="${ readMore.getAttribute( 'href' ) }">${ readMore.textContent.trim() } <span aria-hidden="true">→</span></a>`
381			: '';
382
383		secondary.innerHTML = `
384			<div class="mobile-drawer__secondary-inner">
385				<button class="mobile-drawer-back" aria-label="Back to main menu">
386					<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true" focusable="false">
387						<path d="M10 3L5 8l5 5" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
388					</svg>
389					Back
390				</button>
391				<div class="mobile-drawer__panel-content"></div>
392				${ viewAll }
393			</div>
394		`;
395
396		// .read-more is already hidden by the mobile panel CSS; no need to remove it from clone.
397		secondary.querySelector( '.mobile-drawer__panel-content' ).appendChild( cloned );
398
399		// Reading a layout property forces a reflow so the browser registers
400		// the secondary panel's starting position before the class change that
401		// triggers the CSS transition. Without this, both DOM mutations land in
402		// the same frame and there's no "from" state to animate from.
403		void secondary.offsetWidth;
404
405		drawer.classList.add( 'is-secondary-open' );
406		secondary.setAttribute( 'aria-hidden', 'false' );
407		secondary.querySelector( '.mobile-drawer-back' )?.focus();
408	}
409
410	function closeMobileSubpanel( drawer, secondary, instant = false ) {
411		drawer.classList.remove( 'is-secondary-open' );
412		secondary.setAttribute( 'aria-hidden', 'true' );
413
414		if ( instant ) {
415			secondary.innerHTML = '';
416		} else {
417			secondary.addEventListener( 'transitionend', () => {
418				if ( ! drawer.classList.contains( 'is-secondary-open' ) ) {
419					secondary.innerHTML = '';
420				}
421			}, { once: true } );
422		}
423	}
424
425	// Sticky-bottom bar — slides up from below once scrolled to the bar's
426	// intended DOM position, stays fixed at the bottom, slides out near footer.
427	// Move the block in the editor to control where it "scrolls in" from;
428	// the bar's DOM position is used as the trigger, not any specific sentinel class.
429	const bar = document.querySelector( '.sticks-to-bottom' );
430
431	if ( bar ) {
432		// Insert a 0-height marker at the bar's DOM position.
433		// position:fixed takes the bar out of flow, so we need this in-flow
434		// reference to know the scroll offset that should trigger the bar.
435		const marker = document.createElement( 'div' );
436		marker.setAttribute( 'aria-hidden', 'true' );
437		marker.style.cssText = 'height:0;overflow:hidden;pointer-events:none;';
438		bar.parentNode.insertBefore( marker, bar );
439
440		const barH   = bar.offsetHeight;
441		const rem    = parseFloat( getComputedStyle( document.documentElement ).fontSize );
442		const exitEl = document.querySelector( '.prefooter' ) || document.querySelector( '.site-footer' );
443
444		const updateBar = () => {
445			const vpBottom   = window.scrollY + window.innerHeight;
446			const markerDocY = marker.getBoundingClientRect().top + window.scrollY - barH;
447			const exitDocY   = exitEl
448				? exitEl.getBoundingClientRect().top + window.scrollY
449				: Infinity;
450
451			// Slide in: when viewport bottom reaches the bar's natural bottom
452			// (marker + barH) plus the 2rem bottom offset — seamless handoff.
453			// Slide out: when the prefooter (or footer if no prefooter) enters the viewport.
454			const shouldStick = vpBottom >= markerDocY + barH + rem * 2
455			                 && vpBottom < exitDocY;
456
457			bar.classList.toggle( 'is-stuck', shouldStick );
458		};
459
460		window.addEventListener( 'scroll', updateBar, { passive: true } );
461		updateBar(); // handle page loads where already scrolled
462	}
463	
464} () );

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.