Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a video player component #223

Merged
merged 9 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/organisms/TableV2/TableV2.css.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 54 additions & 0 deletions src/components/organisms/VideoPlayer/VideoPlayer.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/components/organisms/VideoPlayer/VideoPlayer.css.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

219 changes: 219 additions & 0 deletions src/components/organisms/VideoPlayer/VideoPlayer.js
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 };
57 changes: 57 additions & 0 deletions src/components/organisms/VideoPlayer/VideoPlayer.scss
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;
}
2 changes: 2 additions & 0 deletions src/components/organisms/VideoPlayer/cod-videoplayer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import VideoPlayer from './VideoPlayer';
customElements.define('cod-videoplayer', VideoPlayer);
Loading
Loading