Fix + update external modal (#1106)

* Update external action modal. Uses Micromodal. Closes #1020

* refactor modal handling to isolate loading in modal component

* modal style cleanup

* Remove log

Co-authored-by: Ginger Wong <omqmail@gmail.com>
This commit is contained in:
Gabe Kangas 2021-06-20 17:23:39 -07:00 committed by GitHub
parent c848c029d5
commit 57674206b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 341 additions and 194 deletions

View File

@ -1240,6 +1240,11 @@
"picomatch": "^2.0.5"
}
},
"micromodal": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/micromodal/-/micromodal-0.4.6.tgz",
"integrity": "sha512-2VDso2a22jWPpqwuWT/4RomVpoU3Bl9qF9D01xzwlNp5UVsImeA0gY4nSpF44vqcQtQOtkiMUV9EZkAJSRxBsg=="
},
"min-document": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",

View File

@ -9,6 +9,7 @@
"@videojs/themes": "^1.0.1",
"htm": "^3.0.4",
"mark.js": "^8.11.1",
"micromodal": "^0.4.6",
"preact": "10.5.10",
"tailwindcss": "^1.9.6",
"video.js": "7.12.3"
@ -28,7 +29,8 @@
"htm",
"preact",
"mark.js/dist/mark.es6.min.js",
"tailwindcss/dist/tailwind.min.css"
"tailwindcss/dist/tailwind.min.css",
"micromodal/dist/micromodal.min.js"
],
"alias": {
"video.js": "video.js/core.js"

View File

@ -1,115 +1,123 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Owncast</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
<link rel="apple-touch-icon" sizes="57x57" href="/img/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/img/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/img/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/img/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/img/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/img/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/img/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/img/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/img/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/img/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<head>
<title>Owncast</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/img/favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<link rel="apple-touch-icon" sizes="57x57" href="/img/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/img/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/img/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/img/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/img/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/img/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/img/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/img/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/img/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/img/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<link href="/js/web_modules/tailwindcss/dist/tailwind.min.css" rel="stylesheet" />
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/img/favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<link href="/js/web_modules/videojs/video-js.min.css" rel="stylesheet"/>
<link href="/js/web_modules/@videojs/themes/fantasy/index.css" rel="stylesheet" />
<link href="/js/web_modules/tailwindcss/dist/tailwind.min.css" rel="stylesheet" />
<link href="./styles/video.css" rel="stylesheet" />
<link href="./styles/chat.css" rel="stylesheet" />
<link href="./styles/user-content.css" rel="stylesheet" />
<link href="./styles/app.css" rel="stylesheet" />
<link href="/js/web_modules/videojs/video-js.min.css" rel="stylesheet" />
<link href="/js/web_modules/@videojs/themes/fantasy/index.css" rel="stylesheet" />
<!-- The following script tags are not required for the app to run,
<link href="./styles/video.css" rel="stylesheet" />
<link href="./styles/chat.css" rel="stylesheet" />
<link href="./styles/user-content.css" rel="stylesheet" />
<link href="./styles/app.css" rel="stylesheet" />
<!-- The following script tags are not required for the app to run,
however they will make it load a lot faster (fewer round trips) when HTTP/2 is used.
If you wish to re-generate this list, run the following shell command
(assuming a linux or unix-ish system):
find webroot | grep -E '\.js$' | sed -E 's|webroot(.*)|<script type="preload" src="\1"></script>|'
Don't load/preload chat-only or video-only.
Don't load/preload app-standalone-chat.js or app-video-only.js.
-->
<script type="preload" src="/js/app.js"></script>
<script type="preload" src="/js/components/chat/chat-input.js"></script>
<script type="preload" src="/js/components/chat/chat.js"></script>
<script type="preload" src="/js/components/chat/chat-message-view.js"></script>
<script type="preload" src="/js/components/chat/content-editable.js"></script>
<script type="preload" src="/js/components/chat/message.js"></script>
<script type="preload" src="/js/components/chat/username.js"></script>
<script type="preload" src="/js/components/platform-logos-list.js"></script>
<script type="preload" src="/js/components/player.js"></script>
<script type="preload" src="/js/components/video-poster.js"></script>
<script type="preload" src="/js/utils/chat.js"></script>
<script type="preload" src="/js/utils/constants.js"></script>
<script type="preload" src="/js/utils/helpers.js"></script>
<script type="preload" src="/js/utils/user-colors.js"></script>
<script type="preload" src="/js/utils/websocket.js"></script>
<script type="preload" src="/js/web_modules/common/_commonjsHelpers-37fa8da4.js"></script>
<script type="preload" src="/js/web_modules/common/core-d14f1e1c.js"></script>
<script type="preload" src="/js/web_modules/common/core-fed3ccd8.js"></script>
<script type="preload" src="/js/web_modules/htm.js"></script>
<script type="preload" src="/js/web_modules/@joeattardi/emoji-button.js"></script>
<script type="preload" src="/js/web_modules/markjs/dist/mark.es6.min.js"></script>
<script type="preload" src="/js/web_modules/preact.js"></script>
<script type="preload" src="/js/web_modules/@videojs/http-streaming/dist/videojs-http-streaming.min.js"></script>
<script type="preload" src="/js/web_modules/videojs/core.js"></script>
<script type="preload" src="/js/components/platform-logos-list.js"></script>
<script type="preload" src="/js/components/chat/chat-input.js"></script>
<script type="preload" src="/js/components/chat/message.js"></script>
<script type="preload" src="/js/components/chat/content-editable.js"></script>
<script type="preload" src="/js/components/chat/chat.js"></script>
<script type="preload" src="/js/components/chat/chat-message-view.js"></script>
<script type="preload" src="/js/components/chat/username.js"></script>
<script type="preload" src="/js/components/external-action-modal.js"></script>
<script type="preload" src="/js/components/player.js"></script>
<script type="preload" src="/js/components/video-poster.js"></script>
<script type="preload" src="/js/app.js"></script>
<script type="preload" src="/js/web_modules/preact.js"></script>
<script type="preload" src="/js/web_modules/micromodal/dist/micromodal.min.js"></script>
<script type="preload" src="/js/web_modules/common/core-a6c34bea.js"></script>
<script type="preload" src="/js/web_modules/common/_commonjsHelpers-37fa8da4.js"></script>
<script type="preload" src="/js/web_modules/common/core-d14f1e1c.js"></script>
<script type="preload" src="/js/web_modules/common/core-fed3ccd8.js"></script>
<script type="preload" src="/js/web_modules/common/core-f87370e0.js"></script>
<script type="preload" src="/js/web_modules/common/core-d20ff47c.js"></script>
<script type="preload" src="/js/web_modules/@videojs/http-streaming/dist/videojs-http-streaming.min.js"></script>
<script type="preload" src="/js/web_modules/markjs/dist/mark.es6.min.js"></script>
<script type="preload" src="/js/web_modules/@joeattardi/emoji-button.js"></script>
<script type="preload" src="/js/web_modules/htm.js"></script>
<script type="preload" src="/js/web_modules/videojs/core.js"></script>
<script type="preload" src="/js/utils/helpers.js"></script>
<script type="preload" src="/js/utils/user-colors.js"></script>
<script type="preload" src="/js/utils/constants.js"></script>
<script type="preload" src="/js/utils/chat.js"></script>
<script type="preload" src="/js/utils/websocket.js"></script>
</head>
</head>
<body class="scrollbar-hidden bg-gray-300 text-gray-800">
<div id="app">
<div id="loading-logo-container">
<img id="loading-logo" src="/logo">
</div>
<body class="scrollbar-hidden bg-gray-300 text-gray-800">
<div id="app">
<div id="loading-logo-container">
<img id="loading-logo" src="/logo">
</div>
</div>
<script type="module">
import { h, render } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
<script type="module">
import { h, render } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import App from './js/app.js';
render(html`<${App} />`, document.getElementById("app"), document.getElementById("loading-logo-container"));
</script>
import App from './js/app.js';
render(html`<${App} />`, document.getElementById("app"), document.getElementById("loading-logo-container"));
</script>
<noscript>
<style>
.noscript {
text-align: center;
padding: 30px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
<noscript>
<style>
.noscript {
text-align: center;
padding: 30px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.noscript a {
display: inline;
color: blue;
text-decoration: underline;
}
</style>
<div class="noscript">
<img src="/logo" />
<br/>
<p>
This <a href="https://owncast.online" rel="noopener noreferrer" target="_blank">Owncast</a> stream requires Javascript to play.
</p>
</div>
</noscript>
</body>
</html>
.noscript a {
display: inline;
color: blue;
text-decoration: underline;
}
</style>
<div class="noscript">
<img src="/logo" />
<br />
<p>
This <a href="https://owncast.online" rel="noopener noreferrer" target="_blank">Owncast</a> stream requires
Javascript to play.
</p>
</div>
</noscript>
</body>
</html>

View File

@ -424,9 +424,7 @@ export default class App extends Component {
}
handleKeyPressed(e) {
if (e.code === 'Escape' && this.state.externalAction !== null) {
this.closeExternalActionModal();
} else if (
if (
e.target !== document.getElementById('message-input') &&
e.target !== document.getElementById('username-change-input') &&
e.target !== document.getElementsByClassName('emoji-picker__search')[0] &&
@ -456,9 +454,8 @@ export default class App extends Component {
}
}
displayExternalAction(index) {
const { configData, username } = this.state;
const action = configData.externalActions[index];
displayExternalAction(action) {
const { username } = this.state;
if (!action) {
return;
}
@ -475,13 +472,13 @@ export default class App extends Component {
win.focus();
return;
}
action.url = fullUrl;
this.setState({
externalAction: action,
externalAction: {
...action,
url: fullUrl,
},
});
}
closeExternalActionModal() {
this.setState({
externalAction: null,
@ -558,32 +555,25 @@ export default class App extends Component {
? null
: html` <${VideoPoster} offlineImage=${logo} active=${streamOnline} /> `;
// modal buttons
const externalActionButtons =
externalActions && externalActions.length > 0
? html`<div
id="external-actions-container"
class="flex flex-row align-center"
>
${externalActions.map(
function (action, index) {
return html`<${ExternalActionButton}
onClick=${this.displayExternalAction}
action=${action}
index=${index}
/>`;
}.bind(this)
)}
</div>`
: null;
externalActions &&
html`<div
id="external-actions-container"
class="flex flex-row align-center"
>
${externalActions.map(
function (action) {
return html`<${ExternalActionButton}
onClick=${this.displayExternalAction}
action=${action}
/>`;
}.bind(this)
)}
</div>`;
const externalActionModal = externalAction
? html`<${ExternalActionModal}
title=${this.state.externalAction.description ||
this.state.externalAction.title}
url=${this.state.externalAction.url}
onClose=${this.closeExternalActionModal}
/>`
: null;
// modal component
const externalActionModal = externalAction && html`<${ExternalActionModal} action=${externalAction} onClose=${this.closeExternalActionModal} />`;
return html`
<div

View File

@ -1,77 +1,79 @@
import { h } from '/js/web_modules/preact.js';
import { h, Component } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
import MicroModal from '/js/web_modules/micromodal/dist/micromodal.min.js';
const html = htm.bind(h);
export default function ExternalActionModal({ url, title, onClose }) {
const loading = 'background:url(/img/loading.gif) center center no-repeat;';
export default class ExternalActionModal extends Component {
constructor(props) {
super(props);
this.state = {
iframeLoaded: false,
};
function loaded() {
document.querySelector('#external-modal-iframe').style = '';
this.setIframeLoaded = this.setIframeLoaded.bind(this);
}
componentDidMount() {
// initalize and display Micromodal on mount
try {
MicroModal.init({
awaitCloseAnimation: false,
awaitOpenAnimation: true, // if using css animations to open the modal. This allows it to wait for the animation to finish before focusing on an element inside the modal.
});
MicroModal.show('external-actions-modal', {
onClose: this.props.onClose,
});
} catch (e) {
console.log("micromodal error: ", e);
}
}
return html`
<div class="fixed inset-0 overflow-y-auto" style="z-index: 9999">
<div
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
>
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div
onClick=${() => onClose()}
class="absolute inset-0 bg-gray-900 bg-opacity-75"
></div>
</div>
setIframeLoaded() {
this.setState({
iframeLoaded: true,
});
}
<!-- This element is to trick the browser into centering the modal contents. -->
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>&#8203;</span
>
<div
class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:align-middle w-screen md:max-w-2xl lg:max-w-2xl"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<div class="bg-white ">
<div
class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex justify-between items-center"
>
<h3 class="font-bold hidden md:block">${title}</h3>
<span class="" onclick=${onClose}>
<svg
class="h-12 w-12 fill-current text-grey hover:text-grey-darkest"
role="button"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
>
<title>Close</title>
<path
d="M14.348 14.849a1.2 1.2 0 0 1-1.697 0L10 11.819l-2.651 3.029a1.2 1.2 0 1 1-1.697-1.697l2.758-3.15-2.759-3.152a1.2 1.2 0 1 1 1.697-1.697L10 8.183l2.651-3.031a1.2 1.2 0 1 1 1.697 1.697l-2.758 3.152 2.758 3.15a1.2 1.2 0 0 1 0 1.698z"
/>
</svg>
</span>
render() {
const { action } = this.props;
const { url, title, description } = action;
const { iframeLoaded } = this.state;
const iframeStyle = iframeLoaded ? null : {
backgroundImage: 'url(/img/loading.gif)',
};
return html`
<div class="modal micromodal-slide" id="external-actions-modal" aria-hidden="true">
<div class="modal__overlay" tabindex="-1" data-micromodal-close>
<div id="modal-container" class="modal__container rounded-md" role="dialog" aria-modal="true" aria-labelledby="modal-1-title">
<header id="modal-header" class="modal__header flex flex-row justify-between items-center bg-gray-300 p-3 rounded-t-md">
<h2 class="modal__title text-indigo-600 font-semibold">
${title || description}
</h2>
<button class="modal__close" aria-label="Close modal" data-micromodal-close></button>
</header>
<div id="modal-content-content" class="modal-content-content">
<div id="modal-content" class="modal__content text-gray-600 rounded-b-md overflow-y-auto overflow-x-hidden">
<iframe
id="external-modal-iframe"
style=${iframeStyle}
class="bg-gray-100 bg-center bg-no-repeat"
width="100%"
allowpaymentrequest="true"
allowfullscreen="false"
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
src=${url}
onload=${this.setIframeLoaded}
/>
</div>
</div>
<iframe
id="external-modal-iframe"
style=${`${loading}`}
width="100%"
allowpaymentrequest="true"
allowfullscreen="false"
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
src=${url}
onload=${loaded}
/>
</div>
</div>
</div>
</div>
`;
`;
}
}
export function ExternalActionButton({ index, action, onClick }) {
export function ExternalActionButton({ action, onClick }) {
const { title, icon, color = undefined } = action;
const logo =
icon &&
@ -79,11 +81,10 @@ export function ExternalActionButton({ index, action, onClick }) {
<span class="external-action-icon"><img src=${icon} alt="" /></span>
`;
const bgcolor = color && { backgroundColor: `${color}` };
const handleClick = () => onClick(index);
const handleClick = () => onClick(action);
return html`
<button
class="external-action-button rounded-sm flex flex-row justify-center items-center overflow-hidden bg-gray-800"
data-index=${index}
onClick=${handleClick}
style=${bgcolor}
>

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,7 @@
"@videojs/themes/fantasy/index.css": "./@videojs/themes/fantasy/index.css",
"htm": "./htm.js",
"mark.js/dist/mark.es6.min.js": "./markjs/dist/mark.es6.min.js",
"micromodal/dist/micromodal.min.js": "./micromodal/dist/micromodal.min.js",
"preact": "./preact.js",
"tailwindcss/dist/tailwind.min.css": "./tailwindcss/dist/tailwind.min.css",
"video.js": "./video.js/core.js",

File diff suppressed because one or more lines are too long

View File

@ -278,6 +278,139 @@ header {
}
}
#external-modal-iframe {
height: 70vh;
}
/**************************
Basic Modal Styles
**************************/
/*
External modal styling for use with Micromodal from https://gist.github.com/ghosh/4f94cf497d7090359a5c9f81caf60699
See external-action-modal.js
*/
.modal {
z-index: 100;
}
.modal__overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.75);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
}
.modal__container {
background-color: transparent;
padding: 0px;
max-width: 740px;
height: 75vh;
width: 50%;
border-radius: 0px;
overflow: hidden;
box-sizing: border-box;
}
.modal__close {
background: transparent;
border: 0;
cursor: pointer;
margin: 0px;
padding: 0px;
outline: none;
cursor: pointer !important;
}
.modal__close:before { content: "\2715"; font-size: 1.25rem; }
@supports (display: flex) {
.modal__header {
display: flex;
justify-content: space-between;
align-items: center;
height:initial;
}
.modal__title {
position: static;
}
.modal__close {
position: static;
}
}
/**************************
Modal Animation Style
**************************/
@keyframes mmfadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes mmfadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes mmslideIn {
from { transform: translateY(15%); }
to { transform: translateY(0); }
}
@keyframes mmslideOut {
from { transform: translateY(0); }
to { transform: translateY(-10%); }
}
.micromodal-slide {
display: none;
}
.micromodal-slide.is-open {
display: block;
}
.micromodal-slide[aria-hidden="false"] .modal__overlay {
animation: mmfadeIn .3s cubic-bezier(0.0, 0.0, 0.2, 1);
}
.micromodal-slide[aria-hidden="false"] .modal__container {
animation: mmslideIn .3s cubic-bezier(0, 0, .2, 1);
}
.micromodal-slide[aria-hidden="true"] .modal__overlay {
animation: mmfadeOut .3s cubic-bezier(0.0, 0.0, 0.2, 1);
}
.micromodal-slide[aria-hidden="true"] .modal__container {
animation: mmslideOut .3s cubic-bezier(0, 0, .2, 1);
}
.micromodal-slide .modal__container,
.micromodal-slide .modal__overlay {
will-change: transform;
}
/* Miromodal mobile styling */
@media only screen and (min-device-width : 600px) and (max-device-width : 480px) {
.modal__container {
width: 90% !important;
min-width: 90% !important;
}
@supports (display: flex) {
.modal__container {
width: 90% !important;
min-width: 90% !important;
height: 85vh;
}
}
.modal__content {
-webkit-overflow-scrolling: touch;
}
}