PageSourceSearch

https://parcrosslyn.com/Assets/common/wcag/js/video-pause.js

js parcrosslyn.com collected 2026-09-27 01:57:48 UTC 15,440 bytes, 341 lines download raw bytes

1/* ============================================================================
2 * TFS 2987210 -- WCAG 2.2.2 "Pause, Stop, Hide" (Level A), Package D step D7.
3 *
4 * Pause/play wiring for the autoplay BACKGROUND VIDEO embeds -- the Hero
5 * widget (RPWebParts/Layout/Hero.ascx.cs) and the Background Video widget
6 * (RPWebParts/Gallery/BackgroundVideo.ascx.cs). Both offer the same three
7 * providers, and all three autoplay muted and loop forever with no user-facing
8 * stop, which is a straight SC 2.2.2 failure: the criterion covers any moving
9 * content that starts automatically and lasts more than five seconds.
10 *
11 * Each provider needs a different call, so this file normalises them behind
12 * one RPPauseControl (see pause-control.js):
13 *
14 *   YouTube  -- YT.Player instance -> pauseVideo() / playVideo(). The IFrame
15 *               API is already loaded by both widgets (enablejsapi=1 is
16 *               already on both embed URLs), so nothing new is requested.
17 *   Vimeo    -- Vimeo.Player instance -> pause() / play(). The Player SDK is
18 *               NEW in this ticket; it is added server-side in the vimeo
19 *               branch of each widget's code-behind, next to where Wistia
20 *               already loads its own iframe API.
21 *   Wistia   -- Wistia's _wq queue -> video.pause() / video.play(). The iframe
22 *               API is ALREADY loaded by both widgets
23 *               (Hero.ascx.cs:256, BackgroundVideo.ascx.cs:189), so Wistia
24 *               needed no new dependency -- confirmed rather than assumed.
25 *
26 * PLAYER READINESS. None of the three players exists at the moment this file
27 * runs; each becomes available through its own async callback. Rather than
28 * racing them, the control resolves its player lazily, at click time, through
29 * a caller-supplied accessor. A click that lands before the player is ready is
30 * a no-op on the video but still flips the button label -- so the two would
31 * desynchronise. To avoid that, the button is not attached at all until the
32 * player resolves. A background video with a broken player therefore shows no
33 * pause button, which is correct: there is nothing running to pause.
34 *
35 * MERGED MODE (options.merged). Hero and Background Video are the only two
36 * widgets in this repo where two independent moving mechanisms coexist: the
37 * FlexSlider slide rotation AND the video playback. Shipping a separate control
38 * for each is defensible under SC 2.2.2 but poor UX -- the criterion asks for
39 * *a* mechanism to stop the moving content, not one per mechanism. So when a
40 * caller reports that both are present, FlexSlider's own pausePlay control is
41 * suppressed at init (see hero.js and BackgroundVideo.ascx) and the single
42 * button built here drives BOTH.
43 *
44 * Merged mode inverts the readiness rule above, deliberately. The button is
45 * attached IMMEDIATELY rather than waiting on the player, because it always has
46 * real work to do -- stopping the rotation -- regardless of whether the video
47 * embed ever resolves. Deferring it would mean a blocked or broken embed left
48 * the slideshow with no control at all, since FlexSlider's was suppressed on
49 * the synchronous fact that a video is configured. The video half wires itself
50 * in the moment the player resolves, and if the user already paused by then,
51 * the resolve callback applies that pause to the video so the two cannot
52 * desynchronise.
53 * ==========================================================================*/
54
55(function (window, $) {
56    'use strict';
57
58    if (!$) {
59        return;
60    }
61
62    var VIMEO_SDK = 'https://player.vimeo.com/api/player.js';
63    var POLL_INTERVAL = 150;
64    // 20s. Long enough for a slow third-party player on a cold cache, short
65    // enough that a genuinely broken embed stops polling. Hero.js has a
66    // cautionary example of the alternative: an un-cleared 100ms setInterval
67    // whose guard never passes, left spinning for the life of the page.
68    var POLL_TIMEOUT = 20000;
69
70    /**
71     * Poll until resolve() returns something truthy, then hand it to onReady.
72     * Gives up after POLL_TIMEOUT and always clears its own interval.
73     */
74    function whenReady(resolve, onReady) {
75        var immediate;
76        try {
77            immediate = resolve();
78        } catch (e) {
79            immediate = null;
80        }
81        if (immediate) {
82            onReady(immediate);
83            return;
84        }
85
86        var waited = 0;
87        var timer = window.setInterval(function () {
88            var value = null;
89            try {
90                value = resolve();
91            } catch (e) {
92                value = null;
93            }
94
95            if (value) {
96                window.clearInterval(timer);
97                onReady(value);
98                return;
99            }
100
101            waited += POLL_INTERVAL;
102            if (waited >= POLL_TIMEOUT) {
103                window.clearInterval(timer);
104            }
105        }, POLL_INTERVAL);
106    }
107
108    function ensureVimeoSdk(callback) {
109        if (window.Vimeo && window.Vimeo.Player) {
110            callback();
111            return;
112        }
113        // The code-behind emits the SDK <script> tag server-side, so normally
114        // this branch only waits for it. The injection below is a safety net
115        // for a cached page rendered before this ticket deployed.
116        if (!document.getElementById('rp-vimeo-sdk')) {
117            var tag = document.createElement('script');
118            tag.id = 'rp-vimeo-sdk';
119            tag.src = VIMEO_SDK;
120            document.getElementsByTagName('head')[0].appendChild(tag);
121        }
122        whenReady(function () {
123            return (window.Vimeo && window.Vimeo.Player) ? window.Vimeo : null;
124        }, function () {
125            callback();
126        });
127    }
128
129    /**
130     * options.scope             (required) container selector, e.g. '#hero-background-video'.
131     * options.controlContainer  (optional) where the button goes; defaults to
132     *                           the first .slides-video inside scope.
133     * options.source            'youtube' | 'vimeo' | 'wistia'
134     * options.getYouTubePlayer  () => YT.Player, for source 'youtube'
135     * options.videoId           Wistia hashed id, for source 'wistia'
136     * options.key               de-dup key passed through to RPPauseControl
137     * options.merged            true when this widget ALSO has a rotating
138     *                           FlexSlider and the caller has suppressed
139     *                           FlexSlider's own pausePlay control, so this one
140     *                           button must drive both. See MERGED MODE above.
141     * options.slider            (required when merged) selector or jQuery for
142     *                           the .flexslider element to drive.
143     */
144    function attach(options) {
145        options = options || {};
146
147        var $scope = $(options.scope);
148        if (!$scope.length || !window.RPPauseControl) {
149            return;
150        }
151
152        // Attach to the banner container, NOT to the .slides-video <li>. Both widgets run
153        // their video slide inside a FlexSlider, and FlexSlider CLONES slides for its loop --
154        // hero.js even has to delete a duplicated .slides-video.clone. A button appended to a
155        // slide would therefore be duplicated, or land on a clone that is currently hidden.
156        // The banner div is outside the cloned subtree and is already position:absolute in
157        // hero.css, so it is both stable and a valid positioning context for the overlay.
158        var containerSelector = options.controlContainer;
159        var $control = containerSelector ? $scope.find(containerSelector).first()
160                                         : $scope.find('.slides-banner').first();
161        if (!$control.length) {
162            // #hero-background-video IS the .slides-banner element, so find() misses it.
163            $control = $scope.filter('.slides-banner').first();
164        }
165        if (!$control.length) {
166            $control = $scope;
167        }
168
169        var source = (options.source || 'youtube').toLowerCase();
170        var key = options.key || ('video-' + source);
171
172        // A caller that asks for merged mode without naming a slider has nothing
173        // to merge with. Treat that as the ordinary video-only control rather
174        // than building a button whose label promises slideshow control it
175        // cannot deliver.
176        var merged = !!options.merged && !!options.slider;
177
178        // The video handlers are mutable because in merged mode the button is
179        // built BEFORE the player resolves (see MERGED MODE in the file header),
180        // so its click handler has to read them at click time rather than close
181        // over them. No-ops until the player arrives.
182        var videoPause = function () { };
183        var videoPlay = function () { };
184        var control = null;
185        var built = false;
186
187        function getSlider() {
188            if (!options.slider) {
189                return null;
190            }
191            var slider = $(options.slider).first().data('flexslider');
192            return (slider && typeof slider.pause === 'function' &&
193                    typeof slider.play === 'function') ? slider : null;
194        }
195
196        // Mirror FlexSlider's OWN pausePlay handler (jquery.flexslider.js:413-427),
197        // which sets manualPause/manualPlay either side of the call. Those flags are
198        // load-bearing, not decoration: BackgroundVideo's `after` force-resume checks
199        // !slider.manualPause before restarting rotation (BackgroundVideo.ascx:181, :209),
200        // and pauseOnHover resume checks both -- so a pause that skipped the flags
201        // would be silently undone on the next transition.
202        function sliderPause(slider) {
203            slider = slider || getSlider();
204            if (!slider) {
205                return;
206            }
207            slider.manualPause = true;
208            slider.manualPlay = false;
209            try { slider.pause(); } catch (e) { }
210        }
211
212        function sliderPlay(slider) {
213            slider = slider || getSlider();
214            if (!slider) {
215                return;
216            }
217            slider.manualPause = false;
218            slider.manualPlay = true;
219            try { slider.play(); } catch (e) { }
220        }
221
222        function build() {
223            built = true;
224            control = window.RPPauseControl.attach({
225                container: $control,
226                overlay: true,
227                key: key,
228                // The merged name has to cover both mechanisms without implying it
229                // only reaches one of them. It is fixed at build time because both
230                // facts behind `merged` -- a video is configured, and the slideshow
231                // rotates -- are known synchronously by the caller; only the
232                // player's readiness is async, and that does not change the wording.
233                playingLabel: merged ? 'Pause slideshow and video' : 'Pause background video',
234                pausedLabel: merged ? 'Play slideshow and video' : 'Play background video',
235                onPause: function () {
236                    if (merged) { sliderPause(); }
237                    videoPause();
238                },
239                onPlay: function () {
240                    if (merged) { sliderPlay(); }
241                    videoPlay();
242                }
243            });
244        }
245
246        /**
247         * Called by each provider branch once its player resolves.
248         *
249         * Video-only mode: this is what gates the button's existence, exactly as
250         * before -- an unreachable player means no button, because there would be
251         * nothing it could stop.
252         *
253         * Merged mode: the button already exists, so this only wires the video
254         * half in. If the user pressed pause while the player was still
255         * resolving, apply that pause now -- otherwise the video would start
256         * playing under a button reading "Play ...".
257         */
258        function setVideoHandlers(pauseFn, playFn) {
259            videoPause = pauseFn;
260            videoPlay = playFn;
261
262            if (!built) {
263                build();
264                return;
265            }
266            if (control && control.isPaused()) {
267                pauseFn();
268            }
269        }
270
271        if (merged) {
272            build();
273            // The slider is resolved lazily rather than captured here: Background
274            // Video initialises its FlexSlider from an IntersectionObserver
275            // (BackgroundVideo.ascx:31, :115), so it can come into existence AFTER
276            // this button does. This poll exists purely for the state-sync case --
277            // a slider that arrives while the control is already paused must start
278            // paused, not rotate under a button reading "Play ...". Clicks resolve
279            // the slider on their own through getSlider().
280            whenReady(getSlider, function (slider) {
281                if (control && control.isPaused()) {
282                    sliderPause(slider);
283                }
284            });
285        }
286
287        if (source === 'vimeo') {
288            ensureVimeoSdk(function () {
289                var iframe = $scope.find('iframe').get(0);
290                if (!iframe) {
291                    return;
292                }
293                var player;
294                try {
295                    player = new window.Vimeo.Player(iframe);
296                } catch (e) {
297                    return;
298                }
299                setVideoHandlers(
300                    function () { try { player.pause(); } catch (e) { } },
301                    function () { try { player.play(); } catch (e) { } }
302                );
303            });
304            return;
305        }
306
307        if (source === 'wistia') {
308            window._wq = window._wq || [];
309            window._wq.push({
310                id: options.videoId || '_all',
311                onReady: function (video) {
312                    setVideoHandlers(
313                        function () { try { video.pause(); } catch (e) { } },
314                        function () { try { video.play(); } catch (e) { } }
315                    );
316                }
317            });
318            return;
319        }
320
321        // YouTube. The player handle is created by the widget's own
322        // onYouTubeIframeAPIReady callback; wait for it rather than assume it.
323        whenReady(function () {
324            var player = null;
325            if (typeof options.getYouTubePlayer === 'function') {
326                player = options.getYouTubePlayer();
327            }
328            // A YT.Player object exists before its iframe is bound;
329            // pauseVideo only appears once the API has attached.
330            return (player && typeof player.pauseVideo === 'function') ? player : null;
331        }, function (player) {
332            setVideoHandlers(
333                function () { try { player.pauseVideo(); } catch (e) { } },
334                function () { try { player.playVideo(); } catch (e) { } }
335            );
336        });
337    }
338
339    window.RPVideoPause = { attach: attach };
340
341}(window, window.jQuery));

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.