Merge branch 'MDL-62449-master' of git://github.com/sarjona/moodle
[moodle.git] / lib / editor / atto / plugins / media / yui / src / button / js / button.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /*
17  * @package    atto_media
18  * @copyright  2013 Damyon Wiese  <damyon@moodle.com>
19  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
20  */
22 /**
23  * @module moodle-atto_media-button
24  */
26 /**
27  * Atto media selection tool.
28  *
29  * @namespace M.atto_media
30  * @class Button
31  * @extends M.editor_atto.EditorPlugin
32  */
34 var COMPONENTNAME = 'atto_media',
35     MEDIA_TYPES = {LINK: 'LINK', VIDEO: 'VIDEO', AUDIO: 'AUDIO'},
36     TRACK_KINDS = {
37         SUBTITLES: 'SUBTITLES',
38         CAPTIONS: 'CAPTIONS',
39         DESCRIPTIONS: 'DESCRIPTIONS',
40         CHAPTERS: 'CHAPTERS',
41         METADATA: 'METADATA'
42     },
43     CSS = {
44         SOURCE: 'atto_media_source',
45         TRACK: 'atto_media_track',
46         MEDIA_SOURCE: 'atto_media_media_source',
47         LINK_SOURCE: 'atto_media_link_source',
48         POSTER_SOURCE: 'atto_media_poster_source',
49         TRACK_SOURCE: 'atto_media_track_source',
50         DISPLAY_OPTIONS: 'atto_media_display_options',
51         NAME_INPUT: 'atto_media_name_entry',
52         URL_INPUT: 'atto_media_url_entry',
53         POSTER_SIZE: 'atto_media_poster_size',
54         LINK_SIZE: 'atto_media_link_size',
55         WIDTH_INPUT: 'atto_media_width_entry',
56         HEIGHT_INPUT: 'atto_media_height_entry',
57         TRACK_KIND_INPUT: 'atto_media_track_kind_entry',
58         TRACK_LABEL_INPUT: 'atto_media_track_label_entry',
59         TRACK_LANG_INPUT: 'atto_media_track_lang_entry',
60         TRACK_DEFAULT_SELECT: 'atto_media_track_default',
61         MEDIA_CONTROLS_TOGGLE: 'atto_media_controls',
62         MEDIA_AUTOPLAY_TOGGLE: 'atto_media_autoplay',
63         MEDIA_MUTE_TOGGLE: 'atto_media_mute',
64         MEDIA_LOOP_TOGGLE: 'atto_media_loop',
65         ADVANCED_SETTINGS: 'atto_media_advancedsettings',
66         LINK: MEDIA_TYPES.LINK.toLowerCase(),
67         VIDEO: MEDIA_TYPES.VIDEO.toLowerCase(),
68         AUDIO: MEDIA_TYPES.AUDIO.toLowerCase(),
69         TRACK_SUBTITLES: TRACK_KINDS.SUBTITLES.toLowerCase(),
70         TRACK_CAPTIONS: TRACK_KINDS.CAPTIONS.toLowerCase(),
71         TRACK_DESCRIPTIONS: TRACK_KINDS.DESCRIPTIONS.toLowerCase(),
72         TRACK_CHAPTERS: TRACK_KINDS.CHAPTERS.toLowerCase(),
73         TRACK_METADATA: TRACK_KINDS.METADATA.toLowerCase()
74     },
75     SELECTORS = {
76         SOURCE: '.' + CSS.SOURCE,
77         TRACK: '.' + CSS.TRACK,
78         MEDIA_SOURCE: '.' + CSS.MEDIA_SOURCE,
79         POSTER_SOURCE: '.' + CSS.POSTER_SOURCE,
80         TRACK_SOURCE: '.' + CSS.TRACK_SOURCE,
81         DISPLAY_OPTIONS: '.' + CSS.DISPLAY_OPTIONS,
82         NAME_INPUT: '.' + CSS.NAME_INPUT,
83         URL_INPUT: '.' + CSS.URL_INPUT,
84         POSTER_SIZE: '.' + CSS.POSTER_SIZE,
85         LINK_SIZE: '.' + CSS.LINK_SIZE,
86         WIDTH_INPUT: '.' + CSS.WIDTH_INPUT,
87         HEIGHT_INPUT: '.' + CSS.HEIGHT_INPUT,
88         TRACK_KIND_INPUT: '.' + CSS.TRACK_KIND_INPUT,
89         TRACK_LABEL_INPUT: '.' + CSS.TRACK_LABEL_INPUT,
90         TRACK_LANG_INPUT: '.' + CSS.TRACK_LANG_INPUT,
91         TRACK_DEFAULT_SELECT: '.' + CSS.TRACK_DEFAULT_SELECT,
92         MEDIA_CONTROLS_TOGGLE: '.' + CSS.MEDIA_CONTROLS_TOGGLE,
93         MEDIA_AUTOPLAY_TOGGLE: '.' + CSS.MEDIA_AUTOPLAY_TOGGLE,
94         MEDIA_MUTE_TOGGLE: '.' + CSS.MEDIA_MUTE_TOGGLE,
95         MEDIA_LOOP_TOGGLE: '.' + CSS.MEDIA_LOOP_TOGGLE,
96         ADVANCED_SETTINGS: '.' + CSS.ADVANCED_SETTINGS,
97         LINK_TAB: 'li[data-medium-type="' + CSS.LINK + '"]',
98         LINK_PANE: '.tab-pane[data-medium-type="' + CSS.LINK + '"]',
99         VIDEO_TAB: 'li[data-medium-type="' + CSS.VIDEO + '"]',
100         VIDEO_PANE: '.tab-pane[data-medium-type="' + CSS.VIDEO + '"]',
101         AUDIO_TAB: 'li[data-medium-type="' + CSS.AUDIO + '"]',
102         AUDIO_PANE: '.tab-pane[data-medium-type="' + CSS.AUDIO + '"]',
103         TRACK_SUBTITLES_TAB: 'li[data-track-kind="' + CSS.TRACK_SUBTITLES + '"]',
104         TRACK_SUBTITLES_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_SUBTITLES + '"]',
105         TRACK_CAPTIONS_TAB: 'li[data-track-kind="' + CSS.TRACK_CAPTIONS + '"]',
106         TRACK_CAPTIONS_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_CAPTIONS + '"]',
107         TRACK_DESCRIPTIONS_TAB: 'li[data-track-kind="' + CSS.TRACK_DESCRIPTIONS + '"]',
108         TRACK_DESCRIPTIONS_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_DESCRIPTIONS + '"]',
109         TRACK_CHAPTERS_TAB: 'li[data-track-kind="' + CSS.TRACK_CHAPTERS + '"]',
110         TRACK_CHAPTERS_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_CHAPTERS + '"]',
111         TRACK_METADATA_TAB: 'li[data-track-kind="' + CSS.TRACK_METADATA + '"]',
112         TRACK_METADATA_PANE: '.tab-pane[data-track-kind="' + CSS.TRACK_METADATA + '"]'
113     },
114     TEMPLATES = {
115         ROOT: '' +
116             '<form class="mform atto_form atto_media" id="{{elementid}}_atto_media_form">' +
117                 '<ul class="root nav nav-tabs m-b-1" role="tablist">' +
118                     '<li data-medium-type="{{CSS.LINK}}" class="nav-item">' +
119                         '<a class="nav-link active" href="#{{elementid}}_{{CSS.LINK}}" role="tab" data-toggle="tab">' +
120                             '{{get_string "link" component}}' +
121                         '</a>' +
122                     '</li>' +
123                     '<li data-medium-type="{{CSS.VIDEO}}" class="nav-item">' +
124                         '<a class="nav-link" href="#{{elementid}}_{{CSS.VIDEO}}" role="tab" data-toggle="tab">' +
125                             '{{get_string "video" component}}' +
126                         '</a>' +
127                     '</li>' +
128                     '<li data-medium-type="{{CSS.AUDIO}}" class="nav-item">' +
129                         '<a class="nav-link" href="#{{elementid}}_{{CSS.AUDIO}}" role="tab" data-toggle="tab">' +
130                             '{{get_string "audio" component}}' +
131                         '</a>' +
132                     '</li>' +
133                 '</ul>' +
134                 '<div class="root tab-content">' +
135                     '<div data-medium-type="{{CSS.LINK}}" class="tab-pane active" id="{{elementid}}_{{CSS.LINK}}">' +
136                         '{{> tab_panes.link}}' +
137                     '</div>' +
138                     '<div data-medium-type="{{CSS.VIDEO}}" class="tab-pane" id="{{elementid}}_{{CSS.VIDEO}}">' +
139                         '{{> tab_panes.video}}' +
140                     '</div>' +
141                     '<div data-medium-type="{{CSS.AUDIO}}" class="tab-pane" id="{{elementid}}_{{CSS.AUDIO}}">' +
142                         '{{> tab_panes.audio}}' +
143                     '</div>' +
144                 '</div>' +
145                 '<div class="mdl-align">' +
146                     '<br/>' +
147                     '<button class="btn btn-default submit" type="submit">{{get_string "createmedia" component}}</button>' +
148                 '</div>' +
149             '</form>',
150         TAB_PANES: {
151             LINK: '' +
152                 '{{renderPartial "form_components.source" context=this id=CSS.LINK_SOURCE}}' +
153                 '<label for="{{elementid}}_link_nameentry">{{get_string "entername" component}}</label>' +
154                 '<input class="form-control fullwidth {{CSS.NAME_INPUT}}" type="text" id="{{elementid}}_link_nameentry"' +
155                         'size="32" required="true"/>',
156             VIDEO: '' +
157                 '{{renderPartial "form_components.source" context=this id=CSS.MEDIA_SOURCE entersourcelabel="videosourcelabel"' +
158                     ' addcomponentlabel="addsource" multisource="true" addsourcehelp=helpStrings.addsource}}' +
159                 '<fieldset class="collapsible collapsed" id="{{elementid}}_video-display-options">' +
160                     '<input name="mform_isexpanded_{{elementid}}_video-display-options" type="hidden">' +
161                     '<legend class="ftoggler">{{get_string "displayoptions" component}}</legend>' +
162                     '<div class="fcontainer">' +
163                         '{{> form_components.display_options}}' +
164                     '</div>' +
165                 '</fieldset>' +
166                 '<fieldset class="collapsible collapsed" id="{{elementid}}_video-advanced-settings">' +
167                     '<input name="mform_isexpanded_{{elementid}}_video-advanced-settings" type="hidden">' +
168                     '<legend class="ftoggler">{{get_string "advancedsettings" component}}</legend>' +
169                     '<div class="fcontainer">' +
170                         '{{> form_components.advanced_settings}}' +
171                     '</div>' +
172                 '</fieldset>' +
173                 '<fieldset class="collapsible collapsed" id="{{elementid}}_video-tracks">' +
174                     '<input name="mform_isexpanded_{{elementid}}_video-tracks" type="hidden">' +
175                     '<legend class="ftoggler">{{get_string "tracks" component}} {{{helpStrings.tracks}}}</legend>' +
176                     '<div class="fcontainer">' +
177                         '{{renderPartial "form_components.track_tabs" context=this id=CSS.VIDEO}}' +
178                     '</div>' +
179                 '</fieldset>',
180             AUDIO: '' +
181                 '{{renderPartial "form_components.source" context=this id=CSS.MEDIA_SOURCE entersourcelabel="audiosourcelabel"' +
182                     ' addcomponentlabel="addsource" multisource="true" addsourcehelp=helpStrings.addsource}}' +
183                 '<fieldset class="collapsible collapsed" id="{{elementid}}_audio-advanced-settings">' +
184                     '<input name="mform_isexpanded_{{elementid}}_audio-advanced-settings" type="hidden">' +
185                     '<legend class="ftoggler">{{get_string "advancedsettings" component}}</legend>' +
186                     '<div class="fcontainer">' +
187                         '{{> form_components.advanced_settings}}' +
188                     '</div>' +
189                 '</fieldset>' +
190                 '<fieldset class="collapsible collapsed" id="{{elementid}}_audio-tracks">' +
191                     '<input name="mform_isexpanded_{{elementid}}_audio-tracks" type="hidden">' +
192                     '<legend class="ftoggler">{{get_string "tracks" component}} {{{helpStrings.tracks}}}</legend>' +
193                     '<div class="fcontainer">' +
194                         '{{renderPartial "form_components.track_tabs" context=this id=CSS.AUDIO}}' +
195                     '</div>' +
196                 '</fieldset>'
197         },
198         FORM_COMPONENTS: {
199             SOURCE: '' +
200                 '<div class="{{CSS.SOURCE}} {{id}}">' +
201                     '<div class="m-b-1">' +
202                         '<label for="url-input">' +
203                         '{{#entersourcelabel}}{{get_string ../entersourcelabel ../component}}{{/entersourcelabel}}' +
204                         '{{^entersourcelabel}}{{get_string "entersource" ../component}}{{/entersourcelabel}}' +
205                         '</label>' +
206                         '<div class="input-group input-append w-100">' +
207                             '<input id="url-input" class="form-control {{CSS.URL_INPUT}}" type="url" size="32"/>' +
208                             '<span class="input-group-append">' +
209                                 '<button class="btn btn-default openmediabrowser" type="button">' +
210                                 '{{get_string "browserepositories" component}}</button>' +
211                             '</span>' +
212                         '</div>' +
213                     '</div>' +
214                     '{{#multisource}}' +
215                         '{{renderPartial "form_components.add_component" context=../this label=../addcomponentlabel ' +
216                             ' help=../addsourcehelp}}' +
217                     '{{/multisource}}' +
218                 '</div>',
219             ADD_COMPONENT: '' +
220                 '<div>' +
221                     '<a href="#" class="addcomponent">' +
222                         '{{#label}}{{get_string ../label ../component}}{{/label}}' +
223                         '{{^label}}{{get_string "add" ../component}}{{/label}}' +
224                     '</a>' +
225                     '{{#help}}{{{../help}}}{{/help}}' +
226                 '</div>',
227             REMOVE_COMPONENT: '' +
228                 '<div>' +
229                     '<a href="#" class="removecomponent">' +
230                         '{{#label}}{{get_string ../label ../component}}{{/label}}' +
231                         '{{^label}}{{get_string "remove" ../component}}{{/label}}' +
232                     '</a>' +
233                 '</div>',
234             DISPLAY_OPTIONS: '' +
235                 '<div class="{{CSS.DISPLAY_OPTIONS}}">' +
236                     '<div class="m-b-1">' +
237                         '<label>{{get_string "size" component}}</label>' +
238                         '<div class="form-inline {{CSS.POSTER_SIZE}}">' +
239                             '<label class="accesshide">{{get_string "videowidth" component}}</label>' +
240                             '<input type="text" class="form-control m-r-1 {{CSS.WIDTH_INPUT}} input-mini" size="4"/>' +
241                             ' x ' +
242                             '<label class="accesshide">{{get_string "videoheight" component}}</label>' +
243                             '<input type="text" class="form-control m-l-1 {{CSS.HEIGHT_INPUT}} input-mini" size="4"/>' +
244                         '</div>' +
245                     '</div>' +
246                     '<div class="clearfix"></div>' +
247                     '{{renderPartial "form_components.source" context=this id=CSS.POSTER_SOURCE entersourcelabel="poster"}}' +
248                 '<div>',
249             ADVANCED_SETTINGS: '' +
250                 '<div class="{{CSS.ADVANCED_SETTINGS}}">' +
251                     '<div class="form-check">' +
252                         '<input type="checkbox" checked="true" class="form-check-input {{CSS.MEDIA_CONTROLS_TOGGLE}}"' +
253                         'id="media-controls-toggle"/>' +
254                         '<label class="form-check-label" for="media-controls-toggle">{{get_string "controls" component}}</label>' +
255                     '</div>' +
256                     '<div class="form-check">' +
257                         '<input type="checkbox" class="form-check-input {{CSS.MEDIA_AUTOPLAY_TOGGLE}}"' +
258                         'id="media-autoplay-toggle"/>' +
259                         '<label class="form-check-label" for="media-autoplay-toggle">{{get_string "autoplay" component}}</label>' +
260                     '</div>' +
261                     '<div class="form-check">' +
262                         '<input type="checkbox" class="form-check-input {{CSS.MEDIA_MUTE_TOGGLE}}" id="media-mute-toggle"/>' +
263                         '<label class="form-check-label" for="media-mute-toggle">{{get_string "mute" component}}</label>' +
264                     '</div>' +
265                     '<div class="form-check">' +
266                         '<input type="checkbox" class="form-check-input {{CSS.MEDIA_LOOP_TOGGLE}}" id="media-loop-toggle"/>' +
267                         '<label class="form-check-label" for="media-loop-toggle">{{get_string "loop" component}}</label>' +
268                     '</div>' +
269                 '</div>',
270             TRACK_TABS: '' +
271                 '<ul class="nav nav-tabs mb-3">' +
272                     '<li data-track-kind="{{CSS.TRACK_SUBTITLES}}" class="nav-item">' +
273                         '<a class="nav-link active" href="#{{elementid}}_{{id}}_{{CSS.TRACK_SUBTITLES}}"' +
274                             ' role="tab" data-toggle="tab">' +
275                             '{{get_string "subtitles" component}}' +
276                         '</a>' +
277                     '</li>' +
278                     '<li data-track-kind="{{CSS.TRACK_CAPTIONS}}" class="nav-item">' +
279                         '<a class="nav-link" href="#{{elementid}}_{{id}}_{{CSS.TRACK_CAPTIONS}}" role="tab" data-toggle="tab">' +
280                             '{{get_string "captions" component}}' +
281                         '</a>' +
282                     '</li>' +
283                     '<li data-track-kind="{{CSS.TRACK_DESCRIPTIONS}}"  class="nav-item">' +
284                         '<a class="nav-link" href="#{{elementid}}_{{id}}_{{CSS.TRACK_DESCRIPTIONS}}"' +
285                             ' role="tab" data-toggle="tab">' +
286                             '{{get_string "descriptions" component}}' +
287                         '</a>' +
288                     '</li>' +
289                     '<li data-track-kind="{{CSS.TRACK_CHAPTERS}}" class="nav-item">' +
290                         '<a class="nav-link" href="#{{elementid}}_{{id}}_{{CSS.TRACK_CHAPTERS}}" role="tab" data-toggle="tab">' +
291                             '{{get_string "chapters" component}}' +
292                         '</a>' +
293                     '</li>' +
294                     '<li data-track-kind="{{CSS.TRACK_METADATA}}" class="nav-item">' +
295                         '<a class="nav-link" href="#{{elementid}}_{{id}}_{{CSS.TRACK_METADATA}}" role="tab" data-toggle="tab">' +
296                             '{{get_string "metadata" component}}' +
297                         '</a>' +
298                     '</li>' +
299                 '</ul>' +
300                 '<div class="tab-content">' +
301                     '<div data-track-kind="{{CSS.TRACK_SUBTITLES}}" class="tab-pane active"' +
302                         ' id="{{elementid}}_{{id}}_{{CSS.TRACK_SUBTITLES}}">' +
303                         '<div class="trackhelp">{{{helpStrings.subtitles}}}</div>' +
304                         '{{renderPartial "form_components.track" context=this sourcelabel="subtitlessourcelabel"' +
305                             ' addcomponentlabel="addsubtitlestrack"}}' +
306                     '</div>' +
307                     '<div data-track-kind="{{CSS.TRACK_CAPTIONS}}" class="tab-pane"' +
308                         ' id="{{elementid}}_{{id}}_{{CSS.TRACK_CAPTIONS}}">' +
309                         '<div class="trackhelp">{{{helpStrings.captions}}}</div>' +
310                         '{{renderPartial "form_components.track" context=this sourcelabel="captionssourcelabel"' +
311                             ' addcomponentlabel="addcaptionstrack"}}' +
312                     '</div>' +
313                     '<div data-track-kind="{{CSS.TRACK_DESCRIPTIONS}}" class="tab-pane"' +
314                         ' id="{{elementid}}_{{id}}_{{CSS.TRACK_DESCRIPTIONS}}">' +
315                         '<div class="trackhelp">{{{helpStrings.descriptions}}}</div>' +
316                         '{{renderPartial "form_components.track" context=this sourcelabel="descriptionssourcelabel"' +
317                             ' addcomponentlabel="adddescriptionstrack"}}' +
318                     '</div>' +
319                     '<div data-track-kind="{{CSS.TRACK_CHAPTERS}}" class="tab-pane"' +
320                         ' id="{{elementid}}_{{id}}_{{CSS.TRACK_CHAPTERS}}">' +
321                         '<div class="trackhelp">{{{helpStrings.chapters}}}</div>' +
322                         '{{renderPartial "form_components.track" context=this sourcelabel="chapterssourcelabel"' +
323                             ' addcomponentlabel="addchapterstrack"}}' +
324                     '</div>' +
325                     '<div data-track-kind="{{CSS.TRACK_METADATA}}" class="tab-pane"' +
326                         ' id="{{elementid}}_{{id}}_{{CSS.TRACK_METADATA}}">' +
327                         '<div class="trackhelp">{{{helpStrings.metadata}}}</div>' +
328                         '{{renderPartial "form_components.track" context=this sourcelabel="metadatasourcelabel"' +
329                             ' addcomponentlabel="addmetadatatrack"}}' +
330                     '</div>' +
331                 '</div>',
332             TRACK: '' +
333                 '<div class="m-b-1 {{CSS.TRACK}}">' +
334                     '{{renderPartial "form_components.source" context=this id=CSS.TRACK_SOURCE entersourcelabel=sourcelabel}}' +
335                     '<div class="form-group">' +
336                         '<label class="w-100" for="lang-input">{{get_string "srclang" component}}</label>' +
337                         '<select id="lang-input" class="custom-select {{CSS.TRACK_LANG_INPUT}}">' +
338                             '<optgroup label="{{get_string "languagesinstalled" component}}">' +
339                                 '{{#langsinstalled}}' +
340                                     '<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>' +
341                                 '{{/langsinstalled}}' +
342                             '</optgroup>' +
343                             '<optgroup label="{{get_string "languagesavailable" component}} ">' +
344                                 '{{#langsavailable}}<option value="{{code}}">{{lang}}</option>{{/langsavailable}}' +
345                             '</optgroup>' +
346                         '</select>' +
347                     '</div>' +
348                     '<div class="form-group">' +
349                         '<label class="w-100" for="track-input">{{get_string "label" component}}</label>' +
350                         '<input id="track-input" class="form-control {{CSS.TRACK_LABEL_INPUT}}" type="text"/>' +
351                     '</div>' +
352                     '<div class="form-check">' +
353                         '<input type="checkbox" class="form-check-input {{CSS.TRACK_DEFAULT_SELECT}}"/>' +
354                         '<label class="form-check-label">{{get_string "default" component}}</label>' +
355                     '</div>' +
356                     '{{renderPartial "form_components.add_component" context=this label=addcomponentlabel}}' +
357                 '</div>'
358         },
359         HTML_MEDIA: {
360             VIDEO: '' +
361                 '&nbsp;<video ' +
362                     '{{#width}}width="{{../width}}" {{/width}}' +
363                     '{{#height}}height="{{../height}}" {{/height}}' +
364                     '{{#poster}}poster="{{../poster}}" {{/poster}}' +
365                     '{{#showControls}}controls="true" {{/showControls}}' +
366                     '{{#loop}}loop="true" {{/loop}}' +
367                     '{{#muted}}muted="true" {{/muted}}' +
368                     '{{#autoplay}}autoplay="true" {{/autoplay}}' +
369                 '>' +
370                     '{{#sources}}<source src="{{source}}">{{/sources}}' +
371                     '{{#tracks}}' +
372                         '<track src="{{track}}" kind="{{kind}}" srclang="{{srclang}}" label="{{label}}"' +
373                             ' {{#defaultTrack}}default="true"{{/defaultTrack}}>' +
374                     '{{/tracks}}' +
375                     '{{#description}}{{../description}}{{/description}}' +
376                 '</video>&nbsp',
377             AUDIO: '' +
378                 '&nbsp;<audio ' +
379                     '{{#showControls}}controls="true" {{/showControls}}' +
380                     '{{#loop}}loop="true" {{/loop}}' +
381                     '{{#muted}}muted="true" {{/muted}}' +
382                     '{{#autoplay}}autoplay="true" {{/autoplay}}' +
383                 '>' +
384                     '{{#sources}}<source src="{{source}}">{{/sources}}' +
385                     '{{#tracks}}' +
386                         '<track src="{{track}}" kind="{{kind}}" srclang="{{srclang}}" label="{{label}}"' +
387                             ' {{#defaultTrack}}default="true"{{/defaultTrack}}>' +
388                     '{{/tracks}}' +
389                     '{{#description}}{{../description}}{{/description}}' +
390                 '</audio>&nbsp',
391             LINK: '' +
392                 '<a href="{{url}}" ' +
393                     '{{#width}}data-width="{{../width}}" {{/width}}' +
394                     '{{#height}}data-height="{{../height}}"{{/height}}' +
395                 '>{{#name}}{{../name}}{{/name}}{{^name}}{{../url}}{{/name}}</a>'
396          }
397     };
399 Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
401     initializer: function() {
402         if (this.get('host').canShowFilepicker('media')) {
403             this.editor.delegate('dblclick', this._displayDialogue, 'video', this);
404             this.editor.delegate('click', this._handleClick, 'video', this);
406             this.addButton({
407                 icon: 'e/insert_edit_video',
408                 callback: this._displayDialogue,
409                 tags: 'video, audio',
410                 tagMatchRequiresAll: false
411             });
412         }
413     },
415     /**
416      * Gets the root context for all templates, with extra supplied context.
417      *
418      * @method _getContext
419      * @param  {Object} extra The extra context to add
420      * @return {Object}
421      * @private
422      */
423     _getContext: function(extra) {
424         return Y.merge({
425             elementid: this.get('host').get('elementid'),
426             component: COMPONENTNAME,
427             langsinstalled: this.get('langs').installed,
428             langsavailable: this.get('langs').available,
429             helpStrings: this.get('help'),
430             CSS: CSS
431         }, extra);
432     },
434     /**
435      * Handles a click on a media element.
436      *
437      * @method _handleClick
438      * @param  {EventFacade} e
439      * @private
440      */
441     _handleClick: function(e) {
442         var medium = e.target;
444         var selection = this.get('host').getSelectionFromNode(medium);
445         if (this.get('host').getSelection() !== selection) {
446             this.get('host').setSelection(selection);
447         }
448     },
450     /**
451      * Display the media editing tool.
452      *
453      * @method _displayDialogue
454      * @private
455      */
456     _displayDialogue: function() {
457         if (this.get('host').getSelection() === false) {
458             return;
459         }
461         if (!('renderPartial' in Y.Handlebars.helpers)) {
462             (function smashPartials(chain, obj) {
463                 Y.each(obj, function(value, index) {
464                     chain.push(index);
465                     if (typeof value !== "object") {
466                         Y.Handlebars.registerPartial(chain.join('.').toLowerCase(), value);
467                     } else {
468                         smashPartials(chain, value);
469                     }
470                     chain.pop();
471                 });
472             })([], TEMPLATES);
474             Y.Handlebars.registerHelper('renderPartial', function(partialName, options) {
475                 if (!partialName) {
476                     return '';
477                 }
479                 var partial = Y.Handlebars.partials[partialName];
480                 var parentContext = options.hash.context ? Y.clone(options.hash.context) : {};
481                 var context = Y.merge(parentContext, options.hash);
482                 delete context.context;
484                 if (!partial) {
485                     return '';
486                 }
487                 return new Y.Handlebars.SafeString(Y.Handlebars.compile(partial)(context));
488             });
489         }
491         var dialogue = this.getDialogue({
492             headerContent: M.util.get_string('createmedia', COMPONENTNAME),
493             focusAfterHide: true,
494             width: 660,
495             focusOnShowSelector: SELECTORS.URL_INPUT
496         });
498         // Set the dialogue content, and then show the dialogue.
499         dialogue.set('bodyContent', this._getDialogueContent(this.get('host').getSelection())).show();
500         M.form.shortforms({formid: this.get('host').get('elementid') + '_atto_media_form'});
501     },
503     /**
504      * Returns the dialogue content for the tool.
505      *
506      * @method _getDialogueContent
507      * @param  {WrappedRange[]} selection Current editor selection
508      * @return {Y.Node}
509      * @private
510      */
511     _getDialogueContent: function(selection) {
512         var content = Y.Node.create(
513             Y.Handlebars.compile(TEMPLATES.ROOT)(this._getContext())
514         );
516         var medium = this.get('host').getSelectedNodes().filter('video,audio').shift();
517         var mediumProperties = medium ? this._getMediumProperties(medium) : false;
518         return this._attachEvents(this._applyMediumProperties(content, mediumProperties), selection);
519     },
521     /**
522      * Attaches required events to the content node.
523      *
524      * @method _attachEvents
525      * @param  {Y.Node}         content The content to which events will be attached
526      * @param  {WrappedRange[]} selection Current editor selection
527      * @return {Y.Node}
528      * @private
529      */
530     _attachEvents: function(content, selection) {
531         // Delegate add component link for media source fields.
532         content.delegate('click', function(e) {
533             e.preventDefault();
534             this._addMediaSourceComponent(e.currentTarget);
535         }, SELECTORS.MEDIA_SOURCE + ' .addcomponent', this);
537         // Delegate add component link for track fields.
538         content.delegate('click', function(e) {
539             e.preventDefault();
540             this._addTrackComponent(e.currentTarget);
541         }, SELECTORS.TRACK + ' .addcomponent', this);
543         // Only allow one track per tab to be selected as "default".
544         content.delegate('click', function(e) {
545             var element = e.currentTarget;
546             if (element.get('checked')) {
547                 var getKind = function(el) {
548                     return this._getTrackTypeFromTabPane(el.ancestor('.tab-pane'));
549                 }.bind(this);
551                 element.ancestor('.root.tab-content').all(SELECTORS.TRACK_DEFAULT_SELECT).each(function(select) {
552                     if (select !== element && getKind(element) === getKind(select)) {
553                         select.set('checked', false);
554                     }
555                 });
556             }
557         }, SELECTORS.TRACK_DEFAULT_SELECT, this);
559         // Set up filepicker click event.
560         content.delegate('click', function(e) {
561             var element = e.currentTarget;
562             var fptype = (element.ancestor(SELECTORS.POSTER_SOURCE) && 'image') ||
563                     (element.ancestor(SELECTORS.TRACK_SOURCE) && 'subtitle') ||
564                     'media';
565             e.preventDefault();
566             this.get('host').showFilepicker(fptype, this._getFilepickerCallback(element, fptype), this);
567         }, '.openmediabrowser', this);
569         // This is a nasty hack. Basically we are using BS4 markup for the tabs
570         // but it isn't completely backwards compatible with BS2. The main problem is
571         // that the "active" class goes on a different node. So the idea is to put it
572         // the node for BS4, and then use CSS to make it look right in BS2. However,
573         // once another tab is clicked, everything sorts itself out, more or less. Except
574         // that the original "active" tab hasn't had the BS4 "active" class removed
575         // (so the styles will still apply to it). So we need to remove the "active"
576         // class on the BS4 node so that BS2 is happy.
577         //
578         // This doesn't upset BS4 since it removes this class anyway when clicking on
579         // another tab.
580         content.all('.nav-item').on('click', function(elem) {
581             elem.currentTarget.get('parentNode').all('.active').removeClass('active');
582         });
584         content.one('.submit').on('click', function(e) {
585             e.preventDefault();
586             var mediaHTML = this._getMediaHTML(e.currentTarget.ancestor('.atto_form')),
587                 host = this.get('host');
588             this.getDialogue({
589                 focusAfterHide: null
590             }).hide();
591             if (mediaHTML) {
592                 host.setSelection(selection);
593                 host.insertContentAtFocusPoint(mediaHTML);
594                 this.markUpdated();
595             }
596         }, this);
598         return content;
599     },
601     /**
602      * Applies medium properties to the content node.
603      *
604      * @method _applyMediumProperties
605      * @param  {Y.Node} content The content to apply the properties to
606      * @param  {object} properties The medium properties to apply
607      * @return {Y.Node}
608      * @private
609      */
610     _applyMediumProperties: function(content, properties) {
611         if (!properties) {
612             return content;
613         }
615         var applyTrackProperties = function(track, properties) {
616             track.one(SELECTORS.TRACK_SOURCE + ' ' + SELECTORS.URL_INPUT).set('value', properties.src);
617             track.one(SELECTORS.TRACK_LANG_INPUT).set('value', properties.srclang);
618             track.one(SELECTORS.TRACK_LABEL_INPUT).set('value', properties.label);
619             track.one(SELECTORS.TRACK_DEFAULT_SELECT).set('checked', properties.defaultTrack);
620         };
622         var tabPane = content.one('.root.tab-content > .tab-pane#' + this.get('host').get('elementid') +
623                               '_' + properties.type.toLowerCase());
625         // Populate sources.
626         tabPane.one(SELECTORS.MEDIA_SOURCE + ' ' + SELECTORS.URL_INPUT).set('value', properties.sources[0]);
627         Y.Array.each(properties.sources.slice(1), function(source) {
628             this._addMediaSourceComponent(tabPane.one(SELECTORS.MEDIA_SOURCE + ' .addcomponent'), function(newComponent) {
629                 newComponent.one(SELECTORS.URL_INPUT).set('value', source);
630             });
631         }, this);
633         // Populate tracks.
634         Y.Object.each(properties.tracks, function(value, key) {
635             var trackData = value.length ? value : [{src: '', srclang: '', label: '', defaultTrack: false}];
636             var paneSelector = SELECTORS['TRACK_' + key.toUpperCase() + '_PANE'];
638             applyTrackProperties(tabPane.one(paneSelector + ' ' + SELECTORS.TRACK), trackData[0]);
639             Y.Array.each(trackData.slice(1), function(track) {
640                 this._addTrackComponent(
641                     tabPane.one(paneSelector + ' ' + SELECTORS.TRACK + ' .addcomponent'), function(newComponent) {
642                     applyTrackProperties(newComponent, track);
643                 });
644             }, this);
645         }, this);
647         // Populate values.
648         tabPane.one(SELECTORS.POSTER_SOURCE + ' ' + SELECTORS.URL_INPUT).setAttribute('value', properties.poster);
649         tabPane.one(SELECTORS.WIDTH_INPUT).set('value', properties.width);
650         tabPane.one(SELECTORS.HEIGHT_INPUT).set('value', properties.height);
651         tabPane.one(SELECTORS.MEDIA_CONTROLS_TOGGLE).set('checked', properties.controls);
652         tabPane.one(SELECTORS.MEDIA_AUTOPLAY_TOGGLE).set('checked', properties.autoplay);
653         tabPane.one(SELECTORS.MEDIA_MUTE_TOGGLE).set('checked', properties.muted);
654         tabPane.one(SELECTORS.MEDIA_LOOP_TOGGLE).set('checked', properties.loop);
656         // Switch to the correct tab.
657         var mediumType = this._getMediumTypeFromTabPane(tabPane);
659         // Remove active class from all tabs + tab panes.
660         tabPane.siblings('.active').removeClass('active');
661         content.all('.root.nav-tabs .nav-item a').removeClass('active');
663         // Add active class to the desired tab and tab pane.
664         tabPane.addClass('active');
665         content.one(SELECTORS[mediumType.toUpperCase() + '_TAB'] + ' a').addClass('active');
667         return content;
668     },
670     /**
671      * Extracts medium properties.
672      *
673      * @method _getMediumProperties
674      * @param  {Y.Node} medium The medium node from which to extract
675      * @return {Object}
676      * @private
677      */
678     _getMediumProperties: function(medium) {
679         var boolAttr = function(elem, attr) {
680             return elem.getAttribute(attr) ? true : false;
681         };
683         var tracks = {
684             subtitles: [],
685             captions: [],
686             descriptions: [],
687             chapters: [],
688             metadata: []
689         };
691         medium.all('track').each(function(track) {
692             tracks[track.getAttribute('kind')].push({
693                 src: track.getAttribute('src'),
694                 srclang: track.getAttribute('srclang'),
695                 label: track.getAttribute('label'),
696                 defaultTrack: boolAttr(track, 'default')
697             });
698         });
700         return {
701             type: medium.test('video') ? MEDIA_TYPES.VIDEO : MEDIA_TYPES.AUDIO,
702             sources: medium.all('source').get('src'),
703             poster: medium.getAttribute('poster'),
704             width: medium.getAttribute('width'),
705             height: medium.getAttribute('height'),
706             autoplay: boolAttr(medium, 'autoplay'),
707             loop: boolAttr(medium, 'loop'),
708             muted: boolAttr(medium, 'muted'),
709             controls: boolAttr(medium, 'controls'),
710             tracks: tracks
711         };
712     },
714     /**
715      * Adds a track form component.
716      *
717      * @method _addTrackComponent
718      * @param  {Y.Node}   element    The element which was used to trigger this function
719      * @param  {Function} [callback] Function to be called when the new component is added
720      *     @param {Y.Node}    callback.newComponent The compiled component
721      * @private
722      */
723     _addTrackComponent: function(element, callback) {
724         var trackType = this._getTrackTypeFromTabPane(element.ancestor('.tab-pane'));
725         var context = this._getContext({
726             sourcelabel: trackType + 'sourcelabel',
727             addcomponentlabel: 'add' + trackType + 'track'
728         });
730         this._addComponent(element, TEMPLATES.FORM_COMPONENTS.TRACK, SELECTORS.TRACK, context, callback);
731     },
733     /**
734      * Adds a media source form component.
735      *
736      * @method _addMediaSourceComponent
737      * @param  {Y.Node}   element    The element which was used to trigger this function
738      * @param  {Function} [callback] Function to be called when the new component is added
739      *     @param {Y.Node}    callback.newComponent The compiled component
740      * @private
741      */
742     _addMediaSourceComponent: function(element, callback) {
743         var mediumType = this._getMediumTypeFromTabPane(element.ancestor('.tab-pane'));
744         var context = this._getContext({
745             multisource: true,
746             id: CSS.MEDIA_SOURCE,
747             entersourcelabel: mediumType + 'sourcelabel',
748             addcomponentlabel: 'addsource',
749             addsourcehelp: this.get('help').addsource
750         });
751         this._addComponent(element, TEMPLATES.FORM_COMPONENTS.SOURCE, SELECTORS.MEDIA_SOURCE, context, callback);
752     },
754     /**
755      * Adds an arbitrary form component.
756      *
757      * This function Compiles and adds the provided component in the supplied 'ancestor' container.
758      * It will also add links to add/remove the relevant components, attaching the
759      * necessary events.
760      *
761      * @method _addComponent
762      * @param  {Y.Node}   element    The element which was used to trigger this function
763      * @param  {String}   component  The component to compile and add
764      * @param  {String}   ancestor   A selector used to find an ancestor of 'component', to which
765      *                               the compiled component will be appended
766      * @param  {Object}   context    The context with which to render the component
767      * @param  {Function} [callback] Function to be called when the new component is added
768      *     @param {Y.Node}    callback.newComponent The compiled component
769      * @private
770      */
771     _addComponent: function(element, component, ancestor, context, callback) {
772         var currentComponent = element.ancestor(ancestor),
773             newComponent = Y.Node.create(Y.Handlebars.compile(component)(context)),
774             removeNodeContext = this._getContext(context);
776         removeNodeContext.label = "remove";
777         var removeNode = Y.Node.create(Y.Handlebars.compile(TEMPLATES.FORM_COMPONENTS.REMOVE_COMPONENT)(removeNodeContext));
779         removeNode.one('.removecomponent').on('click', function(e) {
780             e.preventDefault();
781             currentComponent.remove(true);
782         });
784         currentComponent.insert(newComponent, 'after');
785         element.ancestor().insert(removeNode, 'after');
786         element.ancestor().remove(true);
788         if (callback) {
789             callback.call(this, newComponent);
790         }
791     },
793     /**
794      * Returns the callback for the file picker to call after a file has been selected.
795      *
796      * @method _getFilepickerCallback
797      * @param  {Y.Node} element The element which triggered the callback
798      * @param  {String} fptype  The file pickertype (as would be passed to `showFilePicker`)
799      * @return {Function} The function to be used as a callback when the file picker returns the file
800      * @private
801      */
802     _getFilepickerCallback: function(element, fptype) {
803         return function(params) {
804             if (params.url !== '') {
805                 var tabPane = element.ancestor('.tab-pane');
806                 element.ancestor(SELECTORS.SOURCE).one(SELECTORS.URL_INPUT).set('value', params.url);
808                 // Links (and only links) have a name field.
809                 if (tabPane.get('id') === this.get('host').get('elementid') + '_' + CSS.LINK) {
810                     tabPane.one(SELECTORS.NAME_INPUT).set('value', params.file);
811                 }
813                 if (fptype === 'subtitle') {
814                     var subtitleLang = params.file.split('.vtt')[0].split('-').slice(-1)[0];
815                     var langObj = this.get('langs').available.reduce(function(carry, lang) {
816                         return lang.code === subtitleLang ? lang : carry;
817                     }, false);
818                     if (langObj) {
819                         element.ancestor(SELECTORS.TRACK).one(SELECTORS.TRACK_LABEL_INPUT).set('value',
820                                 langObj.lang.substr(0, langObj.lang.lastIndexOf(' ')));
821                         element.ancestor(SELECTORS.TRACK).one(SELECTORS.TRACK_LANG_INPUT).set('value', langObj.code);
822                     }
823                 }
824             }
825         };
826     },
828     /**
829      * Given a "medium" tab pane, returns what kind of medium it contains.
830      *
831      * @method _getMediumTypeFromTabPane
832      * @param  {Y.Node} tabPane The tab pane
833      * @return {String} The type of medium in the pane
834      */
835     _getMediumTypeFromTabPane: function(tabPane) {
836         return tabPane.getAttribute('data-medium-type');
837     },
839     /**
840      * Given a "track" tab pane, returns what kind of track it contains.
841      *
842      * @method _getTrackTypeFromTabPane
843      * @param  {Y.Node} tabPane The tab pane
844      * @return {String} The type of track in the pane
845      */
846     _getTrackTypeFromTabPane: function(tabPane) {
847         return tabPane.getAttribute('data-track-kind');
848     },
850     /**
851      * Returns the HTML to be inserted to the text area.
852      *
853      * @method _getMediaHTML
854      * @param  {Y.Node} form The form from which to extract data
855      * @return {String} The compiled markup
856      * @private
857      */
858     _getMediaHTML: function(form) {
859         var mediumType = this._getMediumTypeFromTabPane(form.one('.root.tab-content > .tab-pane.active'));
860         var tabContent = form.one(SELECTORS[mediumType.toUpperCase() + '_PANE']);
862         return this['_getMediaHTML' + mediumType[0].toUpperCase() + mediumType.substr(1)](tabContent);
863     },
865     /**
866      * Returns the HTML to be inserted to the text area for the link tab.
867      *
868      * @method _getMediaHTMLLink
869      * @param  {Y.Node} tab The tab from which to extract data
870      * @return {String} The compiled markup
871      * @private
872      */
873     _getMediaHTMLLink: function(tab) {
874         var context = {
875             url: tab.one(SELECTORS.URL_INPUT).get('value'),
876             name: tab.one(SELECTORS.NAME_INPUT).get('value') || false
877         };
879         return context.url ? Y.Handlebars.compile(TEMPLATES.HTML_MEDIA.LINK)(context) : '';
880     },
882     /**
883      * Returns the HTML to be inserted to the text area for the video tab.
884      *
885      * @method _getMediaHTMLVideo
886      * @param  {Y.Node} tab The tab from which to extract data
887      * @return {String} The compiled markup
888      * @private
889      */
890     _getMediaHTMLVideo: function(tab) {
891         var context = this._getContextForMediaHTML(tab);
892         context.width = tab.one(SELECTORS.WIDTH_INPUT).get('value') || false;
893         context.height = tab.one(SELECTORS.HEIGHT_INPUT).get('value') || false;
894         context.poster = tab.one(SELECTORS.POSTER_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value') || false;
896         return context.sources.length ? Y.Handlebars.compile(TEMPLATES.HTML_MEDIA.VIDEO)(context) : '';
897     },
899     /**
900      * Returns the HTML to be inserted to the text area for the audio tab.
901      *
902      * @method _getMediaHTMLAudio
903      * @param  {Y.Node} tab The tab from which to extract data
904      * @return {String} The compiled markup
905      * @private
906      */
907     _getMediaHTMLAudio: function(tab) {
908         var context = this._getContextForMediaHTML(tab);
910         return context.sources.length ? Y.Handlebars.compile(TEMPLATES.HTML_MEDIA.AUDIO)(context) : '';
911     },
913     /**
914      * Returns the context with which to render a media template.
915      *
916      * @method _getContextForMediaHTML
917      * @param  {Y.Node} tab The tab from which to extract data
918      * @return {Object}
919      * @private
920      */
921     _getContextForMediaHTML: function(tab) {
922         var tracks = [];
924         tab.all(SELECTORS.TRACK).each(function(track) {
925             tracks.push({
926                 track: track.one(SELECTORS.TRACK_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value'),
927                 kind: this._getTrackTypeFromTabPane(track.ancestor('.tab-pane')),
928                 label: track.one(SELECTORS.TRACK_LABEL_INPUT).get('value') ||
929                     track.one(SELECTORS.TRACK_LANG_INPUT).get('value'),
930                 srclang: track.one(SELECTORS.TRACK_LANG_INPUT).get('value'),
931                 defaultTrack: track.one(SELECTORS.TRACK_DEFAULT_SELECT).get('checked') ? "true" : null
932             });
933         }, this);
935         return {
936             sources: tab.all(SELECTORS.MEDIA_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value').filter(function(source) {
937                 return !!source;
938             }).map(function(source) {
939                 return {source: source};
940             }),
941             description: tab.one(SELECTORS.MEDIA_SOURCE + ' ' + SELECTORS.URL_INPUT).get('value') || false,
942             tracks: tracks.filter(function(track) {
943                 return !!track.track;
944             }),
945             showControls: tab.one(SELECTORS.MEDIA_CONTROLS_TOGGLE).get('checked'),
946             autoplay: tab.one(SELECTORS.MEDIA_AUTOPLAY_TOGGLE).get('checked'),
947             muted: tab.one(SELECTORS.MEDIA_MUTE_TOGGLE).get('checked'),
948             loop: tab.one(SELECTORS.MEDIA_LOOP_TOGGLE).get('checked')
949         };
950     }
951 }, {
952     ATTRS: {
953         langs: {},
954         help: {}
955     }
956 });