PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.8
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.8
2.12.8 2.12.7 2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 All 98 releases
sureforms / admin / assets / js / notice-response.js

notice-response.js in SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz 2.12.8, at admin/assets/js/notice-response.js

304 lines 9.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* global srfmNoticeResponse */
2 ( function () {
3 const notices = {
4 'srfm-getting-started-notice': {
5 primary: 'go_to_dashboard',
6 snooze: 'maybe_later',
7 dismiss: 'dismissed',
8 },
9 'srfm-plugin-review-notice': {
10 primary: 'rate_sureforms',
11 snooze: 'maybe_later',
12 dismiss: 'dismissed',
13 },
14 };
15
16 function getAction( el, noticeId ) {
17 const config = notices[ noticeId ];
18 if ( ! config ) {
19 return null;
20 }
21
22 if (
23 el.classList.contains( 'button-primary' ) ||
24 ( el.classList.contains( 'astra-notice-close' ) &&
25 el.getAttribute( 'target' ) === '_blank' )
26 ) {
27 return config.primary;
28 }
29 if ( el.hasAttribute( 'data-repeat-notice-after' ) ) {
30 return config.snooze;
31 }
32 if ( el.classList.contains( 'astra-notice-close' ) ) {
33 return config.dismiss;
34 }
35 return null;
36 }
37
38 function sendResponse( noticeId, button ) {
39 // Guarded like the carousel's read of the same global: this file is
40 // enqueued from several places and a missing localize object should not
41 // throw out of a click handler.
42 if ( typeof srfmNoticeResponse === 'undefined' ) {
43 return;
44 }
45
46 const body = new FormData();
47 body.append( 'action', 'srfm_notice_response' );
48 body.append( 'nonce', srfmNoticeResponse.nonce );
49 body.append( 'notice_id', noticeId );
50 body.append( 'button', button );
51
52 fetch( srfmNoticeResponse.ajaxurl, { method: 'POST', body } ).catch(
53 () => {}
54 );
55 }
56
57 // Action-item notices declare what to record on the element itself, so a new
58 // one needs no entry in the map above. Delegated from the document because
59 // these render on every admin screen, not inside one known container.
60 document.addEventListener( 'click', function ( e ) {
61 const link = e.target.closest( '[data-srfm-notice-id][data-srfm-button]' );
62 if ( ! link ) {
63 return;
64 }
65
66 const noticeId = link.getAttribute( 'data-srfm-notice-id' );
67 const button = link.getAttribute( 'data-srfm-button' );
68
69 if ( noticeId && button ) {
70 sendResponse( noticeId, button );
71 }
72 } );
73
74 Object.keys( notices ).forEach( function ( noticeId ) {
75 const container = document.getElementById( noticeId );
76 if ( ! container ) {
77 return;
78 }
79
80 container.addEventListener( 'click', function ( e ) {
81 const link = e.target.closest( 'a' );
82 if ( ! link ) {
83 return;
84 }
85
86 const action = getAction( link, noticeId );
87 if ( action ) {
88 sendResponse( noticeId, action );
89 }
90 } );
91 } );
92
93 // ------------------------------------------------------------------
94 // Carousel for the action-item notices
95 // ------------------------------------------------------------------
96
97 /**
98 * Show SureForms' stacked admin notices one at a time.
99 *
100 * Four faults at once pushed the actual page below the fold on every admin
101 * screen, so the notices became the page. One at a time with a count keeps the
102 * warning visible without taking over.
103 *
104 * Built here rather than printed by PHP so that with JavaScript off every
105 * notice simply stays visible, exactly as before. Controls that cannot work
106 * must not be the thing that hides a warning.
107 *
108 * The controls are created once, in the wrapper, and never moved. Moving them
109 * into the newly shown card ran the DOM remove steps first, which unfocuses
110 * whatever they contain -- so every activation dropped focus to <body> and a
111 * keyboard user had to Tab in from the top of the page again, once per notice.
112 * It also carried the aria-live counter out of and back into the document with
113 * its text already set, which is generally not announced. Only one card is ever
114 * visible, so the wrapper's box is the visible card's box and pinning the
115 * controls to the wrapper looks identical.
116 */
117 function buildNoticeCarousel() {
118 const cards = Array.prototype.slice.call(
119 document.querySelectorAll( '.srfm-action-item-notice' )
120 );
121
122 // One notice needs no chrome, and none needs nothing.
123 if ( cards.length < 2 ) {
124 return;
125 }
126
127 const labels =
128 ( typeof srfmNoticeResponse !== 'undefined' &&
129 srfmNoticeResponse.carousel ) ||
130 {};
131 let index = 0;
132
133 // Wrap in place, so the cards keep the position WordPress gave them
134 // rather than being moved to the end of the screen.
135 const wrap = document.createElement( 'div' );
136 wrap.className = 'srfm-action-item-carousel';
137
138 // Positioning and spacing live in the stylesheet the renderer prints, so
139 // an RTL sheet can override them and nothing here is a magic number.
140 // Declared above adopt() because adopt() reads it and runs before the
141 // controls are assembled.
142 const nav = document.createElement( 'p' );
143 nav.className = 'srfm-action-item-carousel-nav';
144
145 /**
146 * Put the cards inside the wrapper, wherever they currently are.
147 *
148 * WordPress core relocates every `.notice` into `div.wrap` on jQuery ready,
149 * and that runs after this file's DOMContentLoaded handler -- so wrapping
150 * once at build time left the wrapper behind, empty and zero-height, with
151 * the controls positioned against it off the side of the screen and the
152 * reserved padding matching nothing.
153 *
154 * Driven by a MutationObserver rather than from render(). Called only from
155 * render() it could not fire for a relocation that happens after the first
156 * paint -- reaching it needed an arrow click, and a relocation is exactly
157 * what strands the arrows: the cards move away carrying their `hidden`
158 * attribute while nav stays behind, leaving one notice visible, the rest
159 * permanently hidden, and no control the user can reach to recover. On a
160 * surface whose only job is showing faults that is worse than not having a
161 * carousel. The every() check makes this idempotent, so the observer
162 * seeing its own writes is harmless.
163 */
164 function adopt() {
165 if (
166 cards.every( function ( notice ) {
167 return notice.parentNode === wrap;
168 } )
169 ) {
170 return;
171 }
172
173 // Anchor on the first card still outside the wrapper. cards[0] may
174 // already be inside it, and wrap.insertBefore( wrap, … ) is a
175 // HierarchyRequestError; a card that was removed rather than moved has
176 // no parentNode at all, and dereferencing it would throw out of
177 // render() before the visibility loop ran.
178 const anchor = cards.filter( function ( notice ) {
179 return notice.parentNode && notice.parentNode !== wrap;
180 } )[ 0 ];
181
182 if ( ! anchor ) {
183 return;
184 }
185
186 // Moving the wrapper runs the DOM remove steps over its subtree, which
187 // blurs whatever inside it had focus -- and nav is inside it, so the
188 // arrow the user just pressed loses focus in exactly the case adopt()
189 // exists for.
190 const active = nav.ownerDocument.activeElement;
191 const focused = nav.contains( active ) ? active : null;
192
193 anchor.parentNode.insertBefore( wrap, anchor );
194 cards.forEach( function ( notice ) {
195 wrap.appendChild( notice );
196 } );
197
198 if ( focused ) {
199 focused.focus();
200 }
201 }
202
203 adopt();
204
205 const isRtl =
206 document.documentElement.getAttribute( 'dir' ) === 'rtl' ||
207 document.body.classList.contains( 'rtl' );
208
209 const prev = document.createElement( 'button' );
210 prev.type = 'button';
211 prev.className = 'button button-small';
212 // Previous points at the start of the reading order, which is the right in
213 // an RTL locale. Hardcoding one direction contradicts the aria-label.
214 prev.textContent = isRtl ? '\u203A' : '\u2039';
215 prev.setAttribute( 'aria-label', labels.previous || 'Previous' );
216
217 const next = document.createElement( 'button' );
218 next.type = 'button';
219 next.className = 'button button-small';
220 next.textContent = isRtl ? '\u2039' : '\u203A';
221 next.setAttribute( 'aria-label', labels.next || 'Next' );
222
223 const counter = document.createElement( 'span' );
224 // Announced, because stepping swaps the text above with no other signal.
225 // The node stays put, so the region is in the document before its text
226 // changes -- which is what makes the change announce at all.
227 counter.setAttribute( 'aria-live', 'polite' );
228
229 function render() {
230 adopt();
231
232 cards.forEach( function ( notice, i ) {
233 notice.hidden = i !== index;
234 } );
235
236 counter.textContent = ( labels.counter || '%1$d of %2$d' )
237 .replace( '%1$d', index + 1 )
238 .replace( '%2$d', cards.length );
239 }
240
241 function step( delta ) {
242 return function () {
243 // Wraps, so a run of cards can be read round without hunting for
244 // the end.
245 index = ( index + delta + cards.length ) % cards.length;
246 render();
247 };
248 }
249
250 /**
251 * Reserve exactly the room the controls take, measured rather than assumed.
252 *
253 * A translated counter is wider than "1 of 4", and a fixed padding lets a
254 * long form title run underneath the buttons.
255 */
256 function measure() {
257 const width = Math.ceil( nav.getBoundingClientRect().width );
258
259 if ( width > 0 ) {
260 wrap.style.setProperty(
261 '--srfm-carousel-reserve',
262 width + 24 + 'px'
263 );
264 }
265 }
266
267 prev.addEventListener( 'click', step( -1 ) );
268 next.addEventListener( 'click', step( 1 ) );
269
270 nav.appendChild( prev );
271 nav.appendChild( counter );
272 nav.appendChild( next );
273 wrap.appendChild( nav );
274
275 render();
276 measure();
277
278 // Core has not finished moving notices when DOMContentLoaded handlers run,
279 // so re-run once the queue has drained. render() re-adopts; this only has
280 // to re-measure, because the controls have a box again by then.
281 window.setTimeout( function () {
282 render();
283 measure();
284 }, 0 );
285
286 // And keep watching. A plugin acting on window.load, an admin-notice
287 // manager or a screen-options re-layout can move the notices at any point,
288 // and adopt() driven only from a click cannot recover from that -- the
289 // click it needs is the thing the move breaks.
290 if ( window.MutationObserver ) {
291 new window.MutationObserver( function () {
292 adopt();
293 measure();
294 } ).observe( document.body, { childList: true, subtree: true } );
295 }
296 }
297
298 if ( document.readyState === 'loading' ) {
299 document.addEventListener( 'DOMContentLoaded', buildNoticeCarousel );
300 } else {
301 buildNoticeCarousel();
302 }
303 }() );
304