PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / js / packages / floating-links / s11-floating-links.js

s11-floating-links.js in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More trunk, at js/packages/floating-links/s11-floating-links.js

355 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Class representing a floating action button for showing links.
3 *
4 * @class S11FloatingLinks
5 */
6 class S11FloatingLinks {
7 /**
8 * Create a new S11FloatingLinks instance.
9 *
10 * @class
11 */
12 constructor() {
13 wp.hooks.addAction( 'set_floating_links_config', 'S11FloatingLinks', ( { links, options } ) => {
14 this.validateInputs( links, options );
15
16 this.links = links;
17 this.options = options;
18
19 this.initComponent();
20 } );
21 }
22
23 /**
24 * Validate the input parameters.
25 *
26 * @param {Array<Object>} links The links array.
27 * @param {Object} options The options object.
28 *
29 * @throws {Error} If the links array is empty or not provided.
30 * @throws {Error} If the options.logoIcon is not provided or is an empty string.
31 */
32 validateInputs( links, options ) {
33 if ( ! Array.isArray( links ) || links.length === 0 ) {
34 throw new Error( 'The "links" array is required and must not be empty.' );
35 }
36
37 if ( ! options || typeof options.logoIcon !== 'string' || options.logoIcon.trim() === '' ) {
38 throw new Error( 'The "options.logoIcon" is required and must be a non-empty string.' );
39 }
40 }
41
42 /**
43 * Initialize the floating links component by creating the required elements, and applying styles.
44 *
45 * @memberof S11FloatingLinks
46 */
47 initComponent() {
48 if ( document.querySelector( '.frm-review-notice' ) ) {
49 // Do not load if the review notice is being shown.
50 return;
51 }
52
53 // Create and append elements
54 this.createWrapper();
55 this.createNavMenu();
56 this.createIconButton();
57
58 // Add event listener to close the navigation menu when clicking outside of floating links wrapper
59 this.addOutsideClickListener();
60
61 // Add scroll event listener to hide/show floating links on scroll
62 this.addScrollEventListener();
63
64 // Apply styles
65 this.setCSSVariables();
66
67 // Use a timeout to give time for Pro to add hooks.
68 setTimeout( () => this.setupInboxSlideIn(), 0 );
69 }
70
71 setupInboxSlideIn() {
72 if ( 'object' !== typeof frmGlobal.inboxSlideIn || ! window.frmDom || ! window.wp ) {
73 return;
74 }
75
76 const slideIn = this.getInboxSlideIn();
77 slideIn.classList.add( 's11-fadein' );
78 document.body.append( slideIn );
79
80 // Make sure the events are set for dismissing the inbox message.
81 if ( ! document.querySelector( '.frm-inbox-wrapper' ) ) {
82 frmAdminBuild.inboxInit();
83 }
84 }
85
86 /**
87 * @return {HTMLElement} The inbox slide-in element.
88 */
89 getInboxSlideIn() {
90 const h3 = frmDom.tag(
91 'h3',
92 { id: 'frm_inbox_slidein_title' }
93 );
94 h3.innerHTML = frmAdminBuild.purifyHtml( frmGlobal.inboxSlideIn.subject );
95 const messageSpan = frmDom.span( {
96 id: 'frm_inbox_slidein_message',
97 text: frmGlobal.inboxSlideIn.slidein
98 } );
99 const dismissIcon = frmDom.a( {
100 className: 'dismiss frm_inbox_dismiss',
101 child: frmDom.svg( { href: '#frm_close_icon' } )
102 } );
103 dismissIcon.setAttribute( 'aria-label', wp.i18n.__( 'Dismiss this notice', 'formidable' ) );
104 dismissIcon.setAttribute( 'role', 'button' );
105 const children = frmAdminBuild.hooks.applyFilters(
106 'frm_inbox_slidein_children',
107 [ h3, messageSpan ]
108 );
109 const slideIn = frmDom.div( {
110 id: 'frm_inbox_slide_in',
111 className: 'frm-card-item frm-compact-card-item frm-dismissible frm-box-shadow-xxl',
112 children
113 } );
114 slideIn.setAttribute( 'data-message', frmGlobal.inboxSlideIn.key );
115 slideIn.setAttribute( 'role', 'alert' );
116 slideIn.insertAdjacentHTML( 'beforeend', frmAdminBuild.purifyHtml( frmGlobal.inboxSlideIn.cta ) );
117 slideIn.querySelector( '.frm-button-secondary' )?.remove();
118 this.updateSlideInCtaUtm( slideIn );
119 slideIn.append( frmDom.span( { child: dismissIcon } ) );
120 slideIn.querySelector( 'a[href].frm-button-primary' )?.setAttribute(
121 'aria-description',
122 `${ frmGlobal.inboxSlideIn.subject } ${ frmGlobal.inboxSlideIn.slidein }`.replace( '&amp;', '&' )
123 );
124 this.slideIn = slideIn;
125 return slideIn;
126 }
127
128 updateSlideInCtaUtm( slideIn ) {
129 slideIn.querySelectorAll( 'a[href]' ).forEach(
130 anchor => {
131 if ( '#' === anchor.href ) {
132 return;
133 }
134
135 const urlObj = new URL( anchor.href );
136 const searchParams = new URLSearchParams( urlObj.search );
137
138 searchParams.set( 'utm_medium', 'slidein' );
139
140 urlObj.search = searchParams.toString();
141 anchor.href = urlObj.toString();
142 }
143 );
144 }
145
146 /**
147 * Create the wrapper element and add it to the DOM.
148 *
149 * @memberof S11FloatingLinks
150 */
151 createWrapper() {
152 // Create the wrapper element
153 this.wrapperElement = document.createElement( 'div' );
154 this.wrapperElement.classList.add( 's11-floating-links', 's11-fadein' );
155
156 // Add the wrapper to the DOM
157 document.body.append( this.wrapperElement );
158 }
159
160 /**
161 * Create the navigation menu element with the specified links and append it to the wrapper element.
162 *
163 * @memberof S11FloatingLinks
164 */
165 createNavMenu() {
166 // Create the navigation menu element
167 this.navMenuElement = document.createElement( 'div' );
168 this.navMenuElement.classList.add( 's11-floating-links-nav-menu', 's11-fadeout' );
169
170 // Create and append link elements
171 this.links.forEach( link => {
172 if ( ! this.linkHasRequiredProperties( link ) ) {
173 return;
174 }
175
176 const linkElement = document.createElement( 'a' );
177 linkElement.classList.add( 's11-floating-links-nav-item' );
178
179 linkElement.href = link.url;
180 linkElement.title = link.title;
181
182 if ( link.openInNewTab === true ) {
183 linkElement.target = '_blank';
184 linkElement.rel = 'noopener noreferrer';
185 }
186
187 linkElement.innerHTML = `
188 <span class="s11-floating-links-nav-text">${ link.title }</span>
189 <span class="s11-floating-links-nav-icon">${ link.icon }</span>
190 `;
191
192 this.navMenuElement.append( linkElement );
193 } );
194
195 // Append the navigation menu to the wrapper element
196 this.wrapperElement.append( this.navMenuElement );
197 }
198
199 linkHasRequiredProperties( link ) {
200 const requiredProperties = [ 'title', 'icon', 'url' ];
201 return requiredProperties.every( prop => link.hasOwnProperty( prop ) );
202 }
203
204 /**
205 * Create the icon button element, add a click event listener, and append it to the wrapper element.
206 *
207 * @memberof S11FloatingLinks
208 */
209 createIconButton() {
210 // Create the icon button element
211 this.iconButtonElement = document.createElement( 'div' );
212 this.iconButtonElement.classList.add( 's11-floating-links-logo-icon' );
213 this.iconButtonElement.innerHTML = this.options.logoIcon.trim();
214
215 // Define close icon
216 const closeIcon = `
217 <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32">
218 <path fill="#1D2939" d="M23.625 21.957c.47.467.47 1.225 0 1.693a1.205 1.205 0 0 1-1.699 0l-5.915-5.937-5.958 5.935c-.47.467-1.23.467-1.7 0a1.194 1.194 0 0 1 0-1.693l5.96-5.933-5.961-5.979a1.194 1.194 0 0 1 0-1.693 1.205 1.205 0 0 1 1.699 0l5.96 5.982 5.957-5.935a1.205 1.205 0 0 1 1.7 0 1.194 1.194 0 0 1 0 1.693l-5.96 5.932 5.917 5.935Z"/>
219 </svg>
220 `;
221
222 // Add a click event listener
223 this.iconButtonElement.addEventListener( 'click', () => {
224 // Toggle the navigation menu element
225 this.toggleFade( this.navMenuElement );
226
227 if ( this.slideIn ) {
228 this.toggleFade( this.slideIn );
229 }
230
231 // Switch the icon of the icon button element
232 this.switchIconButton( closeIcon );
233 } );
234
235 // Append the icon button to the wrapper element
236 this.wrapperElement.append( this.iconButtonElement );
237 }
238
239 /**
240 * Switch the icon of the icon button element between the logo icon and the close icon.
241 * This method is called when the icon button is clicked.
242 *
243 * @memberof S11FloatingLinks
244 *
245 * @param {string} closeIcon - The SVG close icon that will replace the logo icon when the navigation menu is opened.
246 */
247 switchIconButton( closeIcon ) {
248 this.iconButtonElement.classList.toggle( 's11-show-close-icon' );
249
250 if ( this.iconButtonElement.classList.contains( 's11-show-close-icon' ) ) {
251 this.iconButtonElement.innerHTML = closeIcon.trim();
252 return;
253 }
254
255 this.iconButtonElement.innerHTML = this.options.logoIcon.trim();
256 }
257
258 /**
259 * Toggle the fade-in and fade-out animation for the specified element.
260 * This method is used to show or hide the navigation menu when the icon button is clicked.
261 *
262 * @memberof S11FloatingLinks
263 *
264 * @param {HTMLElement} element - The element to apply the fade animation on.
265 */
266 toggleFade( element ) {
267 if ( ! element ) {
268 return;
269 }
270
271 element.classList.add( 's11-fading' );
272 element.classList.toggle( 's11-fadein' );
273 element.classList.toggle( 's11-fadeout' );
274
275 element.addEventListener( 'animationend', () => {
276 element.classList.remove( 's11-fading' );
277 }, { once: true } );
278 }
279
280 /**
281 * Add a click event listener to the body to close the navigation when clicked outside of the floating links wrapper.
282 * Prevents the click event from bubbling up to the body when clicking on the floating links wrapper.
283 *
284 * @memberof S11FloatingLinks
285 */
286 addOutsideClickListener() {
287 document.body.addEventListener( 'click', e => {
288 if ( ! this.wrapperElement.contains( e.target ) && this.navMenuElement.classList.contains( 's11-fadein' ) ) {
289 // Toggle the navigation menu element
290 this.toggleFade( this.navMenuElement );
291
292 if ( this.slideIn ) {
293 this.toggleFade( this.slideIn );
294 }
295
296 // Switch the icon of the icon button element
297 this.switchIconButton( this.options.logoIcon.trim() );
298 }
299 } );
300
301 // Prevent click event from bubbling up to the body when clicking on the wrapper element
302 this.wrapperElement.addEventListener( 'click', e => {
303 e.stopPropagation();
304 } );
305 }
306
307 /**
308 * Add a scroll event listener to the window to toggle visibility of the Floating Links.
309 * Show the Floating Links when scrolling up, and hide when scrolling down.
310 *
311 * @memberof S11FloatingLinks
312 */
313 addScrollEventListener() {
314 window.addEventListener( 'scroll', () => {
315 const currentScrollPosition = window.scrollY || document.documentElement.scrollTop;
316
317 if ( currentScrollPosition < this.lastScrollPosition ) {
318 // When scrolling up show the Floating Links
319 if ( ! this.wrapperElement.classList.contains( 's11-fadein' ) ) {
320 this.toggleFade( this.wrapperElement );
321 }
322 } else if ( ! this.wrapperElement.classList.contains( 's11-fadeout' ) && ! this.inboxSlideInIsVisible() ) {
323 // When scrolling down hide the Floating Links
324 this.toggleFade( this.wrapperElement );
325 }
326
327 this.lastScrollPosition = currentScrollPosition;
328 } );
329 }
330
331 inboxSlideInIsVisible() {
332 if ( ! this.slideIn ) {
333 return 'object' === typeof frmGlobal.inboxSlideIn;
334 }
335 return null === this.wrapperElement.querySelector( '.s11-show-close-icon' );
336 }
337
338 /**
339 * Set dynamic CSS properties.
340 *
341 * @memberof S11FloatingLinks
342 */
343 setCSSVariables() {
344 const hoverColor = this.options?.hoverColor ? this.options.hoverColor : '#4199FD';
345 const bgHoverColor = this.options?.bgHoverColor ? this.options.bgHoverColor : '#F5FAFF';
346
347 // Set the CSS variables on the wrapper element
348 this.wrapperElement.style.setProperty( '--floating-links-hover-color', hoverColor );
349 this.wrapperElement.style.setProperty( '--floating-links-bg-hover-color', bgHoverColor );
350 }
351 }
352
353 // Initialize Floating Links
354 new S11FloatingLinks();
355