Skip to content

Instantly share code, notes, and snippets.

@furf
Created September 29, 2020 21:58
Show Gist options
  • Save furf/a54c05511e386261e218cb3880b02cd1 to your computer and use it in GitHub Desktop.
Save furf/a54c05511e386261e218cb3880b02cd1 to your computer and use it in GitHub Desktop.
export type Callback<T> = (value: T) => void;
export type Unsubscribe = () => void;
export default class Callbacks<T> {
private callbacks = new Set<Callback<T>>();
subscribe(callback: Callback<T>): Unsubscribe {
this.callbacks.add(callback);
return () => this.unsubscribe(callback);
}
publish(value: T) {
for (const callback of this.callbacks) {
callback(value);
}
}
unsubscribe(callback: Callback<T>): boolean {
return this.callbacks.delete(callback);
}
unsubscribeAll() {
this.callbacks.clear();
}
}
import { LitElement } from 'lit-element';
import pick from 'lodash-es/pick';
import Callbacks, { Callback } from '../lib/callbacks';
/**
* withContext
*/
export default function withContext<T>() {
/**
* ContextProvider
*/
class ContextProvider extends LitElement {
callbacks = new Callbacks<T>();
disconnectedCallback() {
super.disconnectedCallback();
this.callbacks.unsubscribeAll();
}
subscribeContext(callback: Callback<T>) {
return this.callbacks.subscribe(callback);
}
publishContext(context: T) {
this.callbacks.publish(context);
}
}
/**
* isContextProvider
* @param element
*/
function isContextProvider(element: HTMLElement): element is ContextProvider {
return element instanceof ContextProvider;
}
/**
* findContextProvider
* @param child
*/
function findContextProvider(child: HTMLElement) {
let parent = child.parentElement;
while (parent instanceof HTMLElement && !isContextProvider(parent)) {
parent = parent.parentElement;
}
return parent;
}
function withContextConsumer(contextProps?: string[]) {
// Prefer lodash/fp/pick when you figure out how to import correctly
const filterProps =
Array.isArray(contextProps) && contextProps.length > 0
? (context: T) => pick(context, ...contextProps)
: (context: T) => context;
/**
* ContextConsumer
*/
class ContextConsumer extends LitElement {
connectedCallback() {
super.connectedCallback();
const provider = findContextProvider(this);
if (provider) {
this.unsubscribeContext = provider.subscribeContext(
this.updateContext
);
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.unsubscribeContext();
}
unsubscribeContext() {}
updateContext = (context: T) => {
Object.assign(this, filterProps(context));
};
}
return ContextConsumer;
}
return {
Provider: ContextProvider,
withContextConsumer,
};
}
import { html, css, property, query } from 'lit-element';
import VideoContext from '../VideoContext';
export class RmpVideo extends VideoContext.Provider {
static styles = css`
:host video {
display: block;
}
`;
@property({ type: Boolean })
controls = false;
@property({ type: String })
src = '';
@property({ type: String })
height = '';
@property({ type: String })
width = '';
@query('video')
video: HTMLVideoElement | undefined;
render() {
return html`
<div
@click-pause=${this.handleClickPause}
@click-play=${this.handleClickPlay}
>
<video
?controls=${this.controls}
height=${this.height}
src=${this.src}
width=${this.width}
@pause=${this.handleVideoPause}
@play=${this.handleVideoPlay}
@timeupdate=${this.handleVideoTimeUpdate}
></video>
<slot></slot>
</div>
`;
}
private updateContext() {
if (!this.video) return;
const { currentTime, paused } = this.video;
const context = {
currentTime,
paused,
};
this.publishContext(context);
}
/**
* UI Events
*/
private handleClickPause() {
this.handleUserRequestedPause();
}
private handleClickPlay() {
this.handleUserRequestedPlay();
}
/**
* User intents
*/
private handleUserRequestedPause() {
this.pause();
}
private handleUserRequestedPlay() {
this.play();
}
/**
* Video events
*/
private handleVideoPause() {
this.updateContext();
}
private handleVideoPlay() {
this.updateContext();
}
private handleVideoTimeUpdate() {
this.updateContext();
}
/**
* Actions
*/
private pause() {
this.video?.pause();
}
private play() {
this.video?.play();
}
}
import withContext from '../withContext';
export interface IVideoContext {
paused: boolean;
}
const VideoContext = withContext<IVideoContext>();
export default VideoContext;
import { html, property } from 'lit-element';
import VideoContext from '../VideoContext';
const VideoContextConsumer = VideoContext.withContextConsumer(['paused']);
export class RmpPlayButton extends VideoContextConsumer {
@property({ type: Boolean })
paused = true;
renderSlot() {
return html`<span>Play</span>`;
}
render() {
return html`
<button @click=${this.onClick} ?disabled=${!this.paused}>
<slot>${this.renderSlot()}</slot>
</button>
`;
}
private onClick() {
const event = new CustomEvent('click-play', {
bubbles: true,
composed: true,
});
this.dispatchEvent(event);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment