From 28e93cc41b73bae64cb05547cb3ab88fe3e7367b Mon Sep 17 00:00:00 2001 From: Cameron Ball Date: Thu, 17 Nov 2016 16:43:03 +0800 Subject: [PATCH] MDL-55324 atto_media: Implement HTML compliant media plugin This patch completely reworks the atto media plugin to allow insertion of HTML media elements. --- .../atto/plugins/media/lang/en/atto_media.php | 51 + lib/editor/atto/plugins/media/lib.php | 97 +- lib/editor/atto/plugins/media/styles.css | 106 ++ .../atto/plugins/media/styles_clean.css | 28 + .../plugins/media/tests/behat/media.feature | 181 +++- .../moodle-atto_media-button-debug.js | 943 ++++++++++++++++-- .../moodle-atto_media-button-min.js | 5 +- .../moodle-atto_media-button.js | 943 ++++++++++++++++-- .../plugins/media/yui/src/button/js/button.js | 943 ++++++++++++++++-- .../atto/tests/fixtures/moodle-logo.mp4 | Bin 0 -> 6930 bytes .../atto/tests/fixtures/pretty-good-en.vtt | 54 + .../atto/tests/fixtures/pretty-good-sv.vtt | 54 + lib/form/editor.php | 10 + 13 files changed, 3135 insertions(+), 280 deletions(-) create mode 100644 lib/editor/atto/plugins/media/styles.css create mode 100644 lib/editor/atto/plugins/media/styles_clean.css create mode 100755 lib/editor/atto/tests/fixtures/moodle-logo.mp4 create mode 100755 lib/editor/atto/tests/fixtures/pretty-good-en.vtt create mode 100755 lib/editor/atto/tests/fixtures/pretty-good-sv.vtt diff --git a/lib/editor/atto/plugins/media/lang/en/atto_media.php b/lib/editor/atto/plugins/media/lang/en/atto_media.php index 58b8cabd47e..6c57e130f14 100644 --- a/lib/editor/atto/plugins/media/lang/en/atto_media.php +++ b/lib/editor/atto/plugins/media/lang/en/atto_media.php @@ -22,10 +22,61 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['add'] = 'Add'; +$string['addcaptionstrack'] = 'Add caption track'; +$string['addchapterstrack'] = 'Add chapter track'; +$string['adddescriptionstrack'] = 'Add description track'; +$string['addmetadatatrack'] = 'Add metadata track'; +$string['addsource'] = 'Add alternative source'; +$string['addsource_help'] = 'It is recommended that an alternative media source is provided, since desktop and mobile browsers vary in which file formats they support.'; +$string['addsubtitlestrack'] = 'Add subtitle track'; +$string['addtrack'] = 'Add track'; +$string['advancedsettings'] = 'Advanced settings'; +$string['audio'] = 'Audio'; +$string['audiosourcelabel'] = 'Audio source URL'; +$string['autoplay'] = 'Play automatically'; $string['browserepositories'] = 'Browse repositories...'; +$string['captions'] = 'Captions'; +$string['captions_help'] = 'Captions may be used to describe everything happening in the track, including non-verbal sounds such as a phone ringing.'; +$string['captionssourcelabel'] = 'Caption track URL'; +$string['chapters'] = 'Chapters'; +$string['chapters_help'] = 'Chapter titles may be provided for use in navigating the media resource.'; +$string['chapterssourcelabel'] = 'Chapter track URL'; +$string['controls'] = 'Show controls'; $string['createmedia'] = 'Insert media'; +$string['default'] = 'Default'; +$string['descriptions'] = 'Descriptions'; +$string['descriptions_help'] = 'Audio descriptions may be used to provide a narration which explains visual details not apparent from the audio alone.'; +$string['descriptionssourcelabel'] = 'Description track URL'; +$string['displayoptions'] = 'Display options'; $string['entername'] = 'Enter name'; +$string['entersource'] = 'Source URL'; $string['enterurl'] = 'Enter URL'; $string['height'] = 'Height'; +$string['kind'] = 'Type'; +$string['label'] = 'Label'; +$string['languagesavailable'] = 'Languages available'; +$string['languagesinstalled'] = 'Languages installed'; +$string['link'] = 'Link'; +$string['loop'] = 'Loop'; +$string['metadata'] = 'Metadata'; +$string['metadata_help'] = 'Metadata tracks, for use from a script, may be used only if the player supports metadata'; +$string['metadatasourcelabel'] = 'Metadata track URL'; +$string['mute'] = 'Muted'; $string['pluginname'] = 'Media'; +$string['poster'] = 'Thumbnail URL'; +$string['remove'] = 'Remove'; +$string['size'] = 'Size'; +$string['srclang'] = 'Language'; +$string['subtitles'] = 'Subtitles'; +$string['subtitles_help'] = 'Subtitles may be used to provide a transcription or translation of the dialogue.'; +$string['subtitlessourcelabel'] = 'Subtitle track URL'; +$string['track'] = 'Track URL'; +$string['tracks'] = 'Subtitles and captions'; +$string['tracks_help'] = 'Subtitles, captions, chapters and descriptions can be added via a WebVTT (Web Video Text Tracks) format file. Track labels will be shown in the selection dropdown menu. For each type of track, any track set as default will be pre-selected at the start of the video.'; +$string['video'] = 'Video'; +$string['videoheight'] = 'Video height'; +$string['videosourcelabel'] = 'Video source URL'; +$string['videowidth'] = 'Video width'; $string['width'] = 'Width'; + diff --git a/lib/editor/atto/plugins/media/lib.php b/lib/editor/atto/plugins/media/lib.php index 62e0dd4850e..86a9d379ea5 100644 --- a/lib/editor/atto/plugins/media/lib.php +++ b/lib/editor/atto/plugins/media/lib.php @@ -28,9 +28,100 @@ function atto_media_strings_for_js() { global $PAGE; - $PAGE->requires->strings_for_js(array('createmedia', - 'enterurl', + $PAGE->requires->strings_for_js(array('add', + 'addcaptionstrack', + 'addchapterstrack', + 'adddescriptionstrack', + 'addmetadatatrack', + 'addsource', + 'addsubtitlestrack', + 'addtrack', + 'advancedsettings', + 'audio', + 'audiosourcelabel', + 'autoplay', + 'browserepositories', + 'browserepositories', + 'captions', + 'captionssourcelabel', + 'chapters', + 'chapterssourcelabel', + 'controls', + 'createmedia', + 'default', + 'descriptions', + 'descriptionssourcelabel', + 'displayoptions', 'entername', - 'browserepositories'), + 'entername', + 'entersource', + 'enterurl', + 'height', + 'kind', + 'label', + 'languagesavailable', + 'languagesinstalled', + 'link', + 'loop', + 'metadata', + 'metadatasourcelabel', + 'mute', + 'poster', + 'remove', + 'size', + 'srclang', + 'subtitles', + 'subtitlessourcelabel', + 'track', + 'tracks', + 'video', + 'videoheight', + 'videosourcelabel', + 'videowidth', + 'width'), 'atto_media'); } + +/** + * Sends the parameters to the JS module. + * + * @return array + */ +function atto_media_params_for_js() { + global $OUTPUT; + global $PAGE; + $currentlang = current_language(); + $langsinstalled = get_string_manager()->get_list_of_translations(true); + $langsavailable = get_string_manager()->get_list_of_languages(); + $params = [ + 'langs' => ['installed' => [], 'available' => []], + 'help' => [] + ]; + + foreach ($langsinstalled as $code => $name) { + $params['langs']['installed'][] = [ + 'lang' => $name, + 'code' => $code, + 'default' => $currentlang == $code + ]; + } + + foreach ($langsavailable as $code => $name) { + // See MDL-50829 for an explanation of this lrm thing. + $lrm = json_decode('"\u200E"'); + $params['langs']['available'][] = [ + 'lang' => $name . ' ' . $lrm . '(' . $code . ')' . $lrm, 'code' => $code]; + } + + $params['help'] = [ + 'addsource' => $OUTPUT->help_icon('addsource', 'atto_media'), + 'tracks' => $OUTPUT->help_icon('tracks', 'atto_media'), + 'subtitles' => $OUTPUT->help_icon('subtitles', 'atto_media'), + 'captions' => $OUTPUT->help_icon('captions', 'atto_media'), + 'descriptions' => $OUTPUT->help_icon('descriptions', 'atto_media'), + 'chapters' => $OUTPUT->help_icon('chapters', 'atto_media'), + 'metadata' => $OUTPUT->help_icon('metadata', 'atto_media') + ]; + + return $params; +} diff --git a/lib/editor/atto/plugins/media/styles.css b/lib/editor/atto/plugins/media/styles.css new file mode 100644 index 00000000000..149ad94c03a --- /dev/null +++ b/lib/editor/atto/plugins/media/styles.css @@ -0,0 +1,106 @@ +.atto_form.atto_media #video input, +.atto_form.atto_media #audio input, +.atto_form.atto_media #link input { + box-sizing: border-box; + height: inherit; +} + +.atto_form.atto_media > .tab-content { + max-height: 60vh; + overflow-x: hidden; + padding-left: 20px; + padding-right: 20px; + margin-left: -20px; + margin-right: -21px; +} + +.atto_form.atto_media [id$="-advanced-settings"] label { + margin-right: 10px; +} + +.atto_form.atto_media label { + display: inline-block; +} + +.atto_form.atto_media label > span { + display: inline-block; + min-width: 6em; +} + +.atto_form.atto_media .atto_media_track_lang_entry, +.atto_form.atto_media .atto_media_track_label_entry { + width: 168px; +} + +.atto_form.atto_media .atto_media_track_source { + margin-bottom: 10px; +} + +.atto_form.atto_media select { + margin-right: 10px; +} + +.atto_form.atto_media [id$="-tracks"] input[type=checkbox] { + margin-left: 10px; +} + +.atto_form.atto_media .atto_media_track ~ .atto_media_track { + margin-top: 5px; + padding-top: 10px; + border-top: 1px solid #e5e5e5; +} + +.atto_form.atto_media label.fullwidth { + width: 100%; +} + +.atto_media_postersize { + display: inline-block; +} + +.atto_media_postersize input[type=text] { + width: 3em; +} + +input[size].atto_media_url_entry { + width: calc(100% - 15px); +} + +.openmediabrowser { + margin-top: -4px; +} + +.addcomponent, +.removecomponent { + font-weight: bold; + margin-right: 10px; +} + +.trackhelp { + text-align: right; +} + +.atto_form.atto_media .atto_media_source > label { + width: calc(100% - 153px); +} + +.atto_form.atto_media .atto_media_track_lang_entry, +.atto_form.atto_media .atto_media_track_label_entry { + width: 116px; +} + +.langlabel { + width: 42%; +} + +.labellabel { + width: 44%; +} + +.defaultlabel { + width: 14%; +} + +[data-medium-type=link] label { + width: 100%; +} diff --git a/lib/editor/atto/plugins/media/styles_clean.css b/lib/editor/atto/plugins/media/styles_clean.css new file mode 100644 index 00000000000..e947eab2000 --- /dev/null +++ b/lib/editor/atto/plugins/media/styles_clean.css @@ -0,0 +1,28 @@ +.nav-tabs > .nav-item a.active { + color: #555; + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: transparent; + cursor: default; +} + +.atto_form.atto_media .atto_media_track_lang_entry, +.atto_form.atto_media .atto_media_track_label_entry { + width: 124px; +} + +.atto_form.atto_media .atto_media_source > label { + width: calc(100% - 168px); +} + +.langlabel { + width: 42%; +} + +.labellabel { + width: 44%; +} + +.defaultlabel { + width: 14%; +} diff --git a/lib/editor/atto/plugins/media/tests/behat/media.feature b/lib/editor/atto/plugins/media/tests/behat/media.feature index 5c1326a0bed..aa6aef06de2 100644 --- a/lib/editor/atto/plugins/media/tests/behat/media.feature +++ b/lib/editor/atto/plugins/media/tests/behat/media.feature @@ -2,25 +2,164 @@ Feature: Add media to Atto To write rich text - I need to add media. - @javascript - Scenario: Insert some media - Given I log in as "admin" - And I follow "Manage private files..." - And I upload "lib/editor/atto/tests/fixtures/moodle-logo.webm" file to "Files" filemanager - And I click on "Save changes" "button" - When I follow "Profile" in the user menu - And I follow "Blog entries" - And I follow "Add a new entry" - And I set the field "Blog entry body" to "

Media test

" - And I select the text in the "Blog entry body" Atto editor - And I set the field "Entry title" to "The best video in the entire world (not really)" - And I click on "Media" "button" - And I click on "Browse repositories..." "button" - And I click on "Private files" "link" in the ".fp-repo-area" "css_element" - And I click on "moodle-logo.webm" "link" - And I click on "Select this file" "button" - And I set the field "Enter name" to "It's the logo" - And I click on "Insert media" "button" - And I click on "Save changes" "button" - Then "video" "css_element" should be visible + Background: + Given I log in as "admin" + And I follow "Manage private files..." + And I upload "lib/editor/atto/tests/fixtures/moodle-logo.webm" file to "Files" filemanager + And I upload "lib/editor/atto/tests/fixtures/moodle-logo.mp4" file to "Files" filemanager + And I upload "lib/editor/atto/tests/fixtures/moodle-logo.png" file to "Files" filemanager + And I upload "lib/editor/atto/tests/fixtures/pretty-good-en.vtt" file to "Files" filemanager + And I upload "lib/editor/atto/tests/fixtures/pretty-good-sv.vtt" file to "Files" filemanager + And I click on "Save changes" "button" + And I follow "Profile" in the user menu + And I follow "Blog entries" + And I follow "Add a new entry" + And I set the field "Blog entry body" to "

Media test

" + And I select the text in the "Blog entry body" Atto editor + And I set the field "Entry title" to "The best video in the entire world (not really)" + And I click on "Media" "button" + @javascript + Scenario: Insert some media as a link + Given I click on "Browse repositories..." "button" in the "#id_summary_editor_link .atto_media_source.atto_media_link_source" "css_element" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "moodle-logo.webm" "link" + And I click on "Select this file" "button" + And the field "Enter name" matches value "moodle-logo.webm" + And I wait until the page is ready + And I click on "Insert media" "button" + When I click on "Save changes" "button" + Then "//a[. = 'moodle-logo.webm']" "xpath_element" should exist + + @javascript + Scenario: Insert some media as a plain video + Given I click on "Video" "link" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video .atto_media_source.atto_media_media_source" "css_element" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "moodle-logo.webm" "link" + And I click on "Select this file" "button" + And I click on "Add alternative source" "link" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video .atto_media_source.atto_media_media_source:nth-of-type(2)" "css_element" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "moodle-logo.mp4" "link" + And I click on "Select this file" "button" + When I click on "Insert media" "button" + Then "//video[descendant::source[contains(@src, 'moodle-logo.webm')]][descendant::source[contains(@src, 'moodle-logo.mp4')]]" "xpath_element" should exist + + @javascript + Scenario: Insert some media as a video with display settings + Given I click on "Video" "link" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video .atto_media_source.atto_media_media_source" "css_element" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "moodle-logo.webm" "link" + And I click on "Select this file" "button" + And I click on "Display options" "link" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video .atto_media_source.atto_media_poster_source" "css_element" + And I click on "Private files" "link" in the ".moodle-dialogue-base[aria-hidden='false'] .fp-repo-area" "css_element" + And I click on "moodle-logo.png" "link" + And I click on "Select this file" "button" in the ".moodle-dialogue-base[aria-hidden='false']" "css_element" + And I set the field with xpath "//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_width_entry ')]" to "420" + And I set the field with xpath "//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_height_entry ')]" to "69" + And I click on "Display options" "link" + When I click on "Insert media" "button" + Then "//video[descendant::source[contains(@src, 'moodle-logo.webm')]][contains(@poster, 'moodle-logo.png')][@width=420][@height=69]" "xpath_element" should exist + + @javascript + Scenario: Insert some media as a video with advanced settings + Given I click on "Video" "link" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video .atto_media_source.atto_media_media_source" "css_element" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "moodle-logo.webm" "link" + And I click on "Select this file" "button" + And I click on "Advanced settings" "link" + And the field "Show controls" matches value "1" + And I set the field "Play automatically" to "1" + And I set the field "Muted" to "1" + And I set the field "Loop" to "1" + When I click on "Insert media" "button" + Then "//video[descendant::source[contains(@src, 'moodle-logo.webm')]][@controls='true'][@loop='true'][@autoplay='true'][@autoplay='true']" "xpath_element" should exist + + @javascript + Scenario: Insert some media as a video with tracks + Given I click on "Video" "link" + And I change window size to "large" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video .atto_media_source.atto_media_media_source" "css_element" + And I click on "Private files" "link" in the ".fp-repo-area" "css_element" + And I click on "moodle-logo.webm" "link" + And I click on "Select this file" "button" + And I click on "Subtitles and captions" "link" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video_subtitles .atto_media_track_source" "css_element" + And I click on "Private files" "link" in the ".moodle-dialogue-base[aria-hidden='false'] .fp-repo-area" "css_element" + And I click on "pretty-good-sv.vtt" "link" + And I click on "Select this file" "button" in the ".moodle-dialogue-base[aria-hidden='false']" "css_element" + And the field "Label" matches value "Swedish" + And the field "Language" matches value "sv" + And I click on "Add subtitle track" "link" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video_subtitles .atto_media_track~.atto_media_track .atto_media_source.atto_media_track_source" "css_element" + And I click on "Private files" "link" in the ".moodle-dialogue-base[aria-hidden='false'] .fp-repo-area" "css_element" + And I click on "pretty-good-en.vtt" "link" + And I click on "Select this file" "button" in the ".moodle-dialogue-base[aria-hidden='false']" "css_element" + And the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_label_entry ')])[2]" matches value "English" + And I set the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_default ')])[1]" to "1" + And I click on "Captions" "link" in the ".nav-item[data-track-kind='captions']" "css_element" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video_captions .atto_media_track_source" "css_element" + And I click on "Private files" "link" in the ".moodle-dialogue-base[aria-hidden='false'] .fp-repo-area" "css_element" + And I click on "pretty-good-sv.vtt" "link" + And I click on "Select this file" "button" in the ".moodle-dialogue-base[aria-hidden='false']" "css_element" + And I click on "Overwrite" "button" + And the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_label_entry ')])[3]" matches value "Swedish" + And I click on "Add caption track" "link" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video_captions .atto_media_track~.atto_media_track .atto_media_source.atto_media_track_source" "css_element" + And I click on "Private files" "link" in the ".moodle-dialogue-base[aria-hidden='false'] .fp-repo-area" "css_element" + And I click on "pretty-good-en.vtt" "link" + And I click on "Select this file" "button" in the ".moodle-dialogue-base[aria-hidden='false']" "css_element" + And I click on "Overwrite" "button" + And the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_label_entry ')])[4]" matches value "English" + And I set the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_default ')])[4]" to "1" + And I click on "Descriptions" "link" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video_descriptions .atto_media_track_source" "css_element" + And I click on "Private files" "link" in the ".moodle-dialogue-base[aria-hidden='false'] .fp-repo-area" "css_element" + And I click on "pretty-good-sv.vtt" "link" + And I click on "Select this file" "button" in the ".moodle-dialogue-base[aria-hidden='false']" "css_element" + And I click on "Overwrite" "button" + And the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_label_entry ')])[5]" matches value "Swedish" + And I click on "Add description track" "link" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video_descriptions .atto_media_track~.atto_media_track .atto_media_source.atto_media_track_source" "css_element" + And I click on "Private files" "link" in the ".moodle-dialogue-base[aria-hidden='false'] .fp-repo-area" "css_element" + And I click on "pretty-good-en.vtt" "link" + And I click on "Select this file" "button" in the ".moodle-dialogue-base[aria-hidden='false']" "css_element" + And I click on "Overwrite" "button" + And the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_label_entry ')])[6]" matches value "English" + And I set the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_default ')])[5]" to "1" + And I click on "Chapters" "link" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video_chapters .atto_media_track_source" "css_element" + And I click on "Private files" "link" in the ".moodle-dialogue-base[aria-hidden='false'] .fp-repo-area" "css_element" + And I click on "pretty-good-sv.vtt" "link" + And I click on "Select this file" "button" in the ".moodle-dialogue-base[aria-hidden='false']" "css_element" + And I click on "Overwrite" "button" + And the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_label_entry ')])[7]" matches value "Swedish" + And I click on "Add chapter track" "link" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video_chapters .atto_media_track~.atto_media_track .atto_media_source.atto_media_track_source" "css_element" + And I click on "Private files" "link" in the ".moodle-dialogue-base[aria-hidden='false'] .fp-repo-area" "css_element" + And I click on "pretty-good-en.vtt" "link" + And I click on "Select this file" "button" in the ".moodle-dialogue-base[aria-hidden='false']" "css_element" + And I click on "Overwrite" "button" + And the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_label_entry ')])[8]" matches value "English" + And I set the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_default ')])[8]" to "1" + And I click on "Metadata" "link" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video_metadata .atto_media_track_source" "css_element" + And I click on "Private files" "link" in the ".moodle-dialogue-base[aria-hidden='false'] .fp-repo-area" "css_element" + And I click on "pretty-good-sv.vtt" "link" + And I click on "Select this file" "button" in the ".moodle-dialogue-base[aria-hidden='false']" "css_element" + And I click on "Overwrite" "button" + And the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_label_entry ')])[9]" matches value "Swedish" + And I click on "Add metadata track" "link" + And I click on "Browse repositories..." "button" in the "#id_summary_editor_video_metadata .atto_media_track~.atto_media_track .atto_media_source.atto_media_track_source" "css_element" + And I click on "Private files" "link" in the ".moodle-dialogue-base[aria-hidden='false'] .fp-repo-area" "css_element" + And I click on "pretty-good-en.vtt" "link" + And I click on "Select this file" "button" in the ".moodle-dialogue-base[aria-hidden='false']" "css_element" + And I click on "Overwrite" "button" + And the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_label_entry ')])[10]" matches value "English" + And I set the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_default ')])[9]" to "1" + When I click on "Insert media" "button" + Then "//video[descendant::source[contains(@src, 'moodle-logo.webm')]][descendant::track[contains(@src, 'pretty-good-sv.vtt')][@kind='subtitles'][@label='Swedish'][@srclang='sv'][@default='true']][descendant::track[contains(@src, 'pretty-good-en.vtt')][@kind='subtitles'][@label='English'][@srclang='en'][not(@default)]][descendant::track[contains(@src, 'pretty-good-sv.vtt')][@kind='captions'][@label='Swedish'][@srclang='sv'][not(@default)]][descendant::track[contains(@src, 'pretty-good-en.vtt')][@kind='captions'][@label='English'][@srclang='en'][@default='true']][descendant::track[contains(@src, 'pretty-good-sv.vtt')][@kind='descriptions'][@label='Swedish'][@srclang='sv'][@default='true']][descendant::track[contains(@src, 'pretty-good-en.vtt')][@kind='descriptions'][@label='English'][@srclang='en'][not(@default)]][descendant::track[contains(@src, 'pretty-good-sv.vtt')][@kind='chapters'][@label='Swedish'][@srclang='sv'][not(@default)]][descendant::track[contains(@src, 'pretty-good-en.vtt')][@kind='chapters'][@label='English'][@srclang='en'][@default='true']][descendant::track[contains(@src, 'pretty-good-sv.vtt')][@kind='metadata'][@label='Swedish'][@srclang='sv'][@default='true']][descendant::track[contains(@src, 'pretty-good-en.vtt')][@kind='metadata'][@label='English'][@srclang='en'][not(@default)]]" "xpath_element" should exist diff --git a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js index ed3048a6398..2d4acc6ddfb 100644 --- a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js +++ b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js @@ -34,58 +34,419 @@ YUI.add('moodle-atto_media-button', function (Y, NAME) { */ var COMPONENTNAME = 'atto_media', + MEDIA_TYPES = {LINK: 'LINK', VIDEO: 'VIDEO', AUDIO: 'AUDIO'}, + TRACK_KINDS = { + SUBTITLES: 'SUBTITLES', + CAPTIONS: 'CAPTIONS', + DESCRIPTIONS: 'DESCRIPTIONS', + CHAPTERS: 'CHAPTERS', + METADATA: 'METADATA' + }, CSS = { - URLINPUT: 'atto_media_urlentry', - NAMEINPUT: 'atto_media_nameentry' + SOURCE: 'atto_media_source', + TRACK: 'atto_media_track', + MEDIA_SOURCE: 'atto_media_media_source', + LINK_SOURCE: 'atto_media_link_source', + POSTER_SOURCE: 'atto_media_poster_source', + TRACK_SOURCE: 'atto_media_track_source', + DISPLAY_OPTIONS: 'atto_media_display_options', + NAME_INPUT: 'atto_media_name_entry', + URL_INPUT: 'atto_media_url_entry', + POSTER_SIZE: 'atto_media_poster_size', + LINK_SIZE: 'atto_media_link_size', + WIDTH_INPUT: 'atto_media_width_entry', + HEIGHT_INPUT: 'atto_media_height_entry', + TRACK_KIND_INPUT: 'atto_media_track_kind_entry', + TRACK_LABEL_INPUT: 'atto_media_track_label_entry', + TRACK_LANG_INPUT: 'atto_media_track_lang_entry', + TRACK_DEFAULT_SELECT: 'atto_media_track_default', + MEDIA_CONTROLS_TOGGLE: 'atto_media_controls', + MEDIA_AUTOPLAY_TOGGLE: 'atto_media_autoplay', + MEDIA_MUTE_TOGGLE: 'atto_media_mute', + MEDIA_LOOP_TOGGLE: 'atto_media_loop', + ADVANCED_SETTINGS: 'atto_media_advancedsettings', + LINK: MEDIA_TYPES.LINK.toLowerCase(), + VIDEO: MEDIA_TYPES.VIDEO.toLowerCase(), + AUDIO: MEDIA_TYPES.AUDIO.toLowerCase(), + TRACK_SUBTITLES: TRACK_KINDS.SUBTITLES.toLowerCase(), + TRACK_CAPTIONS: TRACK_KINDS.CAPTIONS.toLowerCase(), + TRACK_DESCRIPTIONS: TRACK_KINDS.DESCRIPTIONS.toLowerCase(), + TRACK_CHAPTERS: TRACK_KINDS.CHAPTERS.toLowerCase(), + TRACK_METADATA: TRACK_KINDS.METADATA.toLowerCase() }, SELECTORS = { - URLINPUT: '.' + CSS.URLINPUT, - NAMEINPUT: '.' + CSS.NAMEINPUT + SOURCE: '.' + CSS.SOURCE, + TRACK: '.' + CSS.TRACK, + MEDIA_SOURCE: '.' + CSS.MEDIA_SOURCE, + POSTER_SOURCE: '.' + CSS.POSTER_SOURCE, + TRACK_SOURCE: '.' + CSS.TRACK_SOURCE, + DISPLAY_OPTIONS: '.' + CSS.DISPLAY_OPTIONS, + NAME_INPUT: '.' + CSS.NAME_INPUT, + URL_INPUT: '.' + CSS.URL_INPUT, + POSTER_SIZE: '.' + CSS.POSTER_SIZE, + LINK_SIZE: '.' + CSS.LINK_SIZE, + WIDTH_INPUT: '.' + CSS.WIDTH_INPUT, + HEIGHT_INPUT: '.' + CSS.HEIGHT_INPUT, + TRACK_KIND_INPUT: '.' + CSS.TRACK_KIND_INPUT, + TRACK_LABEL_INPUT: '.' + CSS.TRACK_LABEL_INPUT, + TRACK_LANG_INPUT: '.' + CSS.TRACK_LANG_INPUT, + TRACK_DEFAULT_SELECT: '.' + CSS.TRACK_DEFAULT_SELECT, + MEDIA_CONTROLS_TOGGLE: '.' + CSS.MEDIA_CONTROLS_TOGGLE, + MEDIA_AUTOPLAY_TOGGLE: '.' + CSS.MEDIA_AUTOPLAY_TOGGLE, + MEDIA_MUTE_TOGGLE: '.' + CSS.MEDIA_MUTE_TOGGLE, + MEDIA_LOOP_TOGGLE: '.' + CSS.MEDIA_LOOP_TOGGLE, + ADVANCED_SETTINGS: '.' + CSS.ADVANCED_SETTINGS, + LINK_TAB: 'li[data-medium-type="' + CSS.LINK + '"]', + LINK_PANE: '.tab-pane[data-medium-type="' + CSS.LINK + '"]', + VIDEO_TAB: 'li[data-medium-type="' + CSS.VIDEO + '"]', + VIDEO_PANE: '.tab-pane[data-medium-type="' + CSS.VIDEO + '"]', + AUDIO_TAB: 'li[data-medium-type="' + CSS.AUDIO + '"]', + AUDIO_PANE: '.tab-pane[data-medium-type="' + CSS.AUDIO + '"]', + TRACK_SUBTITLES_TAB: 'li[data-track-kind="' + CSS.TRACK_SUBTITLES + '"]', + TRACK_SUBTITLES_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_SUBTITLES + '"]', + TRACK_CAPTIONS_TAB: 'li[data-track-kind="' + CSS.TRACK_CAPTIONS + '"]', + TRACK_CAPTIONS_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_CAPTIONS + '"]', + TRACK_DESCRIPTIONS_TAB: 'li[data-track-kind="' + CSS.TRACK_DESCRIPTIONS + '"]', + TRACK_DESCRIPTIONS_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_DESCRIPTIONS + '"]', + TRACK_CHAPTERS_TAB: 'li[data-track-kind="' + CSS.TRACK_CHAPTERS + '"]', + TRACK_CHAPTERS_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_CHAPTERS + '"]', + TRACK_METADATA_TAB: 'li[data-track-kind="' + CSS.TRACK_METADATA + '"]', + TRACK_METADATA_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_METADATA + '"]' }, - TEMPLATE = '' + - '
' + - '' + - '
' + - '' + - '' + - '' + - '
' + - '
' + - '' + - '
' + - '
'; + TEMPLATES = { + ROOT: '' + + '
' + + '' + + '
' + + '
' + + '{{> tab_panes.link}}' + + '
' + + '
' + + '{{> tab_panes.video}}' + + '
' + + '
' + + '{{> tab_panes.audio}}' + + '
' + + '
' + + '
' + + '
' + + '' + + '
' + + '
', + TAB_PANES: { + LINK: '' + + '{{renderPartial "form_components.source" context=this id=CSS.LINK_SOURCE}}' + + '', + VIDEO: '' + + '{{renderPartial "form_components.source" context=this id=CSS.MEDIA_SOURCE entersourcelabel="videosourcelabel"' + + ' addcomponentlabel="addsource" multisource="true" addsourcehelp=helpStrings.addsource}}' + + '' + + '' + + '', + AUDIO: '' + + '{{renderPartial "form_components.source" context=this id=CSS.MEDIA_SOURCE entersourcelabel="audiosourcelabel"' + + ' addcomponentlabel="addsource" multisource="true" addsourcehelp=helpStrings.addsource}}' + + '' + + '' + }, + FORM_COMPONENTS: { + SOURCE: '' + + '
' + + '' + + '' + + '{{#multisource}}' + + '{{renderPartial "form_components.add_component" context=../this label=../addcomponentlabel ' + + ' help=../addsourcehelp}}' + + '{{/multisource}}' + + '
', + ADD_COMPONENT: '' + + '
' + + '' + + '{{#label}}{{get_string ../label ../component}}{{/label}}' + + '{{^label}}{{get_string "add" ../component}}{{/label}}' + + '' + + '{{#help}}{{{../help}}}{{/help}}' + + '
', + REMOVE_COMPONENT: '' + + '
' + + '' + + '{{#label}}{{get_string ../label ../component}}{{/label}}' + + '{{^label}}{{get_string "remove" ../component}}{{/label}}' + + '' + + '
', + DISPLAY_OPTIONS: '' + + '
' + + '' + + '
' + + '{{renderPartial "form_components.source" context=this id=CSS.POSTER_SOURCE entersourcelabel="poster"}}' + + '
', + ADVANCED_SETTINGS: '' + + '
' + + '' + + '' + + '' + + '' + + '
', + TRACK_TABS: '' + + '' + + '
' + + '
' + + '
{{{helpStrings.subtitles}}}
' + + '{{renderPartial "form_components.track" context=this sourcelabel="subtitlessourcelabel"' + + ' addcomponentlabel="addsubtitlestrack"}}' + + '
' + + '
' + + '
{{{helpStrings.captions}}}
' + + '{{renderPartial "form_components.track" context=this sourcelabel="captionssourcelabel"' + + ' addcomponentlabel="addcaptionstrack"}}' + + '
' + + '
' + + '
{{{helpStrings.descriptions}}}
' + + '{{renderPartial "form_components.track" context=this sourcelabel="descriptionssourcelabel"' + + ' addcomponentlabel="adddescriptionstrack"}}' + + '
' + + '
' + + '
{{{helpStrings.chapters}}}
' + + '{{renderPartial "form_components.track" context=this sourcelabel="chapterssourcelabel"' + + ' addcomponentlabel="addchapterstrack"}}' + + '
' + + '
' + + '
{{{helpStrings.metadata}}}
' + + '{{renderPartial "form_components.track" context=this sourcelabel="metadatasourcelabel"' + + ' addcomponentlabel="addmetadatatrack"}}' + + '
' + + '
', + TRACK: '' + + '
' + + '{{renderPartial "form_components.source" context=this id=CSS.TRACK_SOURCE entersourcelabel=sourcelabel}}' + + '' + + '' + + '' + + '{{renderPartial "form_components.add_component" context=this label=addcomponentlabel}}' + + '
' + }, + HTML_MEDIA: { + VIDEO: '' + + '  ', + AUDIO: '' + + '  ', + LINK: '' + + '{{#name}}{{../name}}{{/name}}{{^name}}{{../url}}{{/name}}' + } + }; Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], { - /** - * A reference to the current selection at the time that the dialogue - * was opened. - * - * @property _currentSelection - * @type Range - * @private - */ - _currentSelection: null, - - /** - * A reference to the dialogue content. - * - * @property _content - * @type Node - * @private - */ - _content: null, - initializer: function() { if (this.get('host').canShowFilepicker('media')) { + this.editor.delegate('dblclick', this._displayDialogue, 'video', this); + this.editor.delegate('click', this._handleClick, 'video', this); + this.addButton({ icon: 'e/insert_edit_video', - callback: this._displayDialogue + callback: this._displayDialogue, + tags: 'video, audio', + tagMatchRequiresAll: false }); } }, + /** + * Gets the root context for all templates, with extra supplied context. + * + * @method _getContext + * @param {Object} extra The extra context to add + * @return {Object} + * @private + */ + _getContext: function(extra) { + return Y.merge({ + elementid: this.get('host').get('elementid'), + component: COMPONENTNAME, + langsinstalled: this.get('langs').installed, + langsavailable: this.get('langs').available, + helpStrings: this.get('help'), + CSS: CSS + }, extra); + }, + + /** + * Handles a click on a media element. + * + * @method _handleClick + * @param {EventFacade} e + * @private + */ + _handleClick: function(e) { + var medium = e.target; + + var selection = this.get('host').getSelectionFromNode(medium); + if (this.get('host').getSelection() !== selection) { + this.get('host').setSelection(selection); + } + }, + /** * Display the media editing tool. * @@ -93,91 +454,503 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi * @private */ _displayDialogue: function() { - // Store the current selection. - this._currentSelection = this.get('host').getSelection(); - if (this._currentSelection === false) { + if (this.get('host').getSelection() === false) { return; } + if (!('renderPartial' in Y.Handlebars.helpers)) { + (function smashPartials(chain, obj) { + Y.each(obj, function(value, index) { + chain.push(index); + if (typeof value !== "object") { + Y.Handlebars.registerPartial(chain.join('.').toLowerCase(), value); + } else { + smashPartials(chain, value); + } + chain.pop(); + }); + })([], TEMPLATES); + + Y.Handlebars.registerHelper('renderPartial', function(partialName, options) { + if (!partialName) { + return ''; + } + + var partial = Y.Handlebars.partials[partialName]; + var parentContext = options.hash.context ? Y.clone(options.hash.context) : {}; + var context = Y.merge(parentContext, options.hash); + delete context.context; + + if (!partial) { + return ''; + } + return new Y.Handlebars.SafeString(Y.Handlebars.compile(partial)(context)); + }); + } + var dialogue = this.getDialogue({ headerContent: M.util.get_string('createmedia', COMPONENTNAME), focusAfterHide: true, - focusOnShowSelector: SELECTORS.URLINPUT + width: 660, + focusOnShowSelector: SELECTORS.URL_INPUT }); // Set the dialogue content, and then show the dialogue. - dialogue.set('bodyContent', this._getDialogueContent()) - .show(); + dialogue.set('bodyContent', this._getDialogueContent(this.get('host').getSelection())).show(); + M.form.shortforms({formid: this.get('host').get('elementid') + '_atto_media_form'}); }, /** - * Return the dialogue content for the tool, attaching any required - * events. + * Returns the dialogue content for the tool. * * @method _getDialogueContent - * @return {Node} The content to place in the dialogue. + * @param {WrappedRange[]} selection Current editor selection + * @return {Y.Node} * @private */ - _getDialogueContent: function() { - var template = Y.Handlebars.compile(TEMPLATE); + _getDialogueContent: function(selection) { + var content = Y.Node.create( + Y.Handlebars.compile(TEMPLATES.ROOT)(this._getContext()) + ); - this._content = Y.Node.create(template({ - component: COMPONENTNAME, - elementid: this.get('host').get('elementid'), - CSS: CSS - })); + var medium = this.get('host').getSelectedNodes().filter('video,audio').shift(); + var mediumProperties = medium ? this._getMediumProperties(medium) : false; + return this._attachEvents(this._applyMediumProperties(content, mediumProperties), selection); + }, - this._content.one('.submit').on('click', this._setMedia, this); - this._content.one('.openmediabrowser').on('click', function(e) { + /** + * Attaches required events to the content node. + * + * @method _attachEvents + * @param {Y.Node} content The content to which events will be attached + * @param {WrappedRange[]} selection Current editor selection + * @return {Y.Node} + * @private + */ + _attachEvents: function(content, selection) { + // Delegate add component link for media source fields. + content.delegate('click', function(e) { e.preventDefault(); - this.get('host').showFilepicker('media', this._filepickerCallback, this); + this._addMediaSourceComponent(e.currentTarget); + }, SELECTORS.MEDIA_SOURCE + ' .addcomponent', this); + + // Delegate add component link for track fields. + content.delegate('click', function(e) { + e.preventDefault(); + this._addTrackComponent(e.currentTarget); + }, SELECTORS.TRACK + ' .addcomponent', this); + + // Only allow one track per tab to be selected as "default". + content.delegate('click', function(e) { + var element = e.currentTarget; + if (element.get('checked')) { + var getKind = function(el) { + return this._getTrackTypeFromTabPane(el.ancestor('.tab-pane')); + }.bind(this); + + element.ancestor('.root.tab-content').all(SELECTORS.TRACK_DEFAULT_SELECT).each(function(select) { + if (select !== element && getKind(element) === getKind(select)) { + select.set('checked', false); + } + }); + } + }, SELECTORS.TRACK_DEFAULT_SELECT, this); + + // Set up filepicker click event. + content.delegate('click', function(e) { + var element = e.currentTarget; + var fptype = (element.ancestor(SELECTORS.POSTER_SOURCE) && 'image') || + (element.ancestor(SELECTORS.TRACK_SOURCE) && 'subtitle') || + 'media'; + e.preventDefault(); + this.get('host').showFilepicker(fptype, this._getFilepickerCallback(element, fptype), this); + }, '.openmediabrowser', this); + + // This is a nasty hack. Basically we are using BS4 markup for the tabs + // but it isn't completely backwards compatible with BS2. The main problem is + // that the "active" class goes on a different node. So the idea is to put it + // the node for BS4, and then use CSS to make it look right in BS2. However, + // once another tab is clicked, everything sorts itself out, more or less. Except + // that the original "active" tab hasn't had the BS4 "active" class removed + // (so the styles will still apply to it). So we need to remove the "active" + // class on the BS4 node so that BS2 is happy. + // + // This doesn't upset BS4 since it removes this class anyway when clicking on + // another tab. + content.all('.nav-item').on('click', function(elem) { + elem.currentTarget.get('parentNode').all('.active').removeClass('active'); + }); + + content.one('.submit').on('click', function(e) { + e.preventDefault(); + var mediaHTML = this._getMediaHTML(e.currentTarget.ancestor('.atto_form')), + host = this.get('host'); + this.getDialogue({ + focusAfterHide: null + }).hide(); + if (mediaHTML) { + host.setSelection(selection); + host.insertContentAtFocusPoint(mediaHTML); + this.markUpdated(); + } }, this); - return this._content; + return content; }, /** - * Update the dialogue after an media was selected in the File Picker. + * Applies medium properties to the content node. * - * @method _filepickerCallback - * @param {object} params The parameters provided by the filepicker - * containing information about the image. + * @method _applyMediumProperties + * @param {Y.Node} content The content to apply the properties to + * @param {object} properties The medium properties to apply + * @return {Y.Node} * @private */ - _filepickerCallback: function(params) { - if (params.url !== '') { - this._content.one(SELECTORS.URLINPUT) - .set('value', params.url); - this._content.one(SELECTORS.NAMEINPUT) - .set('value', params.file); + _applyMediumProperties: function(content, properties) { + if (!properties) { + return content; + } + + var applyTrackProperties = function(track, properties) { + track.one(SELECTORS.TRACK_SOURCE + ' ' + SELECTORS.URL_INPUT).set('value', properties.src); + track.one(SELECTORS.TRACK_LANG_INPUT).set('value', properties.srclang); + track.one(SELECTORS.TRACK_LABEL_INPUT).set('value', properties.label); + track.one(SELECTORS.TRACK_DEFAULT_SELECT).set('checked', properties.defaultTrack); + }; + + var tabPane = content.one('.root.tab-content > .tab-pane#' + this.get('host').get('elementid') + + '_' + properties.type.toLowerCase()); + + // Populate sources. + tabPane.one(SELECTORS.MEDIA_SOURCE + ' ' + SELECTORS.URL_INPUT).set('value', properties.sources[0]); + Y.Array.each(properties.sources.slice(1), function(source) { + this._addMediaSourceComponent(tabPane.one(SELECTORS.MEDIA_SOURCE + ' .addcomponent'), function(newComponent) { + newComponent.one(SELECTORS.URL_INPUT).set('value', source); + }); + }, this); + + // Populate tracks. + Y.Object.each(properties.tracks, function(value, key) { + var trackData = value.length ? value : [{src: '', srclang: '', label: '', defaultTrack: false}]; + var paneSelector = SELECTORS['TRACK_' + key.toUpperCase() + '_PANE']; + + applyTrackProperties(tabPane.one(paneSelector + ' ' + SELECTORS.TRACK), trackData[0]); + Y.Array.each(trackData.slice(1), function(track) { + this._addTrackComponent( + tabPane.one(paneSelector + ' ' + SELECTORS.TRACK + ' .addcomponent'), function(newComponent) { + applyTrackProperties(newComponent, track); + }); + }, this); + }, this); + + // Populate values. + tabPane.one(SELECTORS.POSTER_SOURCE + ' ' + SELECTORS.URL_INPUT).setAttribute('value', properties.poster); + tabPane.one(SELECTORS.WIDTH_INPUT).set('value', properties.width); + tabPane.one(SELECTORS.HEIGHT_INPUT).set('value', properties.height); + tabPane.one(SELECTORS.MEDIA_CONTROLS_TOGGLE).set('checked', properties.controls); + tabPane.one(SELECTORS.MEDIA_AUTOPLAY_TOGGLE).set('checked', properties.autoplay); + tabPane.one(SELECTORS.MEDIA_MUTE_TOGGLE).set('checked', properties.muted); + tabPane.one(SELECTORS.MEDIA_LOOP_TOGGLE).set('checked', properties.loop); + + // Switch to the correct tab. + var mediumType = this._getMediumTypeFromTabPane(tabPane); + + // Remove active class from all tabs + tab panes. + tabPane.siblings('.active').removeClass('active'); + content.all('.root.nav-tabs .nav-item a').removeClass('active'); + + // Add active class to the desired tab and tab pane. + tabPane.addClass('active'); + content.one(SELECTORS[mediumType.toUpperCase() + '_TAB'] + ' a').addClass('active'); + + return content; + }, + + /** + * Extracts medium properties. + * + * @method _getMediumProperties + * @param {Y.Node} medium The medium node from which to extract + * @return {Object} + * @private + */ + _getMediumProperties: function(medium) { + var boolAttr = function(elem, attr) { + return elem.getAttribute(attr) ? true : false; + }; + + var tracks = { + subtitles: [], + captions: [], + descriptions: [], + chapters: [], + metadata: [] + }; + + medium.all('track').each(function(track) { + tracks[track.getAttribute('kind')].push({ + src: track.getAttribute('src'), + srclang: track.getAttribute('srclang'), + label: track.getAttribute('label'), + defaultTrack: boolAttr(track, 'default') + }); + }); + + return { + type: medium.test('video') ? MEDIA_TYPES.VIDEO : MEDIA_TYPES.AUDIO, + sources: medium.all('source').get('src'), + poster: medium.getAttribute('poster'), + width: medium.getAttribute('width'), + height: medium.getAttribute('height'), + autoplay: boolAttr(medium, 'autoplay'), + loop: boolAttr(medium, 'loop'), + muted: boolAttr(medium, 'muted'), + controls: boolAttr(medium, 'controls'), + tracks: tracks + }; + }, + + /** + * Adds a track form component. + * + * @method _addTrackComponent + * @param {Y.Node} element The element which was used to trigger this function + * @param {Function} [callback] Function to be called when the new component is added + * @param {Y.Node} callback.newComponent The compiled component + * @private + */ + _addTrackComponent: function(element, callback) { + var trackType = this._getTrackTypeFromTabPane(element.ancestor('.tab-pane')); + var context = this._getContext({ + sourcelabel: trackType + 'sourcelabel', + addcomponentlabel: 'add' + trackType + 'track' + }); + + this._addComponent(element, TEMPLATES.FORM_COMPONENTS.TRACK, SELECTORS.TRACK, context, callback); + }, + + /** + * Adds a media source form component. + * + * @method _addMediaSourceComponent + * @param {Y.Node} element The element which was used to trigger this function + * @param {Function} [callback] Function to be called when the new component is added + * @param {Y.Node} callback.newComponent The compiled component + * @private + */ + _addMediaSourceComponent: function(element, callback) { + var mediumType = this._getMediumTypeFromTabPane(element.ancestor('.tab-pane')); + var context = this._getContext({ + multisource: true, + id: CSS.MEDIA_SOURCE, + entersourcelabel: mediumType + 'sourcelabel', + addcomponentlabel: 'addsource' + }); + this._addComponent(element, TEMPLATES.FORM_COMPONENTS.SOURCE, SELECTORS.MEDIA_SOURCE, context, callback); + }, + + /** + * Adds an arbitrary form component. + * + * This function Compiles and adds the provided component in the supplied 'ancestor' container. + * It will also add links to add/remove the relevant components, attaching the + * necessary events. + * + * @method _addComponent + * @param {Y.Node} element The element which was used to trigger this function + * @param {String} component The component to compile and add + * @param {String} ancestor A selector used to find an ancestor of 'component', to which + * the compiled component will be appended + * @param {Object} context The context with which to render the component + * @param {Function} [callback] Function to be called when the new component is added + * @param {Y.Node} callback.newComponent The compiled component + * @private + */ + _addComponent: function(element, component, ancestor, context, callback) { + var currentComponent = element.ancestor(ancestor), + newComponent = Y.Node.create(Y.Handlebars.compile(component)(context)), + removeNodeContext = this._getContext(context); + + removeNodeContext.label = "remove"; + var removeNode = Y.Node.create(Y.Handlebars.compile(TEMPLATES.FORM_COMPONENTS.REMOVE_COMPONENT)(removeNodeContext)); + + removeNode.one('.removecomponent').on('click', function(e) { + e.preventDefault(); + currentComponent.remove(true); + }); + + currentComponent.insert(newComponent, 'after'); + element.ancestor().insert(removeNode, 'after'); + element.ancestor().remove(true); + + if (callback) { + callback.call(this, newComponent); } }, /** - * Update the media in the contenteditable. + * Returns the callback for the file picker to call after a file has been selected. * - * @method setMedia - * @param {EventFacade} e + * @method _getFilepickerCallback + * @param {Y.Node} element The element which triggered the callback + * @param {String} fptype The file pickertype (as would be passed to `showFilePicker`) + * @return {Function} The function to be used as a callback when the file picker returns the file * @private */ - _setMedia: function(e) { - e.preventDefault(); - this.getDialogue({ - focusAfterHide: null - }).hide(); + _getFilepickerCallback: function(element, fptype) { + return function(params) { + if (params.url !== '') { + var tabPane = element.ancestor('.tab-pane'); + element.ancestor(SELECTORS.SOURCE).one(SELECTORS.URL_INPUT).set('value', params.url); - var form = e.currentTarget.ancestor('.atto_form'), - url = form.one(SELECTORS.URLINPUT).get('value'), - name = form.one(SELECTORS.NAMEINPUT).get('value'), - host = this.get('host'); + // Links (and only links) have a name field. + if (tabPane.get('id') === this.get('host').get('elementid') + '_' + CSS.LINK) { + tabPane.one(SELECTORS.NAME_INPUT).set('value', params.file); + } - if (url !== '' && name !== '') { - host.setSelection(this._currentSelection); - var mediahtml = '' + name + ''; + if (fptype === 'subtitle') { + var subtitleLang = params.file.split('.vtt')[0].split('-').slice(-1)[0]; + var langObj = this.get('langs').available.reduce(function(carry, lang) { + return lang.code === subtitleLang ? lang : carry; + }, false); + if (langObj) { + element.ancestor(SELECTORS.TRACK).one(SELECTORS.TRACK_LABEL_INPUT).set('value', + langObj.lang.substr(0, langObj.lang.lastIndexOf(' '))); + element.ancestor(SELECTORS.TRACK).one(SELECTORS.TRACK_LANG_INPUT).set('value', langObj.code); + } + } + } + }; + }, - host.insertContentAtFocusPoint(mediahtml); - this.markUpdated(); - } + /** + * Given a "medium" tab pane, returns what kind of medium it contains. + * + * @method _getMediumTypeFromTabPane + * @param {Y.Node} tabPane The tab pane + * @return {String} The type of medium in the pane + */ + _getMediumTypeFromTabPane: function(tabPane) { + return tabPane.getAttribute('data-medium-type'); + }, + + /** + * Given a "track" tab pane, returns what kind of track it contains. + * + * @method _getTrackTypeFromTabPane + * @param {Y.Node} tabPane The tab pane + * @return {String} The type of track in the pane + */ + _getTrackTypeFromTabPane: function(tabPane) { + return tabPane.getAttribute('data-track-kind'); + }, + + /** + * Returns the HTML to be inserted to the text area. + * + * @method _getMediaHTML + * @param {Y.Node} form The form from which to extract data + * @return {String} The compiled markup + * @private + */ + _getMediaHTML: function(form) { + var mediumType = this._getMediumTypeFromTabPane(form.one('.root.tab-content > .tab-pane.active')); + var tabContent = form.one(SELECTORS[mediumType.toUpperCase() + '_PANE']); + + return this['_getMediaHTML' + mediumType[0].toUpperCase() + mediumType.substr(1)](tabContent); + }, + + /** + * Returns the HTML to be inserted to the text area for the link tab. + * + * @method _getMediaHTMLLink + * @param {Y.Node} tab The tab from which to extract data + * @return {String} The compiled markup + * @private + */ + _getMediaHTMLLink: function(tab) { + var context = { + url: tab.one(SELECTORS.URL_INPUT).get('value'), + name: tab.one(SELECTORS.NAME_INPUT).get('value') || false + }; + + return context.url ? Y.Handlebars.compile(TEMPLATES.HTML_MEDIA.LINK)(context) : ''; + }, + + /** + * Returns the HTML to be inserted to the text area for the video tab. + * + * @method _getMediaHTMLVideo + * @param {Y.Node} tab The tab from which to extract data + * @return {String} The compiled markup + * @private + */ + _getMediaHTMLVideo: function(tab) { + var context = this._getContextForMediaHTML(tab); + context.width = tab.one(SELECTORS.WIDTH_INPUT).get('value') || false; + context.height = tab.one(SELECTORS.HEIGHT_INPUT).get('value') || false; + context.poster = tab.one(SELECTORS.POSTER_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value') || false; + + return context.sources.length ? Y.Handlebars.compile(TEMPLATES.HTML_MEDIA.VIDEO)(context) : ''; + }, + + /** + * Returns the HTML to be inserted to the text area for the audio tab. + * + * @method _getMediaHTMLAudio + * @param {Y.Node} tab The tab from which to extract data + * @return {String} The compiled markup + * @private + */ + _getMediaHTMLAudio: function(tab) { + var context = this._getContextForMediaHTML(tab); + + return context.sources.length ? Y.Handlebars.compile(TEMPLATES.HTML_MEDIA.AUDIO)(context) : ''; + }, + + /** + * Returns the context with which to render a media template. + * + * @method _getContextForMediaHTML + * @param {Y.Node} tab The tab from which to extract data + * @return {Object} + * @private + */ + _getContextForMediaHTML: function(tab) { + var tracks = []; + + tab.all(SELECTORS.TRACK).each(function(track) { + tracks.push({ + track: track.one(SELECTORS.TRACK_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value'), + kind: this._getTrackTypeFromTabPane(track.ancestor('.tab-pane')), + label: track.one(SELECTORS.TRACK_LABEL_INPUT).get('value') || + track.one(SELECTORS.TRACK_LANG_INPUT).get('value'), + srclang: track.one(SELECTORS.TRACK_LANG_INPUT).get('value'), + defaultTrack: track.one(SELECTORS.TRACK_DEFAULT_SELECT).get('checked') ? "true" : null + }); + }, this); + + return { + sources: tab.all(SELECTORS.MEDIA_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value').filter(function(source) { + return !!source; + }).map(function(source) { + return {source: source}; + }), + description: tab.one(SELECTORS.MEDIA_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value') || false, + tracks: tracks.filter(function(track) { + return !!track.track; + }), + showControls: tab.one(SELECTORS.MEDIA_CONTROLS_TOGGLE).get('checked'), + autoplay: tab.one(SELECTORS.MEDIA_AUTOPLAY_TOGGLE).get('checked'), + muted: tab.one(SELECTORS.MEDIA_MUTE_TOGGLE).get('checked'), + loop: tab.one(SELECTORS.MEDIA_LOOP_TOGGLE).get('checked') + }; + } +}, { + ATTRS: { + langs: {}, + help: {} } }); diff --git a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js index d84c7a96751..77af4cef756 100644 --- a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js +++ b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js @@ -1 +1,4 @@ -YUI.add("moodle-atto_media-button",function(e,t){var n="atto_media",r={URLINPUT:"atto_media_urlentry",NAMEINPUT:"atto_media_nameentry"},i={URLINPUT:"."+r.URLINPUT,NAMEINPUT:"."+r.NAMEINPUT},s='


';e.namespace("M.atto_media").Button=e.Base.create("button",e.M.editor_atto.EditorPlugin,[],{_currentSelection:null,_content:null,initializer:function(){this.get("host").canShowFilepicker("media")&&this.addButton({icon:"e/insert_edit_video",callback:this._displayDialogue})},_displayDialogue:function(){this._currentSelection=this.get("host").getSelection();if(this._currentSelection===!1)return;var e=this.getDialogue({headerContent:M.util.get_string("createmedia",n),focusAfterHide:!0,focusOnShowSelector:i.URLINPUT});e.set("bodyContent",this._getDialogueContent()).show()},_getDialogueContent:function(){var t=e.Handlebars.compile(s);return this._content=e.Node.create(t({component:n,elementid:this.get("host").get("elementid"),CSS:r})),this._content.one(".submit").on("click",this._setMedia,this),this._content.one(".openmediabrowser").on("click",function(e){e.preventDefault(),this.get("host").showFilepicker("media",this._filepickerCallback,this)},this),this._content},_filepickerCallback:function(e){e.url!==""&&(this._content.one(i.URLINPUT).set("value",e.url),this._content.one(i.NAMEINPUT).set("value",e.file))},_setMedia:function(t){t.preventDefault(),this.getDialogue({focusAfterHide:null}).hide();var n=t.currentTarget.ancestor(".atto_form"),r=n.one(i.URLINPUT).get("value"),s=n.one(i.NAMEINPUT).get("value"),o=this.get("host");if(r!==""&&s!==""){o.setSelection(this._currentSelection);var u=''+s+"";o.insertContentAtFocusPoint(u),this.markUpdated()}}})},"@VERSION@",{requires:["moodle-editor_atto-plugin"]}); +YUI.add("moodle-atto_media-button",function(e,t){var n="atto_media",r={LINK:"LINK",VIDEO:"VIDEO",AUDIO:"AUDIO"},i={SUBTITLES:"SUBTITLES",CAPTIONS:"CAPTIONS",DESCRIPTIONS:"DESCRIPTIONS",CHAPTERS:"CHAPTERS",METADATA:"METADATA"},s={SOURCE:"atto_media_source",TRACK:"atto_media_track",MEDIA_SOURCE:"atto_media_media_source",LINK_SOURCE:"atto_media_link_source",POSTER_SOURCE:"atto_media_poster_source",TRACK_SOURCE:"atto_media_track_source",DISPLAY_OPTIONS:"atto_media_display_options",NAME_INPUT:"atto_media_name_entry",URL_INPUT:"atto_media_url_entry",POSTER_SIZE:"atto_media_poster_size",LINK_SIZE:"atto_media_link_size",WIDTH_INPUT:"atto_media_width_entry",HEIGHT_INPUT:"atto_media_height_entry",TRACK_KIND_INPUT:"atto_media_track_kind_entry",TRACK_LABEL_INPUT:"atto_media_track_label_entry",TRACK_LANG_INPUT:"atto_media_track_lang_entry",TRACK_DEFAULT_SELECT:"atto_media_track_default",MEDIA_CONTROLS_TOGGLE:"atto_media_controls",MEDIA_AUTOPLAY_TOGGLE:"atto_media_autoplay",MEDIA_MUTE_TOGGLE:"atto_media_mute",MEDIA_LOOP_TOGGLE:"atto_media_loop",ADVANCED_SETTINGS:"atto_media_advancedsettings",LINK:r.LINK.toLowerCase(),VIDEO:r.VIDEO.toLowerCase(),AUDIO:r.AUDIO.toLowerCase(),TRACK_SUBTITLES:i.SUBTITLES.toLowerCase(),TRACK_CAPTIONS:i.CAPTIONS.toLowerCase(),TRACK_DESCRIPTIONS:i.DESCRIPTIONS.toLowerCase(),TRACK_CHAPTERS:i.CHAPTERS.toLowerCase(),TRACK_METADATA:i.METADATA.toLowerCase()},o={SOURCE:"."+s.SOURCE,TRACK:"."+s.TRACK,MEDIA_SOURCE:"."+s.MEDIA_SOURCE,POSTER_SOURCE:"."+s.POSTER_SOURCE,TRACK_SOURCE:"."+s.TRACK_SOURCE,DISPLAY_OPTIONS:"."+s.DISPLAY_OPTIONS,NAME_INPUT:"."+s.NAME_INPUT,URL_INPUT:"."+s.URL_INPUT,POSTER_SIZE:"."+s.POSTER_SIZE,LINK_SIZE:"."+s.LINK_SIZE,WIDTH_INPUT:"."+s.WIDTH_INPUT,HEIGHT_INPUT:"."+s.HEIGHT_INPUT,TRACK_KIND_INPUT:"."+s.TRACK_KIND_INPUT,TRACK_LABEL_INPUT:"."+s.TRACK_LABEL_INPUT,TRACK_LANG_INPUT:"."+s.TRACK_LANG_INPUT,TRACK_DEFAULT_SELECT:"."+s.TRACK_DEFAULT_SELECT,MEDIA_CONTROLS_TOGGLE:"."+s.MEDIA_CONTROLS_TOGGLE,MEDIA_AUTOPLAY_TOGGLE:"."+s.MEDIA_AUTOPLAY_TOGGLE,MEDIA_MUTE_TOGGLE:"."+s.MEDIA_MUTE_TOGGLE,MEDIA_LOOP_TOGGLE:"."+s.MEDIA_LOOP_TOGGLE,ADVANCED_SETTINGS:"."+s.ADVANCED_SETTINGS,LINK_TAB:'li[data-medium-type="'+s.LINK+'"]',LINK_PANE:'.tab-pane[data-medium-type="'+s.LINK+'"]',VIDEO_TAB:'li[data-medium-type="'+s.VIDEO+'"]',VIDEO_PANE:'.tab-pane[data-medium-type="'+s.VIDEO+'"]',AUDIO_TAB:'li[data-medium-type="'+s.AUDIO+'"]',AUDIO_PANE:'.tab-pane[data-medium-type="'+s.AUDIO+'"]',TRACK_SUBTITLES_TAB:'li[data-track-kind="'+s.TRACK_SUBTITLES+'"]',TRACK_SUBTITLES_PANE:'.tab-pane[data-track-kind="'+s.TRACK_SUBTITLES+'"]',TRACK_CAPTIONS_TAB:'li[data-track-kind="'+s.TRACK_CAPTIONS+'"]',TRACK_CAPTIONS_PANE:'.tab-pane[data-track-kind="'+s.TRACK_CAPTIONS+'"]',TRACK_DESCRIPTIONS_TAB:'li[data-track-kind="'+s.TRACK_DESCRIPTIONS+'"]',TRACK_DESCRIPTIONS_PANE:'.tab-pane[data-track-kind="'+s.TRACK_DESCRIPTIONS+'"]',TRACK_CHAPTERS_TAB:'li[data-track-kind="'+s.TRACK_CHAPTERS+'"]',TRACK_CHAPTERS_PANE:'.tab-pane[data-track-kind="'+s.TRACK_CHAPTERS+'"]',TRACK_METADATA_TAB:'li[data-track-kind="'+s.TRACK_METADATA+'"]',TRACK_METADATA_PANE:'.tab-pane[data-track-kind="'+s.TRACK_METADATA+'"]'},u={ROOT:'
{{> tab_panes.link}}
{{> tab_panes.video}}
{{> tab_panes.audio}}

',TAB_PANES:{LINK:'{{renderPartial "form_components.source" context=this id=CSS.LINK_SOURCE}}',VIDEO:'{{renderPartial "form_components.source" context=this id=CSS.MEDIA_SOURCE entersourcelabel="videosourcelabel" addcomponentlabel="addsource" multisource="true" addsourcehelp=helpStrings.addsource}}',AUDIO:'{{renderPartial "form_components.source" context=this id=CSS.MEDIA_SOURCE entersourcelabel="audiosourcelabel" addcomponentlabel="addsource" multisource="true" addsourcehelp=helpStrings.addsource}}' +},FORM_COMPONENTS:{SOURCE:'
{{#multisource}}{{renderPartial "form_components.add_component" context=../this label=../addcomponentlabel help=../addsourcehelp}}{{/multisource}}
',ADD_COMPONENT:'
{{#label}}{{get_string ../label ../component}}{{/label}}{{^label}}{{get_string "add" ../component}}{{/label}}{{#help}}{{{../help}}}{{/help}}
',REMOVE_COMPONENT:'
{{#label}}{{get_string ../label ../component}}{{/label}}{{^label}}{{get_string "remove" ../component}}{{/label}}
',DISPLAY_OPTIONS:'
{{renderPartial "form_components.source" context=this id=CSS.POSTER_SOURCE entersourcelabel="poster"}}
',ADVANCED_SETTINGS:'
',TRACK_TABS:'
{{{helpStrings.subtitles}}}
{{renderPartial "form_components.track" context=this sourcelabel="subtitlessourcelabel" addcomponentlabel="addsubtitlestrack"}}
{{{helpStrings.captions}}}
{{renderPartial "form_components.track" context=this sourcelabel="captionssourcelabel" addcomponentlabel="addcaptionstrack"}}
{{{helpStrings.descriptions}}}
{{renderPartial "form_components.track" context=this sourcelabel="descriptionssourcelabel" addcomponentlabel="adddescriptionstrack"}}
{{{helpStrings.chapters}}}
{{renderPartial "form_components.track" context=this sourcelabel="chapterssourcelabel" addcomponentlabel="addchapterstrack"}}
{{{helpStrings.metadata}}}
{{renderPartial "form_components.track" context=this sourcelabel="metadatasourcelabel" addcomponentlabel="addmetadatatrack"}}
',TRACK:'
{{renderPartial "form_components.source" context=this id=CSS.TRACK_SOURCE entersourcelabel=sourcelabel}}{{renderPartial "form_components.add_component" context=this label=addcomponentlabel}}
'},HTML_MEDIA:{VIDEO:'  ' +,AUDIO:'  ',LINK:'{{#name}}{{../name}}{{/name}}{{^name}}{{../url}}{{/name}}'}};e.namespace("M.atto_media").Button=e.Base.create("button",e.M.editor_atto.EditorPlugin,[],{initializer:function(){this.get("host").canShowFilepicker("media")&&(this.editor.delegate("dblclick",this._displayDialogue,"video",this),this.editor.delegate("click",this._handleClick,"video",this),this.addButton({icon:"e/insert_edit_video",callback:this._displayDialogue,tags:"video, audio",tagMatchRequiresAll:!1}))},_getContext:function(t){return e.merge({elementid:this.get("host").get("elementid"),component:n,langsinstalled:this.get("langs").installed,langsavailable:this.get("langs").available,helpStrings:this.get("help"),CSS:s},t)},_handleClick:function(e){var t=e.target,n=this.get("host").getSelectionFromNode(t);this.get("host").getSelection()!==n&&this.get("host").setSelection(n)},_displayDialogue:function(){if(this.get("host").getSelection()===!1)return;"renderPartial"in e.Handlebars.helpers||(function r(t,n){e.each(n,function(n,i){t.push(i),typeof n!="object"?e.Handlebars.registerPartial(t.join(".").toLowerCase(),n):r(t,n),t.pop()})}([],u),e.Handlebars.registerHelper("renderPartial",function(t,n){if(!t)return"";var r=e.Handlebars.partials[t],i=n.hash.context?e.clone(n.hash.context):{},s=e.merge(i,n.hash);return delete s.context,r?new e.Handlebars.SafeString(e.Handlebars.compile(r)(s)):""}));var t=this.getDialogue({headerContent:M.util.get_string("createmedia",n),focusAfterHide:!0,width:660,focusOnShowSelector:o.URL_INPUT});t.set("bodyContent",this._getDialogueContent(this.get("host").getSelection())).show(),M.form.shortforms({formid:this.get("host").get("elementid")+"_atto_media_form"})},_getDialogueContent:function(t){var n=e.Node.create(e.Handlebars.compile(u.ROOT)(this._getContext())),r=this.get("host").getSelectedNodes().filter("video,audio").shift(),i=r?this._getMediumProperties(r):!1;return this._attachEvents(this._applyMediumProperties(n,i),t)},_attachEvents:function(e,t){return e.delegate("click",function(e){e.preventDefault(),this._addMediaSourceComponent(e.currentTarget)},o.MEDIA_SOURCE+" .addcomponent",this),e.delegate("click",function(e){e.preventDefault(),this._addTrackComponent(e.currentTarget)},o.TRACK+" .addcomponent",this),e.delegate("click",function(e){var t=e.currentTarget;if(t.get("checked")){var n=function(e){return this._getTrackTypeFromTabPane(e.ancestor(".tab-pane"))}.bind(this);t.ancestor(".root.tab-content").all(o.TRACK_DEFAULT_SELECT).each(function(e){e!==t&&n(t)===n(e)&&e.set("checked",!1)})}},o.TRACK_DEFAULT_SELECT,this),e.delegate("click",function(e){var t=e.currentTarget,n=t.ancestor(o.POSTER_SOURCE)&&"image"||t.ancestor(o.TRACK_SOURCE)&&"subtitle"||"media";e.preventDefault(),this.get("host").showFilepicker(n,this._getFilepickerCallback(t,n),this)},".openmediabrowser",this),e.all(".nav-item").on("click",function(e){e.currentTarget.get("parentNode").all(".active").removeClass("active")}),e.one(".submit").on("click",function(e){e.preventDefault();var n=this._getMediaHTML(e.currentTarget.ancestor(".atto_form")),r=this.get("host");this.getDialogue({focusAfterHide:null}).hide(),n&&(r.setSelection(t),r.insertContentAtFocusPoint(n),this.markUpdated())},this),e},_applyMediumProperties:function(t,n){if(!n)return t;var r=function(e,t){e.one(o.TRACK_SOURCE+" "+o.URL_INPUT).set("value",t.src),e.one(o.TRACK_LANG_INPUT).set("value",t.srclang),e.one(o.TRACK_LABEL_INPUT).set("value",t.label),e.one(o.TRACK_DEFAULT_SELECT).set("checked",t.defaultTrack)},i=t.one(".root.tab-content > .tab-pane#"+this.get("host").get("elementid")+"_"+n.type.toLowerCase());i.one(o.MEDIA_SOURCE+" "+o.URL_INPUT).set("value",n.sources[0]),e.Array.each(n.sources.slice(1),function(e){this._addMediaSourceComponent(i.one(o.MEDIA_SOURCE+" .addcomponent"),function(t){t.one(o.URL_INPUT).set("value",e)})},this),e.Object.each(n.tracks,function(t,n){var s=t.length?t:[{src:"",srclang:"",label:"",defaultTrack:!1}],u=o["TRACK_"+n.toUpperCase()+"_PANE"];r(i.one(u+" "+o.TRACK),s[0]),e.Array.each(s.slice(1),function(e){this._addTrackComponent(i.one(u+" "+o.TRACK+" .addcomponent"),function(t){r(t,e)})},this)},this),i.one(o.POSTER_SOURCE+" "+o.URL_INPUT).setAttribute("value",n.poster),i.one(o.WIDTH_INPUT).set("value",n.width),i.one(o.HEIGHT_INPUT).set("value",n.height),i.one(o.MEDIA_CONTROLS_TOGGLE).set("checked",n.controls),i.one(o.MEDIA_AUTOPLAY_TOGGLE).set("checked",n.autoplay),i.one(o.MEDIA_MUTE_TOGGLE).set("checked",n.muted),i.one(o.MEDIA_LOOP_TOGGLE).set("checked",n.loop);var s=this._getMediumTypeFromTabPane(i);return i.siblings(".active").removeClass("active"),t.all(".root.nav-tabs .nav-item a").removeClass("active"),i.addClass("active"),t.one(o[s.toUpperCase()+"_TAB"]+" a").addClass("active"),t},_getMediumProperties:function(e){var t=function(e,t){return e.getAttribute(t)?!0:!1},n={subtitles:[],captions:[],descriptions:[],chapters:[],metadata:[]};return e.all("track").each(function(e){n[e.getAttribute("kind")].push({src:e.getAttribute("src"),srclang:e.getAttribute("srclang"),label:e.getAttribute("label"),defaultTrack:t(e,"default")})}),{type:e.test("video")?r.VIDEO:r.AUDIO,sources:e.all("source").get("src"),poster:e.getAttribute("poster"),width:e.getAttribute("width"),height:e.getAttribute("height"),autoplay:t(e,"autoplay"),loop:t(e,"loop"),muted:t(e,"muted"),controls:t(e,"controls"),tracks:n}},_addTrackComponent:function(e,t){var n=this._getTrackTypeFromTabPane(e.ancestor(".tab-pane")),r=this._getContext +({sourcelabel:n+"sourcelabel",addcomponentlabel:"add"+n+"track"});this._addComponent(e,u.FORM_COMPONENTS.TRACK,o.TRACK,r,t)},_addMediaSourceComponent:function(e,t){var n=this._getMediumTypeFromTabPane(e.ancestor(".tab-pane")),r=this._getContext({multisource:!0,id:s.MEDIA_SOURCE,entersourcelabel:n+"sourcelabel",addcomponentlabel:"addsource"});this._addComponent(e,u.FORM_COMPONENTS.SOURCE,o.MEDIA_SOURCE,r,t)},_addComponent:function(t,n,r,i,s){var o=t.ancestor(r),a=e.Node.create(e.Handlebars.compile(n)(i)),f=this._getContext(i);f.label="remove";var l=e.Node.create(e.Handlebars.compile(u.FORM_COMPONENTS.REMOVE_COMPONENT)(f));l.one(".removecomponent").on("click",function(e){e.preventDefault(),o.remove(!0)}),o.insert(a,"after"),t.ancestor().insert(l,"after"),t.ancestor().remove(!0),s&&s.call(this,a)},_getFilepickerCallback:function(e,t){return function(n){if(n.url!==""){var r=e.ancestor(".tab-pane");e.ancestor(o.SOURCE).one(o.URL_INPUT).set("value",n.url),r.get("id")===this.get("host").get("elementid")+"_"+s.LINK&&r.one(o.NAME_INPUT).set("value",n.file);if(t==="subtitle"){var i=n.file.split(".vtt")[0].split("-").slice(-1)[0],u=this.get("langs").available.reduce(function(e,t){return t.code===i?t:e},!1);u&&(e.ancestor(o.TRACK).one(o.TRACK_LABEL_INPUT).set("value",u.lang.substr(0,u.lang.lastIndexOf(" "))),e.ancestor(o.TRACK).one(o.TRACK_LANG_INPUT).set("value",u.code))}}}},_getMediumTypeFromTabPane:function(e){return e.getAttribute("data-medium-type")},_getTrackTypeFromTabPane:function(e){return e.getAttribute("data-track-kind")},_getMediaHTML:function(e){var t=this._getMediumTypeFromTabPane(e.one(".root.tab-content > .tab-pane.active")),n=e.one(o[t.toUpperCase()+"_PANE"]);return this["_getMediaHTML"+t[0].toUpperCase()+t.substr(1)](n)},_getMediaHTMLLink:function(t){var n={url:t.one(o.URL_INPUT).get("value"),name:t.one(o.NAME_INPUT).get("value")||!1};return n.url?e.Handlebars.compile(u.HTML_MEDIA.LINK)(n):""},_getMediaHTMLVideo:function(t){var n=this._getContextForMediaHTML(t);return n.width=t.one(o.WIDTH_INPUT).get("value")||!1,n.height=t.one(o.HEIGHT_INPUT).get("value")||!1,n.poster=t.one(o.POSTER_SOURCE+" "+o.URL_INPUT).get("value")||!1,n.sources.length?e.Handlebars.compile(u.HTML_MEDIA.VIDEO)(n):""},_getMediaHTMLAudio:function(t){var n=this._getContextForMediaHTML(t);return n.sources.length?e.Handlebars.compile(u.HTML_MEDIA.AUDIO)(n):""},_getContextForMediaHTML:function(e){var t=[];return e.all(o.TRACK).each(function(e){t.push({track:e.one(o.TRACK_SOURCE+" "+o.URL_INPUT).get("value"),kind:this._getTrackTypeFromTabPane(e.ancestor(".tab-pane")),label:e.one(o.TRACK_LABEL_INPUT).get("value")||e.one(o.TRACK_LANG_INPUT).get("value"),srclang:e.one(o.TRACK_LANG_INPUT).get("value"),defaultTrack:e.one(o.TRACK_DEFAULT_SELECT).get("checked")?"true":null})},this),{sources:e.all(o.MEDIA_SOURCE+" "+o.URL_INPUT).get("value").filter(function(e){return!!e}).map(function(e){return{source:e}}),description:e.one(o.MEDIA_SOURCE+" "+o.URL_INPUT).get("value")||!1,tracks:t.filter(function(e){return!!e.track}),showControls:e.one(o.MEDIA_CONTROLS_TOGGLE).get("checked"),autoplay:e.one(o.MEDIA_AUTOPLAY_TOGGLE).get("checked"),muted:e.one(o.MEDIA_MUTE_TOGGLE).get("checked"),loop:e.one(o.MEDIA_LOOP_TOGGLE).get("checked")}}},{ATTRS:{langs:{},help:{}}})},"@VERSION@",{requires:["moodle-editor_atto-plugin"]}); diff --git a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js index ed3048a6398..2d4acc6ddfb 100644 --- a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js +++ b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js @@ -34,58 +34,419 @@ YUI.add('moodle-atto_media-button', function (Y, NAME) { */ var COMPONENTNAME = 'atto_media', + MEDIA_TYPES = {LINK: 'LINK', VIDEO: 'VIDEO', AUDIO: 'AUDIO'}, + TRACK_KINDS = { + SUBTITLES: 'SUBTITLES', + CAPTIONS: 'CAPTIONS', + DESCRIPTIONS: 'DESCRIPTIONS', + CHAPTERS: 'CHAPTERS', + METADATA: 'METADATA' + }, CSS = { - URLINPUT: 'atto_media_urlentry', - NAMEINPUT: 'atto_media_nameentry' + SOURCE: 'atto_media_source', + TRACK: 'atto_media_track', + MEDIA_SOURCE: 'atto_media_media_source', + LINK_SOURCE: 'atto_media_link_source', + POSTER_SOURCE: 'atto_media_poster_source', + TRACK_SOURCE: 'atto_media_track_source', + DISPLAY_OPTIONS: 'atto_media_display_options', + NAME_INPUT: 'atto_media_name_entry', + URL_INPUT: 'atto_media_url_entry', + POSTER_SIZE: 'atto_media_poster_size', + LINK_SIZE: 'atto_media_link_size', + WIDTH_INPUT: 'atto_media_width_entry', + HEIGHT_INPUT: 'atto_media_height_entry', + TRACK_KIND_INPUT: 'atto_media_track_kind_entry', + TRACK_LABEL_INPUT: 'atto_media_track_label_entry', + TRACK_LANG_INPUT: 'atto_media_track_lang_entry', + TRACK_DEFAULT_SELECT: 'atto_media_track_default', + MEDIA_CONTROLS_TOGGLE: 'atto_media_controls', + MEDIA_AUTOPLAY_TOGGLE: 'atto_media_autoplay', + MEDIA_MUTE_TOGGLE: 'atto_media_mute', + MEDIA_LOOP_TOGGLE: 'atto_media_loop', + ADVANCED_SETTINGS: 'atto_media_advancedsettings', + LINK: MEDIA_TYPES.LINK.toLowerCase(), + VIDEO: MEDIA_TYPES.VIDEO.toLowerCase(), + AUDIO: MEDIA_TYPES.AUDIO.toLowerCase(), + TRACK_SUBTITLES: TRACK_KINDS.SUBTITLES.toLowerCase(), + TRACK_CAPTIONS: TRACK_KINDS.CAPTIONS.toLowerCase(), + TRACK_DESCRIPTIONS: TRACK_KINDS.DESCRIPTIONS.toLowerCase(), + TRACK_CHAPTERS: TRACK_KINDS.CHAPTERS.toLowerCase(), + TRACK_METADATA: TRACK_KINDS.METADATA.toLowerCase() }, SELECTORS = { - URLINPUT: '.' + CSS.URLINPUT, - NAMEINPUT: '.' + CSS.NAMEINPUT + SOURCE: '.' + CSS.SOURCE, + TRACK: '.' + CSS.TRACK, + MEDIA_SOURCE: '.' + CSS.MEDIA_SOURCE, + POSTER_SOURCE: '.' + CSS.POSTER_SOURCE, + TRACK_SOURCE: '.' + CSS.TRACK_SOURCE, + DISPLAY_OPTIONS: '.' + CSS.DISPLAY_OPTIONS, + NAME_INPUT: '.' + CSS.NAME_INPUT, + URL_INPUT: '.' + CSS.URL_INPUT, + POSTER_SIZE: '.' + CSS.POSTER_SIZE, + LINK_SIZE: '.' + CSS.LINK_SIZE, + WIDTH_INPUT: '.' + CSS.WIDTH_INPUT, + HEIGHT_INPUT: '.' + CSS.HEIGHT_INPUT, + TRACK_KIND_INPUT: '.' + CSS.TRACK_KIND_INPUT, + TRACK_LABEL_INPUT: '.' + CSS.TRACK_LABEL_INPUT, + TRACK_LANG_INPUT: '.' + CSS.TRACK_LANG_INPUT, + TRACK_DEFAULT_SELECT: '.' + CSS.TRACK_DEFAULT_SELECT, + MEDIA_CONTROLS_TOGGLE: '.' + CSS.MEDIA_CONTROLS_TOGGLE, + MEDIA_AUTOPLAY_TOGGLE: '.' + CSS.MEDIA_AUTOPLAY_TOGGLE, + MEDIA_MUTE_TOGGLE: '.' + CSS.MEDIA_MUTE_TOGGLE, + MEDIA_LOOP_TOGGLE: '.' + CSS.MEDIA_LOOP_TOGGLE, + ADVANCED_SETTINGS: '.' + CSS.ADVANCED_SETTINGS, + LINK_TAB: 'li[data-medium-type="' + CSS.LINK + '"]', + LINK_PANE: '.tab-pane[data-medium-type="' + CSS.LINK + '"]', + VIDEO_TAB: 'li[data-medium-type="' + CSS.VIDEO + '"]', + VIDEO_PANE: '.tab-pane[data-medium-type="' + CSS.VIDEO + '"]', + AUDIO_TAB: 'li[data-medium-type="' + CSS.AUDIO + '"]', + AUDIO_PANE: '.tab-pane[data-medium-type="' + CSS.AUDIO + '"]', + TRACK_SUBTITLES_TAB: 'li[data-track-kind="' + CSS.TRACK_SUBTITLES + '"]', + TRACK_SUBTITLES_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_SUBTITLES + '"]', + TRACK_CAPTIONS_TAB: 'li[data-track-kind="' + CSS.TRACK_CAPTIONS + '"]', + TRACK_CAPTIONS_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_CAPTIONS + '"]', + TRACK_DESCRIPTIONS_TAB: 'li[data-track-kind="' + CSS.TRACK_DESCRIPTIONS + '"]', + TRACK_DESCRIPTIONS_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_DESCRIPTIONS + '"]', + TRACK_CHAPTERS_TAB: 'li[data-track-kind="' + CSS.TRACK_CHAPTERS + '"]', + TRACK_CHAPTERS_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_CHAPTERS + '"]', + TRACK_METADATA_TAB: 'li[data-track-kind="' + CSS.TRACK_METADATA + '"]', + TRACK_METADATA_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_METADATA + '"]' }, - TEMPLATE = '' + - '
' + - '' + - '
' + - '' + - '' + - '' + - '
' + - '
' + - '' + - '
' + - '
'; + TEMPLATES = { + ROOT: '' + + '
' + + '' + + '
' + + '
' + + '{{> tab_panes.link}}' + + '
' + + '
' + + '{{> tab_panes.video}}' + + '
' + + '
' + + '{{> tab_panes.audio}}' + + '
' + + '
' + + '
' + + '
' + + '' + + '
' + + '
', + TAB_PANES: { + LINK: '' + + '{{renderPartial "form_components.source" context=this id=CSS.LINK_SOURCE}}' + + '', + VIDEO: '' + + '{{renderPartial "form_components.source" context=this id=CSS.MEDIA_SOURCE entersourcelabel="videosourcelabel"' + + ' addcomponentlabel="addsource" multisource="true" addsourcehelp=helpStrings.addsource}}' + + '' + + '' + + '', + AUDIO: '' + + '{{renderPartial "form_components.source" context=this id=CSS.MEDIA_SOURCE entersourcelabel="audiosourcelabel"' + + ' addcomponentlabel="addsource" multisource="true" addsourcehelp=helpStrings.addsource}}' + + '' + + '' + }, + FORM_COMPONENTS: { + SOURCE: '' + + '
' + + '' + + '' + + '{{#multisource}}' + + '{{renderPartial "form_components.add_component" context=../this label=../addcomponentlabel ' + + ' help=../addsourcehelp}}' + + '{{/multisource}}' + + '
', + ADD_COMPONENT: '' + + '', + REMOVE_COMPONENT: '' + + '', + DISPLAY_OPTIONS: '' + + '
' + + '' + + '
' + + '{{renderPartial "form_components.source" context=this id=CSS.POSTER_SOURCE entersourcelabel="poster"}}' + + '
', + ADVANCED_SETTINGS: '' + + '
' + + '' + + '' + + '' + + '' + + '
', + TRACK_TABS: '' + + '' + + '
' + + '
' + + '
{{{helpStrings.subtitles}}}
' + + '{{renderPartial "form_components.track" context=this sourcelabel="subtitlessourcelabel"' + + ' addcomponentlabel="addsubtitlestrack"}}' + + '
' + + '
' + + '
{{{helpStrings.captions}}}
' + + '{{renderPartial "form_components.track" context=this sourcelabel="captionssourcelabel"' + + ' addcomponentlabel="addcaptionstrack"}}' + + '
' + + '
' + + '
{{{helpStrings.descriptions}}}
' + + '{{renderPartial "form_components.track" context=this sourcelabel="descriptionssourcelabel"' + + ' addcomponentlabel="adddescriptionstrack"}}' + + '
' + + '
' + + '
{{{helpStrings.chapters}}}
' + + '{{renderPartial "form_components.track" context=this sourcelabel="chapterssourcelabel"' + + ' addcomponentlabel="addchapterstrack"}}' + + '
' + + '
' + + '
{{{helpStrings.metadata}}}
' + + '{{renderPartial "form_components.track" context=this sourcelabel="metadatasourcelabel"' + + ' addcomponentlabel="addmetadatatrack"}}' + + '
' + + '
', + TRACK: '' + + '
' + + '{{renderPartial "form_components.source" context=this id=CSS.TRACK_SOURCE entersourcelabel=sourcelabel}}' + + '' + + '' + + '' + + '{{renderPartial "form_components.add_component" context=this label=addcomponentlabel}}' + + '
' + }, + HTML_MEDIA: { + VIDEO: '' + + '  ', + AUDIO: '' + + '  ', + LINK: '' + + '{{#name}}{{../name}}{{/name}}{{^name}}{{../url}}{{/name}}' + } + }; Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], { - /** - * A reference to the current selection at the time that the dialogue - * was opened. - * - * @property _currentSelection - * @type Range - * @private - */ - _currentSelection: null, - - /** - * A reference to the dialogue content. - * - * @property _content - * @type Node - * @private - */ - _content: null, - initializer: function() { if (this.get('host').canShowFilepicker('media')) { + this.editor.delegate('dblclick', this._displayDialogue, 'video', this); + this.editor.delegate('click', this._handleClick, 'video', this); + this.addButton({ icon: 'e/insert_edit_video', - callback: this._displayDialogue + callback: this._displayDialogue, + tags: 'video, audio', + tagMatchRequiresAll: false }); } }, + /** + * Gets the root context for all templates, with extra supplied context. + * + * @method _getContext + * @param {Object} extra The extra context to add + * @return {Object} + * @private + */ + _getContext: function(extra) { + return Y.merge({ + elementid: this.get('host').get('elementid'), + component: COMPONENTNAME, + langsinstalled: this.get('langs').installed, + langsavailable: this.get('langs').available, + helpStrings: this.get('help'), + CSS: CSS + }, extra); + }, + + /** + * Handles a click on a media element. + * + * @method _handleClick + * @param {EventFacade} e + * @private + */ + _handleClick: function(e) { + var medium = e.target; + + var selection = this.get('host').getSelectionFromNode(medium); + if (this.get('host').getSelection() !== selection) { + this.get('host').setSelection(selection); + } + }, + /** * Display the media editing tool. * @@ -93,91 +454,503 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi * @private */ _displayDialogue: function() { - // Store the current selection. - this._currentSelection = this.get('host').getSelection(); - if (this._currentSelection === false) { + if (this.get('host').getSelection() === false) { return; } + if (!('renderPartial' in Y.Handlebars.helpers)) { + (function smashPartials(chain, obj) { + Y.each(obj, function(value, index) { + chain.push(index); + if (typeof value !== "object") { + Y.Handlebars.registerPartial(chain.join('.').toLowerCase(), value); + } else { + smashPartials(chain, value); + } + chain.pop(); + }); + })([], TEMPLATES); + + Y.Handlebars.registerHelper('renderPartial', function(partialName, options) { + if (!partialName) { + return ''; + } + + var partial = Y.Handlebars.partials[partialName]; + var parentContext = options.hash.context ? Y.clone(options.hash.context) : {}; + var context = Y.merge(parentContext, options.hash); + delete context.context; + + if (!partial) { + return ''; + } + return new Y.Handlebars.SafeString(Y.Handlebars.compile(partial)(context)); + }); + } + var dialogue = this.getDialogue({ headerContent: M.util.get_string('createmedia', COMPONENTNAME), focusAfterHide: true, - focusOnShowSelector: SELECTORS.URLINPUT + width: 660, + focusOnShowSelector: SELECTORS.URL_INPUT }); // Set the dialogue content, and then show the dialogue. - dialogue.set('bodyContent', this._getDialogueContent()) - .show(); + dialogue.set('bodyContent', this._getDialogueContent(this.get('host').getSelection())).show(); + M.form.shortforms({formid: this.get('host').get('elementid') + '_atto_media_form'}); }, /** - * Return the dialogue content for the tool, attaching any required - * events. + * Returns the dialogue content for the tool. * * @method _getDialogueContent - * @return {Node} The content to place in the dialogue. + * @param {WrappedRange[]} selection Current editor selection + * @return {Y.Node} * @private */ - _getDialogueContent: function() { - var template = Y.Handlebars.compile(TEMPLATE); + _getDialogueContent: function(selection) { + var content = Y.Node.create( + Y.Handlebars.compile(TEMPLATES.ROOT)(this._getContext()) + ); - this._content = Y.Node.create(template({ - component: COMPONENTNAME, - elementid: this.get('host').get('elementid'), - CSS: CSS - })); + var medium = this.get('host').getSelectedNodes().filter('video,audio').shift(); + var mediumProperties = medium ? this._getMediumProperties(medium) : false; + return this._attachEvents(this._applyMediumProperties(content, mediumProperties), selection); + }, - this._content.one('.submit').on('click', this._setMedia, this); - this._content.one('.openmediabrowser').on('click', function(e) { + /** + * Attaches required events to the content node. + * + * @method _attachEvents + * @param {Y.Node} content The content to which events will be attached + * @param {WrappedRange[]} selection Current editor selection + * @return {Y.Node} + * @private + */ + _attachEvents: function(content, selection) { + // Delegate add component link for media source fields. + content.delegate('click', function(e) { e.preventDefault(); - this.get('host').showFilepicker('media', this._filepickerCallback, this); + this._addMediaSourceComponent(e.currentTarget); + }, SELECTORS.MEDIA_SOURCE + ' .addcomponent', this); + + // Delegate add component link for track fields. + content.delegate('click', function(e) { + e.preventDefault(); + this._addTrackComponent(e.currentTarget); + }, SELECTORS.TRACK + ' .addcomponent', this); + + // Only allow one track per tab to be selected as "default". + content.delegate('click', function(e) { + var element = e.currentTarget; + if (element.get('checked')) { + var getKind = function(el) { + return this._getTrackTypeFromTabPane(el.ancestor('.tab-pane')); + }.bind(this); + + element.ancestor('.root.tab-content').all(SELECTORS.TRACK_DEFAULT_SELECT).each(function(select) { + if (select !== element && getKind(element) === getKind(select)) { + select.set('checked', false); + } + }); + } + }, SELECTORS.TRACK_DEFAULT_SELECT, this); + + // Set up filepicker click event. + content.delegate('click', function(e) { + var element = e.currentTarget; + var fptype = (element.ancestor(SELECTORS.POSTER_SOURCE) && 'image') || + (element.ancestor(SELECTORS.TRACK_SOURCE) && 'subtitle') || + 'media'; + e.preventDefault(); + this.get('host').showFilepicker(fptype, this._getFilepickerCallback(element, fptype), this); + }, '.openmediabrowser', this); + + // This is a nasty hack. Basically we are using BS4 markup for the tabs + // but it isn't completely backwards compatible with BS2. The main problem is + // that the "active" class goes on a different node. So the idea is to put it + // the node for BS4, and then use CSS to make it look right in BS2. However, + // once another tab is clicked, everything sorts itself out, more or less. Except + // that the original "active" tab hasn't had the BS4 "active" class removed + // (so the styles will still apply to it). So we need to remove the "active" + // class on the BS4 node so that BS2 is happy. + // + // This doesn't upset BS4 since it removes this class anyway when clicking on + // another tab. + content.all('.nav-item').on('click', function(elem) { + elem.currentTarget.get('parentNode').all('.active').removeClass('active'); + }); + + content.one('.submit').on('click', function(e) { + e.preventDefault(); + var mediaHTML = this._getMediaHTML(e.currentTarget.ancestor('.atto_form')), + host = this.get('host'); + this.getDialogue({ + focusAfterHide: null + }).hide(); + if (mediaHTML) { + host.setSelection(selection); + host.insertContentAtFocusPoint(mediaHTML); + this.markUpdated(); + } }, this); - return this._content; + return content; }, /** - * Update the dialogue after an media was selected in the File Picker. + * Applies medium properties to the content node. * - * @method _filepickerCallback - * @param {object} params The parameters provided by the filepicker - * containing information about the image. + * @method _applyMediumProperties + * @param {Y.Node} content The content to apply the properties to + * @param {object} properties The medium properties to apply + * @return {Y.Node} * @private */ - _filepickerCallback: function(params) { - if (params.url !== '') { - this._content.one(SELECTORS.URLINPUT) - .set('value', params.url); - this._content.one(SELECTORS.NAMEINPUT) - .set('value', params.file); + _applyMediumProperties: function(content, properties) { + if (!properties) { + return content; + } + + var applyTrackProperties = function(track, properties) { + track.one(SELECTORS.TRACK_SOURCE + ' ' + SELECTORS.URL_INPUT).set('value', properties.src); + track.one(SELECTORS.TRACK_LANG_INPUT).set('value', properties.srclang); + track.one(SELECTORS.TRACK_LABEL_INPUT).set('value', properties.label); + track.one(SELECTORS.TRACK_DEFAULT_SELECT).set('checked', properties.defaultTrack); + }; + + var tabPane = content.one('.root.tab-content > .tab-pane#' + this.get('host').get('elementid') + + '_' + properties.type.toLowerCase()); + + // Populate sources. + tabPane.one(SELECTORS.MEDIA_SOURCE + ' ' + SELECTORS.URL_INPUT).set('value', properties.sources[0]); + Y.Array.each(properties.sources.slice(1), function(source) { + this._addMediaSourceComponent(tabPane.one(SELECTORS.MEDIA_SOURCE + ' .addcomponent'), function(newComponent) { + newComponent.one(SELECTORS.URL_INPUT).set('value', source); + }); + }, this); + + // Populate tracks. + Y.Object.each(properties.tracks, function(value, key) { + var trackData = value.length ? value : [{src: '', srclang: '', label: '', defaultTrack: false}]; + var paneSelector = SELECTORS['TRACK_' + key.toUpperCase() + '_PANE']; + + applyTrackProperties(tabPane.one(paneSelector + ' ' + SELECTORS.TRACK), trackData[0]); + Y.Array.each(trackData.slice(1), function(track) { + this._addTrackComponent( + tabPane.one(paneSelector + ' ' + SELECTORS.TRACK + ' .addcomponent'), function(newComponent) { + applyTrackProperties(newComponent, track); + }); + }, this); + }, this); + + // Populate values. + tabPane.one(SELECTORS.POSTER_SOURCE + ' ' + SELECTORS.URL_INPUT).setAttribute('value', properties.poster); + tabPane.one(SELECTORS.WIDTH_INPUT).set('value', properties.width); + tabPane.one(SELECTORS.HEIGHT_INPUT).set('value', properties.height); + tabPane.one(SELECTORS.MEDIA_CONTROLS_TOGGLE).set('checked', properties.controls); + tabPane.one(SELECTORS.MEDIA_AUTOPLAY_TOGGLE).set('checked', properties.autoplay); + tabPane.one(SELECTORS.MEDIA_MUTE_TOGGLE).set('checked', properties.muted); + tabPane.one(SELECTORS.MEDIA_LOOP_TOGGLE).set('checked', properties.loop); + + // Switch to the correct tab. + var mediumType = this._getMediumTypeFromTabPane(tabPane); + + // Remove active class from all tabs + tab panes. + tabPane.siblings('.active').removeClass('active'); + content.all('.root.nav-tabs .nav-item a').removeClass('active'); + + // Add active class to the desired tab and tab pane. + tabPane.addClass('active'); + content.one(SELECTORS[mediumType.toUpperCase() + '_TAB'] + ' a').addClass('active'); + + return content; + }, + + /** + * Extracts medium properties. + * + * @method _getMediumProperties + * @param {Y.Node} medium The medium node from which to extract + * @return {Object} + * @private + */ + _getMediumProperties: function(medium) { + var boolAttr = function(elem, attr) { + return elem.getAttribute(attr) ? true : false; + }; + + var tracks = { + subtitles: [], + captions: [], + descriptions: [], + chapters: [], + metadata: [] + }; + + medium.all('track').each(function(track) { + tracks[track.getAttribute('kind')].push({ + src: track.getAttribute('src'), + srclang: track.getAttribute('srclang'), + label: track.getAttribute('label'), + defaultTrack: boolAttr(track, 'default') + }); + }); + + return { + type: medium.test('video') ? MEDIA_TYPES.VIDEO : MEDIA_TYPES.AUDIO, + sources: medium.all('source').get('src'), + poster: medium.getAttribute('poster'), + width: medium.getAttribute('width'), + height: medium.getAttribute('height'), + autoplay: boolAttr(medium, 'autoplay'), + loop: boolAttr(medium, 'loop'), + muted: boolAttr(medium, 'muted'), + controls: boolAttr(medium, 'controls'), + tracks: tracks + }; + }, + + /** + * Adds a track form component. + * + * @method _addTrackComponent + * @param {Y.Node} element The element which was used to trigger this function + * @param {Function} [callback] Function to be called when the new component is added + * @param {Y.Node} callback.newComponent The compiled component + * @private + */ + _addTrackComponent: function(element, callback) { + var trackType = this._getTrackTypeFromTabPane(element.ancestor('.tab-pane')); + var context = this._getContext({ + sourcelabel: trackType + 'sourcelabel', + addcomponentlabel: 'add' + trackType + 'track' + }); + + this._addComponent(element, TEMPLATES.FORM_COMPONENTS.TRACK, SELECTORS.TRACK, context, callback); + }, + + /** + * Adds a media source form component. + * + * @method _addMediaSourceComponent + * @param {Y.Node} element The element which was used to trigger this function + * @param {Function} [callback] Function to be called when the new component is added + * @param {Y.Node} callback.newComponent The compiled component + * @private + */ + _addMediaSourceComponent: function(element, callback) { + var mediumType = this._getMediumTypeFromTabPane(element.ancestor('.tab-pane')); + var context = this._getContext({ + multisource: true, + id: CSS.MEDIA_SOURCE, + entersourcelabel: mediumType + 'sourcelabel', + addcomponentlabel: 'addsource' + }); + this._addComponent(element, TEMPLATES.FORM_COMPONENTS.SOURCE, SELECTORS.MEDIA_SOURCE, context, callback); + }, + + /** + * Adds an arbitrary form component. + * + * This function Compiles and adds the provided component in the supplied 'ancestor' container. + * It will also add links to add/remove the relevant components, attaching the + * necessary events. + * + * @method _addComponent + * @param {Y.Node} element The element which was used to trigger this function + * @param {String} component The component to compile and add + * @param {String} ancestor A selector used to find an ancestor of 'component', to which + * the compiled component will be appended + * @param {Object} context The context with which to render the component + * @param {Function} [callback] Function to be called when the new component is added + * @param {Y.Node} callback.newComponent The compiled component + * @private + */ + _addComponent: function(element, component, ancestor, context, callback) { + var currentComponent = element.ancestor(ancestor), + newComponent = Y.Node.create(Y.Handlebars.compile(component)(context)), + removeNodeContext = this._getContext(context); + + removeNodeContext.label = "remove"; + var removeNode = Y.Node.create(Y.Handlebars.compile(TEMPLATES.FORM_COMPONENTS.REMOVE_COMPONENT)(removeNodeContext)); + + removeNode.one('.removecomponent').on('click', function(e) { + e.preventDefault(); + currentComponent.remove(true); + }); + + currentComponent.insert(newComponent, 'after'); + element.ancestor().insert(removeNode, 'after'); + element.ancestor().remove(true); + + if (callback) { + callback.call(this, newComponent); } }, /** - * Update the media in the contenteditable. + * Returns the callback for the file picker to call after a file has been selected. * - * @method setMedia - * @param {EventFacade} e + * @method _getFilepickerCallback + * @param {Y.Node} element The element which triggered the callback + * @param {String} fptype The file pickertype (as would be passed to `showFilePicker`) + * @return {Function} The function to be used as a callback when the file picker returns the file * @private */ - _setMedia: function(e) { - e.preventDefault(); - this.getDialogue({ - focusAfterHide: null - }).hide(); + _getFilepickerCallback: function(element, fptype) { + return function(params) { + if (params.url !== '') { + var tabPane = element.ancestor('.tab-pane'); + element.ancestor(SELECTORS.SOURCE).one(SELECTORS.URL_INPUT).set('value', params.url); - var form = e.currentTarget.ancestor('.atto_form'), - url = form.one(SELECTORS.URLINPUT).get('value'), - name = form.one(SELECTORS.NAMEINPUT).get('value'), - host = this.get('host'); + // Links (and only links) have a name field. + if (tabPane.get('id') === this.get('host').get('elementid') + '_' + CSS.LINK) { + tabPane.one(SELECTORS.NAME_INPUT).set('value', params.file); + } - if (url !== '' && name !== '') { - host.setSelection(this._currentSelection); - var mediahtml = '' + name + ''; + if (fptype === 'subtitle') { + var subtitleLang = params.file.split('.vtt')[0].split('-').slice(-1)[0]; + var langObj = this.get('langs').available.reduce(function(carry, lang) { + return lang.code === subtitleLang ? lang : carry; + }, false); + if (langObj) { + element.ancestor(SELECTORS.TRACK).one(SELECTORS.TRACK_LABEL_INPUT).set('value', + langObj.lang.substr(0, langObj.lang.lastIndexOf(' '))); + element.ancestor(SELECTORS.TRACK).one(SELECTORS.TRACK_LANG_INPUT).set('value', langObj.code); + } + } + } + }; + }, - host.insertContentAtFocusPoint(mediahtml); - this.markUpdated(); - } + /** + * Given a "medium" tab pane, returns what kind of medium it contains. + * + * @method _getMediumTypeFromTabPane + * @param {Y.Node} tabPane The tab pane + * @return {String} The type of medium in the pane + */ + _getMediumTypeFromTabPane: function(tabPane) { + return tabPane.getAttribute('data-medium-type'); + }, + + /** + * Given a "track" tab pane, returns what kind of track it contains. + * + * @method _getTrackTypeFromTabPane + * @param {Y.Node} tabPane The tab pane + * @return {String} The type of track in the pane + */ + _getTrackTypeFromTabPane: function(tabPane) { + return tabPane.getAttribute('data-track-kind'); + }, + + /** + * Returns the HTML to be inserted to the text area. + * + * @method _getMediaHTML + * @param {Y.Node} form The form from which to extract data + * @return {String} The compiled markup + * @private + */ + _getMediaHTML: function(form) { + var mediumType = this._getMediumTypeFromTabPane(form.one('.root.tab-content > .tab-pane.active')); + var tabContent = form.one(SELECTORS[mediumType.toUpperCase() + '_PANE']); + + return this['_getMediaHTML' + mediumType[0].toUpperCase() + mediumType.substr(1)](tabContent); + }, + + /** + * Returns the HTML to be inserted to the text area for the link tab. + * + * @method _getMediaHTMLLink + * @param {Y.Node} tab The tab from which to extract data + * @return {String} The compiled markup + * @private + */ + _getMediaHTMLLink: function(tab) { + var context = { + url: tab.one(SELECTORS.URL_INPUT).get('value'), + name: tab.one(SELECTORS.NAME_INPUT).get('value') || false + }; + + return context.url ? Y.Handlebars.compile(TEMPLATES.HTML_MEDIA.LINK)(context) : ''; + }, + + /** + * Returns the HTML to be inserted to the text area for the video tab. + * + * @method _getMediaHTMLVideo + * @param {Y.Node} tab The tab from which to extract data + * @return {String} The compiled markup + * @private + */ + _getMediaHTMLVideo: function(tab) { + var context = this._getContextForMediaHTML(tab); + context.width = tab.one(SELECTORS.WIDTH_INPUT).get('value') || false; + context.height = tab.one(SELECTORS.HEIGHT_INPUT).get('value') || false; + context.poster = tab.one(SELECTORS.POSTER_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value') || false; + + return context.sources.length ? Y.Handlebars.compile(TEMPLATES.HTML_MEDIA.VIDEO)(context) : ''; + }, + + /** + * Returns the HTML to be inserted to the text area for the audio tab. + * + * @method _getMediaHTMLAudio + * @param {Y.Node} tab The tab from which to extract data + * @return {String} The compiled markup + * @private + */ + _getMediaHTMLAudio: function(tab) { + var context = this._getContextForMediaHTML(tab); + + return context.sources.length ? Y.Handlebars.compile(TEMPLATES.HTML_MEDIA.AUDIO)(context) : ''; + }, + + /** + * Returns the context with which to render a media template. + * + * @method _getContextForMediaHTML + * @param {Y.Node} tab The tab from which to extract data + * @return {Object} + * @private + */ + _getContextForMediaHTML: function(tab) { + var tracks = []; + + tab.all(SELECTORS.TRACK).each(function(track) { + tracks.push({ + track: track.one(SELECTORS.TRACK_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value'), + kind: this._getTrackTypeFromTabPane(track.ancestor('.tab-pane')), + label: track.one(SELECTORS.TRACK_LABEL_INPUT).get('value') || + track.one(SELECTORS.TRACK_LANG_INPUT).get('value'), + srclang: track.one(SELECTORS.TRACK_LANG_INPUT).get('value'), + defaultTrack: track.one(SELECTORS.TRACK_DEFAULT_SELECT).get('checked') ? "true" : null + }); + }, this); + + return { + sources: tab.all(SELECTORS.MEDIA_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value').filter(function(source) { + return !!source; + }).map(function(source) { + return {source: source}; + }), + description: tab.one(SELECTORS.MEDIA_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value') || false, + tracks: tracks.filter(function(track) { + return !!track.track; + }), + showControls: tab.one(SELECTORS.MEDIA_CONTROLS_TOGGLE).get('checked'), + autoplay: tab.one(SELECTORS.MEDIA_AUTOPLAY_TOGGLE).get('checked'), + muted: tab.one(SELECTORS.MEDIA_MUTE_TOGGLE).get('checked'), + loop: tab.one(SELECTORS.MEDIA_LOOP_TOGGLE).get('checked') + }; + } +}, { + ATTRS: { + langs: {}, + help: {} } }); diff --git a/lib/editor/atto/plugins/media/yui/src/button/js/button.js b/lib/editor/atto/plugins/media/yui/src/button/js/button.js index b9f43b0f265..c6073f3f53f 100644 --- a/lib/editor/atto/plugins/media/yui/src/button/js/button.js +++ b/lib/editor/atto/plugins/media/yui/src/button/js/button.js @@ -32,58 +32,419 @@ */ var COMPONENTNAME = 'atto_media', + MEDIA_TYPES = {LINK: 'LINK', VIDEO: 'VIDEO', AUDIO: 'AUDIO'}, + TRACK_KINDS = { + SUBTITLES: 'SUBTITLES', + CAPTIONS: 'CAPTIONS', + DESCRIPTIONS: 'DESCRIPTIONS', + CHAPTERS: 'CHAPTERS', + METADATA: 'METADATA' + }, CSS = { - URLINPUT: 'atto_media_urlentry', - NAMEINPUT: 'atto_media_nameentry' + SOURCE: 'atto_media_source', + TRACK: 'atto_media_track', + MEDIA_SOURCE: 'atto_media_media_source', + LINK_SOURCE: 'atto_media_link_source', + POSTER_SOURCE: 'atto_media_poster_source', + TRACK_SOURCE: 'atto_media_track_source', + DISPLAY_OPTIONS: 'atto_media_display_options', + NAME_INPUT: 'atto_media_name_entry', + URL_INPUT: 'atto_media_url_entry', + POSTER_SIZE: 'atto_media_poster_size', + LINK_SIZE: 'atto_media_link_size', + WIDTH_INPUT: 'atto_media_width_entry', + HEIGHT_INPUT: 'atto_media_height_entry', + TRACK_KIND_INPUT: 'atto_media_track_kind_entry', + TRACK_LABEL_INPUT: 'atto_media_track_label_entry', + TRACK_LANG_INPUT: 'atto_media_track_lang_entry', + TRACK_DEFAULT_SELECT: 'atto_media_track_default', + MEDIA_CONTROLS_TOGGLE: 'atto_media_controls', + MEDIA_AUTOPLAY_TOGGLE: 'atto_media_autoplay', + MEDIA_MUTE_TOGGLE: 'atto_media_mute', + MEDIA_LOOP_TOGGLE: 'atto_media_loop', + ADVANCED_SETTINGS: 'atto_media_advancedsettings', + LINK: MEDIA_TYPES.LINK.toLowerCase(), + VIDEO: MEDIA_TYPES.VIDEO.toLowerCase(), + AUDIO: MEDIA_TYPES.AUDIO.toLowerCase(), + TRACK_SUBTITLES: TRACK_KINDS.SUBTITLES.toLowerCase(), + TRACK_CAPTIONS: TRACK_KINDS.CAPTIONS.toLowerCase(), + TRACK_DESCRIPTIONS: TRACK_KINDS.DESCRIPTIONS.toLowerCase(), + TRACK_CHAPTERS: TRACK_KINDS.CHAPTERS.toLowerCase(), + TRACK_METADATA: TRACK_KINDS.METADATA.toLowerCase() }, SELECTORS = { - URLINPUT: '.' + CSS.URLINPUT, - NAMEINPUT: '.' + CSS.NAMEINPUT + SOURCE: '.' + CSS.SOURCE, + TRACK: '.' + CSS.TRACK, + MEDIA_SOURCE: '.' + CSS.MEDIA_SOURCE, + POSTER_SOURCE: '.' + CSS.POSTER_SOURCE, + TRACK_SOURCE: '.' + CSS.TRACK_SOURCE, + DISPLAY_OPTIONS: '.' + CSS.DISPLAY_OPTIONS, + NAME_INPUT: '.' + CSS.NAME_INPUT, + URL_INPUT: '.' + CSS.URL_INPUT, + POSTER_SIZE: '.' + CSS.POSTER_SIZE, + LINK_SIZE: '.' + CSS.LINK_SIZE, + WIDTH_INPUT: '.' + CSS.WIDTH_INPUT, + HEIGHT_INPUT: '.' + CSS.HEIGHT_INPUT, + TRACK_KIND_INPUT: '.' + CSS.TRACK_KIND_INPUT, + TRACK_LABEL_INPUT: '.' + CSS.TRACK_LABEL_INPUT, + TRACK_LANG_INPUT: '.' + CSS.TRACK_LANG_INPUT, + TRACK_DEFAULT_SELECT: '.' + CSS.TRACK_DEFAULT_SELECT, + MEDIA_CONTROLS_TOGGLE: '.' + CSS.MEDIA_CONTROLS_TOGGLE, + MEDIA_AUTOPLAY_TOGGLE: '.' + CSS.MEDIA_AUTOPLAY_TOGGLE, + MEDIA_MUTE_TOGGLE: '.' + CSS.MEDIA_MUTE_TOGGLE, + MEDIA_LOOP_TOGGLE: '.' + CSS.MEDIA_LOOP_TOGGLE, + ADVANCED_SETTINGS: '.' + CSS.ADVANCED_SETTINGS, + LINK_TAB: 'li[data-medium-type="' + CSS.LINK + '"]', + LINK_PANE: '.tab-pane[data-medium-type="' + CSS.LINK + '"]', + VIDEO_TAB: 'li[data-medium-type="' + CSS.VIDEO + '"]', + VIDEO_PANE: '.tab-pane[data-medium-type="' + CSS.VIDEO + '"]', + AUDIO_TAB: 'li[data-medium-type="' + CSS.AUDIO + '"]', + AUDIO_PANE: '.tab-pane[data-medium-type="' + CSS.AUDIO + '"]', + TRACK_SUBTITLES_TAB: 'li[data-track-kind="' + CSS.TRACK_SUBTITLES + '"]', + TRACK_SUBTITLES_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_SUBTITLES + '"]', + TRACK_CAPTIONS_TAB: 'li[data-track-kind="' + CSS.TRACK_CAPTIONS + '"]', + TRACK_CAPTIONS_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_CAPTIONS + '"]', + TRACK_DESCRIPTIONS_TAB: 'li[data-track-kind="' + CSS.TRACK_DESCRIPTIONS + '"]', + TRACK_DESCRIPTIONS_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_DESCRIPTIONS + '"]', + TRACK_CHAPTERS_TAB: 'li[data-track-kind="' + CSS.TRACK_CHAPTERS + '"]', + TRACK_CHAPTERS_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_CHAPTERS + '"]', + TRACK_METADATA_TAB: 'li[data-track-kind="' + CSS.TRACK_METADATA + '"]', + TRACK_METADATA_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_METADATA + '"]' }, - TEMPLATE = '' + - '
' + - '' + - '
' + - '' + - '' + - '' + - '
' + - '
' + - '' + - '
' + - '
'; + TEMPLATES = { + ROOT: '' + + '
' + + '' + + '
' + + '
' + + '{{> tab_panes.link}}' + + '
' + + '
' + + '{{> tab_panes.video}}' + + '
' + + '
' + + '{{> tab_panes.audio}}' + + '
' + + '
' + + '
' + + '
' + + '' + + '
' + + '
', + TAB_PANES: { + LINK: '' + + '{{renderPartial "form_components.source" context=this id=CSS.LINK_SOURCE}}' + + '', + VIDEO: '' + + '{{renderPartial "form_components.source" context=this id=CSS.MEDIA_SOURCE entersourcelabel="videosourcelabel"' + + ' addcomponentlabel="addsource" multisource="true" addsourcehelp=helpStrings.addsource}}' + + '' + + '' + + '', + AUDIO: '' + + '{{renderPartial "form_components.source" context=this id=CSS.MEDIA_SOURCE entersourcelabel="audiosourcelabel"' + + ' addcomponentlabel="addsource" multisource="true" addsourcehelp=helpStrings.addsource}}' + + '' + + '' + }, + FORM_COMPONENTS: { + SOURCE: '' + + '
' + + '' + + '' + + '{{#multisource}}' + + '{{renderPartial "form_components.add_component" context=../this label=../addcomponentlabel ' + + ' help=../addsourcehelp}}' + + '{{/multisource}}' + + '
', + ADD_COMPONENT: '' + + '', + REMOVE_COMPONENT: '' + + '', + DISPLAY_OPTIONS: '' + + '
' + + '' + + '
' + + '{{renderPartial "form_components.source" context=this id=CSS.POSTER_SOURCE entersourcelabel="poster"}}' + + '
', + ADVANCED_SETTINGS: '' + + '
' + + '' + + '' + + '' + + '' + + '
', + TRACK_TABS: '' + + '' + + '
' + + '
' + + '
{{{helpStrings.subtitles}}}
' + + '{{renderPartial "form_components.track" context=this sourcelabel="subtitlessourcelabel"' + + ' addcomponentlabel="addsubtitlestrack"}}' + + '
' + + '
' + + '
{{{helpStrings.captions}}}
' + + '{{renderPartial "form_components.track" context=this sourcelabel="captionssourcelabel"' + + ' addcomponentlabel="addcaptionstrack"}}' + + '
' + + '
' + + '
{{{helpStrings.descriptions}}}
' + + '{{renderPartial "form_components.track" context=this sourcelabel="descriptionssourcelabel"' + + ' addcomponentlabel="adddescriptionstrack"}}' + + '
' + + '
' + + '
{{{helpStrings.chapters}}}
' + + '{{renderPartial "form_components.track" context=this sourcelabel="chapterssourcelabel"' + + ' addcomponentlabel="addchapterstrack"}}' + + '
' + + '
' + + '
{{{helpStrings.metadata}}}
' + + '{{renderPartial "form_components.track" context=this sourcelabel="metadatasourcelabel"' + + ' addcomponentlabel="addmetadatatrack"}}' + + '
' + + '
', + TRACK: '' + + '
' + + '{{renderPartial "form_components.source" context=this id=CSS.TRACK_SOURCE entersourcelabel=sourcelabel}}' + + '' + + '' + + '' + + '{{renderPartial "form_components.add_component" context=this label=addcomponentlabel}}' + + '
' + }, + HTML_MEDIA: { + VIDEO: '' + + '  ', + AUDIO: '' + + '  ', + LINK: '' + + '{{#name}}{{../name}}{{/name}}{{^name}}{{../url}}{{/name}}' + } + }; Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], { - /** - * A reference to the current selection at the time that the dialogue - * was opened. - * - * @property _currentSelection - * @type Range - * @private - */ - _currentSelection: null, - - /** - * A reference to the dialogue content. - * - * @property _content - * @type Node - * @private - */ - _content: null, - initializer: function() { if (this.get('host').canShowFilepicker('media')) { + this.editor.delegate('dblclick', this._displayDialogue, 'video', this); + this.editor.delegate('click', this._handleClick, 'video', this); + this.addButton({ icon: 'e/insert_edit_video', - callback: this._displayDialogue + callback: this._displayDialogue, + tags: 'video, audio', + tagMatchRequiresAll: false }); } }, + /** + * Gets the root context for all templates, with extra supplied context. + * + * @method _getContext + * @param {Object} extra The extra context to add + * @return {Object} + * @private + */ + _getContext: function(extra) { + return Y.merge({ + elementid: this.get('host').get('elementid'), + component: COMPONENTNAME, + langsinstalled: this.get('langs').installed, + langsavailable: this.get('langs').available, + helpStrings: this.get('help'), + CSS: CSS + }, extra); + }, + + /** + * Handles a click on a media element. + * + * @method _handleClick + * @param {EventFacade} e + * @private + */ + _handleClick: function(e) { + var medium = e.target; + + var selection = this.get('host').getSelectionFromNode(medium); + if (this.get('host').getSelection() !== selection) { + this.get('host').setSelection(selection); + } + }, + /** * Display the media editing tool. * @@ -91,90 +452,502 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi * @private */ _displayDialogue: function() { - // Store the current selection. - this._currentSelection = this.get('host').getSelection(); - if (this._currentSelection === false) { + if (this.get('host').getSelection() === false) { return; } + if (!('renderPartial' in Y.Handlebars.helpers)) { + (function smashPartials(chain, obj) { + Y.each(obj, function(value, index) { + chain.push(index); + if (typeof value !== "object") { + Y.Handlebars.registerPartial(chain.join('.').toLowerCase(), value); + } else { + smashPartials(chain, value); + } + chain.pop(); + }); + })([], TEMPLATES); + + Y.Handlebars.registerHelper('renderPartial', function(partialName, options) { + if (!partialName) { + return ''; + } + + var partial = Y.Handlebars.partials[partialName]; + var parentContext = options.hash.context ? Y.clone(options.hash.context) : {}; + var context = Y.merge(parentContext, options.hash); + delete context.context; + + if (!partial) { + return ''; + } + return new Y.Handlebars.SafeString(Y.Handlebars.compile(partial)(context)); + }); + } + var dialogue = this.getDialogue({ headerContent: M.util.get_string('createmedia', COMPONENTNAME), focusAfterHide: true, - focusOnShowSelector: SELECTORS.URLINPUT + width: 660, + focusOnShowSelector: SELECTORS.URL_INPUT }); // Set the dialogue content, and then show the dialogue. - dialogue.set('bodyContent', this._getDialogueContent()) - .show(); + dialogue.set('bodyContent', this._getDialogueContent(this.get('host').getSelection())).show(); + M.form.shortforms({formid: this.get('host').get('elementid') + '_atto_media_form'}); }, /** - * Return the dialogue content for the tool, attaching any required - * events. + * Returns the dialogue content for the tool. * * @method _getDialogueContent - * @return {Node} The content to place in the dialogue. + * @param {WrappedRange[]} selection Current editor selection + * @return {Y.Node} * @private */ - _getDialogueContent: function() { - var template = Y.Handlebars.compile(TEMPLATE); + _getDialogueContent: function(selection) { + var content = Y.Node.create( + Y.Handlebars.compile(TEMPLATES.ROOT)(this._getContext()) + ); - this._content = Y.Node.create(template({ - component: COMPONENTNAME, - elementid: this.get('host').get('elementid'), - CSS: CSS - })); + var medium = this.get('host').getSelectedNodes().filter('video,audio').shift(); + var mediumProperties = medium ? this._getMediumProperties(medium) : false; + return this._attachEvents(this._applyMediumProperties(content, mediumProperties), selection); + }, - this._content.one('.submit').on('click', this._setMedia, this); - this._content.one('.openmediabrowser').on('click', function(e) { + /** + * Attaches required events to the content node. + * + * @method _attachEvents + * @param {Y.Node} content The content to which events will be attached + * @param {WrappedRange[]} selection Current editor selection + * @return {Y.Node} + * @private + */ + _attachEvents: function(content, selection) { + // Delegate add component link for media source fields. + content.delegate('click', function(e) { e.preventDefault(); - this.get('host').showFilepicker('media', this._filepickerCallback, this); + this._addMediaSourceComponent(e.currentTarget); + }, SELECTORS.MEDIA_SOURCE + ' .addcomponent', this); + + // Delegate add component link for track fields. + content.delegate('click', function(e) { + e.preventDefault(); + this._addTrackComponent(e.currentTarget); + }, SELECTORS.TRACK + ' .addcomponent', this); + + // Only allow one track per tab to be selected as "default". + content.delegate('click', function(e) { + var element = e.currentTarget; + if (element.get('checked')) { + var getKind = function(el) { + return this._getTrackTypeFromTabPane(el.ancestor('.tab-pane')); + }.bind(this); + + element.ancestor('.root.tab-content').all(SELECTORS.TRACK_DEFAULT_SELECT).each(function(select) { + if (select !== element && getKind(element) === getKind(select)) { + select.set('checked', false); + } + }); + } + }, SELECTORS.TRACK_DEFAULT_SELECT, this); + + // Set up filepicker click event. + content.delegate('click', function(e) { + var element = e.currentTarget; + var fptype = (element.ancestor(SELECTORS.POSTER_SOURCE) && 'image') || + (element.ancestor(SELECTORS.TRACK_SOURCE) && 'subtitle') || + 'media'; + e.preventDefault(); + this.get('host').showFilepicker(fptype, this._getFilepickerCallback(element, fptype), this); + }, '.openmediabrowser', this); + + // This is a nasty hack. Basically we are using BS4 markup for the tabs + // but it isn't completely backwards compatible with BS2. The main problem is + // that the "active" class goes on a different node. So the idea is to put it + // the node for BS4, and then use CSS to make it look right in BS2. However, + // once another tab is clicked, everything sorts itself out, more or less. Except + // that the original "active" tab hasn't had the BS4 "active" class removed + // (so the styles will still apply to it). So we need to remove the "active" + // class on the BS4 node so that BS2 is happy. + // + // This doesn't upset BS4 since it removes this class anyway when clicking on + // another tab. + content.all('.nav-item').on('click', function(elem) { + elem.currentTarget.get('parentNode').all('.active').removeClass('active'); + }); + + content.one('.submit').on('click', function(e) { + e.preventDefault(); + var mediaHTML = this._getMediaHTML(e.currentTarget.ancestor('.atto_form')), + host = this.get('host'); + this.getDialogue({ + focusAfterHide: null + }).hide(); + if (mediaHTML) { + host.setSelection(selection); + host.insertContentAtFocusPoint(mediaHTML); + this.markUpdated(); + } }, this); - return this._content; + return content; }, /** - * Update the dialogue after an media was selected in the File Picker. + * Applies medium properties to the content node. * - * @method _filepickerCallback - * @param {object} params The parameters provided by the filepicker - * containing information about the image. + * @method _applyMediumProperties + * @param {Y.Node} content The content to apply the properties to + * @param {object} properties The medium properties to apply + * @return {Y.Node} * @private */ - _filepickerCallback: function(params) { - if (params.url !== '') { - this._content.one(SELECTORS.URLINPUT) - .set('value', params.url); - this._content.one(SELECTORS.NAMEINPUT) - .set('value', params.file); + _applyMediumProperties: function(content, properties) { + if (!properties) { + return content; + } + + var applyTrackProperties = function(track, properties) { + track.one(SELECTORS.TRACK_SOURCE + ' ' + SELECTORS.URL_INPUT).set('value', properties.src); + track.one(SELECTORS.TRACK_LANG_INPUT).set('value', properties.srclang); + track.one(SELECTORS.TRACK_LABEL_INPUT).set('value', properties.label); + track.one(SELECTORS.TRACK_DEFAULT_SELECT).set('checked', properties.defaultTrack); + }; + + var tabPane = content.one('.root.tab-content > .tab-pane#' + this.get('host').get('elementid') + + '_' + properties.type.toLowerCase()); + + // Populate sources. + tabPane.one(SELECTORS.MEDIA_SOURCE + ' ' + SELECTORS.URL_INPUT).set('value', properties.sources[0]); + Y.Array.each(properties.sources.slice(1), function(source) { + this._addMediaSourceComponent(tabPane.one(SELECTORS.MEDIA_SOURCE + ' .addcomponent'), function(newComponent) { + newComponent.one(SELECTORS.URL_INPUT).set('value', source); + }); + }, this); + + // Populate tracks. + Y.Object.each(properties.tracks, function(value, key) { + var trackData = value.length ? value : [{src: '', srclang: '', label: '', defaultTrack: false}]; + var paneSelector = SELECTORS['TRACK_' + key.toUpperCase() + '_PANE']; + + applyTrackProperties(tabPane.one(paneSelector + ' ' + SELECTORS.TRACK), trackData[0]); + Y.Array.each(trackData.slice(1), function(track) { + this._addTrackComponent( + tabPane.one(paneSelector + ' ' + SELECTORS.TRACK + ' .addcomponent'), function(newComponent) { + applyTrackProperties(newComponent, track); + }); + }, this); + }, this); + + // Populate values. + tabPane.one(SELECTORS.POSTER_SOURCE + ' ' + SELECTORS.URL_INPUT).setAttribute('value', properties.poster); + tabPane.one(SELECTORS.WIDTH_INPUT).set('value', properties.width); + tabPane.one(SELECTORS.HEIGHT_INPUT).set('value', properties.height); + tabPane.one(SELECTORS.MEDIA_CONTROLS_TOGGLE).set('checked', properties.controls); + tabPane.one(SELECTORS.MEDIA_AUTOPLAY_TOGGLE).set('checked', properties.autoplay); + tabPane.one(SELECTORS.MEDIA_MUTE_TOGGLE).set('checked', properties.muted); + tabPane.one(SELECTORS.MEDIA_LOOP_TOGGLE).set('checked', properties.loop); + + // Switch to the correct tab. + var mediumType = this._getMediumTypeFromTabPane(tabPane); + + // Remove active class from all tabs + tab panes. + tabPane.siblings('.active').removeClass('active'); + content.all('.root.nav-tabs .nav-item a').removeClass('active'); + + // Add active class to the desired tab and tab pane. + tabPane.addClass('active'); + content.one(SELECTORS[mediumType.toUpperCase() + '_TAB'] + ' a').addClass('active'); + + return content; + }, + + /** + * Extracts medium properties. + * + * @method _getMediumProperties + * @param {Y.Node} medium The medium node from which to extract + * @return {Object} + * @private + */ + _getMediumProperties: function(medium) { + var boolAttr = function(elem, attr) { + return elem.getAttribute(attr) ? true : false; + }; + + var tracks = { + subtitles: [], + captions: [], + descriptions: [], + chapters: [], + metadata: [] + }; + + medium.all('track').each(function(track) { + tracks[track.getAttribute('kind')].push({ + src: track.getAttribute('src'), + srclang: track.getAttribute('srclang'), + label: track.getAttribute('label'), + defaultTrack: boolAttr(track, 'default') + }); + }); + + return { + type: medium.test('video') ? MEDIA_TYPES.VIDEO : MEDIA_TYPES.AUDIO, + sources: medium.all('source').get('src'), + poster: medium.getAttribute('poster'), + width: medium.getAttribute('width'), + height: medium.getAttribute('height'), + autoplay: boolAttr(medium, 'autoplay'), + loop: boolAttr(medium, 'loop'), + muted: boolAttr(medium, 'muted'), + controls: boolAttr(medium, 'controls'), + tracks: tracks + }; + }, + + /** + * Adds a track form component. + * + * @method _addTrackComponent + * @param {Y.Node} element The element which was used to trigger this function + * @param {Function} [callback] Function to be called when the new component is added + * @param {Y.Node} callback.newComponent The compiled component + * @private + */ + _addTrackComponent: function(element, callback) { + var trackType = this._getTrackTypeFromTabPane(element.ancestor('.tab-pane')); + var context = this._getContext({ + sourcelabel: trackType + 'sourcelabel', + addcomponentlabel: 'add' + trackType + 'track' + }); + + this._addComponent(element, TEMPLATES.FORM_COMPONENTS.TRACK, SELECTORS.TRACK, context, callback); + }, + + /** + * Adds a media source form component. + * + * @method _addMediaSourceComponent + * @param {Y.Node} element The element which was used to trigger this function + * @param {Function} [callback] Function to be called when the new component is added + * @param {Y.Node} callback.newComponent The compiled component + * @private + */ + _addMediaSourceComponent: function(element, callback) { + var mediumType = this._getMediumTypeFromTabPane(element.ancestor('.tab-pane')); + var context = this._getContext({ + multisource: true, + id: CSS.MEDIA_SOURCE, + entersourcelabel: mediumType + 'sourcelabel', + addcomponentlabel: 'addsource' + }); + this._addComponent(element, TEMPLATES.FORM_COMPONENTS.SOURCE, SELECTORS.MEDIA_SOURCE, context, callback); + }, + + /** + * Adds an arbitrary form component. + * + * This function Compiles and adds the provided component in the supplied 'ancestor' container. + * It will also add links to add/remove the relevant components, attaching the + * necessary events. + * + * @method _addComponent + * @param {Y.Node} element The element which was used to trigger this function + * @param {String} component The component to compile and add + * @param {String} ancestor A selector used to find an ancestor of 'component', to which + * the compiled component will be appended + * @param {Object} context The context with which to render the component + * @param {Function} [callback] Function to be called when the new component is added + * @param {Y.Node} callback.newComponent The compiled component + * @private + */ + _addComponent: function(element, component, ancestor, context, callback) { + var currentComponent = element.ancestor(ancestor), + newComponent = Y.Node.create(Y.Handlebars.compile(component)(context)), + removeNodeContext = this._getContext(context); + + removeNodeContext.label = "remove"; + var removeNode = Y.Node.create(Y.Handlebars.compile(TEMPLATES.FORM_COMPONENTS.REMOVE_COMPONENT)(removeNodeContext)); + + removeNode.one('.removecomponent').on('click', function(e) { + e.preventDefault(); + currentComponent.remove(true); + }); + + currentComponent.insert(newComponent, 'after'); + element.ancestor().insert(removeNode, 'after'); + element.ancestor().remove(true); + + if (callback) { + callback.call(this, newComponent); } }, /** - * Update the media in the contenteditable. + * Returns the callback for the file picker to call after a file has been selected. * - * @method setMedia - * @param {EventFacade} e + * @method _getFilepickerCallback + * @param {Y.Node} element The element which triggered the callback + * @param {String} fptype The file pickertype (as would be passed to `showFilePicker`) + * @return {Function} The function to be used as a callback when the file picker returns the file * @private */ - _setMedia: function(e) { - e.preventDefault(); - this.getDialogue({ - focusAfterHide: null - }).hide(); + _getFilepickerCallback: function(element, fptype) { + return function(params) { + if (params.url !== '') { + var tabPane = element.ancestor('.tab-pane'); + element.ancestor(SELECTORS.SOURCE).one(SELECTORS.URL_INPUT).set('value', params.url); - var form = e.currentTarget.ancestor('.atto_form'), - url = form.one(SELECTORS.URLINPUT).get('value'), - name = form.one(SELECTORS.NAMEINPUT).get('value'), - host = this.get('host'); + // Links (and only links) have a name field. + if (tabPane.get('id') === this.get('host').get('elementid') + '_' + CSS.LINK) { + tabPane.one(SELECTORS.NAME_INPUT).set('value', params.file); + } - if (url !== '' && name !== '') { - host.setSelection(this._currentSelection); - var mediahtml = '' + name + ''; + if (fptype === 'subtitle') { + var subtitleLang = params.file.split('.vtt')[0].split('-').slice(-1)[0]; + var langObj = this.get('langs').available.reduce(function(carry, lang) { + return lang.code === subtitleLang ? lang : carry; + }, false); + if (langObj) { + element.ancestor(SELECTORS.TRACK).one(SELECTORS.TRACK_LABEL_INPUT).set('value', + langObj.lang.substr(0, langObj.lang.lastIndexOf(' '))); + element.ancestor(SELECTORS.TRACK).one(SELECTORS.TRACK_LANG_INPUT).set('value', langObj.code); + } + } + } + }; + }, - host.insertContentAtFocusPoint(mediahtml); - this.markUpdated(); - } + /** + * Given a "medium" tab pane, returns what kind of medium it contains. + * + * @method _getMediumTypeFromTabPane + * @param {Y.Node} tabPane The tab pane + * @return {String} The type of medium in the pane + */ + _getMediumTypeFromTabPane: function(tabPane) { + return tabPane.getAttribute('data-medium-type'); + }, + + /** + * Given a "track" tab pane, returns what kind of track it contains. + * + * @method _getTrackTypeFromTabPane + * @param {Y.Node} tabPane The tab pane + * @return {String} The type of track in the pane + */ + _getTrackTypeFromTabPane: function(tabPane) { + return tabPane.getAttribute('data-track-kind'); + }, + + /** + * Returns the HTML to be inserted to the text area. + * + * @method _getMediaHTML + * @param {Y.Node} form The form from which to extract data + * @return {String} The compiled markup + * @private + */ + _getMediaHTML: function(form) { + var mediumType = this._getMediumTypeFromTabPane(form.one('.root.tab-content > .tab-pane.active')); + var tabContent = form.one(SELECTORS[mediumType.toUpperCase() + '_PANE']); + + return this['_getMediaHTML' + mediumType[0].toUpperCase() + mediumType.substr(1)](tabContent); + }, + + /** + * Returns the HTML to be inserted to the text area for the link tab. + * + * @method _getMediaHTMLLink + * @param {Y.Node} tab The tab from which to extract data + * @return {String} The compiled markup + * @private + */ + _getMediaHTMLLink: function(tab) { + var context = { + url: tab.one(SELECTORS.URL_INPUT).get('value'), + name: tab.one(SELECTORS.NAME_INPUT).get('value') || false + }; + + return context.url ? Y.Handlebars.compile(TEMPLATES.HTML_MEDIA.LINK)(context) : ''; + }, + + /** + * Returns the HTML to be inserted to the text area for the video tab. + * + * @method _getMediaHTMLVideo + * @param {Y.Node} tab The tab from which to extract data + * @return {String} The compiled markup + * @private + */ + _getMediaHTMLVideo: function(tab) { + var context = this._getContextForMediaHTML(tab); + context.width = tab.one(SELECTORS.WIDTH_INPUT).get('value') || false; + context.height = tab.one(SELECTORS.HEIGHT_INPUT).get('value') || false; + context.poster = tab.one(SELECTORS.POSTER_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value') || false; + + return context.sources.length ? Y.Handlebars.compile(TEMPLATES.HTML_MEDIA.VIDEO)(context) : ''; + }, + + /** + * Returns the HTML to be inserted to the text area for the audio tab. + * + * @method _getMediaHTMLAudio + * @param {Y.Node} tab The tab from which to extract data + * @return {String} The compiled markup + * @private + */ + _getMediaHTMLAudio: function(tab) { + var context = this._getContextForMediaHTML(tab); + + return context.sources.length ? Y.Handlebars.compile(TEMPLATES.HTML_MEDIA.AUDIO)(context) : ''; + }, + + /** + * Returns the context with which to render a media template. + * + * @method _getContextForMediaHTML + * @param {Y.Node} tab The tab from which to extract data + * @return {Object} + * @private + */ + _getContextForMediaHTML: function(tab) { + var tracks = []; + + tab.all(SELECTORS.TRACK).each(function(track) { + tracks.push({ + track: track.one(SELECTORS.TRACK_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value'), + kind: this._getTrackTypeFromTabPane(track.ancestor('.tab-pane')), + label: track.one(SELECTORS.TRACK_LABEL_INPUT).get('value') || + track.one(SELECTORS.TRACK_LANG_INPUT).get('value'), + srclang: track.one(SELECTORS.TRACK_LANG_INPUT).get('value'), + defaultTrack: track.one(SELECTORS.TRACK_DEFAULT_SELECT).get('checked') ? "true" : null + }); + }, this); + + return { + sources: tab.all(SELECTORS.MEDIA_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value').filter(function(source) { + return !!source; + }).map(function(source) { + return {source: source}; + }), + description: tab.one(SELECTORS.MEDIA_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value') || false, + tracks: tracks.filter(function(track) { + return !!track.track; + }), + showControls: tab.one(SELECTORS.MEDIA_CONTROLS_TOGGLE).get('checked'), + autoplay: tab.one(SELECTORS.MEDIA_AUTOPLAY_TOGGLE).get('checked'), + muted: tab.one(SELECTORS.MEDIA_MUTE_TOGGLE).get('checked'), + loop: tab.one(SELECTORS.MEDIA_LOOP_TOGGLE).get('checked') + }; + } +}, { + ATTRS: { + langs: {}, + help: {} } }); diff --git a/lib/editor/atto/tests/fixtures/moodle-logo.mp4 b/lib/editor/atto/tests/fixtures/moodle-logo.mp4 new file mode 100755 index 0000000000000000000000000000000000000000..db475919fc8d966f8e4ee5f40daaebb872db534f GIT binary patch literal 6930 zcmZ`-2|SeB`#Bw7Ew^q`DTWzKS;h>a#5KljdrZi>LV`k$I6!R%1p-!?!@2%GZ@OGhs_k8o#&vqL#j z8RS4%NO&Lw)R?fJM*r!y0sAkW7MsVB%1E1(Pmbn=qJLpzB|pkP6ykIIDE_a6NM&-R zNrR)b{nO@Olm~Fz?hs15wEbU+zv%yd(j*c%v4mEg5hsznq#I}j!LbAH@mVs27vWoB zaY)VJ0Zv(UjynQmf|~`rMyNyKviMv$N&{^VJB=UpGt?0NTvq`ArW8}*4x}lNHmN_- zt_5iUQUYwhXi)wqWe|z(Lb6U z#e(C;FwW0W%G3L2I$;U$SX{0&;{R$UA%E43VQEx^)Sxz-^;22uFz{r>1X|gcTH2a2 z=u(eJfjkZ@h?%8fPzqKJOIAiA+5Fmd;P(3t_dkvrx1$~Up9f>jtu0X#G$4YQ z!x*j@$l^!&a{1drxKQ+O1CdNTC(7Q$9E}R*K@GmWIT{)fv5gfBC%(UE43r-l z5&(VqMWCbMyq{|XgoLr8q<(~iM{#(eEGPu&{-M!4R-A7@L>QO#(<2xnc>aWhLlaPt z$CAnf@>pRUK1AC&Awj`WT!^-D;vn1J;urFT?=`f|58#Az0-~erE$PyAcr0m5cpQE( zRN)2q{>xvfA}_!`fES2{`NP0S{elx10aH2}DfPylZfXr7S8Dg?$cnYMUI+>NC=S=& z5)I+PD8r1xB*3*;kuZVMbN~zhwH=)F6oQ=|Mp*i1jSy(oN~VF5|GeU?TGe|UovMFH{*ZSfQC$ffL|nJ*ks_(X8Qlb@r*Zjgf_-Ier`Qyo79pT=ToeKl?m*`6>kX5DL$JkTw<-6S zl`B_Tly&QWo!c#`nZ(tM8fe!+1U7`s}F< z9$SwkAxCNCI;SsnO(zK*}BhN6KBRX3+31#uw~&!jk=Wg@&ai2!NUhQ(ub}`<(Mt+YU+smAgmsy%*mGHa9c#ZfSFUBbcp~jW3P=M- zLa!K3Cmf#jCA0Buz5`IBKl#mO<=QFb?Ka@S6|bxoKqAc0B3;aVHac8jAk+Q5Iw31<1w6`iF-CToMEZG`*vvt4UnkL5e^3~qR7y90v!o3@L9U2<@ZY|RP zA*6cPS1s;+mL&#*KMD>%YSDkOhI(+40C0FF=&M2jMFYq#0!sB$e5XubCxD>9gAI!V zG~#U`Ke0ihBgF+LsznrD1x`Mir%`AtR7H`t@!u)e*i zK`{{rFFp~Vs9EVj*Ngy3MrD(doav>;m!I`u7V*6=-Xtvkou`Z2qdn?a3iE!pQSr3Y>cljMWwfDLnC`yvt zqPn=JUh$NSwIot!Zru%(^wyRiCS`0-Ie3JLpOYcDxuwow-Js`ftrPbN4y-+2yBx157is04c(aoIctcH*u5+)4(W+emn?~*}Hn`DXt!w80EQ80kz0UR(xk1h3=?<`7G1;kSC{Q7>VI)-XTx%E{{rgz=Q`g@pBEnj>w3k% zit&QV*I}=El#&@V7NSDAV(ro-iML>apr*hb24VOu{_FI6vNE^Sw;0j?%k^wfyo58O%t zGMH$Xi-8g&7G}2@z4XQD=2xypSM z_qEX_?Rna5BBUd;5}zQp8s;brfej`%R_?d&qxx>tQ_=LWxRoVg8ojdFIkv zyL-`f#~EdKjG%R|IW}lgzE>loz~jfO*f&4sJ?g6t2e=id=U7b~dQOVGGMus|y~?mN zY4w>FKedlV0+_K45W81#+koz`D$)<#Eabln;1V!hF=$+KF*mk7}9 z{-c+Y>C5&#LL8iOWWHm_z{B(OcJ{_j2c-DbI}%Y|@lp$u_WHK>DZzl_!L;%TP#c*x z%G`sG5zJC{I(**FY|0oF8hWnPuUP*zCSz+O<+fSb2Vx0bsq5P1snk=-+nDfO2?2;V z6JDyTL5qT|i-VY8NuEYfcW$?4X&r_yy>e}W3IP+<<|c6xVMHR z1?9G7*x$92pLZ^9eN!MZIh#hDzPFQ6E92Rnd-+U)a<;$^p!0zd22f*=?Wbp4Rp%%3 z+zuu9hf|Obk>3d>ti8kK2zwiebjAmju@YU?KQ?ZgI4-zc;43M5GmS{Bwd}g&--+>O zD7Z6xH&u@D;upzZ=YZoIL~E8(ua=d+8ol~biGVG&oV~a4{`XZ~@R~%JM0GKYoIH|h zZAxfqyxu?>sIQUC6r0u-TDHoke{=T!o!Inb**l#-gttVkjX&@@OuhJ=1JA&~j7Y6~ z3%j6qx*JDM;62rjw|f<5{`PQ?I1S|XP>UT^PChHp70XfCX4WmGY6grNnW5(EGnCvU zhojZ*7`a4D*Otscy^0!G;H=KjyVf-@JS%(q+UU zzmsY*L5U4X#%C2sUm&DcQ7Wah`;tS&T+qsWjiPW>om(8|I*R$-0Jbup@Q*XSQry>-&YgZw z$2%0X%T69MA3t-*(yiHhT;e*tk5GrEePBBGe9LD%_I=3G(BBLY0q$v9mS0Z?r^=3dLfvixKvRis`*iMzig(>qo3*?BYA98coPu>xb38!oq-`6l)&!hzf0!2`g`aMPypR;6OB}(D5 zb}cjd7>jFI{2b|RRO;E?6hj@G9R2YJ1_XVwk2^Rg&YzY@FUoX4RYl<`E{{=InOaW^ zXRAe7i(0p$l+uUWz8xp@3=YV9S6q?Boy+x57RogYV&`^g&U!chmFd%==of8Et}qG? z2}>P4LIpG9Yu(EiEUtc;x|y84RP2O;3b8@)dciMudZL-`oVVR#P`#u=c|StRH6VKg1>&2_HX>YEC8AxAUaGtT7D^n{2Z~)a37W zeK}TL{GM3L0K2MYI9Hz@W=!cmjLB~k!k=42$V*+G$<@j-CJZ^i9bL@?@dUouYyD9_ z2Yk)6K%6&E6qHgYyZVo-1GNS>)Jm*Qka0KIytU_*$$y@E_vaa2i#8w&QD6-m$2Y}& zIO%<_6*%j^irhCDw;daCnx~L|zxRVALG%^5JaA}^qq4%w!Q0mjM@#oaP5}iNNLYyg zi$zP|`63IC8o?QpLmTQIX|Z}gyoH88*?O}$y}NKL{4xt+tZxOi>7qAP5_MW{`Ed*{}tbDXC& z_50g->=N4XuszF{b9b2>QrIDrX^@^bK4mKlmH>s_4sGQth!kzNTbP`y+NR*s^U>eR zHyfMf2a@~N&Db8Ys!Jk&;KD^5;L#RnV=cxQyf! zv7HvAhV7o7#y~6)f28T$j3!U?pqjIy0Y~$!huZO)ZSBV@b1cwB%3T=_;_@Ns$36mW zKbqHCD?M{Txhe_B?S?@>HOaUOgfowI29Yr@;0J(QLygl8w>P7tU@uIsn#&9>RIpq4>; zPI63`(<=CsOqOxeo0XcgQ$gdJSC86(qJ6I6MH882dTBboKV&atH`Y{`Np~!yS!Tji z5dgQ4-RaE*bod6$_uXTU$B<0G1(gYUHzoa6@Sky%3R%kcD&m1I9V|m#g(K-XtqEeb zmb6qN17S%6SxT@i&;)o)@mL-G?vBty3s-cWtEO>*F*F)gJ)5%Eh3ZdIxqG;-WNdnt zA_bTsBv$Ok6KhN#4^G~ZdE=OR&8MxD;815g4&JGk5RX46neZGYt8LyE6{=Yyh_puyTmhfy^w2N($}tnAS1&t&dgliN~h7AV-BF zXtt{em1s(c-+U!4!I(n3?i=N~S0_6uH3pQA_v1@Hox+m=#Xr#a1tZ~}iNQGr3&2EE zF*kM1y5Xwfl71Co=u#Oi*A7Hi2=IyVuh@3@6U)C0+K9P?FYZZW&oT(f5MY?_R^HqQ?pio z?}U#jdfGI`Ty-ub`LV9Vv8w2F14fPo0CQcF-_-HoZdmF}MNFN70~&sQ zWL1*24tOwxy-Y&j;@QvR70}G*XtWLP-Fjy zw=-O&Rj_9HplF`(xA^Rjr>kH5b}tRwah^`z>|RY~I<0(K@v;PZiWw0UrPl-i-OA}2 zWhAfw01@8o?6^%_Iru09LR`S$QGG}uT2;+T>pxjcoj=od_L)@kvMq@05IX0YfFm7GBziY@^WA23u@CMj~-ky`}byvsZ`yzpV|^iHeqKQ2?ID?bcx z84o@~ZkYSgFDUNh$yUdwL%q5(bx)cGt*7Un3LhWZ{PkOEF*>z*Q-hrf1 zmYY{m>tCKfNgN&=YS@;oTBQEr?CHpY4XOGE9qc|Yi6nQmtCua|8O|d+=iN74P?y-3 z9Xa#;$`!+}UT&w2X)62H9mX`A^dgQbS2(6rUFaeQfu)8QgxeEjyxzUCjEEI_k@z4HL&)KZn}rE}WkdHdY4KRDv%v>F^4;81x; 00:00:00.620 +Hey! + +2 +00:00:00.620 --> 00:00:00.710 +Heey! + +3 +00:00:00.710 --> 00:00:00.800 +Heeey! + +4 +00:00:00.800 --> 00:00:00.890 +Heeeey! + +5 +00:00:00.890 --> 00:00:00.980 +Heeeeey! + +6 +00:00:00.980 --> 00:00:01.070 +Heeeeeey! + +7 +00:00:01.070 --> 00:00:01.160 +Heeeeeeey! + +8 +00:00:01.160 --> 00:00:01.250 +Heeeeeeeey! + +9 +00:00:01.250 --> 00:00:01.340 +Heeeeeeeey! + +1 +00:00:01.340 --> 00:00:01.430 +Heeeeeeeeey! + +1 +00:00:01.430 --> 00:00:01.510 +That's + +1 +00:00:01.510 --> 00:00:01.770 +Pretty + +1 +00:00:01.770 --> 00:00:03.970 +Good! + diff --git a/lib/editor/atto/tests/fixtures/pretty-good-sv.vtt b/lib/editor/atto/tests/fixtures/pretty-good-sv.vtt new file mode 100755 index 00000000000..32c8f367f1c --- /dev/null +++ b/lib/editor/atto/tests/fixtures/pretty-good-sv.vtt @@ -0,0 +1,54 @@ +WEBVTT + +1 +00:00:00.530 --> 00:00:00.620 +Hej! + +2 +00:00:00.620 --> 00:00:00.710 +Heej! + +3 +00:00:00.710 --> 00:00:00.800 +Heeej! + +4 +00:00:00.800 --> 00:00:00.890 +Heeeej! + +5 +00:00:00.890 --> 00:00:00.980 +Heeeeej! + +6 +00:00:00.980 --> 00:00:01.070 +Heeeeeej! + +7 +00:00:01.070 --> 00:00:01.160 +Heeeeeeej! + +8 +00:00:01.160 --> 00:00:01.250 +Heeeeeeeej! + +9 +00:00:01.250 --> 00:00:01.340 +Heeeeeeeej! + +1 +00:00:01.340 --> 00:00:01.430 +Heeeeeeeeej! + +1 +00:00:01.430 --> 00:00:01.510 +Det är + +1 +00:00:01.510 --> 00:00:01.770 +Ganska + +1 +00:00:01.770 --> 00:00:03.970 +Bra! + diff --git a/lib/form/editor.php b/lib/form/editor.php index 2409966981d..551e30645b5 100644 --- a/lib/form/editor.php +++ b/lib/form/editor.php @@ -381,9 +381,19 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element implements templatab $link_options->env = 'editor'; $link_options->itemid = $draftitemid; + $args->accepted_types = array('.vtt'); + $subtitle_options = initialise_filepicker($args); + $subtitle_options->context = $ctx; + $subtitle_options->client_id = uniqid(); + $subtitle_options->maxbytes = $this->_options['maxbytes']; + $subtitle_options->areamaxbytes = $this->_options['areamaxbytes']; + $subtitle_options->env = 'editor'; + $subtitle_options->itemid = $draftitemid; + $fpoptions['image'] = $image_options; $fpoptions['media'] = $media_options; $fpoptions['link'] = $link_options; + $fpoptions['subtitle'] = $subtitle_options; } //If editor is required and tinymce, then set required_tinymce option to initalize tinymce validation.