UNPKG

formstone

Version:

Library of modular front end components.

609 lines (463 loc) 13.3 kB
import MediaQuery from './mediaquery.js'; import { isFn, isObj, extend, // element, select, iterate, // once, trigger, // addClass, removeClass, // getAttr, setAttr, } from './utils.js'; // Class class Background { static #_guid = 1; static #_defaults = { // alt: '', autoPlay: true, lazy: false, lazyOffset: '100px', loop: true, mute: true, source: null, youtubeOptions: {} }; // static defaults(options) { this.#_defaults = extend(true, this.#_defaults, options); } static construct(selector, options) { let targets = select(selector); iterate(targets, (el) => { if (!el.Background) { new Background(el, options); } }); return targets; } // constructor(el, options) { if (el.Background) { console.warn('Background: Instance already exists', el); return; } // Parse JSON Options let optionsData = {}; let dataset = el.dataset; try { optionsData = JSON.parse(dataset.backgroundOptions || '{}'); } catch (e) { console.warn('Background: Error parsing options JSON', el); } // Internal Data Object.assign(this, extend(true, this.constructor.#_defaults, options || {}, optionsData)); this.el = el; this.guid = this.constructor.#_guid++; this.guidClass = `fs-background-element-${this.guid}`; this.layerGuid = 0; // this.container = element('div'); addClass(this.container, 'fs-background-container'); setAttr(this.container, { 'aria-hidden': 'true', 'role': 'presentation', 'tabindex': '-1', }); el.append(this.container); addClass(el, 'fs-background', this.guidClass); if (this.lazy) { addClass(el, 'fs-background-lazy'); this.observer = new IntersectionObserver((entries, observer) => { iterate(entries, (entry) => { this.#observe(entry); }); }, { // root: this.parentEl || null, root: null, threshold: 0, rootMargin: `0px 0px ${this.lazyOffset} 0px` }); this.observer.observe(el); } else { this.#initialLoad(this.source); } el.Background = this; } // destroy() { this.container.remove(); removeClass(this.el, 'fs-background', this.guidClass); this.el.Background = null; delete this.el.Background; } // #observe(entry) { if (entry.isIntersecting) { this.observer.unobserve(this.el); this.observer = null; this.#initialLoad(this.source); } } #initialLoad() { let source = this.source; this.source = ''; this.load(source); } // load(source) { addClass(this.el, 'fs-background-loaded'); if (source !== this.source) { this.source = source; this.sources = null; this.responsive = false; this.player = null; this.playerReady = true; this.playing = false; this.isVideo = (isObj(source) && !!(source.mp4 || source.ogg || source.webm)); this.isYouTube = (!this.isVideo && isObj(source) && !!source.youtube); this.isImage = (!this.isVideo && !this.isYouTube); // Load Image if (this.isImage) { let newSource = source; // Responsive image handling if (isObj(source)) { let sources = []; let keys = []; iterate(Object.keys(source), (key) => { keys.push(key); }); keys.sort((a, b) => { return parseInt(a, 10) - parseInt(b, 10); }); for (let i = 0; i < keys.length; i++) { let key = keys[i]; let width = parseInt(key, 10); let mq = `(min-width: ${width}px)`; if (i + 1 < keys.length) { let maxWidth = parseInt(keys[i + 1], 10) - 1; mq = `(min-width: ${width}px) and (max-width: ${maxWidth}px)`; } sources.push({ width: width, url: source[key], // mq: window.matchMedia(`(min-width: ${mq}px)`) mediaquery: MediaQuery.bind(`background-${this.guid}-${mq}`, mq, { enter: () => { this.#loadImage(source[key]); } }) }); } this.sources = sources; // TODO do we need to track this way? } else { // Single image this.#loadImage(newSource); } } // Load Video if (this.isVideo) { this.#loadVideo(source); } // Load YouTube if (this.isYouTube) { this.#loadYouTube(source); } } else { trigger(this.el, 'background:loaded'); } } // #loadImage(source) { this.layerGuid++; let media = element('div'); media.ariaHidden = true; media.className = `fs-background-media fs-background-image`; // fs-background-layer-${this.layerGuid}`; media.style.opacity = 0; media.style.backgroundImage = `url(${source})`; setAttr(media, 'data-background-layer', this.layerGuid); this.container.append(media); let image = new Image(); // image.onload = () => { this.#onLoad(media); }; image.src = source; this.source = source; } // #loadVideo(source) { if (source.poster) { this.#loadImage(source.poster); } this.layerGuid++; let media = element('div'); let video = element('video'); media.ariaHidden = true; media.className = 'fs-background-media fs-background-video'; media.style.opacity = 0; setAttr(media, 'data-background-layer', this.layerGuid); setAttr(video, { 'playsinline': '', 'playsinline': 'true', 'preload': 'auto', 'role': 'presentation', 'tabindex': '-1', }); if (this.loop) { video.loop = true; } if (this.mute) { video.muted = true; } if (this.autoPlay) { video.autoplay = true; } if (source.webm) { let src = element('source'); src.type = 'video/webm'; src.src = source.webm; video.append(src); } if (source.mp4) { let src = element('source'); src.type = 'video/mp4'; src.src = source.mp4; video.append(src); } if (source.ogg) { let src = element('source'); src.type = 'video/ogg'; src.src = source.ogg; video.append(src); } // once(video, 'loadeddata', (e) => { this.#onLoad(media); if (this.autoPlay) { this.play(); } }); media.append(video); this.container.append(media); this.source = source; } // #loadYouTube(source) { // let parts = source.youtube.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/i); let parts = source.youtube.match(/(?:(?:youtube\.com|youtube-nocookie\.com)\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/i); this.youTubeId = parts[1]; let ytURL = 'youtube.com'; if (!source.poster) { // source.poster = '//img.youtube.com/vi/' + this.youTubeId + '/0.jpg'; source.poster = `//img.${ytURL}/vi/${this.youTubeId}/maxresdefault.jpg`; } this.#loadImage(source.poster); this.layerGuid++; if (!select(`script[src*="${ytURL}/iframe_api"]`).length) { let script = element('script'); script.src = `//www.${ytURL}/iframe_api`; document.head.append(script); } if (!YouTubeReady) { YouTubeQueue.push({ el: this, source: source }); } else { let guid = `fs-background-${this.guid}-${this.layerGuid}`; let media = element('div'); let video = element('div'); video.id = guid; media.ariaHidden = true; media.className = 'fs-background-media fs-background-embed'; media.style.opacity = 0; setAttr(media, 'data-background-layer', this.layerGuid); media.append(video); this.container.append(media); let options = extend(true, {}, { controls: 0, rel: 0, showinfo: 0, wmode: 'transparent', enablejsapi: 1, version: 3, playerapiid: guid, loop: (this.loop) ? 1 : 0, // autoplay: 1, mute: 1, origin: `${window.location.protocol}//${window.location.host}` }, this.youtubeOptions, { autoplay: 1 // Must be enabled for initial API callbacks to fire }); this.player = new window.YT.Player(guid, { host: `https://www.${ytURL.replace('.com', '-nocookie.com')}`, videoId: this.youTubeId, playerVars: options, events: { onReady: (e) => { // console.log('onReady', e); let iframe = select('iframe', this.container); setAttr(iframe, { 'role': 'presentation', 'tabindex': '-1', }); this.playerReady = true; // data.player.setPlaybackQuality('highres'); if (this.mute) { this.player.mute(); } if (this.autoPlay) { this.player.playVideo(); } else { this.player.pauseVideo(); } // TODO trigger ready event? }, onStateChange: (e) => { // console.log('onStateChange', e); // -1 = unstarted // 0 = ended // 1 = playing // 2 = paused // 3 = buffering // 4 = // 5 = cued if (!this.playing && e.data === window.YT.PlayerState.PLAYING) { this.playing = true; this.#onLoad(media); } else if (this.loop && this.playing && e.data === window.YT.PlayerState.ENDED) { // fix looping option this.player.playVideo(); } // Fix for Safari's overly secure security settings // this.el.find(Classes.embed) // .addClass(RawClasses.ready); }, onPlaybackQualityChange: (e) => { // console.log('onPlaybackQualityChange', e); }, onPlaybackRateChange: (e) => { // console.log('onPlaybackRateChange', e); }, onError: (e) => { // console.log('onError', e); }, onApiChange: (e) => { // console.log('onApiChange', e); } } }); } } // play() { if (this.isYouTube) { if (this.playerReady) { this.player.playVideo(); } else { this.autoPlay = true; } } else if (this.isVideo) { let video = select('video', this.container); if (video.length) { video[0].play(); } } // TODO trigger play event? this.playing = true; } pause() { if (this.isYouTube) { if (this.playerReady) { this.player.pauseVideo(); } else { this.autoPlay = false; } } else if (this.video) { let video = select('video', this.container); if (video.length) { video[0].pause(); } } // TODO trigger pause event? this.playing = false; } mute() { if (this.isYouTube && this.playerReady) { this.player.mute(); // this.player.setVolume(0); } else if (this.video) { let video = select('video', this.container); if (video.length) { video[0].muted = true; } } // TODO trigger mute event? this.mute = true; } // Mute / Unmute is now blocked unless the user interacts with the video unmute() { if (this.isYouTube && this.playerReady) { this.player.unMute(); // this.player.setVolume(100); if (this.playing) { this.player.playVideo(); } } else if (this.video) { let video = select('video', this.container); if (video.length) { video[0].muted = false; } } // TODO trigger unmute event? this.mute = false; } // TODO unload method and event? // TODO set background video volume and event? // #onLoad(media) { let layer = getAttr(media, 'data-background-layer'); once(media, 'transitionend', (e) => { if (this.onLoadTimer) { clearTimeout(this.onLoadTimer); this.onLoadTimer = null; } this.onLoadTimer = setTimeout(() => { let extra = select(`div:not([data-background-layer="${layer}"]):not([data-background-layer="${this.layerGuid}"])`, this.container); iterate(extra, (node) => { node.remove(); }); }, 500); }); media.style.opacity = 1; // TODO handle errors and trigger events? trigger(this.el, 'background:loaded'); } }; // YouTube Helper let YouTubeReady = false; let YouTubeQueue = []; function onYouTubeReady() { YouTubeReady = true; iterate(YouTubeQueue, (item) => { item.el.load(item.source); }); YouTubeQueue = []; } let originalYouTubeCallback = (isFn(window.onYouTubeIframeAPIReady)) ? window.onYouTubeIframeAPIReady : null; window.onYouTubeIframeAPIReady = () => { if (originalYouTubeCallback) { originalYouTubeCallback.call(); } onYouTubeReady(); }; // Export export default Background;