@mux/mux-player 0.1.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +281 -0
- package/LICENSE +9 -0
- package/README.md +231 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +161 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov-report/src/dialog.ts.html +247 -0
- package/coverage/lcov-report/src/errors.ts.html +574 -0
- package/coverage/lcov-report/src/helpers.ts.html +478 -0
- package/coverage/lcov-report/src/html.ts.html +580 -0
- package/coverage/lcov-report/src/index.html +251 -0
- package/coverage/lcov-report/src/index.ts.html +2941 -0
- package/coverage/lcov-report/src/logger.ts.html +163 -0
- package/coverage/lcov-report/src/media-chrome/dialog.ts.html +661 -0
- package/coverage/lcov-report/src/media-chrome/index.html +131 -0
- package/coverage/lcov-report/src/media-chrome/time-display.ts.html +295 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/airplay.svg.html +109 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/captions-off.svg.html +100 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/captions-on.svg.html +100 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/fullscreen-enter.svg.html +100 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/fullscreen-exit.svg.html +100 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/index.html +326 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/pause.svg.html +100 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/pip-enter.svg.html +100 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/pip-exit.svg.html +100 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/play.svg.html +100 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/seek-backward.svg.html +124 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/seek-forward.svg.html +124 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/volume-high.svg.html +103 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/volume-low.svg.html +103 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/volume-medium.svg.html +103 -0
- package/coverage/lcov-report/src/media-theme-mux/icons/volume-off.svg.html +103 -0
- package/coverage/lcov-report/src/media-theme-mux/icons.ts.html +184 -0
- package/coverage/lcov-report/src/media-theme-mux/index.html +146 -0
- package/coverage/lcov-report/src/media-theme-mux/media-theme-mux.ts.html +1279 -0
- package/coverage/lcov-report/src/media-theme-mux/styles.css.html +586 -0
- package/coverage/lcov-report/src/styles.css.html +211 -0
- package/coverage/lcov-report/src/template.ts.html +463 -0
- package/coverage/lcov-report/src/utils.ts.html +385 -0
- package/coverage/lcov-report/src/video-api.ts.html +979 -0
- package/coverage/lcov.info +4058 -0
- package/dist/index.cjs.js +1432 -0
- package/dist/index.mjs +709 -0
- package/dist/mux-player.js +1478 -0
- package/dist/mux-player.mjs +1478 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/dialog.d.ts +6 -0
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/helpers.d.ts +26 -0
- package/dist/types/html.d.ts +18 -0
- package/dist/types/index.d.ts +199 -0
- package/dist/types/logger.d.ts +5 -0
- package/dist/types/media-chrome/dialog.d.ts +12 -0
- package/dist/types/media-chrome/time-display.d.ts +9 -0
- package/dist/types/media-theme-mux/icons.d.ts +15 -0
- package/dist/types/media-theme-mux/media-theme-mux.d.ts +29 -0
- package/dist/types/template.d.ts +5 -0
- package/dist/types/utils.d.ts +10 -0
- package/dist/types/video-api.d.ts +64 -0
- package/dist/types-ts3.4/dialog.d.ts +6 -0
- package/dist/types-ts3.4/errors.d.ts +6 -0
- package/dist/types-ts3.4/helpers.d.ts +26 -0
- package/dist/types-ts3.4/html.d.ts +18 -0
- package/dist/types-ts3.4/index.d.ts +180 -0
- package/dist/types-ts3.4/logger.d.ts +5 -0
- package/dist/types-ts3.4/media-chrome/dialog.d.ts +12 -0
- package/dist/types-ts3.4/media-chrome/time-display.d.ts +9 -0
- package/dist/types-ts3.4/media-theme-mux/icons.d.ts +15 -0
- package/dist/types-ts3.4/media-theme-mux/media-theme-mux.d.ts +29 -0
- package/dist/types-ts3.4/template.d.ts +5 -0
- package/dist/types-ts3.4/utils.d.ts +10 -0
- package/dist/types-ts3.4/video-api.d.ts +53 -0
- package/lang/en.json +32 -0
- package/lang/nl.json +31 -0
- package/package.json +107 -0
- package/src/dialog.ts +54 -0
- package/src/errors.ts +163 -0
- package/src/helpers.ts +131 -0
- package/src/html.ts +165 -0
- package/src/index.ts +952 -0
- package/src/logger.ts +26 -0
- package/src/media-chrome/dialog.ts +192 -0
- package/src/media-chrome/time-display.ts +70 -0
- package/src/media-theme-mux/icons/airplay.svg +8 -0
- package/src/media-theme-mux/icons/captions-off.svg +5 -0
- package/src/media-theme-mux/icons/captions-on.svg +5 -0
- package/src/media-theme-mux/icons/fullscreen-enter.svg +5 -0
- package/src/media-theme-mux/icons/fullscreen-exit.svg +5 -0
- package/src/media-theme-mux/icons/pause.svg +5 -0
- package/src/media-theme-mux/icons/pip-enter.svg +5 -0
- package/src/media-theme-mux/icons/pip-exit.svg +5 -0
- package/src/media-theme-mux/icons/play.svg +5 -0
- package/src/media-theme-mux/icons/seek-backward.svg +13 -0
- package/src/media-theme-mux/icons/seek-forward.svg +13 -0
- package/src/media-theme-mux/icons/volume-high.svg +6 -0
- package/src/media-theme-mux/icons/volume-low.svg +6 -0
- package/src/media-theme-mux/icons/volume-medium.svg +6 -0
- package/src/media-theme-mux/icons/volume-off.svg +6 -0
- package/src/media-theme-mux/icons.ts +33 -0
- package/src/media-theme-mux/media-theme-mux.ts +398 -0
- package/src/media-theme-mux/styles.css +167 -0
- package/src/styles.css +42 -0
- package/src/template.ts +126 -0
- package/src/types.d.ts +52 -0
- package/src/utils.ts +100 -0
- package/src/video-api.ts +298 -0
- package/test/errors.test.js +169 -0
- package/test/helpers.test.js +78 -0
- package/test/player.test.js +696 -0
- package/test/template.test.js +70 -0
- package/test/utils.test.js +21 -0
- package/test/web-test-runner.config.mjs +29 -0
- package/tsconfig.json +21 -0
package/src/errors.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { MediaError } from '@mux/mux-video';
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import lang from '../lang/en.json';
|
|
4
|
+
import { i18n, parseJwt } from './utils';
|
|
5
|
+
import type { DialogOptions, DevlogOptions } from './types';
|
|
6
|
+
|
|
7
|
+
export function getErrorLogs(
|
|
8
|
+
error: MediaError,
|
|
9
|
+
offline?: boolean,
|
|
10
|
+
playbackId?: string,
|
|
11
|
+
playbackToken?: string,
|
|
12
|
+
translate?: boolean
|
|
13
|
+
): { dialog: DialogOptions; devlog: DevlogOptions } {
|
|
14
|
+
let dialog: DialogOptions = {};
|
|
15
|
+
let devlog: DevlogOptions = {};
|
|
16
|
+
|
|
17
|
+
switch (error.code) {
|
|
18
|
+
case MediaError.MEDIA_ERR_NETWORK: {
|
|
19
|
+
dialog.title = i18n(`Network Error`, translate);
|
|
20
|
+
dialog.message = error.message;
|
|
21
|
+
|
|
22
|
+
switch (error.data?.response.code) {
|
|
23
|
+
case 412: {
|
|
24
|
+
dialog.title = i18n(`Video is not currently available`, translate);
|
|
25
|
+
dialog.message = i18n(`The live stream or video file are not yet ready.`, translate);
|
|
26
|
+
devlog.message = i18n(
|
|
27
|
+
`This playback-id may belong to a live stream that is not currently active or an asset that is not ready.`,
|
|
28
|
+
translate
|
|
29
|
+
);
|
|
30
|
+
devlog.file = '412-not-playable.md';
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case 404: {
|
|
34
|
+
dialog.title = i18n(`Video does not exist`, translate);
|
|
35
|
+
dialog.message = '';
|
|
36
|
+
devlog.message = i18n(
|
|
37
|
+
`This playback-id does not exist. You may have used an Asset ID or an ID from a different resource.`,
|
|
38
|
+
translate
|
|
39
|
+
);
|
|
40
|
+
devlog.file = '404-not-found.md';
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case 403: {
|
|
44
|
+
dialog.title = i18n(`Invalid playback URL`, translate);
|
|
45
|
+
dialog.message = i18n(
|
|
46
|
+
`The video URL or playback-token are formatted with incorrect or incomplete information.`,
|
|
47
|
+
translate
|
|
48
|
+
);
|
|
49
|
+
devlog.message = i18n(
|
|
50
|
+
`403 error trying to access this playback URL. If this is a signed URL, you might need to provide a playback-token.`,
|
|
51
|
+
translate
|
|
52
|
+
);
|
|
53
|
+
devlog.file = 'missing-signed-tokens.md';
|
|
54
|
+
|
|
55
|
+
if (!playbackToken) break;
|
|
56
|
+
|
|
57
|
+
const { exp: tokenExpiry, aud: tokenType, sub: tokenPlaybackId } = parseJwt(playbackToken);
|
|
58
|
+
const tokenExpired = Date.now() > tokenExpiry * 1000;
|
|
59
|
+
const playbackIdMismatch = tokenPlaybackId !== playbackId;
|
|
60
|
+
const badTokenType = tokenType !== 'v';
|
|
61
|
+
const dateOptions: any = {
|
|
62
|
+
timeStyle: 'medium',
|
|
63
|
+
dateStyle: 'medium',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (tokenExpired) {
|
|
67
|
+
dialog.title = i18n(`Video URL has expired`, translate);
|
|
68
|
+
dialog.message = i18n(`The video’s secured playback-token has expired.`, translate);
|
|
69
|
+
devlog.message = i18n(
|
|
70
|
+
`This playback is using signed URLs and the playback-token has expired. Expired at: {expiredDate}. Current time: {currentDate}.`,
|
|
71
|
+
translate
|
|
72
|
+
).format({
|
|
73
|
+
expiredDate: new Intl.DateTimeFormat(lang.code, dateOptions).format(tokenExpiry * 1000),
|
|
74
|
+
currentDate: new Intl.DateTimeFormat(lang.code, dateOptions).format(Date.now()),
|
|
75
|
+
});
|
|
76
|
+
devlog.file = '403-expired-token.md';
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (playbackIdMismatch) {
|
|
81
|
+
dialog.title = i18n(`Video URL is formatted incorrectly`, translate);
|
|
82
|
+
dialog.message = i18n(
|
|
83
|
+
`The video’s playback ID does not match the one encoded in the playback-token.`,
|
|
84
|
+
translate
|
|
85
|
+
);
|
|
86
|
+
devlog.message = i18n(
|
|
87
|
+
`The specified playback ID {playbackId} and the playback ID encoded in the playback-token {tokenPlaybackId} do not match.`,
|
|
88
|
+
translate
|
|
89
|
+
).format({
|
|
90
|
+
playbackId,
|
|
91
|
+
tokenPlaybackId,
|
|
92
|
+
});
|
|
93
|
+
devlog.file = '403-playback-id-mismatch.md';
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (badTokenType) {
|
|
98
|
+
dialog.title = i18n(`Video URL is formatted incorrectly`, translate);
|
|
99
|
+
dialog.message = i18n(`The playback-token is formatted with incorrect information.`, translate);
|
|
100
|
+
devlog.message = i18n(
|
|
101
|
+
`The playback-token has an incorrect aud value: {tokenType}. aud value should be v.`,
|
|
102
|
+
translate
|
|
103
|
+
).format({
|
|
104
|
+
tokenType,
|
|
105
|
+
});
|
|
106
|
+
devlog.file = '403-incorrect-aud-value.md';
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
devlog.message = i18n(
|
|
111
|
+
`403 error trying to access this playback URL. If this is a signed playback ID, the token might not have been generated correctly.`,
|
|
112
|
+
translate
|
|
113
|
+
);
|
|
114
|
+
devlog.file = '403-malformatted-token.md';
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case MediaError.MEDIA_ERR_DECODE: {
|
|
121
|
+
const { message } = error;
|
|
122
|
+
dialog = {
|
|
123
|
+
title: i18n(`Media Error`, translate),
|
|
124
|
+
message,
|
|
125
|
+
};
|
|
126
|
+
devlog.file = 'media-decode-error.md';
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: {
|
|
130
|
+
// If native HLS is used on Safari, M3U8 response errors cause media src not supported errors.
|
|
131
|
+
// If the response returns an error code, fix the MediaError.code and get detailed error logs.
|
|
132
|
+
const status = error.data?.response?.code;
|
|
133
|
+
if (status >= 400 && status < 500) {
|
|
134
|
+
error.code = MediaError.MEDIA_ERR_NETWORK;
|
|
135
|
+
error.data = { response: { code: status } };
|
|
136
|
+
({ dialog, devlog } = getErrorLogs(error, offline, playbackId, playbackToken));
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
dialog = {
|
|
141
|
+
title: i18n(`Source Not Supported`, translate),
|
|
142
|
+
message: error.message,
|
|
143
|
+
};
|
|
144
|
+
devlog.file = 'media-src-not-supported.md';
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
default:
|
|
148
|
+
dialog = {
|
|
149
|
+
title: i18n(`Error`, translate),
|
|
150
|
+
message: error.message,
|
|
151
|
+
};
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (offline) {
|
|
156
|
+
dialog = {
|
|
157
|
+
title: i18n(`Your device appears to be offline`, translate),
|
|
158
|
+
message: i18n(`Check your internet connection and try reloading this video.`, translate),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { dialog, devlog };
|
|
163
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { toQuery, camelCase } from './utils';
|
|
2
|
+
import type MuxPlayerElement from '.';
|
|
3
|
+
import { StreamTypes } from '@mux/playback-core';
|
|
4
|
+
|
|
5
|
+
const MUX_VIDEO_DOMAIN = 'mux.com';
|
|
6
|
+
|
|
7
|
+
/* eslint-disable */
|
|
8
|
+
const getEnvPlayerVersion = () => {
|
|
9
|
+
try {
|
|
10
|
+
// @ts-ignore
|
|
11
|
+
return PLAYER_VERSION;
|
|
12
|
+
} catch {}
|
|
13
|
+
return 'UNKNOWN';
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const player_version = getEnvPlayerVersion();
|
|
17
|
+
export const getPlayerVersion = () => player_version;
|
|
18
|
+
|
|
19
|
+
export const getSrcFromPlaybackId = (
|
|
20
|
+
playbackId?: string,
|
|
21
|
+
{ token, domain = MUX_VIDEO_DOMAIN }: { token?: string; domain?: string } = {}
|
|
22
|
+
) => {
|
|
23
|
+
/*
|
|
24
|
+
* 2022-04-01 djhaveri
|
|
25
|
+
*
|
|
26
|
+
* `redundant_streams` query param can only be added to public
|
|
27
|
+
* playback IDs, in order to use this feature with signed URLs
|
|
28
|
+
* the query param must be added to the signing token.
|
|
29
|
+
*
|
|
30
|
+
* https://docs.mux.com/guides/video/play-your-videos#add-delivery-redundancy-with-redundant-streams
|
|
31
|
+
*
|
|
32
|
+
* */
|
|
33
|
+
const isSignedUrl = !!token;
|
|
34
|
+
const query = isSignedUrl ? { token } : { redundant_streams: true };
|
|
35
|
+
return `https://stream.${domain}/${playbackId}.m3u8${toQuery(query)}`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const getPosterURLFromPlaybackId = (
|
|
39
|
+
playbackId?: string,
|
|
40
|
+
{ token, thumbnailTime, domain = MUX_VIDEO_DOMAIN }: { token?: string; domain?: string; thumbnailTime?: number } = {}
|
|
41
|
+
) => {
|
|
42
|
+
// NOTE: thumbnailTime is not supported when using a signedURL/token. Remove under these cases. (CJP)
|
|
43
|
+
const time = token == null ? thumbnailTime : undefined;
|
|
44
|
+
return `https://image.${domain}/${playbackId}/thumbnail.jpg${toQuery({
|
|
45
|
+
token,
|
|
46
|
+
time,
|
|
47
|
+
})}`;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const getStoryboardURLFromPlaybackId = (
|
|
51
|
+
playbackId?: string,
|
|
52
|
+
{ token, domain = MUX_VIDEO_DOMAIN }: { token?: string; domain?: string } = {}
|
|
53
|
+
) => {
|
|
54
|
+
return `https://image.${domain}/${playbackId}/storyboard.vtt${toQuery({
|
|
55
|
+
token,
|
|
56
|
+
})}`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const attrToPropNameMap: Record<string, string> = {
|
|
60
|
+
crossorigin: 'crossOrigin',
|
|
61
|
+
playsinline: 'playsInline',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function toPropName(attrName: string) {
|
|
65
|
+
return attrToPropNameMap[attrName] ?? camelCase(attrName);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let testMediaEl: HTMLMediaElement | undefined;
|
|
69
|
+
export const getTestMediaEl = (nodeName = 'video') => {
|
|
70
|
+
if (testMediaEl) return testMediaEl;
|
|
71
|
+
if (typeof window !== 'undefined') {
|
|
72
|
+
testMediaEl = document.createElement(nodeName as 'video' | 'audio');
|
|
73
|
+
}
|
|
74
|
+
return testMediaEl;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const hasVolumeSupportAsync = async (mediaEl: HTMLMediaElement | undefined = getTestMediaEl()) => {
|
|
78
|
+
if (!mediaEl) return false;
|
|
79
|
+
const prevVolume = mediaEl.volume;
|
|
80
|
+
mediaEl.volume = prevVolume / 2 + 0.1;
|
|
81
|
+
return new Promise<boolean>((resolve, reject) => {
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
resolve(mediaEl.volume !== prevVolume);
|
|
84
|
+
}, 0);
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export function getCcSubTracks(el: MuxPlayerElement) {
|
|
89
|
+
return Array.from(el.media?.textTracks ?? []).filter(({ kind }) => kind === 'subtitles' || kind === 'captions');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const getLiveTime = (el: MuxPlayerElement) => {
|
|
93
|
+
const { media } = el;
|
|
94
|
+
return (
|
|
95
|
+
media?._hls?.liveSyncPosition ??
|
|
96
|
+
(media?.seekable.length ? media?.seekable.end(media.seekable.length - 1) : undefined)
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const seekToLive = (el: MuxPlayerElement) => {
|
|
101
|
+
const liveTime = getLiveTime(el);
|
|
102
|
+
if (liveTime == undefined) {
|
|
103
|
+
console.warn('attempting to seek to live but cannot determine live edge time!');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
el.currentTime = liveTime;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const LL_LIVE_SEGMENT_SECS = 1;
|
|
110
|
+
export const LIVE_SEGMENT_SECS = 5;
|
|
111
|
+
export const DEFAULT_HOLDBACK = 3;
|
|
112
|
+
export const LIVE_HOLDBACK_MOE = 0.5;
|
|
113
|
+
|
|
114
|
+
export const isInLiveWindow = (el: MuxPlayerElement) => {
|
|
115
|
+
const { streamType } = el;
|
|
116
|
+
const liveTime = getLiveTime(el);
|
|
117
|
+
const currentTime = el.media?.currentTime;
|
|
118
|
+
if (liveTime == undefined || currentTime == undefined) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
const delta = liveTime - currentTime;
|
|
122
|
+
// The live window is based on whether or not the current playhead is within n segment durations (plus a margin of error)
|
|
123
|
+
// of the live edge (CJP)
|
|
124
|
+
if (streamType === StreamTypes.LL_LIVE || streamType === StreamTypes.LL_DVR) {
|
|
125
|
+
return delta <= LL_LIVE_SEGMENT_SECS * (DEFAULT_HOLDBACK + LIVE_HOLDBACK_MOE);
|
|
126
|
+
}
|
|
127
|
+
if (streamType === StreamTypes.LIVE || streamType === StreamTypes.DVR) {
|
|
128
|
+
return delta <= LIVE_SEGMENT_SECS * (DEFAULT_HOLDBACK + LIVE_HOLDBACK_MOE);
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
};
|
package/src/html.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { TemplateInstance, NodeTemplatePart, createProcessor, AttributeTemplatePart } from '@github/template-parts';
|
|
2
|
+
import type { TemplatePart, TemplateTypeInit } from '@github/template-parts';
|
|
3
|
+
|
|
4
|
+
// NOTE: These are either direct ports or significantly based off of github's jtml template part processing logic. For more, see: https://github.com/github/jtml
|
|
5
|
+
|
|
6
|
+
const eventListeners = new WeakMap<Element, Map<string, EventHandler>>();
|
|
7
|
+
class EventHandler {
|
|
8
|
+
handleEvent!: EventListener;
|
|
9
|
+
constructor(private element: Element, private type: string) {
|
|
10
|
+
this.element.addEventListener(this.type, this);
|
|
11
|
+
const elementMap = eventListeners.get(this.element);
|
|
12
|
+
if (elementMap) {
|
|
13
|
+
elementMap.set(this.type, this);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
set(listener: EventListener) {
|
|
17
|
+
if (typeof listener == 'function') {
|
|
18
|
+
this.handleEvent = listener.bind(this.element);
|
|
19
|
+
} else if (typeof listener === 'object' && typeof (listener as EventHandler).handleEvent === 'function') {
|
|
20
|
+
this.handleEvent = (listener as EventHandler).handleEvent.bind(listener);
|
|
21
|
+
} else {
|
|
22
|
+
this.element.removeEventListener(this.type, this);
|
|
23
|
+
const elementMap = eventListeners.get(this.element);
|
|
24
|
+
if (elementMap) {
|
|
25
|
+
elementMap.delete(this.type);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
static for(part: AttributeTemplatePart): EventHandler {
|
|
30
|
+
if (!eventListeners.has(part.element)) eventListeners.set(part.element, new Map());
|
|
31
|
+
const type = part.attributeName.slice(2);
|
|
32
|
+
const elementListeners = eventListeners.get(part.element);
|
|
33
|
+
if (elementListeners && elementListeners.has(type)) return elementListeners.get(type) as EventHandler;
|
|
34
|
+
return new EventHandler(part.element, type);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function processEvent(part: TemplatePart, value: unknown): boolean {
|
|
39
|
+
if (part instanceof AttributeTemplatePart && part.attributeName.startsWith('on')) {
|
|
40
|
+
EventHandler.for(part).set(value as unknown as EventListener);
|
|
41
|
+
part.element.removeAttributeNS(part.attributeNamespace, part.attributeName);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function processSubTemplate(part: TemplatePart, value: unknown): boolean {
|
|
48
|
+
if (value instanceof TemplateResult && part instanceof NodeTemplatePart) {
|
|
49
|
+
value.renderInto(part);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function processDocumentFragment(part: TemplatePart, value: unknown): boolean {
|
|
56
|
+
if (value instanceof DocumentFragment && part instanceof NodeTemplatePart) {
|
|
57
|
+
if (value.childNodes.length) part.replace(...value.childNodes);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function processPropertyIdentity(part: TemplatePart, value: unknown): boolean {
|
|
64
|
+
if (part instanceof AttributeTemplatePart) {
|
|
65
|
+
const ns = part.attributeNamespace;
|
|
66
|
+
const oldValue = part.element.getAttributeNS(ns, part.attributeName);
|
|
67
|
+
if (String(value) !== oldValue) {
|
|
68
|
+
part.value = String(value);
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
part.value = String(value);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function processBooleanAttribute(part: TemplatePart, value: unknown): boolean {
|
|
77
|
+
if (
|
|
78
|
+
typeof value === 'boolean' &&
|
|
79
|
+
part instanceof AttributeTemplatePart
|
|
80
|
+
// can't use this because on custom elements the props are always undefined
|
|
81
|
+
// typeof part.element[part.attributeName as keyof Element] === 'boolean'
|
|
82
|
+
) {
|
|
83
|
+
const ns = part.attributeNamespace;
|
|
84
|
+
const oldValue = part.element.hasAttributeNS(ns, part.attributeName);
|
|
85
|
+
if (value !== oldValue) {
|
|
86
|
+
part.booleanValue = value;
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function processBooleanNode(part: TemplatePart, value: unknown): boolean {
|
|
94
|
+
if (value === false && part instanceof NodeTemplatePart) {
|
|
95
|
+
part.replace('');
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function processPart(part: TemplatePart, value: unknown): void {
|
|
102
|
+
processBooleanAttribute(part, value) ||
|
|
103
|
+
processEvent(part, value) ||
|
|
104
|
+
processBooleanNode(part, value) ||
|
|
105
|
+
processSubTemplate(part, value) ||
|
|
106
|
+
processDocumentFragment(part, value) ||
|
|
107
|
+
processPropertyIdentity(part, value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const templates = new WeakMap<TemplateStringsArray, HTMLTemplateElement>();
|
|
111
|
+
const renderedTemplates = new WeakMap<Node | NodeTemplatePart, HTMLTemplateElement>();
|
|
112
|
+
const renderedTemplateInstances = new WeakMap<Node | NodeTemplatePart, TemplateInstance>();
|
|
113
|
+
export class TemplateResult {
|
|
114
|
+
constructor(
|
|
115
|
+
public readonly strings: TemplateStringsArray,
|
|
116
|
+
public readonly values: unknown[],
|
|
117
|
+
public readonly processor: TemplateTypeInit
|
|
118
|
+
) {}
|
|
119
|
+
|
|
120
|
+
get template(): HTMLTemplateElement {
|
|
121
|
+
if (templates.has(this.strings)) {
|
|
122
|
+
return templates.get(this.strings) as HTMLTemplateElement;
|
|
123
|
+
} else {
|
|
124
|
+
const template = document.createElement('template');
|
|
125
|
+
const end = this.strings.length - 1;
|
|
126
|
+
template.innerHTML = this.strings.reduce((str, cur, i) => str + cur + (i < end ? `{{ ${i} }}` : ''), '');
|
|
127
|
+
templates.set(this.strings, template);
|
|
128
|
+
return template;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
renderInto(element: Node | NodeTemplatePart): void {
|
|
133
|
+
const template = this.template;
|
|
134
|
+
if (renderedTemplates.get(element) !== template) {
|
|
135
|
+
renderedTemplates.set(element, template);
|
|
136
|
+
const instance = new TemplateInstance(template, this.values, this.processor);
|
|
137
|
+
renderedTemplateInstances.set(element, instance);
|
|
138
|
+
if (element instanceof NodeTemplatePart) {
|
|
139
|
+
element.replace(...instance.children);
|
|
140
|
+
} else {
|
|
141
|
+
element.appendChild(instance);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const templateInstance = renderedTemplateInstances.get(element);
|
|
146
|
+
if (templateInstance) {
|
|
147
|
+
templateInstance.update(this.values as unknown as Record<string, unknown>);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const defaultProcessor = createProcessor(processPart);
|
|
153
|
+
export function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult {
|
|
154
|
+
return new TemplateResult(strings, values, defaultProcessor);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function render(result: TemplateResult, element: Node | NodeTemplatePart): void {
|
|
158
|
+
result.renderInto(element);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function createTemplateInstance(content: string, props?: any) {
|
|
162
|
+
const template = document.createElement('template');
|
|
163
|
+
template.innerHTML = content;
|
|
164
|
+
return new TemplateInstance(template, props);
|
|
165
|
+
}
|