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
← All changes | admin/assets/js/notice-response.js +235 -0 2.7.0 → 2.12.8 View file →
@@ -35,8 +35,15 @@
35 35 return null;
36 36 }
37 37
38 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 +
39 46 const body = new FormData();
40 47 body.append( 'action', 'srfm_notice_response' );
41 48 body.append( 'nonce', srfmNoticeResponse.nonce );
42 49 body.append( 'notice_id', noticeId );
@@ -46,8 +53,25 @@
46 53 () => {}
47 54 );
48 55 }
49 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 +
50 74 Object.keys( notices ).forEach( function ( noticeId ) {
51 75 const container = document.getElementById( noticeId );
52 76 if ( ! container ) {
53 77 return;
@@ -64,5 +88,216 @@
64 88 sendResponse( noticeId, action );
65 89 }
66 90 } );
67 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 + }
68 303 }() );