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

Theme architecture proposal #42

Merged
merged 6 commits into from
Oct 17, 2021
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
7 changes: 6 additions & 1 deletion src/js/media-container.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,12 @@ class MediaContainer extends window.HTMLElement {

// First direct child with slot=media, or null
get media() {
return this.querySelector(':scope > [slot=media]');
let media = this.querySelector(':scope > [slot=media]');

// Chaining media slots for media templates
if (media.nodeName == 'SLOT') media = media.assignedElements({flatten:true})[0];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine for now. May be some weird edge cases that are missed, but we can file followup issues if/when they show up and are a priority.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Such a pessimist. :) It's like you've had to work in the browser before or something.


return media;
}

mediaSetCallback(media) {
Expand Down
7 changes: 5 additions & 2 deletions src/js/media-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/
import MediaContainer from './media-container.js';
import { defineCustomElement } from './utils/defineCustomElement.js';
import { Window as window } from './utils/server-safe-globals.js';
import { Window as window, Document as document } from './utils/server-safe-globals.js';
import { fullscreenApi } from './utils/fullscreenApi.js';
import { constToCamel } from './utils/stringUtils.js';

Expand Down Expand Up @@ -89,7 +89,10 @@ class MediaController extends MediaContainer {
super[fullscreenApi.enter]();
},
MEDIA_EXIT_FULLSCREEN_REQUEST: () => {
this.getRootNode()[fullscreenApi.exit]();
document[fullscreenApi.exit]();

// Shadow root throws an error for this function
// this.getRootNode()[fullscreenApi.exit]();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know why we were using this.getRootNode() here originally? will this work for iframe embeds? Noticed we were using getRootNode() in several places. If this is safe, should we be changing it in other locations as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like it's gonna be context specific, depending on if we need the shadowRoot when that's the element's context and if it supports the needed APIs.

},
MEDIA_ENTER_PIP_REQUEST: () => {
const docOrRoot = this.getRootNode();
Expand Down
169 changes: 169 additions & 0 deletions src/js/themes/media-theme-netflix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
<media-theme-netflix>
<video
slot="media"
src="https://stream.mux.com/DS00Spx1CV902MCtPj5WknGlR102V5HFkDe/high.mp4"
></video>
</media-theme-netflix>
*/

import MediaTheme from './media-theme.js';

const template = `
<style>
body {
font-family: "Helvetica Neue", sans-serif;
}

media-controller {
width: 100%;
/* Keep the buttons from overflowing */
min-width: 420px;
height: 720px;

--media-range-thumb-background: rgba(255,0,0, 1);
--media-range-track-height: 4px;
--media-range-track-transition: height .2s ease;
--media-range-track-background: #555;
--media-range-bar-color: rgb(229, 9, 20);

--media-button-icon-width: 50px;
--media-button-icon-height: 50px;
}

media-time-range {
width: 100%;
height: 25px;
margin-bottom: 10px;
--media-range-thumb-height: 20px;
--media-range-thumb-width: 20px;
--media-range-thumb-border-radius: 20px;
--media-time-buffered-color: #777;
}

media-time-range:hover {
--media-range-track-height: 9px;
}

media-control-bar {
background: none;
justify-content: space-between;
-webkit-box-align: center;
-ms-align-items: center;
align-items: center;
display: flex;
-webkit-box-pack: justify;
flex-wrap: nowrap;
}

media-control-bar > * {
background: none;
display: flex;
flex: 0 1 auto;
width: 60px;
min-width: 60px;
height: 80px;
padding-bottom: 20px;
margin: 0 3px;

--media-button-icon-transform: scale(1.2);
--media-button-icon-transition: transform .2s ease;
}

/* For some reason media-control-bar > *:hover doesn't work...
while media-control-bar > *:focus-within does.
And also media-control-bar > *:not(:hover)
Really annoying. Need to submit a bug. */
media-control-bar > *:not(:hover) {
--media-button-icon-transform: scale(1);
--media-button-icon-transition: transform .2s ease;
}

media-play-button,
media-seek-backward-button,
media-seek-forward-button,
media-mute-button,
media-fullscreen-button {
height: 80px;
}

media-fullscreen-button {
margin-right: 10px;
}

media-control-bar > *:focus,
media-control-bar > *:focus-within {
outline: 0;
}

media-volume-range {
width: 100px;
}

.videoTitle {
flex-grow: 1;
height: 80px;
line-height: 64px;
vertical-align: middle;
overflow: hidden;
padding: 0 10px;
min-width: 0;
}

.videoTitleText {
width: 100%;
vertical-align: middle;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}

.videoTitleText h4 {
margin: 0;
display: inline-block;
white-space: nowrap;
}

.videoTitle span {
display: inline;
font-weight: 200;
margin-left: 5px;
}

</style>

<media-controller>

<slot name="media" slot="media"></slot>

<media-time-range></media-time-range>
<media-control-bar>
<media-play-button></media-play-button>
<media-seek-backward-button></media-seek-backward-button>
<media-seek-forward-button></media-seek-forward-button>
<media-mute-button></media-mute-button>
<media-volume-range></media-volume-range>
<div class="videoTitle">
<div class="videoTitleText">
<h4>My Title</h4><span>P2:E4 Episode 4</span>
</div>
</div>
<media-fullscreen-button>
<svg slot="enter" viewBox="0 0 28 28"><g transform="translate(2, 6)"><polygon points="8 0 6 0 5.04614258 0 0 0 0 5 2 5 2 2 8 2"></polygon><polygon transform="translate(4, 13.5) scale(1, -1) translate(-4, -13.5) " points="8 11 6 11 5.04614258 11 0 11 0 16 2 16 2 13 8 13"></polygon><polygon transform="translate(20, 2.5) scale(-1, 1) translate(-20, -2.5) " points="24 0 22 0 21.0461426 0 16 0 16 5 18 5 18 2 24 2"></polygon><polygon transform="translate(20, 13.5) scale(-1, -1) translate(-20, -13.5) " points="24 11 22 11 21.0461426 11 16 11 16 16 18 16 18 13 24 13"></polygon></g></svg>
<svg slot="exit" viewBox="0 0 28 28"><g transform="translate(3, 6)"><polygon transform="translate(19.000000, 3.000000) scale(-1, 1) translate(-19.000000, -3.000000) " points="22 0 20 0 20 4 16 4 16 6 22 6"></polygon><polygon transform="translate(19.000000, 13.000000) scale(-1, -1) translate(-19.000000, -13.000000) " points="22 10 20 10 20 14 16 14 16 16 22 16"></polygon><polygon points="6 0 4 0 4 4 0 4 0 6 6 6"></polygon><polygon transform="translate(3.000000, 13.000000) scale(1, -1) translate(-3.000000, -13.000000) " points="6 10 4 10 4 14 0 14 0 16 6 16"></polygon></g></svg>
</media-fullscreen-button>
</media-control-bar>
</media-controller>
`;

class MediaThemeNetflix extends MediaTheme {
constructor(options={}) {
super(template, { /* allow ...defaultOptions, */ ...options });
}
}

if (!customElements.get('media-theme-netflix')) {
customElements.define('media-theme-netflix', MediaThemeNetflix);
}

export default MediaThemeNetflix;
31 changes: 31 additions & 0 deletions src/js/themes/media-theme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

class MediaTheme extends HTMLElement {
constructor(template=``, options={}) {
super();

options = Object.assign({
// Default options
}, options);

// Expose the template publicaly for other uses
this.template = template;

// Not sure if this is best practice or if we should just
// innerHTML the template string in the shadow dom
const templateEl = document.createElement('template');
templateEl.innerHTML = template;

// Clone the template in the shadow dom
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(templateEl.content.cloneNode(true));

// Expose the media controller if API access is needed
this.mediaController = shadow.querySelector('media-controller');
}
}

if (!customElements.get('media-theme')) {
customElements.define('media-theme', MediaTheme);
}

export default MediaTheme;