diff --git a/packages/cxl-ui/package.json b/packages/cxl-ui/package.json index 493fbe6a8..9e9eb7855 100644 --- a/packages/cxl-ui/package.json +++ b/packages/cxl-ui/package.json @@ -32,8 +32,10 @@ "@vaadin/tooltip": "^23.3.7", "@vaadin/vaadin-themable-mixin": "^23.3.7", "cross-env": "~7.0.2", + "crypto-js": "^4.1.1", "headroom.js": "^0.12.0", "imports-loader": "^2.0.0", + "jose": "^4.13.1", "laravel-mix": "^6.0.39", "lit": "^2.2.5", "lodash-es": "^4.17.21", diff --git a/packages/cxl-ui/scss/jw-player/jw-player.scss b/packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player-shadow.scss similarity index 86% rename from packages/cxl-ui/scss/jw-player/jw-player.scss rename to packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player-shadow.scss index 5868a2c64..47d442925 100644 --- a/packages/cxl-ui/scss/jw-player/jw-player.scss +++ b/packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player-shadow.scss @@ -1,4 +1,4 @@ -@use "~@conversionxl/cxl-lumo-styles/scss/mixins"; +@use '~@conversionxl/cxl-lumo-styles/scss/mixins'; :host { box-sizing: border-box; @@ -8,8 +8,11 @@ } } -:host([captions]) #container { - grid-template-rows: 1fr max-content 1fr; +:host([captions]) { + #container { + display: grid; + grid-template-rows: 1fr max-content 1fr; + } } .captions { @@ -39,6 +42,10 @@ justify-content: center; } +.cxl-jw-player-container { + height: 100%; +} + .flex { display: flex; height: 100%; diff --git a/packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player-transcript-shadow.scss b/packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player-transcript-shadow.scss new file mode 100644 index 000000000..769714c22 --- /dev/null +++ b/packages/cxl-ui/scss/cxl-jw-player/cxl-jw-player-transcript-shadow.scss @@ -0,0 +1,3 @@ +:host(:not([hidden])) { + display: block; +} diff --git a/packages/cxl-ui/scss/jw-player/chapter.scss b/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-chapter-navigation.scss similarity index 97% rename from packages/cxl-ui/scss/jw-player/chapter.scss rename to packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-chapter-navigation.scss index 83f43539d..54de81403 100644 --- a/packages/cxl-ui/scss/jw-player/chapter.scss +++ b/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-chapter-navigation.scss @@ -14,6 +14,10 @@ background: var(--lumo-shade); gap: var(--lumo-space-s); + &[hidden] { + display: none; + } + .close { font-size: var(--lumo-font-size-xs); cursor: pointer; diff --git a/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-nextup.scss b/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-nextup.scss new file mode 100644 index 000000000..3a1eb4ef2 --- /dev/null +++ b/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-nextup.scss @@ -0,0 +1,35 @@ +cxl-jw-player { + &[wide] { + .jw-nextup-cta-mobile { + display: none; + } + } + + &:not([wide]) { + .jw-nextup-cta { + display: none; + } + } + + .jw-nextup-container { + display: flex; + flex-direction: column; + align-items: flex-end; + + .jw-nextup-cta, + .jw-nextup-cta-mobile { + a { + pointer-events: all; + } + } + + .jw-nextup-cta { + max-width: 400px; + width: 64%; + + vaadin-button { + width: 100%; + } + } + } +} diff --git a/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-transcript.scss b/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-transcript.scss new file mode 100644 index 000000000..769714c22 --- /dev/null +++ b/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player-transcript.scss @@ -0,0 +1,3 @@ +:host(:not([hidden])) { + display: block; +} diff --git a/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player.scss b/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player.scss new file mode 100644 index 000000000..29e15a17d --- /dev/null +++ b/packages/cxl-ui/scss/global/cxl-jw-player/cxl-jw-player.scss @@ -0,0 +1,25 @@ +cxl-jw-player { + .jw-player-button { + width: 32px; + fill: rgba(255, 255, 255, 0.8); + + &:hover { + fill: rgba(255, 255, 255, 1); + } + } + + .jw-related-item { + height: 100% !important; /* stylelint-disable-line declaration-no-important */ + } + + .jwplayer:not(.jw-flag-small-player) .jw-related-item-next-up { + .jw-related-item-poster { + height: 100%; + } + + .jw-related-item-title { + position: absolute; + bottom: 0; + } + } +} diff --git a/packages/cxl-ui/src/components/cxl-course-dialog.js b/packages/cxl-ui/src/components/cxl-course-dialog.js index 45e8b063c..6f0cc5386 100644 --- a/packages/cxl-ui/src/components/cxl-course-dialog.js +++ b/packages/cxl-ui/src/components/cxl-course-dialog.js @@ -5,7 +5,7 @@ import { registerGlobalStyles } from '@conversionxl/cxl-lumo-styles/src/utils'; import '@vaadin/button'; import '@vaadin/dialog'; import './cxl-time.js'; -import './jw-player/index.js'; +import './cxl-jw-player/index.js'; import { dialogFooterRenderer, dialogRenderer } from '@vaadin/dialog/lit.js'; import cxlCourseDialogGlobalStyles from '../styles/global/cxl-course-dialog-css.js'; @@ -64,14 +64,14 @@ export class CXLCourseDialogElement extends LitElement {
${this.video - ? html` ` + >` : ''}

${this.course.description}

diff --git a/packages/cxl-ui/src/components/jw-player/README.md b/packages/cxl-ui/src/components/cxl-jw-player/README.md similarity index 87% rename from packages/cxl-ui/src/components/jw-player/README.md rename to packages/cxl-ui/src/components/cxl-jw-player/README.md index d8d40eb94..0baa00e76 100644 --- a/packages/cxl-ui/src/components/jw-player/README.md +++ b/packages/cxl-ui/src/components/cxl-jw-player/README.md @@ -1,15 +1,16 @@ -# JW Player +# CXL JW Player ## Usage ``` - + > ``` ## Features: @@ -61,13 +62,13 @@ Order is important as each mixin extends the previous one. In this case, `MixinT There are currently two methods which are important to the lifecycle of the component: -`__setup()` +`_setup()` -This method is async and called when the component is first created. You must call `await super.__setup()` at the beginning of this method to make sure each parent class's setup method is called. +This method is async and called when the component is first created. You must call `await super._setup()` at the beginning of this method to make sure each parent class's setup method is called. -`__onTimeListener()` +`_onTimeListener()` -This method is async and called when the player's time changes. As with `__setup()`, you must call `await super.__onTimeListener()` at the beginning of this method. +This method is async and called when the player's time changes. As with `_setup()`, you must call `await super._onTimeListener()` at the beginning of this method. Current mixins available for use: diff --git a/packages/cxl-ui/src/components/cxl-jw-player/cxl-jw-player-transcript/index.js b/packages/cxl-ui/src/components/cxl-jw-player/cxl-jw-player-transcript/index.js new file mode 100644 index 000000000..35a3a47c2 --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-jw-player/cxl-jw-player-transcript/index.js @@ -0,0 +1,21 @@ +import { html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import style from '../../../styles/global/cxl-jw-player/cxl-jw-player-transcript-css'; +import shadowStyle from '../../../styles/cxl-jw-player/cxl-jw-player-transcript-shadow-css'; + +@customElement('cxl-jw-player-transcript') +export class CXLJWPlayerTranscriptElement extends LitElement { + static get styles() { + return [shadowStyle]; + } + + render() { + return html``; + } + + async _setup() { + await super._setup(); + + this._addStyle(style); + } +} diff --git a/packages/cxl-ui/src/components/jw-player/index.html.js b/packages/cxl-ui/src/components/cxl-jw-player/index.html.js similarity index 72% rename from packages/cxl-ui/src/components/jw-player/index.html.js rename to packages/cxl-ui/src/components/cxl-jw-player/index.html.js index fd37a2de1..c248e1a3e 100644 --- a/packages/cxl-ui/src/components/jw-player/index.html.js +++ b/packages/cxl-ui/src/components/cxl-jw-player/index.html.js @@ -11,7 +11,7 @@ export const template = function () { ? html`
- ${this.__tracks.map( + ${this._tracks.map( (track, index) => html`${track.isChapter - ? html`

+ ? html`

${track.data.text}

` : html` ${track.data.text} diff --git a/packages/cxl-ui/src/components/cxl-jw-player/index.js b/packages/cxl-ui/src/components/cxl-jw-player/index.js new file mode 100644 index 000000000..1feb2ec0b --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-jw-player/index.js @@ -0,0 +1,49 @@ +import { LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import style from '../../styles/global/cxl-jw-player/cxl-jw-player-css'; +import shadowStyle from '../../styles/cxl-jw-player/cxl-jw-player-shadow-css'; +import { template } from './index.html'; +import { + BaseMixin, + ChapterNavigationMixin, + NextUpMixin, + StateMixin, + TranscriptMixin, +} from './mixins'; +import { mixin } from './utility'; + +@customElement('cxl-jw-player') +export class CXLJWPlayerElement extends mixin(LitElement, [ + BaseMixin, + TranscriptMixin, + ChapterNavigationMixin, + NextUpMixin, + StateMixin, +]) { + config = { + height: '100%', + width: '100%', + playbackRateControls: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], + plugins: { + // 'http://192.168.0.101:8080/telemetry-8.20.0.js': {}, + }, + skin: { + name: 'cxl-institute', + }, + stretching: 'uniform', + }; + + static get styles() { + return [shadowStyle]; + } + + render() { + return template.bind(this)(); + } + + async _setup() { + await super._setup(); + + this._addStyle(style); + } +} diff --git a/packages/cxl-ui/src/components/cxl-jw-player/mixins/BaseMixin.js b/packages/cxl-ui/src/components/cxl-jw-player/mixins/BaseMixin.js new file mode 100644 index 000000000..6068c042a --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-jw-player/mixins/BaseMixin.js @@ -0,0 +1,235 @@ +import * as jose from 'jose'; +import { render } from 'lit'; +import { property } from 'lit/decorators.js'; +import { throttle } from 'lodash-es'; +import { parseSync } from 'subtitle'; +import { MD5 } from 'crypto-js'; +import { MediaQueryController } from '@vaadin/component-base/src/media-query-controller.js'; + +export function BaseMixin(BaseClass) { + class Mixin extends BaseClass { + _boundOnTimeListener; + + _chapters; + + _jwPlayer; + + _jwPlayerContainer; + + // Device Detector media query. + _wideMediaQuery = '(min-width: 750px)'; + + @property({ attribute: 'api-secret', type: String }) apiSecret = ''; + + @property({ attribute: 'is-public', type: Boolean }) isPublic; + + @property({ attribute: 'library-id', type: String }) libraryId; + + @property({ attribute: 'library-source', type: String }) librarySource; + + @property({ attribute: 'media-id', type: String }) mediaId; + + @property({ attribute: 'media-source', type: String }) mediaSource; + + @property({ attribute: 'playlist-id', type: String }) playlistId; + + @property({ attribute: 'playlist-source', type: String }) playlistSource; + + // MediaQueryController. + @property({ type: Boolean, reflect: true }) + wide; + + constructor() { + super(); + + this.addController( + new MediaQueryController(this._wideMediaQuery, (matches) => { + this.wide = matches; + }) + ); + } + + async firstUpdated(_changedProperties) { + await super.firstUpdated(_changedProperties); + + await this._beforeSetup(); + this._setup(); + } + + updated(_changedProperties) { + super.updated(_changedProperties); + if (_changedProperties.has('captions') || _changedProperties.has('mediaId')) { + // this._setup(); + } + } + + get _scriptUrl() { + if (!this.libraryId && !this.librarySource) return false; + + let scriptUrl; + + if (this.libraryId) { + if (this.isPublic) { + scriptUrl = `https://content.jwplatform.com/libraries/${this.libraryId}.js`; + } else { + scriptUrl = this.__signedURL(`libraries/${this.libraryId}.js`); + } + } + + if (this.librarySource) { + scriptUrl = this.librarySource; + } + + return scriptUrl; + } + + _addStyle(style) { + const el = document.createElement('style'); + render(style, el); + this.appendChild(el); + } + + // eslint-disable-next-line class-methods-use-this, no-empty-function + _beforeSetup() {} + + async _getChapters() { + const playlistItem = this._jwPlayer.getPlaylistItem(); + const chapters = playlistItem.tracks.filter((track) => track.kind === 'chapters'); + + if (chapters.length === 0) { + return []; + } + + const { file } = chapters.length > 0 ? chapters[0] : ''; + const response = await (await fetch(file)).text(); + + return parseSync(response); + } + + async _getMedia() { + if (!this.mediaId && !this.mediaSource) return false; + + let response; + + if (this.mediaId) { + if (this.isPublic) { + response = await fetch(`https://cdn.jwplayer.com/v2/media/${this.mediaId}`); + } else { + response = await fetch(await this._signedJWTURL(`/v2/media/${this.mediaId}`)); + } + } + + if (this.mediaSource) { + response = await fetch(this.mediaSource); + } + + return response.json(); + } + + async _getPlaylist() { + if (!this.playlistId && !this.playlistSource) return false; + + let response; + + if (this.playlistId) { + if (this.isPublic) { + response = await fetch(`https://cdn.jwplayer.com/v2/playlists/${this.playlistId}`); + } else { + response = await fetch(await this._signedJWTURL(`/v2/playlists/${this.playlistId}`)); + } + } + + if (this.playlistSource) { + response = await fetch(this.playlistSource); + } + + return response.json(); + } + + async _loadScript() { + return new Promise(async (resolve) => { + const el = document.createElement('script'); + el.src = this._scriptUrl; + el.onload = () => { + resolve(self.jwplayer); + }; + document.head.appendChild(el); + }); + } + + /** + * Each mixin has the ability to hook onto this method. + */ + + // eslint-disable-next-line class-methods-use-this, no-unused-vars, no-empty-function + async _onReadyListener() {} + + // eslint-disable-next-line class-methods-use-this, no-unused-vars, no-empty-function + async _onTimeListener(event) {} + + _registerListeners() { + this._boundOnTimeListener = throttle(this._onTimeListener.bind(this), 1000); + this._jwPlayer.on('time', this._boundOnTimeListener); + } + + /** + * Each mixin has the ability to hook onto this method. + */ + async _setup() { + // Merge configs from `cxlJWPlayerData`. + if (typeof window.cxlJWPlayerData !== 'undefined') { + // eslint-disable-next-line camelcase + const { media_config } = window.cxlJWPlayerData[this.mediaId]; + // eslint-disable-next-line camelcase + this.config = { ...this.config, ...media_config }; + } + + const jwPlayer = await this._loadScript(); + + const el = document.createElement('div'); + this.appendChild(el); + + this._jwPlayer = jwPlayer(el).setup({ + ...this.config, + ...(await this._getMedia()), + ...(await this._getPlaylist()), + }); + + await new Promise((resolve) => { + this._jwPlayer.on('ready', async () => { + await this._onReadyListener(); + + resolve(); + }); + }); + + this._jwPlayerContainer = this._jwPlayer.getContainer(); + + this._registerListeners(); + + this._chapters = await this._getChapters(); + } + + async _signedJWTURL(path) { + const secret = new TextEncoder().encode(this.apiSecret); + const alg = 'HS256'; + const typ = 'JWT'; + + const token = await new jose.SignJWT({ resource: path }) + .setProtectedHeader({ alg, typ }) + .setExpirationTime('2h') + .sign(secret); + + return `https://cdn.jwplayer.com${path}?token=${token}`; + } + + __signedURL(path) { + const expires = Math.ceil((new Date().getTime() + 3600) / 300) * 300; + const signature = MD5(`${path}:${expires}:${this.apiSecret}`); + + return `https://cdn.jwplayer.com/${path}?exp=${expires}&sig=${signature}`; + } + } + + return Mixin; +} diff --git a/packages/cxl-ui/src/components/cxl-jw-player/mixins/NextUpMixin.js b/packages/cxl-ui/src/components/cxl-jw-player/mixins/NextUpMixin.js new file mode 100644 index 000000000..b654ccc58 --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-jw-player/mixins/NextUpMixin.js @@ -0,0 +1,65 @@ +import { html, render } from 'lit'; +import { property } from 'lit/decorators.js'; +import style from '../../../styles/global/cxl-jw-player/cxl-jw-player-nextup-css'; +export function NextUpMixin(BaseClass) { + class Mixin extends BaseClass { + _nextupCTA; + _nextupCTAMobile; + + @property({ attribute: 'nextupoffset', type: String }) nextupoffset = '-100%`'; + + async _beforeSetup() { + await super._beforeSetup(); + + this.config.nextupoffset = this.nextupoffset; + } + + async _setup() { + await super._setup(); + + this._addStyle(style); + + this._nextupCTA = document.createElement('div'); + this._nextupCTA.classList.add('jw-nextup-cta'); + + this._nextupCTAMobile = document.createElement('div'); + this._nextupCTAMobile.classList.add('jw-nextup-cta-mobile'); + + const container = this.querySelector('.jw-nextup-container'); + container.insertBefore(this._nextupCTA, container.firstChild); + container.insertBefore(this._nextupCTAMobile, container.firstChild); + + this._updateNextUp(); + this._jwPlayer.on('playlistItem', this._updateNextUp.bind(this)); + } + + _updateNextUp() { + const playlistItem = this._jwPlayer.getPlaylistItem(); + + if (playlistItem && playlistItem.coursePage) { + render(this._getTemplate(playlistItem), this._nextupCTA); + render(this._getMobileTemplate(playlistItem), this._nextupCTAMobile); + } + } + + // eslint-disable-next-line class-methods-use-this + _getMobileTemplate(playlistItem) { + return html` + + Go to course + + `; + } + + // eslint-disable-next-line class-methods-use-this + _getTemplate(playlistItem) { + return html` + + Go to course + + `; + } + } + + return Mixin; +} diff --git a/packages/cxl-ui/src/components/cxl-jw-player/mixins/StateMixin.js b/packages/cxl-ui/src/components/cxl-jw-player/mixins/StateMixin.js new file mode 100644 index 000000000..1046210a7 --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-jw-player/mixins/StateMixin.js @@ -0,0 +1,78 @@ +export function StateMixin(BaseClass) { + class Mixin extends BaseClass { + _endpoint; + + _nonce; + + _userId; + + async _index() { + if (this.playlistId) { + const index = + localStorage.getItem(`cxl-jw-player-${this.playlistId}-index`) || + this._jwPlayer.getPlaylistIndex(); + + this._jwPlayer.playlistItem(index); + + this._jwPlayer.on('playlistItem', async ({ index }) => { + localStorage.setItem(`cxl-jw-player-${this.playlistId}-index`, index); + }); + } + } + + async _onReadyListener() { + await this._index(); + this._position(); + this._playbackRate(); + } + + async _setup() { + await super._setup(); + + this._endpoint = `${window.ajaxurl}?action=jwplayer_save_position`; + + if (typeof window.cxl_pum_vars !== 'undefined') { + this._nonce = window.cxl_pum_vars.nonce; + } + } + + _playbackRate() { + const playbackRate = localStorage.getItem(`cxl-jw-player-playback-rate`); + + if (playbackRate) { + this._jwPlayer.setPlaybackRate(Number(playbackRate)); + } + + this._jwPlayer.on('playbackRateChanged', ({ playbackRate }) => { + localStorage.setItem(`cxl-jw-player-playback-rate`, playbackRate); + }); + } + + _position() { + if (this.mediaId) { + this._setPosition(); + } + + if (this.playlistId) { + this._jwPlayer.on('playlistItem', async ({ index }) => { + await jwplayer().getPlaylistItemPromise(index); + this._setPosition(); + }); + } + + this._jwPlayer.on('seek time', ({ position }) => { + const mediaId = this.mediaId || this._jwPlayer.getPlaylistItem().mediaid; + localStorage.setItem(`cxl-jw-player-${mediaId}-position`, position); + }); + } + + _setPosition() { + const mediaId = this.mediaId || this._jwPlayer.getPlaylistItem().mediaid; + const position = localStorage.getItem(`cxl-jw-player-${mediaId}-position`); + + this._jwPlayer.seek(Number(position)); + } + } + + return Mixin; +} diff --git a/packages/cxl-ui/src/components/cxl-jw-player/mixins/TranscriptMixin.js b/packages/cxl-ui/src/components/cxl-jw-player/mixins/TranscriptMixin.js new file mode 100644 index 000000000..9f72c6f90 --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-jw-player/mixins/TranscriptMixin.js @@ -0,0 +1,191 @@ +import { property, state, query } from 'lit/decorators.js'; +import { debounce } from 'lodash-es'; +import Mark from 'mark.js'; +import { parseSync } from 'subtitle'; + +export function TranscriptMixin(BaseClass) { + class Mixin extends BaseClass { + _debouncedSearch; + + _mark; + + @property({ reflect: true, type: Boolean }) captions = false; + @property({ attribute: 'has-captions', reflect: true, type: Boolean }) hasCaptions = false; + + @state() _currentCue = 0; + + @state() _currentTrack = 0; + + @state() _isSearchMinimumLength = false; + + @state() _matches = 0; + + @property({ attribute: 'minimum-search-length', type: Number }) minimumSearchLength = 3; + + @property({ type: Boolean }) shouldScroll = true; + + @query('#search') _searchElement; + + @state() _searchValue; + + @state() _tracks = []; + + constructor() { + super(); + + this._debouncedSearch = debounce(this._search, 300); + } + + async _getCaptions() { + const playlistItem = this._jwPlayer.getPlaylistItem(); + const track = playlistItem.tracks.filter((track) => track.kind === 'captions')[0]; + + if (!track) { + return []; + } + + const response = await (await fetch(track.file)).text(); + + return parseSync(response); + } + + /* eslint-disable array-callback-return, class-methods-use-this, consistent-return, no-return-assign */ + _getCaptionsInChapter(chapters, captions, index) { + return captions.filter((caption) => { + if (caption.data.start >= chapters[index].data.start) { + if (chapters[index + 1]) { + if (caption.data.start <= chapters[index + 1].data.start) { + return caption; + } + } else { + return caption; + } + } + }); + } + /* eslint-enable array-callback-return, class-methods-use-this, consistent-return, no-return-assign */ + + async _getTracks() { + const tracks = []; + + const captions = await this._getCaptions(); + + if (captions.length) { + const chapters = [...[{ data: { start: 0, text: '' } }], ...(await this._getChapters())]; + + chapters.forEach((chapter, index) => { + tracks.push({ ...chapter, ...{ isChapter: true } }); + tracks.push(...this._getCaptionsInChapter(chapters, captions, index)); + }); + } + + return tracks; + } + + _onCaptionClick(e) { + const index = Number(e.currentTarget.dataset.index); + this._jwPlayer.seek(this._tracks[index].data.start / 1000); + } + + _onTimeListener(event) { + super._onTimeListener(event); + + const position = event.position * 1000; // Convert to milliseconds + + this._tracks.forEach(({ data: { end, start } }, index) => { + if (start <= position && end >= position) { + if (this.shouldScroll) { + const el = this.renderRoot.querySelector(`[data-index="${index}"]`); + if (el) { + el.scrollIntoView(true); + } + } + + this._currentTrack = index; + } + }); + } + + _search() { + this._mark.unmark(); + + if (this._searchElement.value.length >= this.minimumSearchLength) { + this._isSearchMinimumLength = true; + + this._mark.mark(this._searchElement.value, { + done: (total) => { + this._matches = total; + }, + separateWordSearch: false, + }); + } else { + this._isSearchMinimumLength = false; + } + } + + async _setup() { + await super._setup(); + + this._setupTranscript(); + + this._jwPlayer.on('playlistItem', this._setupTranscript.bind(this)); + } + + async _setupTranscript() { + if (!this._jwPlayer) return; + + this._tracks = []; + + if (this.captions) { + this._tracks = await this._getTracks(); + } + + if (this._tracks.length) { + this.captions = true; + + // Make sure the DOM is up to date + await this.updateComplete; + + this._mark = new Mark(this.renderRoot.querySelectorAll('.captions h2, .captions span')); + + this._jwPlayer.addButton( + ``, + 'Transcript', + this._toggleTranscript.bind(this), + 'toggle-transcript' + ); + } else { + this.captions = false; + this._jwPlayer.removeButton('toggle-transcript'); + } + } + + updated(changedProperties) { + super.updated(changedProperties); + + if (changedProperties.has('captions')) { + if (this.captions) { + this._setupTranscript(); + } else if (this.mark) { + this._mark.unmark(); + } + } + } + + _attachListeners() { + super._attachListeners(); + } + + _toggleShouldScroll() { + this.shouldScroll = !this.shouldScroll; + } + + _toggleTranscript() { + // this.dispatchEvent(new CustomEvent('toggle-transcript')); + + this.captions = !this.captions; + } + } + + return Mixin; +} diff --git a/packages/cxl-ui/src/components/jw-player/mixins/chapter/index.html.js b/packages/cxl-ui/src/components/cxl-jw-player/mixins/chapter-navigation/index.html.js similarity index 73% rename from packages/cxl-ui/src/components/jw-player/mixins/chapter/index.html.js rename to packages/cxl-ui/src/components/cxl-jw-player/mixins/chapter-navigation/index.html.js index 88b64537d..cb89afd14 100644 --- a/packages/cxl-ui/src/components/jw-player/mixins/chapter/index.html.js +++ b/packages/cxl-ui/src/components/cxl-jw-player/mixins/chapter-navigation/index.html.js @@ -8,20 +8,19 @@ export const chapterNavigationTemplate = function (chapters) {
Chapters - - ✕ +
    ${chapters.map( (chapter, index) => html` -
  • +
  • ${chapter.data.text}
  • ` diff --git a/packages/cxl-ui/src/components/cxl-jw-player/mixins/chapter-navigation/index.js b/packages/cxl-ui/src/components/cxl-jw-player/mixins/chapter-navigation/index.js new file mode 100644 index 000000000..009a4c0aa --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-jw-player/mixins/chapter-navigation/index.js @@ -0,0 +1,63 @@ +import { render } from 'lit'; +import { property } from 'lit/decorators.js'; +import style from '../../../../styles/global/cxl-jw-player/cxl-jw-player-chapter-navigation-css'; +import { chapterNavigationTemplate } from './index.html'; + +export function ChapterNavigationMixin(BaseClass) { + class Mixin extends BaseClass { + _chapterNavigation; + + @property({ attribute: 'plugin-path', type: String }) pluginPath; + + async _setupChapterNavigation() { + const chapters = await this._getChapters(); + + if (!chapters.length) { + this._jwPlayer.removeButton('toggle-chapters'); + + return; + } + + render(chapterNavigationTemplate.bind(this)(chapters), this._chapterNavigation); + + this._jwPlayer.addButton( + ``, + 'Show Chapters', + this._toggleChapterNavigation.bind(this), + 'toggle-chapters' + ); + } + + _chapterSeek(e) { + const index = Number(e.currentTarget.dataset.index); + this._jwPlayer.seek(this._chapters[index].data.start / 1000); + } + + async _setup() { + await super._setup(); + + this._addStyle(style); + + this._chapterNavigation = document.createElement('div'); + this._chapterNavigation.classList.add('chapter-navigation'); + this._chapterNavigation.hidden = true; + this._jwPlayerContainer.appendChild(this._chapterNavigation); + + if (this.mediaId) { + this._setupChapterNavigation(); + } + + if (this.playlistId) { + this._jwPlayer.on('playlistItem', () => { + this._setupChapterNavigation(); + }); + } + } + + _toggleChapterNavigation() { + this._chapterNavigation.hidden = !this._chapterNavigation.hidden; + } + } + + return Mixin; +} diff --git a/packages/cxl-ui/src/components/cxl-jw-player/mixins/index.js b/packages/cxl-ui/src/components/cxl-jw-player/mixins/index.js new file mode 100644 index 000000000..ba2613c5a --- /dev/null +++ b/packages/cxl-ui/src/components/cxl-jw-player/mixins/index.js @@ -0,0 +1,5 @@ +export { BaseMixin } from './BaseMixin'; +export { ChapterNavigationMixin } from './chapter-navigation/index.js'; +export { NextUpMixin } from './NextUpMixin'; +export { StateMixin } from './StateMixin'; +export { TranscriptMixin } from './TranscriptMixin'; diff --git a/packages/cxl-ui/src/components/jw-player/utility.js b/packages/cxl-ui/src/components/cxl-jw-player/utility.js similarity index 100% rename from packages/cxl-ui/src/components/jw-player/utility.js rename to packages/cxl-ui/src/components/cxl-jw-player/utility.js diff --git a/packages/cxl-ui/src/components/jw-player/index.js b/packages/cxl-ui/src/components/jw-player/index.js deleted file mode 100644 index c0a80cb51..000000000 --- a/packages/cxl-ui/src/components/jw-player/index.js +++ /dev/null @@ -1,34 +0,0 @@ -import { LitElement } from 'lit'; -import { customElement } from 'lit/decorators.js'; -import { BaseMixin, CaptionMixin, ChapterMixin, SavePositionMixin } from './mixins'; -import style from '../../styles/jw-player/jw-player-css'; -import { mixin } from './utility'; -import { template } from './index.html'; - -@customElement('jw-player') -export class JWPlayerElement extends mixin(LitElement, [ - BaseMixin, - CaptionMixin, - ChapterMixin, - SavePositionMixin, -]) { - config = { - width: '100%', - height: '100%', - playbackRateControls: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], - plugins: { - // 'http://192.168.0.101:8080/telemetry-8.20.0.js': {}, - }, - skin: { - name: 'cxl-institute', - }, - }; - - static get styles() { - return [style]; - } - - render() { - return template.bind(this)(); - } -} diff --git a/packages/cxl-ui/src/components/jw-player/mixins/BaseMixin.js b/packages/cxl-ui/src/components/jw-player/mixins/BaseMixin.js deleted file mode 100644 index 5f49c5e83..000000000 --- a/packages/cxl-ui/src/components/jw-player/mixins/BaseMixin.js +++ /dev/null @@ -1,117 +0,0 @@ -import { property } from 'lit/decorators.js'; -import { throttle } from 'lodash-es'; -import { parseSync } from 'subtitle'; - -export function BaseMixin(BaseClass) { - class Mixin extends BaseClass { - __boundOnTimeListener; - - __chapters; - - __jwPlayer; - - __jwPlayerContainer; - - __position; - - @property({ attribute: 'media-id', type: String }) mediaId; - - @property({ attribute: 'player-id', type: String }) playerId; - - @property({ attribute: 'playlist-id', type: String }) playlistId; - - firstUpdated(_changedProperties) { - super.firstUpdated(_changedProperties); - - this.__setup(); - } - - updated(_changedProperties) { - super.updated(_changedProperties); - if (_changedProperties.has('captions') || _changedProperties.has('mediaId')) { - // this.__setup(); - } - } - - get __scriptUrl() { - return `https://content.jwplatform.com/libraries/${this.playerId}.js`; - } - - async __getChapters() { - const playlistItem = this.__jwPlayer.getPlaylistItem(); - const { file } = playlistItem.tracks.filter((track) => track.kind === 'chapters')[0]; - const response = await (await fetch(file)).text(); - - return parseSync(response); - } - - async __getMedia() { - if (!this.mediaId) return false; - - const response = await fetch(`https://cdn.jwplayer.com/v2/media/${this.mediaId}`); - const result = await response.json(); - - return result; - } - - async __getPlaylist() { - if (!this.playlistId) return false; - - const response = await fetch(`https://cdn.jwplayer.com/v2/playlists/${this.playlistId}`); - const result = await response.json(); - - return result; - } - - async __loadScript() { - return new Promise((resolve) => { - const el = document.createElement('script'); - el.src = this.__scriptUrl; - el.onload = () => { - resolve(self.jwplayer); - }; - document.head.appendChild(el); - }); - } - - /** - * Each mixin has the ability to hook onto this method. - */ - - // eslint-disable-next-line class-methods-use-this, no-unused-vars, no-empty-function - async __onTimeListener(event) {} - - __registerListeners() { - this.__boundOnTimeListener = throttle(this.__onTimeListener.bind(this), 1000); - this.__jwPlayer.on('time', this.__boundOnTimeListener); - } - - /** - * Each mixin has the ability to hook onto this method. - */ - async __setup() { - const jwPlayer = await this.__loadScript(); - - const el = document.createElement('div'); - this.appendChild(el); - - this.__jwPlayer = jwPlayer(el).setup({ - ...this.config, - ...(await this.__getMedia()), - ...(await this.__getPlaylist()), - }); - - await new Promise((resolve) => { - this.__jwPlayer.on('ready', resolve); - }); - - this.__jwPlayerContainer = this.__jwPlayer.getContainer(); - - this.__registerListeners(); - - this.__chapters = await this.__getChapters(); - } - } - - return Mixin; -} diff --git a/packages/cxl-ui/src/components/jw-player/mixins/CaptionMixin.js b/packages/cxl-ui/src/components/jw-player/mixins/CaptionMixin.js deleted file mode 100644 index fbeb3f765..000000000 --- a/packages/cxl-ui/src/components/jw-player/mixins/CaptionMixin.js +++ /dev/null @@ -1,154 +0,0 @@ -import { property, state, query } from 'lit/decorators.js'; -import { debounce } from 'lodash-es'; -import Mark from 'mark.js'; -import { parseSync } from 'subtitle'; - -export function CaptionMixin(BaseClass) { - class Mixin extends BaseClass { - __debouncedSearch; - - __mark; - - @property({ type: Boolean }) captions = false; - - @state() __currentCue = 0; - - @state() __currentTrack = 0; - - @state() __isSearchMinimumLength = false; - - @state() __matches = 0; - - @property({ attribute: 'minimum-search-length', type: Number }) minimumSearchLength = 3; - - @property({ type: Boolean }) shouldScroll = true; - - @query('#search') __searchElement; - - @state() __searchValue; - - @state() __tracks = []; - - constructor() { - super(); - - this.__debouncedSearch = debounce(this.__search, 300); - } - - async __getCaptions() { - const playlistItem = this.__jwPlayer.getPlaylistItem(); - const { file } = playlistItem.tracks.filter((track) => track.kind === 'captions')[0]; - const response = await (await fetch(file)).text(); - - return parseSync(response); - } - - /* eslint-disable array-callback-return, class-methods-use-this, consistent-return, no-return-assign */ - __getCaptionsInChapter(chapters, captions, index) { - return captions.filter((caption) => { - if (caption.data.start >= chapters[index].data.start) { - if (chapters[index + 1]) { - if (caption.data.start <= chapters[index + 1].data.start) { - return caption; - } - } else { - return caption; - } - } - }); - } - /* eslint-enable array-callback-return, class-methods-use-this, consistent-return, no-return-assign */ - - async __getTracks() { - const tracks = []; - - const captions = await this.__getCaptions(); - const chapters = [...[{ data: { start: 0, text: '' } }], ...(await this.__getChapters())]; - - chapters.forEach((chapter, index) => { - tracks.push({ ...chapter, ...{ isChapter: true } }); - tracks.push(...this.__getCaptionsInChapter(chapters, captions, index)); - }); - - return tracks; - } - - __onCaptionClick(e) { - const index = Number(e.currentTarget.dataset.index); - this.__jwPlayer.seek(this.__tracks[index].data.start / 1000); - } - - __onTimeListener(event) { - super.__onTimeListener(event); - - const position = event.position * 1000; // Convert to milliseconds - - this.__tracks.forEach(({ data: { end, start } }, index) => { - if (start <= position && end >= position) { - if (this.shouldScroll) { - const el = this.renderRoot.querySelector(`[data-index="${index}"]`); - if (el) { - el.scrollIntoView(true); - } - } - - this.__currentTrack = index; - } - }); - } - - __search() { - this.__mark.unmark(); - - if (this.__searchElement.value.length >= this.minimumSearchLength) { - this.__isSearchMinimumLength = true; - - this.__mark.mark(this.__searchElement.value, { - done: (total) => { - this.__matches = total; - }, - separateWordSearch: false, - }); - } else { - this.__isSearchMinimumLength = false; - } - } - - async __setup() { - await super.__setup(); - - this.__setupCaptions(); - } - - async __setupCaptions() { - if (!this.__jwPlayer) return; - - if (this.captions) { - this.__tracks = await this.__getTracks(); - - // Make sure the DOM is up to date - await this.updateComplete; - - this.__mark = new Mark(this.renderRoot.querySelectorAll('.captions h2, .captions span')); - } - } - - updated(changedProperties) { - super.updated(changedProperties); - - if (changedProperties.has('captions')) { - if (this.captions) { - this.__setupCaptions(); - } else if (this.mark) { - this.__mark.unmark(); - } - } - } - - __toggleShouldScroll() { - this.shouldScroll = !this.shouldScroll; - } - } - - return Mixin; -} diff --git a/packages/cxl-ui/src/components/jw-player/mixins/SavePositionMixin.js b/packages/cxl-ui/src/components/jw-player/mixins/SavePositionMixin.js deleted file mode 100644 index 6ae10b0b6..000000000 --- a/packages/cxl-ui/src/components/jw-player/mixins/SavePositionMixin.js +++ /dev/null @@ -1,44 +0,0 @@ -export function SavePositionMixin(BaseClass) { - class Mixin extends BaseClass { - __endpoint = ''; - - __nonce; - - __userId; - - async __setup() { - await super.__setup(); - - this.__loadPosition(); - } - - async __loadPosition() { - this.__jwPlayer.seek(Number(localStorage.getItem(`jw-player-${this.mediaId}-position`))); - this.__jwPlayer.pause(); - } - - /** - * The listener is registered in the base class (../index.js). - */ - __onTimeListener(event) { - super.__onTimeListener(event); - - this.__savePosition(event); - } - - __savePosition({ position }) { - localStorage.setItem(`jw-player-${this.mediaId}-position`, position); - - fetch(this.__endpoint, { - _ajax_nonce: this.__nonce, - body: JSON.stringify({ mediaId: this.mediaId, position }), - headers: { - 'Content-Type': 'application/json', - }, - method: 'POST', - }); - } - } - - return Mixin; -} diff --git a/packages/cxl-ui/src/components/jw-player/mixins/chapter/index.js b/packages/cxl-ui/src/components/jw-player/mixins/chapter/index.js deleted file mode 100644 index aa386cd9e..000000000 --- a/packages/cxl-ui/src/components/jw-player/mixins/chapter/index.js +++ /dev/null @@ -1,53 +0,0 @@ -import { render } from 'lit'; -import { property } from 'lit/decorators.js'; -import style from '../../../../styles/jw-player/chapter-css'; -import { chapterNavigationTemplate } from './index.html'; - -export function ChapterMixin(BaseClass) { - class Mixin extends BaseClass { - @property({ attribute: 'plugin-path', type: String }) pluginPath; - - async __createChapterNavigation() { - const chapters = await this.__getChapters(); - - this.__chapterNavigation = document.createElement('div'); - this.__chapterNavigation.classList.add('chapter-navigation'); - this.__chapterNavigation.hidden = true; - - render(chapterNavigationTemplate.bind(this)(chapters), this.__chapterNavigation); - - this.__jwPlayerContainer.appendChild(this.__chapterNavigation); - - this.__jwPlayer.addButton( - `${this.pluginPath}images/chapter-bookmark-icon.svg`, - 'Show Chapters', - this.__toggleChapterNavigation.bind(this), - 'toggle-chapters' - ); - } - - __addStyle() { - const el = document.createElement('style'); - render(style, el); - this.appendChild(el); - } - - __chapterSeek(e) { - const index = Number(e.currentTarget.dataset.index); - this.__jwPlayer.seek(this.__chapters[index].data.start / 1000); - } - - async __setup() { - await super.__setup(); - - this.__addStyle(); - this.__createChapterNavigation(); - } - - __toggleChapterNavigation() { - this.__chapterNavigation.hidden = !this.__chapterNavigation.hidden; - } - } - - return Mixin; -} diff --git a/packages/cxl-ui/src/components/jw-player/mixins/index.js b/packages/cxl-ui/src/components/jw-player/mixins/index.js deleted file mode 100644 index ca56fdb7c..000000000 --- a/packages/cxl-ui/src/components/jw-player/mixins/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export { BaseMixin } from './BaseMixin'; -export { CaptionMixin } from './CaptionMixin'; -export { ChapterMixin } from './chapter'; -export { SavePositionMixin } from './SavePositionMixin'; diff --git a/packages/cxl-ui/src/index-jwplayer.js b/packages/cxl-ui/src/index-jwplayer.js index 6106a8c1f..557a5aaa7 100644 --- a/packages/cxl-ui/src/index-jwplayer.js +++ b/packages/cxl-ui/src/index-jwplayer.js @@ -1 +1 @@ -export { JWPlayerElement } from './components/jw-player/index.js'; +export { CXLJWPlayerElement } from './components/cxl-jw-player/index.js'; diff --git a/packages/cxl-ui/src/index-storybook.js b/packages/cxl-ui/src/index-storybook.js index 0e071ca35..826de3c28 100644 --- a/packages/cxl-ui/src/index-storybook.js +++ b/packages/cxl-ui/src/index-storybook.js @@ -19,7 +19,7 @@ export { CXLSaveFavoriteElement } from './components/cxl-save-favorite.js'; export { CXLStarRatingElement } from './components/cxl-star-rating.js'; export { CXLLikeOrDislikeElement } from './components/cxl-like-or-dislike.js'; export { CXLPaywallElement } from './components/cxl-paywall.js'; -export { JWPlayerElement } from './components/jw-player/index.js'; +export { CXLJWPlayerElement } from './components/cxl-jw-player/index.js'; // @todo maybe https://github.com/tc39/proposal-export-default-from export { Headroom }; diff --git a/packages/cxl-ui/src/styles/jw-player/.gitignore b/packages/cxl-ui/src/styles/cxl-jw-player/.gitignore similarity index 100% rename from packages/cxl-ui/src/styles/jw-player/.gitignore rename to packages/cxl-ui/src/styles/cxl-jw-player/.gitignore diff --git a/packages/cxl-ui/src/styles/global/cxl-jw-player/.gitignore b/packages/cxl-ui/src/styles/global/cxl-jw-player/.gitignore new file mode 100644 index 000000000..f935021a8 --- /dev/null +++ b/packages/cxl-ui/src/styles/global/cxl-jw-player/.gitignore @@ -0,0 +1 @@ +!.gitignore diff --git a/packages/storybook/cxl-ui/cxl-jw-player/index.stories.js b/packages/storybook/cxl-ui/cxl-jw-player/index.stories.js new file mode 100644 index 000000000..60b129bc8 --- /dev/null +++ b/packages/storybook/cxl-ui/cxl-jw-player/index.stories.js @@ -0,0 +1,68 @@ +import { html } from 'lit'; +import '@conversionxl/cxl-ui/src/components/cxl-jw-player/index.js'; + +export default { + title: 'CXL UI/cxl-jw-player', +}; + +const Template = ({ + apiSecret, + captions, + isPublic, + libraryId, + librarySource, + mediaId, + mediaSource, + minimumSearchLength, + playlistId, + playlistSource, + pluginPath, +}) => + html` + + + + `; + +export const Default = Template.bind({}); + +Object.assign(Default, { + args: { + apiSecret: '', + captions: true, + isPublic: true, + libraryId: '5CFJNXKb', + librarySource: '', + mediaId: 'fZ0XiGdb', + mediaSource: '', + minimumSearchLength: 3, + playlistId: '', + playlistSource: '', + pluginPath: 'https://cxl.com/institute/wp-content/plugins/cxl-jwplayer/', + }, +}); diff --git a/packages/storybook/cxl-ui/cxl-jw-player/playlist.stories.js b/packages/storybook/cxl-ui/cxl-jw-player/playlist.stories.js new file mode 100644 index 000000000..7e32092de --- /dev/null +++ b/packages/storybook/cxl-ui/cxl-jw-player/playlist.stories.js @@ -0,0 +1,71 @@ +import { html } from 'lit'; +import '@conversionxl/cxl-ui/src/components/cxl-jw-player/index.js'; + +export default { + title: 'CXL UI/cxl-jw-player', +}; + +const Template = ({ + apiSecret, + captions, + isPublic, + libraryId, + librarySource, + mediaId, + mediaSource, + minimumSearchLength, + nextupoffset, + playlistId, + playlistSource, + pluginPath, +}) => + html` + + + + `; + +export const Playlist = Template.bind({}); + +Object.assign(Playlist, { + args: { + apiSecret: '', + captions: true, + isPublic: true, + libraryId: '5CFJNXKb', + librarySource: '', + mediaId: '', + mediaSource: '', + minimumSearchLength: 3, + nextupoffset: '-100%', + playlistId: 'tAxwbNsA', + playlistSource: '', + pluginPath: 'https://cxl.com/institute/wp-content/plugins/cxl-jwplayer/', + }, +}); diff --git a/packages/storybook/cxl-ui/jw-player/index.stories.js b/packages/storybook/cxl-ui/jw-player/index.stories.js deleted file mode 100644 index 17bf14379..000000000 --- a/packages/storybook/cxl-ui/jw-player/index.stories.js +++ /dev/null @@ -1,45 +0,0 @@ -import { html } from 'lit'; -import '@conversionxl/cxl-ui/src/components/jw-player/index.js'; - -export default { - title: 'JW Player/JW Player', -}; - -const Template = ({ captions, mediaId, minimumSearchLength, playerId, playlistId, pluginPath }) => - html` - - - `; - -export const Default = Template.bind({}); - -Object.assign(Default, { - args: { - captions: true, - mediaId: 'fZ0XiGdb', - minimumSearchLength: 3, - playerId: '5CFJNXKb', - playlistId: '', - pluginPath: 'https://cxl.com/institute/wp-content/plugins/cxl-jwplayer/', - }, -}); diff --git a/packages/storybook/cxl-ui/jw-player/playlist.stories.js b/packages/storybook/cxl-ui/jw-player/playlist.stories.js deleted file mode 100644 index 752c77387..000000000 --- a/packages/storybook/cxl-ui/jw-player/playlist.stories.js +++ /dev/null @@ -1,45 +0,0 @@ -import { html } from 'lit'; -import '@conversionxl/cxl-ui/src/components/jw-player/index.js'; - -export default { - title: 'JW Player/JW Player', -}; - -const Template = ({ captions, mediaId, playerId, playlistId, pluginPath }) => - html` - - - `; - -export const Playlist = Template.bind({}); - -Object.assign(Playlist, { - args: { - captions: true, - mediaId: '', - playerId: '5CFJNXKb', - playlistId: 'tAxwbNsA', - pluginPath: 'https://cxl.com/institute/wp-content/plugins/cxl-jwplayer/', - }, -}); diff --git a/yarn.lock b/yarn.lock index b3ac10475..08b0f8282 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7127,6 +7127,11 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" +crypto-js@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" + integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== + crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" @@ -11213,6 +11218,11 @@ jest-worker@^27.4.5: merge-stream "^2.0.0" supports-color "^8.0.0" +jose@^4.13.1: + version "4.13.1" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.13.1.tgz#449111bb5ab171db85c03f1bd2cb1647ca06db1c" + integrity sha512-MSJQC5vXco5Br38mzaQKiq9mwt7lwj2eXpgpRyQYNHYt2lq1PjkWa7DLXX0WVcQLE9HhMh3jPiufS7fhJf+CLQ== + js-string-escape@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"