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 2 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 @@ -143,7 +143,12 @@ class MediaContainer extends MediaChromeHTMLElement {

// 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
8 changes: 6 additions & 2 deletions src/js/media-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,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 Expand Up @@ -409,7 +412,8 @@ function propagateMediaState(nodeList, stateName, val) {
}

// Make sure custom els are ready
if (childName.includes('-') && !window.customElements.get(childName)) {
// Even if customElements.get returns something, it may not be ready!
if (childName.includes('-')) {
window.customElements.whenDefined(childName).then(setAndPropagate);
} else {
setAndPropagate();
Expand Down
18 changes: 18 additions & 0 deletions themes/example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<html>
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: We should probably keep "examples" separate from "source"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

👍

<head>
<script type="module" src="../index.js"></script>
<script type="module" src="./media-theme-netflix.js"></script>
</head>
<body>

<h2>Example: Using the custom element directly</h2>

<media-theme-netflix>
<video
slot="media"
src="https://stream.mux.com/DS00Spx1CV902MCtPj5WknGlR102V5HFkDe/high.mp4"
></video>
</media-theme-netflix>

</body>
</html>
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: missing newline for EOF

164 changes: 164 additions & 0 deletions themes/media-theme-netflix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import MediaTheme from './media-theme.js';
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, but there are several inconsistencies with the current browser-based netflix player. Should make followup issue(s) to clean up. Also may want to consider splitting out the part that sizes this to full width from the rest of the theme.


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>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the thing that feels nuckin futs. Chaining slots. But it works!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Whoa.

So from the outside, an application does this:

<media-theme-netflix>
  <video slot="media"></video>
</media-theme-netflix>

That "media" slot gets passed through into the "media" slot for media-controller. Pretty nuckin futs


<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={}) {
options = Object.assign({
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: no need to mutate options (may cause unexpected side effects for implementors). consider instead simply inline object spread, a la:

super(template, { /* allow ...defaultOptions, */ ...options });

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

How is options getting mutated? Either way I'm good with the spread.

// allow options
}, options);

super(template, options);
}
}

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

export default MediaThemeNetflix;
31 changes: 31 additions & 0 deletions 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({
Copy link
Collaborator

Choose a reason for hiding this comment

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

see comment ☝️

// 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;