From 5966d73ecdde383ec73dc2a5c73e66a19a8c9604 Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Thu, 30 Sep 2021 12:55:42 +0100 Subject: [PATCH 1/4] issue/14-b Ported changes to ES6 variant --- example.json | 9 +++++---- properties.schema | 27 +++++++++++++++++++-------- templates/youtube.hbs | 16 +++++++++++++++- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/example.json b/example.json index 0bd72e8..8f427ed 100644 --- a/example.json +++ b/example.json @@ -5,15 +5,16 @@ "_classes": "", "_layout": "full", "_component": "youtube", - "title": "Karle pyar karle", - "displayTitle": "Karle pyar karle", - "body": "The legendary Asha Bosle…", + "title": "The first video on YouTube", + "displayTitle": "The first video on YouTube", + "body": "Entitled 'Me at the zoo', this was the first video ever uploaded to YouTube, by platform co-founder Jawed Karim", "instruction": "", "_setCompletionOn": "play", "_media": { - "_source": "//www.youtube.com/embed/mDa42EkgO1A", + "_source": "//www.youtube.com/embed/jNQXAC9IVRw", "_controls": true, "_allowFullscreen": true, + "_playsinline": false, "_aspectRatio": 1.33, "_autoplay": false, "_showRelated": false, diff --git a/properties.schema b/properties.schema index 08d366c..d46595b 100644 --- a/properties.schema +++ b/properties.schema @@ -53,16 +53,19 @@ "_source": { "type": "string", "required": true, - "default": "//www.youtube.com/embed/6qzOMCIlzt4", + "default": "", "title": "Source URL", "inputType": "Text", + "editorAttrs": { + "placeholder": "//www.youtube.com/embed/jNQXAC9IVRw" + }, "validators": [], "help": "The 'embed' URL of the YouTube video you want to be displayed" }, "_controls": { "type": "boolean", "required": false, - "default": "true", + "default": true, "title": "Show Player Controls", "inputType": "Checkbox", "validators": [], @@ -71,11 +74,19 @@ "_allowFullscreen": { "type": "boolean", "required": false, - "default": "true", + "default": true, "title": "Allow Fullscreen?", "inputType": "Checkbox", "validators": [] }, + "_playsinline": { + "type": "boolean", + "required": false, + "default": false, + "title": "If enabled, videos will play 'inline' on iPhones (the same way they do on iPads).", + "inputType": "Checkbox", + "validators": [] + }, "_aspectRatio": { "type": "number", "required": false, @@ -88,7 +99,7 @@ "_autoplay": { "type": "boolean", "required": false, - "default": "false", + "default": false, "title": "Autoplay", "inputType": "Checkbox", "validators": [], @@ -97,7 +108,7 @@ "_showRelated": { "type": "boolean", "required": false, - "default": "false", + "default": false, "title": "Show related videos", "inputType": "Checkbox", "validators": [], @@ -106,7 +117,7 @@ "_loop": { "type": "boolean", "required": false, - "default": "false", + "default": false, "title": "Loop", "inputType": "Checkbox", "validators": [], @@ -115,7 +126,7 @@ "_modestBranding": { "type": "boolean", "required": false, - "default": "true", + "default": true, "title": "Modest branding", "inputType": "Checkbox", "validators": [], @@ -137,7 +148,7 @@ "_showAnnotations": { "type": "boolean", "required": false, - "default": "false", + "default": false, "title": "Video annotations", "inputType": "Checkbox", "validators": [], diff --git a/templates/youtube.hbs b/templates/youtube.hbs index 0ceda07..b9be04c 100644 --- a/templates/youtube.hbs +++ b/templates/youtube.hbs @@ -9,7 +9,21 @@
- + {{#if _media._source}} + + {{/if}}
From 49d6df04cebadf4249155f53c6d9ebab4dbd7a1e Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Thu, 30 Sep 2021 13:13:02 +0100 Subject: [PATCH 2/4] issue/14-b Fixed no source recommendation --- js/adapt-youtube.js | 24 ++++++------------------ templates/youtube.hbs | 2 ++ 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/js/adapt-youtube.js b/js/adapt-youtube.js index 95c9eeb..d96cab7 100644 --- a/js/adapt-youtube.js +++ b/js/adapt-youtube.js @@ -18,12 +18,9 @@ class YouTubeView extends ComponentView { initialize() { super.initialize(); - _.bindAll(this, 'onPlayerStateChange', 'onPlayerReady', 'onInview'); - this.player = null; this.debouncedTriggerGlobalEvent = _.debounce(this.triggerGlobalEvent.bind(this), 1000); - if (window.onYouTubeIframeAPIReady !== undefined) return; window.onYouTubeIframeAPIReady = () => { Adapt.log.info('YouTube iframe API loaded'); @@ -43,9 +40,7 @@ class YouTubeView extends ComponentView { setIFrameSize() { const $iframe = this.$('iframe'); const widgetWidth = this.$('.component__widget').width(); - $iframe.width(widgetWidth); - // default aspect ratio to 16:9 if not specified const aspectRatio = parseFloat(this.model.get('_media')._aspectRatio) || 1.778; if (isNaN(aspectRatio)) return; @@ -54,24 +49,27 @@ class YouTubeView extends ComponentView { postRender() { // for HTML/HBS parameters: https://developers.google.com/youtube/player_parameters + if (!this.model.get('_media')?._source) { + this.setReadyStatus(); + this.model.setCompletionStatus(); + return; + } if (Adapt.youTubeIframeAPIReady === true) { this.onYouTubeIframeAPIReady(); return; } - Adapt.once('youTubeIframeAPIReady', this.onYouTubeIframeAPIReady, this); + this.listenToOnce(Adapt, 'youTubeIframeAPIReady', this.onYouTubeIframeAPIReady); } remove() { if (this.player !== null) { this.player.destroy(); } - super.remove(); } setupEventListeners() { this.completionEvent = (this.model.get('_setCompletionOn') || 'play'); - if (this.completionEvent !== 'inview') return; this.setupInviewCompletion('.component__widget'); } @@ -83,20 +81,15 @@ class YouTubeView extends ComponentView { onReady: this.onPlayerReady } }); - this.isPlaying = false; - this.setReadyStatus(); - this.setupEventListeners(); - this.setIFrameSize(); } onMediaStop(view) { // if it was this view that triggered the media:stop event, ignore it if (view && view.cid === this.cid) return; - if (!this.isPlaying) return; this.player.pauseVideo(); } @@ -118,23 +111,18 @@ class YouTubeView extends ComponentView { switch (e.data) { case window.YT.PlayerState.PLAYING: Adapt.trigger('media:stop', this); - this.debouncedTriggerGlobalEvent('play');// use debounced version because seeking whilst playing will trigger two 'play' events - this.isPlaying = true; - if (this.model.get('_setCompletionOn') === 'play') { this.setCompletionStatus(); } break; case window.YT.PlayerState.PAUSED: this.isPlaying = false; - this.triggerGlobalEvent('pause'); break; case window.YT.PlayerState.ENDED: this.triggerGlobalEvent('ended'); - if (this.model.get('_setCompletionOn') === 'ended') { this.setCompletionStatus(); } diff --git a/templates/youtube.hbs b/templates/youtube.hbs index b9be04c..9aba031 100644 --- a/templates/youtube.hbs +++ b/templates/youtube.hbs @@ -23,6 +23,8 @@ {{#if _media._allowFullscreen}} allowfullscreen="true"{{/if}} frameborder="0"> + {{else}} + ERROR: No media source set! {{/if}} From a68fc2ab1a509a4e7c1521db512bffddc9410c59 Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Thu, 30 Sep 2021 13:51:39 +0100 Subject: [PATCH 3/4] issue/14-b Moved view into separate file --- js/YouTubeView.js | 176 ++++++++++++++++++++++++++++++++++++++++++++ js/adapt-youtube.js | 176 +------------------------------------------- 2 files changed, 177 insertions(+), 175 deletions(-) create mode 100644 js/YouTubeView.js diff --git a/js/YouTubeView.js b/js/YouTubeView.js new file mode 100644 index 0000000..2d6ed75 --- /dev/null +++ b/js/YouTubeView.js @@ -0,0 +1,176 @@ +import Adapt from 'core/js/adapt'; +import ComponentView from 'core/js/views/componentView'; + +export default class YouTubeView extends ComponentView { + + get template() { + return 'youtube'; + } + + events() { + return { + 'click .js-youtube-inline-transcript-toggle': 'onToggleInlineTranscript', + 'click .js-youtube-external-transcript-click': 'onExternalTranscriptClicked', + 'click .js-skip-to-transcript': 'onSkipToTranscript' + }; + } + + initialize() { + super.initialize(); + _.bindAll(this, 'onPlayerStateChange', 'onPlayerReady', 'onInview'); + this.player = null; + this.debouncedTriggerGlobalEvent = _.debounce(this.triggerGlobalEvent.bind(this), 1000); + if (window.onYouTubeIframeAPIReady !== undefined) return; + window.onYouTubeIframeAPIReady = () => { + Adapt.log.info('YouTube iframe API loaded'); + Adapt.youTubeIframeAPIReady = true; + Adapt.trigger('youTubeIframeAPIReady'); + }; + $.getScript('//www.youtube.com/iframe_api'); + } + + preRender() { + this.listenTo(Adapt, { + 'device:resize device:changed': this.setIFrameSize, + 'media:stop': this.onMediaStop + }); + } + + setIFrameSize() { + const $iframe = this.$('iframe'); + const widgetWidth = this.$('.component__widget').width(); + $iframe.width(widgetWidth); + // default aspect ratio to 16:9 if not specified + const aspectRatio = parseFloat(this.model.get('_media')._aspectRatio) || 1.778; + if (isNaN(aspectRatio)) return; + $iframe.height(widgetWidth / aspectRatio); + } + + postRender() { + // for HTML/HBS parameters: https://developers.google.com/youtube/player_parameters + if (!this.model.get('_media')?._source) { + this.setReadyStatus(); + this.model.setCompletionStatus(); + return; + } + if (Adapt.youTubeIframeAPIReady === true) { + this.onYouTubeIframeAPIReady(); + return; + } + this.listenToOnce(Adapt, 'youTubeIframeAPIReady', this.onYouTubeIframeAPIReady); + } + + remove() { + if (this.player !== null) { + this.player.destroy(); + } + super.remove(); + } + + setupEventListeners() { + this.completionEvent = (this.model.get('_setCompletionOn') || 'play'); + if (this.completionEvent !== 'inview') return; + this.setupInviewCompletion('.component__widget'); + } + + onYouTubeIframeAPIReady() { + this.player = new window.YT.Player(this.$('iframe').get(0), { + events: { + onStateChange: this.onPlayerStateChange, + onReady: this.onPlayerReady + } + }); + this.isPlaying = false; + this.setReadyStatus(); + this.setupEventListeners(); + this.setIFrameSize(); + } + + onMediaStop(view) { + // if it was this view that triggered the media:stop event, ignore it + if (view && view.cid === this.cid) return; + if (!this.isPlaying) return; + this.player.pauseVideo(); + } + + onPlayerReady() { + if (!this.model.get('_media')._playbackQuality) return; + this.player.setPlaybackQuality(this.model.get('_media')._playbackQuality); + } + + /** + * this seems to have issues in Chrome if the user is logged into YouTube (possibly any Google account) - the API just doesn't broadcast the events + * but instead throws the error: + * Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('https://www.youtube.com') does not match the recipient window's origin ('http://www.youtube.com'). + * This is documented here: + * https://code.google.com/p/gdata-issues/issues/detail?id=5788 + * but I haven't managed to get any of the workarounds to work... :-( + */ + onPlayerStateChange(e) { + switch (e.data) { + case window.YT.PlayerState.PLAYING: + Adapt.trigger('media:stop', this); + this.debouncedTriggerGlobalEvent('play');// use debounced version because seeking whilst playing will trigger two 'play' events + this.isPlaying = true; + if (this.model.get('_setCompletionOn') === 'play') { + this.setCompletionStatus(); + } + break; + case window.YT.PlayerState.PAUSED: + this.isPlaying = false; + this.triggerGlobalEvent('pause'); + break; + case window.YT.PlayerState.ENDED: + this.triggerGlobalEvent('ended'); + if (this.model.get('_setCompletionOn') === 'ended') { + this.setCompletionStatus(); + } + break; + } + } + + onSkipToTranscript() { + // need slight delay before focussing button to make it work when JAWS is running + // see https://github.com/adaptlearning/adapt_framework/issues/2427 + _.delay(() => { + Adapt.a11y.focusFirst(this.$('.youtube__transcript-btn'), { defer: true }); + }, 250); + } + + onToggleInlineTranscript(e) { + if (e && e.preventDefault) e.preventDefault(); + + const $transcriptBodyContainer = this.$('.youtube__transcript-body-inline'); + const $button = this.$('.youtube__transcript-btn-inline'); + const $buttonText = $button.find('.youtube__transcript-btn-text'); + const config = this.model.get('_transcript'); + const shouldOpen = !$transcriptBodyContainer.hasClass('inline-transcript-open'); + const buttonText = shouldOpen ? + config.inlineTranscriptCloseButton : + config.inlineTranscriptButton; + + $transcriptBodyContainer + .stop(true).slideToggle(() => $(window).resize()) + .toggleClass('inline-transcript-open', shouldOpen); + $button.attr('aria-expanded', shouldOpen); + $buttonText.html(buttonText); + + if (!shouldOpen || config._setCompletionOnView === false) return; + this.setCompletionStatus(); + } + + onExternalTranscriptClicked() { + if (this.model.get('_transcript')._setCompletionOnView === false) return; + this.setCompletionStatus(); + } + + triggerGlobalEvent(eventType) { + Adapt.trigger('media', { + isVideo: true, + type: eventType, + src: this.model.get('_media')._source, + platform: 'YouTube' + }); + } + +} diff --git a/js/adapt-youtube.js b/js/adapt-youtube.js index d96cab7..5d681bc 100644 --- a/js/adapt-youtube.js +++ b/js/adapt-youtube.js @@ -1,180 +1,6 @@ import Adapt from 'core/js/adapt'; -import ComponentView from 'core/js/views/componentView'; import ComponentModel from 'core/js/models/componentModel'; - -class YouTubeView extends ComponentView { - - get template() { - return 'youtube'; - } - - events() { - return { - 'click .js-youtube-inline-transcript-toggle': 'onToggleInlineTranscript', - 'click .js-youtube-external-transcript-click': 'onExternalTranscriptClicked', - 'click .js-skip-to-transcript': 'onSkipToTranscript' - }; - } - - initialize() { - super.initialize(); - _.bindAll(this, 'onPlayerStateChange', 'onPlayerReady', 'onInview'); - this.player = null; - this.debouncedTriggerGlobalEvent = _.debounce(this.triggerGlobalEvent.bind(this), 1000); - if (window.onYouTubeIframeAPIReady !== undefined) return; - window.onYouTubeIframeAPIReady = () => { - Adapt.log.info('YouTube iframe API loaded'); - Adapt.youTubeIframeAPIReady = true; - Adapt.trigger('youTubeIframeAPIReady'); - }; - $.getScript('//www.youtube.com/iframe_api'); - } - - preRender() { - this.listenTo(Adapt, { - 'device:resize device:changed': this.setIFrameSize, - 'media:stop': this.onMediaStop - }); - } - - setIFrameSize() { - const $iframe = this.$('iframe'); - const widgetWidth = this.$('.component__widget').width(); - $iframe.width(widgetWidth); - // default aspect ratio to 16:9 if not specified - const aspectRatio = parseFloat(this.model.get('_media')._aspectRatio) || 1.778; - if (isNaN(aspectRatio)) return; - $iframe.height(widgetWidth / aspectRatio); - } - - postRender() { - // for HTML/HBS parameters: https://developers.google.com/youtube/player_parameters - if (!this.model.get('_media')?._source) { - this.setReadyStatus(); - this.model.setCompletionStatus(); - return; - } - if (Adapt.youTubeIframeAPIReady === true) { - this.onYouTubeIframeAPIReady(); - return; - } - this.listenToOnce(Adapt, 'youTubeIframeAPIReady', this.onYouTubeIframeAPIReady); - } - - remove() { - if (this.player !== null) { - this.player.destroy(); - } - super.remove(); - } - - setupEventListeners() { - this.completionEvent = (this.model.get('_setCompletionOn') || 'play'); - if (this.completionEvent !== 'inview') return; - this.setupInviewCompletion('.component__widget'); - } - - onYouTubeIframeAPIReady() { - this.player = new window.YT.Player(this.$('iframe').get(0), { - events: { - onStateChange: this.onPlayerStateChange, - onReady: this.onPlayerReady - } - }); - this.isPlaying = false; - this.setReadyStatus(); - this.setupEventListeners(); - this.setIFrameSize(); - } - - onMediaStop(view) { - // if it was this view that triggered the media:stop event, ignore it - if (view && view.cid === this.cid) return; - if (!this.isPlaying) return; - this.player.pauseVideo(); - } - - onPlayerReady() { - if (!this.model.get('_media')._playbackQuality) return; - this.player.setPlaybackQuality(this.model.get('_media')._playbackQuality); - } - - /** - * this seems to have issues in Chrome if the user is logged into YouTube (possibly any Google account) - the API just doesn't broadcast the events - * but instead throws the error: - * Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('https://www.youtube.com') does not match the recipient window's origin ('http://www.youtube.com'). - * This is documented here: - * https://code.google.com/p/gdata-issues/issues/detail?id=5788 - * but I haven't managed to get any of the workarounds to work... :-( - */ - onPlayerStateChange(e) { - switch (e.data) { - case window.YT.PlayerState.PLAYING: - Adapt.trigger('media:stop', this); - this.debouncedTriggerGlobalEvent('play');// use debounced version because seeking whilst playing will trigger two 'play' events - this.isPlaying = true; - if (this.model.get('_setCompletionOn') === 'play') { - this.setCompletionStatus(); - } - break; - case window.YT.PlayerState.PAUSED: - this.isPlaying = false; - this.triggerGlobalEvent('pause'); - break; - case window.YT.PlayerState.ENDED: - this.triggerGlobalEvent('ended'); - if (this.model.get('_setCompletionOn') === 'ended') { - this.setCompletionStatus(); - } - break; - } - } - - onSkipToTranscript() { - // need slight delay before focussing button to make it work when JAWS is running - // see https://github.com/adaptlearning/adapt_framework/issues/2427 - _.delay(() => { - Adapt.a11y.focusFirst(this.$('.youtube__transcript-btn'), { defer: true }); - }, 250); - } - - onToggleInlineTranscript(e) { - if (e && e.preventDefault) e.preventDefault(); - - const $transcriptBodyContainer = this.$('.youtube__transcript-body-inline'); - const $button = this.$('.youtube__transcript-btn-inline'); - const $buttonText = $button.find('.youtube__transcript-btn-text'); - const config = this.model.get('_transcript'); - const shouldOpen = !$transcriptBodyContainer.hasClass('inline-transcript-open'); - const buttonText = shouldOpen ? - config.inlineTranscriptCloseButton : - config.inlineTranscriptButton; - - $transcriptBodyContainer - .stop(true).slideToggle(() => $(window).resize()) - .toggleClass('inline-transcript-open', shouldOpen); - $button.attr('aria-expanded', shouldOpen); - $buttonText.html(buttonText); - - if (!shouldOpen || config._setCompletionOnView === false) return; - this.setCompletionStatus(); - } - - onExternalTranscriptClicked() { - if (this.model.get('_transcript')._setCompletionOnView === false) return; - this.setCompletionStatus(); - } - - triggerGlobalEvent(eventType) { - Adapt.trigger('media', { - isVideo: true, - type: eventType, - src: this.model.get('_media')._source, - platform: 'YouTube' - }); - } - -} +import YouTubeView from './YouTubeView'; export default Adapt.register('youtube', { model: ComponentModel.extend({}), From c48853d82f42a8c1951c04b2afe82201b382c5cb Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Wed, 13 Oct 2021 14:40:06 +0100 Subject: [PATCH 4/4] Update js/YouTubeView.js Co-authored-by: eleanor-heath <61159216+eleanor-heath@users.noreply.github.com> --- js/YouTubeView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/YouTubeView.js b/js/YouTubeView.js index 2d6ed75..ea3cca4 100644 --- a/js/YouTubeView.js +++ b/js/YouTubeView.js @@ -41,7 +41,7 @@ export default class YouTubeView extends ComponentView { const widgetWidth = this.$('.component__widget').width(); $iframe.width(widgetWidth); // default aspect ratio to 16:9 if not specified - const aspectRatio = parseFloat(this.model.get('_media')._aspectRatio) || 1.778; + const aspectRatio = (parseFloat(this.model.get('_media')._aspectRatio) || 1.778); if (isNaN(aspectRatio)) return; $iframe.height(widgetWidth / aspectRatio); }