add modal dialog to fix fullscreen video

This commit is contained in:
Nolan Lawson 2018-01-20 19:19:24 -08:00
parent 8951b1ba48
commit adfa5d5fb5
9 changed files with 179 additions and 19 deletions

5
package-lock.json generated
View file

@ -1295,6 +1295,11 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
}, },
"dialog-polyfill": {
"version": "0.4.9",
"resolved": "https://registry.npmjs.org/dialog-polyfill/-/dialog-polyfill-0.4.9.tgz",
"integrity": "sha512-iM4ZXRLOA/qpbW6XznGOq7Iq58JoXhZGo+OMG8K3wFxvIatnMTnIancLGEKa6WYy6oTkndfb2UXQ6FGtoUBn1g=="
},
"diffie-hellman": { "diffie-hellman": {
"version": "5.0.2", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz",

View file

@ -20,6 +20,7 @@
"concurrently": "^3.5.1", "concurrently": "^3.5.1",
"cross-env": "^5.1.3", "cross-env": "^5.1.3",
"css-loader": "^0.28.7", "css-loader": "^0.28.7",
"dialog-polyfill": "^0.4.9",
"express": "^4.16.2", "express": "^4.16.2",
"extract-text-webpack-plugin": "^3.0.2", "extract-text-webpack-plugin": "^3.0.2",
"fg-loadcss": "^2.0.1", "fg-loadcss": "^2.0.1",

View file

@ -2,12 +2,19 @@
{{#each mediaAttachments as media}} {{#each mediaAttachments as media}}
<div class="status-media-container {{hasNoNativeWidthHeight(media) ? 'no-native-width-height' : ''}}"> <div class="status-media-container {{hasNoNativeWidthHeight(media) ? 'no-native-width-height' : ''}}">
{{#if media.type === 'video'}} {{#if media.type === 'video'}}
<video poster="{{media.preview_url}}" <button type="button"
src="{{media.url}}" class="play-video-button"
aria-label="Play video"
on:click="onClickPlayVideoButton(media, getSmallWidth(media), getSmallHeight(media))">
<div class="svg-wrapper">
<svg>
<use xlink:href="#fa-play-circle" />
</svg>
</div>
<img src="{{media.preview_url}}"
width="{{getSmallWidth(media)}}" width="{{getSmallWidth(media)}}"
height="{{getSmallHeight(media)}}" height="{{getSmallHeight(media)}}" />
controls </button>
/>
{{elseif media.type === 'gifv'}} {{elseif media.type === 'gifv'}}
<video <video
poster="{{media.preview_url}}" poster="{{media.preview_url}}"
@ -59,11 +66,40 @@
max-width: calc(100vw - 40px); max-width: calc(100vw - 40px);
object-fit: cover; object-fit: cover;
} }
.play-video-button {
margin: 0;
padding: 0;
position: relative;
border-radius: 0;
border: none;
background: none;
}
.play-video-button .svg-wrapper {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 40;
pointer-events: none;
}
.play-video-button svg {
width: 72px;
height: 72px;
fill: var(--mask-svg-fill);
border-radius: 100%;
background: var(--mask-opaque-bg);
}
</style> </style>
<script> <script>
const DEFAULT_MEDIA_WIDTH = 300 const DEFAULT_MEDIA_WIDTH = 300
const DEFAULT_MEDIA_HEIGHT = 200 const DEFAULT_MEDIA_HEIGHT = 200
import VideoDialog from './VideoDialog.html'
export default { export default {
helpers: { helpers: {
getSmallWidth: media => media.meta && media.meta.small && typeof media.meta.small.width === 'number' ? media.meta.small.width : DEFAULT_MEDIA_WIDTH, getSmallWidth: media => media.meta && media.meta.small && typeof media.meta.small.width === 'number' ? media.meta.small.width : DEFAULT_MEDIA_WIDTH,
@ -72,6 +108,24 @@
}, },
computed: { computed: {
minMediaWidth: (mediaAttachments) => Math.min.apply(Math, mediaAttachments.map(media => media.meta && media.meta.small && typeof media.meta.small.width === 'number' ? media.meta.small.width : DEFAULT_MEDIA_WIDTH)) minMediaWidth: (mediaAttachments) => Math.min.apply(Math, mediaAttachments.map(media => media.meta && media.meta.small && typeof media.meta.small.width === 'number' ? media.meta.small.width : DEFAULT_MEDIA_WIDTH))
},
methods: {
async onClickPlayVideoButton(media, width, height) {
let dialog = document.createElement('dialog')
dialog.classList.add('video-dialog')
document.body.appendChild(dialog)
let videoDialog = new VideoDialog({
target: dialog,
data: {
poster: media.preview_url,
src: media.url,
dialog: dialog,
width: width,
height: height
}
})
videoDialog.showModal()
}
} }
} }
</script> </script>

View file

@ -0,0 +1,80 @@
<div class="video-dialog-wrapper">
<div class="close-video-button-wrapper">
<button class="close-video-button" aria-label="Close video" on:click="onCloseButtonClicked()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<video poster="{{poster}}"
src="{{src}}"
width="{{width}}"
height="{{height}}"
controls
/>
</div>
<style>
:global(.video-dialog) {
position: fixed;
top: 50%;
transform: translate(0, -50%);
background: #000;
padding: 0;
}
:global(.video-dialog-wrapper) {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
max-width: calc(100vw - 40px);
}
:global(.video-dialog video) {
max-width: 100%;
}
.close-video-button-wrapper {
text-align: right;
width: 100%;
background: var(--nav-bg)
}
.close-video-button {
margin: 0 0 2px;
padding: 0;
background: none;
border: none;
}
.close-video-button span {
padding: 0 15px;
font-size: 48px;
color: var(--button-primary-text);
}
:global(dialog.video-dialog::backdrop) {
background: rgba(51, 51, 51, 0.8);
}
:global(dialog.video-dialog + .backdrop) {
background: rgba(51, 51, 51, 0.8);
}
</style>
<script>
import { importDialogPolyfill } from '../_utils/asyncModules'
export default {
oncreate() {
this.registration = this.register()
},
methods: {
async register() {
if (typeof HTMLDialogElement === 'undefined') {
let dialogPolyfill = await importDialogPolyfill()
dialogPolyfill.registerDialog(this.get('dialog'))
}
},
async showModal() {
await this.registration
this.get('dialog').showModal()
},
onCloseButtonClicked() {
this.get('dialog').close()
document.body.removeChild(this.get('dialog'))
}
}
}
</script>

View file

@ -1,3 +1,5 @@
import { loadCSS } from 'fg-loadcss';
const importURLSearchParams = () => import( const importURLSearchParams = () => import(
/* webpackChunkName: 'url-search-params' */ 'url-search-params' /* webpackChunkName: 'url-search-params' */ 'url-search-params'
).then(Params => { ).then(Params => {
@ -25,10 +27,25 @@ const importIndexedDBGetAllShim = () => import(
/* webpackChunkName: 'indexeddb-getall-shim' */ 'indexeddb-getall-shim' /* webpackChunkName: 'indexeddb-getall-shim' */ 'indexeddb-getall-shim'
) )
const importDialogPolyfill = (() => {
let cached
return () => {
if (cached) {
return Promise.resolve(cached)
}
loadCSS('/dialog-polyfill.css') // TODO: handle error
return import(/* webpackChunkName: 'dialog-polyfill' */ 'dialog-polyfill').then(res => {
cached = res
return cached
})
}
})()
export { export {
importURLSearchParams, importURLSearchParams,
importTimeline, importTimeline,
importIntersectionObserver, importIntersectionObserver,
importRequestIdleCallback, importRequestIdleCallback,
importIndexedDBGetAllShim importIndexedDBGetAllShim,
importDialogPolyfill
} }

View file

@ -7,12 +7,6 @@
width: 100%; width: 100%;
border: 1px solid var(--settings-list-item-border); border: 1px solid var(--settings-list-item-border);
margin: 20px auto; margin: 20px auto;
max-width: 80%;
} }
@media (max-width: 767px) {
ul.settings-list {
max-width: 90%;
}
}
</style> </style>

View file

@ -54,6 +54,7 @@
--mask-bg: $toast-bg; --mask-bg: $toast-bg;
--mask-svg-fill: $secondary-text-color; --mask-svg-fill: $secondary-text-color;
--mask-opaque-bg: rgba($toast-bg, 0.8);
--deemphasized-text-color: #666; --deemphasized-text-color: #666;
} }

View file

@ -10,9 +10,9 @@
<style> <style>
/* auto-generated w/ build-sass.js */ /* auto-generated w/ build-sass.js */
body{--button-primary-bg:#6081e6;--button-primary-text:#fff;--button-primary-border:#132c76;--button-primary-bg-active:#456ce2;--button-primary-bg-hover:#6988e7;--button-bg:#e6e6e6;--button-text:#333;--button-border:#a7a7a7;--button-bg-active:#bfbfbf;--button-bg-hover:#f2f2f2;--input-border:#dadada;--anchor-text:#4169e1;--main-bg:#fff;--body-bg:#e8edfb;--body-text-color:#333;--main-border:#dadada;--svg-fill:#4169e1;--form-bg:#f7f7f7;--form-border:#c1c1c1;--nav-bg:#4169e1;--nav-border:#214cce;--nav-a-border:#4169e1;--nav-a-selected-border:#fff;--nav-a-selected-bg:#6d8ce8;--nav-svg-fill:#fff;--nav-text-color:#fff;--nav-a-selected-border-hover:#fff;--nav-a-selected-bg-hover:#839deb;--nav-a-bg-hover:#577ae4;--nav-a-border-hover:#4169e1;--nav-svg-fill-hover:#fff;--nav-text-color-hover:#fff;--action-button-fill-color:#839deb;--action-button-fill-color-hover:#99afef;--action-button-fill-color-active:#577ae4;--settings-list-item-bg:#fff;--settings-list-item-text:#4169e1;--settings-list-item-text-hover:#4169e1;--settings-list-item-border:#dadada;--settings-list-item-bg-active:#e6e6e6;--settings-list-item-bg-hover:#fafafa;--toast-bg:#333;--toast-border:#fafafa;--toast-text:#fff;--mask-bg:#333;--mask-svg-fill:#fff;--deemphasized-text-color:#666} body{--button-primary-bg:#6081e6;--button-primary-text:#fff;--button-primary-border:#132c76;--button-primary-bg-active:#456ce2;--button-primary-bg-hover:#6988e7;--button-bg:#e6e6e6;--button-text:#333;--button-border:#a7a7a7;--button-bg-active:#bfbfbf;--button-bg-hover:#f2f2f2;--input-border:#dadada;--anchor-text:#4169e1;--main-bg:#fff;--body-bg:#e8edfb;--body-text-color:#333;--main-border:#dadada;--svg-fill:#4169e1;--form-bg:#f7f7f7;--form-border:#c1c1c1;--nav-bg:#4169e1;--nav-border:#214cce;--nav-a-border:#4169e1;--nav-a-selected-border:#fff;--nav-a-selected-bg:#6d8ce8;--nav-svg-fill:#fff;--nav-text-color:#fff;--nav-a-selected-border-hover:#fff;--nav-a-selected-bg-hover:#839deb;--nav-a-bg-hover:#577ae4;--nav-a-border-hover:#4169e1;--nav-svg-fill-hover:#fff;--nav-text-color-hover:#fff;--action-button-fill-color:#839deb;--action-button-fill-color-hover:#99afef;--action-button-fill-color-active:#577ae4;--settings-list-item-bg:#fff;--settings-list-item-text:#4169e1;--settings-list-item-text-hover:#4169e1;--settings-list-item-border:#dadada;--settings-list-item-bg-active:#e6e6e6;--settings-list-item-bg-hover:#fafafa;--toast-bg:#333;--toast-border:#fafafa;--toast-text:#fff;--mask-bg:#333;--mask-svg-fill:#fff;--mask-opaque-bg:rgba(51,51,51,0.8);--deemphasized-text-color:#666}
body{margin:0;font-family:system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue;font-size:14px;line-height:1.3;color:var(--body-text-color);background:var(--body-bg);position:fixed;left:0;right:0;bottom:0;top:0}.container{overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;position:absolute;top:72px;left:0;right:0;bottom:0;will-change:transform}main{position:relative;width:602px;max-width:100vw;padding:15px 0;box-sizing:border-box;margin:15px auto 15px;background:var(--main-bg);border:1px solid var(--main-border);border-radius:1px}@media (max-width: 767px){main{margin:5px auto 15px}}h1,h2,h3,h4,h5,h6{margin:0 0 0.5em 0;font-weight:400;line-height:1.2}h1{font-size:2em}a{color:var(--anchor-text);text-decoration:none}a:visited{color:var(--anchor-text)}a:hover{text-decoration:underline}input{border:1px solid var(--input-border);padding:5px}button{font-size:1.2em;background:var(--button-bg);border-radius:2px;padding:10px 15px;border:1px solid var(--button-border);cursor:pointer;color:var(--button-text)}button:hover{background:var(--button-bg-hover)}button:active{background:var(--button-bg-active)}button[disabled]{opacity:0.35;pointer-events:none;cursor:not-allowed}button.primary{border:1px solid var(--button-primary-border);background:var(--button-primary-bg);color:var(--button-primary-text)}button.primary:hover{background:var(--button-primary-bg-hover)}button.primary:active{background:var(--button-primary-bg-active)}p,label,input{font-size:1.3em}ul,li,p{padding:0;margin:0}.hidden{opacity:0} body{margin:0;font-family:system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue;font-size:14px;line-height:1.3;color:var(--body-text-color);background:var(--body-bg);position:fixed;left:0;right:0;bottom:0;top:0}.container{overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;position:absolute;top:72px;left:0;right:0;bottom:0;will-change:transform}main{position:relative;width:602px;max-width:100vw;padding:15px 0;box-sizing:border-box;margin:15px auto 15px;background:var(--main-bg);border:1px solid var(--main-border);border-radius:1px}@media (max-width: 767px){main{margin:5px auto 15px}}h1,h2,h3,h4,h5,h6{margin:0 0 0.5em 0;font-weight:400;line-height:1.2}h1{font-size:2em}a{color:var(--anchor-text);text-decoration:none}a:visited{color:var(--anchor-text)}a:hover{text-decoration:underline}input{border:1px solid var(--input-border);padding:5px}button{font-size:1.2em;background:var(--button-bg);border-radius:2px;padding:10px 15px;border:1px solid var(--button-border);cursor:pointer;color:var(--button-text)}button:hover{background:var(--button-bg-hover)}button:active{background:var(--button-bg-active)}button[disabled]{opacity:0.35;pointer-events:none;cursor:not-allowed}button.primary{border:1px solid var(--button-primary-border);background:var(--button-primary-bg);color:var(--button-primary-text)}button.primary:hover{background:var(--button-primary-bg-hover)}button.primary:active{background:var(--button-primary-bg-active)}p,label,input{font-size:1.3em}ul,li,p{padding:0;margin:0}.hidden{opacity:0}
body.offline,body.theme-hotpants.offline,body.theme-majesty.offline,body.theme-oaken.offline,body.theme-scarlet.offline,body.theme-seafoam.offline{--button-primary-bg:#ababab;--button-primary-text:#fff;--button-primary-border:#4d4d4d;--button-primary-bg-active:#9c9c9c;--button-primary-bg-hover:#b0b0b0;--button-bg:#e6e6e6;--button-text:#333;--button-border:#a7a7a7;--button-bg-active:#bfbfbf;--button-bg-hover:#f2f2f2;--input-border:#dadada;--anchor-text:#999;--main-bg:#fff;--body-bg:#fafafa;--body-text-color:#333;--main-border:#dadada;--svg-fill:#999;--form-bg:#f7f7f7;--form-border:#c1c1c1;--nav-bg:#999;--nav-border:gray;--nav-a-border:#999;--nav-a-selected-border:#fff;--nav-a-selected-bg:#b3b3b3;--nav-svg-fill:#fff;--nav-text-color:#fff;--nav-a-selected-border-hover:#fff;--nav-a-selected-bg-hover:#bfbfbf;--nav-a-bg-hover:#a6a6a6;--nav-a-border-hover:#999;--nav-svg-fill-hover:#fff;--nav-text-color-hover:#fff;--action-button-fill-color:#bfbfbf;--action-button-fill-color-hover:#ccc;--action-button-fill-color-active:#a6a6a6;--settings-list-item-bg:#fff;--settings-list-item-text:#999;--settings-list-item-text-hover:#999;--settings-list-item-border:#dadada;--settings-list-item-bg-active:#e6e6e6;--settings-list-item-bg-hover:#fafafa;--toast-bg:#333;--toast-border:#fafafa;--toast-text:#fff;--mask-bg:#333;--mask-svg-fill:#fff;--deemphasized-text-color:#666} body.offline,body.theme-hotpants.offline,body.theme-majesty.offline,body.theme-oaken.offline,body.theme-scarlet.offline,body.theme-seafoam.offline{--button-primary-bg:#ababab;--button-primary-text:#fff;--button-primary-border:#4d4d4d;--button-primary-bg-active:#9c9c9c;--button-primary-bg-hover:#b0b0b0;--button-bg:#e6e6e6;--button-text:#333;--button-border:#a7a7a7;--button-bg-active:#bfbfbf;--button-bg-hover:#f2f2f2;--input-border:#dadada;--anchor-text:#999;--main-bg:#fff;--body-bg:#fafafa;--body-text-color:#333;--main-border:#dadada;--svg-fill:#999;--form-bg:#f7f7f7;--form-border:#c1c1c1;--nav-bg:#999;--nav-border:gray;--nav-a-border:#999;--nav-a-selected-border:#fff;--nav-a-selected-bg:#b3b3b3;--nav-svg-fill:#fff;--nav-text-color:#fff;--nav-a-selected-border-hover:#fff;--nav-a-selected-bg-hover:#bfbfbf;--nav-a-bg-hover:#a6a6a6;--nav-a-border-hover:#999;--nav-svg-fill-hover:#fff;--nav-text-color-hover:#fff;--action-button-fill-color:#bfbfbf;--action-button-fill-color-hover:#ccc;--action-button-fill-color-active:#a6a6a6;--settings-list-item-bg:#fff;--settings-list-item-text:#999;--settings-list-item-text-hover:#999;--settings-list-item-border:#dadada;--settings-list-item-bg-active:#e6e6e6;--settings-list-item-bg-hover:#fafafa;--toast-bg:#333;--toast-border:#fafafa;--toast-text:#fff;--mask-bg:#333;--mask-svg-fill:#fff;--mask-opaque-bg:rgba(51,51,51,0.8);--deemphasized-text-color:#666}
</style> </style>
@ -105,6 +105,12 @@ body.offline,body.theme-hotpants.offline,body.theme-majesty.offline,body.theme-o
<path d="M1536 1399q0 109-62.5 187t-150.5 78H469q-88 0-150.5-78T256 1399q0-85 8.5-160.5t31.5-152 58.5-131 94-89T583 832q131 128 313 128t313-128q76 0 134.5 34.5t94 89 58.5 131 31.5 152 8.5 160.5zm-256-887q0 159-112.5 271.5T896 896 624.5 783.5 512 512t112.5-271.5T896 128t271.5 112.5T1280 512z"></path> <path d="M1536 1399q0 109-62.5 187t-150.5 78H469q-88 0-150.5-78T256 1399q0-85 8.5-160.5t31.5-152 58.5-131 94-89T583 832q131 128 313 128t313-128q76 0 134.5 34.5t94 89 58.5 131 31.5 152 8.5 160.5zm-256-887q0 159-112.5 271.5T896 896 624.5 783.5 512 512t112.5-271.5T896 128t271.5 112.5T1280 512z"></path>
</symbol> </symbol>
<symbol id="fa-play-circle" viewBox="0 0 1792 1792">
<title>Play</title>
<path d="M896 128q209 0 385.5 103T1561 510.5 1664 896t-103 385.5-279.5 279.5T896 1664t-385.5-103T231 1281.5 128 896t103-385.5T510.5 231 896 128zm384 823q32-18 32-55t-32-55L736 521q-31-19-64-1-32 19-32 56v640q0 37 32 56 16 8 32 8 17 0 32-9z"/>
</symbol>
</svg> </svg>
<!-- The application will be rendered inside this element, <!-- The application will be rendered inside this element,
because `templates/main.js` references it --> because `templates/main.js` references it -->

View file

@ -6,7 +6,8 @@ import {
importURLSearchParams, importURLSearchParams,
importIntersectionObserver, importIntersectionObserver,
importRequestIdleCallback, importRequestIdleCallback,
importIndexedDBGetAllShim importIndexedDBGetAllShim,
importDialogPolyfill
} from '../routes/_utils/asyncModules' } from '../routes/_utils/asyncModules'
// polyfills // polyfills
@ -14,7 +15,8 @@ Promise.all([
typeof URLSearchParams === 'undefined' && importURLSearchParams(), typeof URLSearchParams === 'undefined' && importURLSearchParams(),
typeof IntersectionObserver === 'undefined' && importIntersectionObserver(), typeof IntersectionObserver === 'undefined' && importIntersectionObserver(),
typeof requestIdleCallback === 'undefined' && importRequestIdleCallback(), typeof requestIdleCallback === 'undefined' && importRequestIdleCallback(),
!IDBObjectStore.prototype.getAll && importIndexedDBGetAllShim() !IDBObjectStore.prototype.getAll && importIndexedDBGetAllShim(),
typeof HTMLDialogElement === 'undefined' && importDialogPolyfill()
]).then(() => { ]).then(() => {
// `routes` is an array of route objects injected by Sapper // `routes` is an array of route objects injected by Sapper
init(document.querySelector('#sapper'), __routes__) init(document.querySelector('#sapper'), __routes__)