-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #223 from CityOfDetroit/issue.220-videoplayer
Create a video player component
- Loading branch information
Showing
7 changed files
with
396 additions
and
1 deletion.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
import styles from '!!raw-loader!./VideoPlayer.css'; | ||
import varStyles from '!!raw-loader!../../../shared/variables.css'; | ||
import bootstrapStyles from '!!raw-loader!../../../shared/themed-bootstrap.css'; | ||
|
||
const template = document.createElement('template'); | ||
template.innerHTML = ` | ||
<div id="layoutContainer" class="d-flex justify-content-center"> | ||
<button type="button" class="btn" id="modalOpenButton"> | ||
<div class="container-fluid video-placehold player-container"></div> | ||
</button> | ||
<!-- Next line is an example of an open modal. --> | ||
<!--<div class="modal fade show" id="videoPlayerModal" tabindex="-1" aria-labelledby="videoPlayerModalLabel" style="display: block;" aria-modal="true" role="dialog">--> | ||
<div class="modal fade" id="videoPlayerModal" tabindex="-1" aria-labelledby="videoPlayerModalLabel" aria-hidden="true"> | ||
<div class="modal-dialog"> | ||
<div class="modal-content"> | ||
<div class="modal-header"> | ||
<h1 class="modal-title visually-hidden" id="videoPlayerModalLabel">Video Title</h1> | ||
<button id="modalCloseButton" type="button" class="btn-close" aria-label="Close"></button> | ||
</div> | ||
<div class="modal-body"> | ||
<div id="ytPlayerContainer"></div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
`; | ||
|
||
class VideoPlayer extends HTMLElement { | ||
static observedAttributes = []; | ||
|
||
constructor() { | ||
// Always call super first in constructor | ||
super(); | ||
// Create a shadow root | ||
const shadow = this.attachShadow({ mode: 'open' }); | ||
shadow.appendChild(template.content.cloneNode(true)); | ||
|
||
// Set the playercontainer from the template for later use. | ||
this.playerContainer = this.shadowRoot.querySelector('#ytPlayerContainer'); | ||
this.player = null; | ||
|
||
// Add styles | ||
const bootStyles = document.createElement('style'); | ||
bootStyles.textContent = bootstrapStyles; | ||
const variableStyles = document.createElement('style'); | ||
variableStyles.textContent = varStyles; | ||
const itemStyles = document.createElement('style'); | ||
itemStyles.textContent = styles; | ||
shadow.appendChild(bootStyles); | ||
shadow.appendChild(variableStyles); | ||
shadow.appendChild(itemStyles); | ||
} | ||
|
||
connectedCallback() { | ||
this._buildThumbnailDisplayMode(); | ||
this._replacePlaceholderWithThumbnail(); | ||
|
||
const playerDisplayMode = this.getAttribute('player-display'); | ||
const videoType = this.getAttribute('video-type'); | ||
const videoId = this.getAttribute('video-id'); | ||
switch (playerDisplayMode) { | ||
case 'modal': { | ||
this._setModalOpenCloseEventHandlers(); | ||
const videoPlayerLabel = this.shadowRoot.querySelector( | ||
'#videoPlayerModalLabel', | ||
); | ||
videoPlayerLabel.textContent = this.getAttribute('title'); | ||
|
||
switch (videoType) { | ||
case 'youtube': { | ||
this._loadVideo(videoId); | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Updates the thumbnail display mode based on the 'thumbnail-display' attribute value. | ||
* @private | ||
*/ | ||
_buildThumbnailDisplayMode() { | ||
const thumbnailDisplayMode = this.getAttribute('thumbnail-display'); | ||
const layoutContainer = this.shadowRoot.querySelector('#layoutContainer'); | ||
switch (thumbnailDisplayMode) { | ||
case 'fullwidth': { | ||
layoutContainer.classList.add('w-100'); | ||
break; | ||
} | ||
case 'inline': { | ||
layoutContainer.classList.remove('d-flex'); | ||
layoutContainer.classList.add('d-inline-flex'); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Sets the event handlers for opening and closing the modal. | ||
* @private | ||
*/ | ||
_setModalOpenCloseEventHandlers() { | ||
const modalOpenButton = this.shadowRoot.querySelector('#modalOpenButton'); | ||
modalOpenButton.addEventListener('click', this._onOpenModal.bind(this)); | ||
|
||
const modalCloseButton = this.shadowRoot.querySelector('#modalCloseButton'); | ||
modalCloseButton.addEventListener('click', this._onCloseModal.bind(this)); | ||
|
||
const modal = this.shadowRoot.querySelector('#videoPlayerModal'); | ||
modal.addEventListener('click', this._onClickOutsideModal.bind(this)); | ||
} | ||
|
||
/** | ||
* Replaces the placeholder with a thumbnail image and play icon. | ||
* @private | ||
*/ | ||
_replacePlaceholderWithThumbnail() { | ||
const playerContainer = this.shadowRoot.querySelector( | ||
'div.video-placehold', | ||
); | ||
const imgSrc = this.getAttribute('thumbnail-src'); | ||
const imgAlt = this.getAttribute('thumbnail-alt'); | ||
const imgElt = document.createElement('img'); | ||
imgElt.setAttribute('src', imgSrc); | ||
imgElt.setAttribute('alt', imgAlt); | ||
imgElt.classList.add('video-placehold', 'img-fluid'); | ||
playerContainer.appendChild(imgElt); | ||
const playIcon = document.createElement('div'); | ||
playIcon.classList.add('play-icon'); | ||
playerContainer.appendChild(playIcon); | ||
playerContainer.classList.remove('video-placehold'); | ||
} | ||
|
||
disconnectedCallback() { | ||
this.removeEventListener('click', this._onOpenModal.bind(this)); | ||
this.removeEventListener('click', this._onCloseModal.bind(this)); | ||
} | ||
|
||
/** | ||
* Opens the video player modal. | ||
* @private | ||
*/ | ||
_onOpenModal() { | ||
const videoModal = this.shadowRoot.querySelector('#videoPlayerModal'); | ||
videoModal.classList.add('show'); | ||
videoModal.style.display = 'block'; | ||
videoModal.removeAttribute('aria-hidden'); | ||
videoModal.setAttribute('aria-modal', 'true'); | ||
videoModal.setAttribute('role', 'modal'); | ||
this.player?.playVideo(); | ||
} | ||
|
||
/** | ||
* Closes the video player modal and performs necessary cleanup actions. | ||
* @private | ||
*/ | ||
_onCloseModal() { | ||
const videoModal = this.shadowRoot.querySelector('#videoPlayerModal'); | ||
videoModal.classList.remove('show'); | ||
videoModal.style.display = 'none'; | ||
videoModal.removeAttribute('aria-modal'); | ||
videoModal.removeAttribute('role'); | ||
videoModal.setAttribute('aria-hidden', 'true'); | ||
this.player?.pauseVideo(); | ||
} | ||
|
||
/** | ||
* Handles the click event outside the modal content. | ||
* | ||
* @param {Event} event - The click event object. | ||
* @returns {void} | ||
*/ | ||
_onClickOutsideModal(event) { | ||
if (!event.target.closest('.modal-content')) { | ||
this._onCloseModal(); | ||
} | ||
} | ||
|
||
/** | ||
* Loads the video with the specified videoId. | ||
* If the YouTube API is already loaded, it creates the player immediately. | ||
* Otherwise, it loads the YouTube API and creates the player once it's ready. | ||
* | ||
* @param {string} videoId - The ID of the video to load. | ||
*/ | ||
_loadVideo(videoId) { | ||
if (window.YT) { | ||
this._createPlayer(videoId); | ||
} else { | ||
const tag = document.createElement('script'); | ||
tag.src = 'https://www.youtube.com/iframe_api'; | ||
const firstScriptTag = document.getElementsByTagName('script')[0]; | ||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); | ||
|
||
window.onYouTubeIframeAPIReady = () => { | ||
this._createPlayer(videoId); | ||
}; | ||
} | ||
} | ||
|
||
/** | ||
* Creates a YouTube player instance and initializes it with the specified video. | ||
* | ||
* @param {string} videoId - The ID of the YouTube video to be played. | ||
* @returns {void} | ||
*/ | ||
_createPlayer(videoId) { | ||
// YT is defined by youtube iFrame API 3rd party script. | ||
// eslint-disable-next-line no-undef | ||
this.player = new YT.Player(this.playerContainer, { | ||
videoId: videoId, | ||
events: {}, | ||
}); | ||
} | ||
} | ||
|
||
export { VideoPlayer as default }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
@import 'bootstrap/scss/functions'; | ||
@import 'bootstrap/scss/variables'; | ||
@import 'bootstrap/scss/mixins/breakpoints'; | ||
|
||
div.modal { | ||
--bs-modal-width: 90vw; | ||
} | ||
|
||
@include media-breakpoint-up(lg) { | ||
div.modal { | ||
--bs-modal-width: 50vw; | ||
} | ||
} | ||
|
||
.modal { | ||
background-color: rgba(0, 0, 0, 0.5); | ||
} | ||
|
||
div.video-placehold { | ||
background-color: lightgray; | ||
min-height: 343px; | ||
} | ||
|
||
div.player-container { | ||
position: relative; | ||
} | ||
|
||
div.player-container img.video-placehold { | ||
display: block; | ||
} | ||
|
||
div.player-container .play-icon { | ||
position: absolute; | ||
top: 50%; | ||
left: 50%; | ||
transform: translate(-50%, -50%); | ||
width: 50px; | ||
height: 50px; | ||
} | ||
|
||
.play-icon::before { | ||
content: ''; | ||
position: absolute; | ||
top: 50%; | ||
left: 50%; | ||
transform: translate(-50%, -50%); | ||
width: 100%; | ||
height: 100%; | ||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-play-circle-fill' viewBox='0 0 16 16'%3E%3Cpath d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.79 5.093A.5.5 0 0 0 6 5.5v5a.5.5 0 0 0 .79.407l3.5-2.5a.5.5 0 0 0 0-.814z'/%3E%3C/svg%3E"); | ||
background-repeat: no-repeat; | ||
background-size: contain; | ||
} | ||
|
||
iframe#ytPlayerContainer { | ||
width: 100%; | ||
aspect-ratio: 16/9; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import VideoPlayer from './VideoPlayer'; | ||
customElements.define('cod-videoplayer', VideoPlayer); |
Oops, something went wrong.