MDL-64506 templates: replace spans where and col-x appears
[moodle.git] / lib / editor / atto / plugins / image / yui / build / moodle-atto_image-button / moodle-atto_image-button-debug.js
1 YUI.add('moodle-atto_image-button', function (Y, NAME) {
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /*
19  * @package    atto_image
20  * @copyright  2013 Damyon Wiese  <damyon@moodle.com>
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  */
24 /**
25  * @module moodle-atto_image_alignment-button
26  */
28 /**
29  * Atto image selection tool.
30  *
31  * @namespace M.atto_image
32  * @class Button
33  * @extends M.editor_atto.EditorPlugin
34  */
36 var CSS = {
37         RESPONSIVE: 'img-responsive',
38         INPUTALIGNMENT: 'atto_image_alignment',
39         INPUTALT: 'atto_image_altentry',
40         INPUTHEIGHT: 'atto_image_heightentry',
41         INPUTSUBMIT: 'atto_image_urlentrysubmit',
42         INPUTURL: 'atto_image_urlentry',
43         INPUTSIZE: 'atto_image_size',
44         INPUTWIDTH: 'atto_image_widthentry',
45         IMAGEALTWARNING: 'atto_image_altwarning',
46         IMAGEBROWSER: 'openimagebrowser',
47         IMAGEPRESENTATION: 'atto_image_presentation',
48         INPUTCONSTRAIN: 'atto_image_constrain',
49         INPUTCUSTOMSTYLE: 'atto_image_customstyle',
50         IMAGEPREVIEW: 'atto_image_preview',
51         IMAGEPREVIEWBOX: 'atto_image_preview_box',
52         ALIGNSETTINGS: 'atto_image_button'
53     },
54     SELECTORS = {
55         INPUTURL: '.' + CSS.INPUTURL
56     },
57     ALIGNMENTS = [
58         // Vertical alignment.
59         {
60             name: 'verticalAlign',
61             str: 'alignment_top',
62             value: 'text-top',
63             margin: '0 0.5em'
64         }, {
65             name: 'verticalAlign',
66             str: 'alignment_middle',
67             value: 'middle',
68             margin: '0 0.5em'
69         }, {
70             name: 'verticalAlign',
71             str: 'alignment_bottom',
72             value: 'text-bottom',
73             margin: '0 0.5em',
74             isDefault: true
75         },
77         // Floats.
78         {
79             name: 'float',
80             str: 'alignment_left',
81             value: 'left',
82             margin: '0 0.5em 0 0'
83         }, {
84             name: 'float',
85             str: 'alignment_right',
86             value: 'right',
87             margin: '0 0 0 0.5em'
88         }
89     ],
91     REGEX = {
92         ISPERCENT: /\d+%/
93     },
95     COMPONENTNAME = 'atto_image',
97     TEMPLATE = '' +
98             '<form class="atto_form">' +
100                 // Add the repository browser button.
101                 '{{#if showFilepicker}}' +
102                     '<div class="mb-1">' +
103                         '<label for="{{elementid}}_{{CSS.INPUTURL}}">{{get_string "enterurl" component}}</label>' +
104                         '<div class="input-group input-append w-100">' +
105                             '<input class="form-control {{CSS.INPUTURL}}" type="url" ' +
106                             'id="{{elementid}}_{{CSS.INPUTURL}}" size="32"/>' +
107                             '<span class="input-group-append">' +
108                                 '<button class="btn btn-secondary {{CSS.IMAGEBROWSER}}" type="button">' +
109                                 '{{get_string "browserepositories" component}}</button>' +
110                             '</span>' +
111                         '</div>' +
112                     '</div>' +
113                 '{{else}}' +
114                     '<div class="mb-1">' +
115                         '<label for="{{elementid}}_{{CSS.INPUTURL}}">{{get_string "enterurl" component}}</label>' +
116                         '<input class="form-control fullwidth {{CSS.INPUTURL}}" type="url" ' +
117                         'id="{{elementid}}_{{CSS.INPUTURL}}" size="32"/>' +
118                     '</div>' +
119                 '{{/if}}' +
121                 // Add the Alt box.
122                 '<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.IMAGEALTWARNING}}">' +
123                     '{{get_string "presentationoraltrequired" component}}' +
124                 '</div>' +
125                 '<div class="mb-1">' +
126                 '<label for="{{elementid}}_{{CSS.INPUTALT}}">{{get_string "enteralt" component}}</label>' +
127                 '<input class="form-control fullwidth {{CSS.INPUTALT}}" type="text" value="" ' +
128                 'id="{{elementid}}_{{CSS.INPUTALT}}" size="32"/>' +
130                 // Add the presentation select box.
131                 '<div class="form-check">' +
132                 '<input type="checkbox" class="form-check-input {{CSS.IMAGEPRESENTATION}}" ' +
133                     'id="{{elementid}}_{{CSS.IMAGEPRESENTATION}}"/>' +
134                 '<label class="form-check-label" for="{{elementid}}_{{CSS.IMAGEPRESENTATION}}">' +
135                     '{{get_string "presentation" component}}' +
136                 '</label>' +
137                 '</div>' +
138                 '</div>' +
140                 // Add the size entry boxes.
141                 '<div class="mb-1">' +
142                 '<label class="" for="{{elementid}}_{{CSS.INPUTSIZE}}">{{get_string "size" component}}</label>' +
143                 '<div id="{{elementid}}_{{CSS.INPUTSIZE}}" class="form-inline {{CSS.INPUTSIZE}}">' +
144                 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTWIDTH}}">{{get_string "width" component}}</label>' +
145                 '<input type="text" class="form-control mr-1 input-mini {{CSS.INPUTWIDTH}}" ' +
146                 'id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="4"/> x' +
148                 // Add the height entry box.
149                 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTHEIGHT}}">{{get_string "height" component}}</label>' +
150                 '<input type="text" class="form-control ml-1 input-mini {{CSS.INPUTHEIGHT}}" ' +
151                 'id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +
153                 // Add the constrain checkbox.
154                 '<div class="form-check ml-2">' +
155                 '<input type="checkbox" class="form-check-input {{CSS.INPUTCONSTRAIN}}" ' +
156                 'id="{{elementid}}_{{CSS.INPUTCONSTRAIN}}"/>' +
157                 '<label class="form-check-label" for="{{elementid}}_{{CSS.INPUTCONSTRAIN}}">' +
158                 '{{get_string "constrain" component}}</label>' +
159                 '</div>' +
160                 '</div>' +
161                 '</div>' +
163                 // Add the alignment selector.
164                 '<div class="form-inline mb-1">' +
165                 '<label class="for="{{elementid}}_{{CSS.INPUTALIGNMENT}}">{{get_string "alignment" component}}</label>' +
166                 '<select class="custom-select {{CSS.INPUTALIGNMENT}}" id="{{elementid}}_{{CSS.INPUTALIGNMENT}}">' +
167                     '{{#each alignments}}' +
168                         '<option value="{{value}}">{{get_string str ../component}}</option>' +
169                     '{{/each}}' +
170                 '</select>' +
171                 '</div>' +
172                 // Hidden input to store custom styles.
173                 '<input type="hidden" class="{{CSS.INPUTCUSTOMSTYLE}}"/>' +
174                 '<br/>' +
176                 // Add the image preview.
177                 '<div class="mdl-align">' +
178                 '<div class="{{CSS.IMAGEPREVIEWBOX}}">' +
179                     '<img src="#" class="{{CSS.IMAGEPREVIEW}}" alt="" style="display: none;"/>' +
180                 '</div>' +
182                 // Add the submit button and close the form.
183                 '<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
184                     '{{get_string "saveimage" component}}</button>' +
185                 '</div>' +
186             '</form>',
188         IMAGETEMPLATE = '' +
189             '<img src="{{url}}" alt="{{alt}}" ' +
190                 '{{#if width}}width="{{width}}" {{/if}}' +
191                 '{{#if height}}height="{{height}}" {{/if}}' +
192                 '{{#if presentation}}role="presentation" {{/if}}' +
193                 '{{#if customstyle}}style="{{customstyle}}" {{/if}}' +
194                 '{{#if classlist}}class="{{classlist}}" {{/if}}' +
195                 '{{#if id}}id="{{id}}" {{/if}}' +
196                 '/>';
198 Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
199     /**
200      * A reference to the current selection at the time that the dialogue
201      * was opened.
202      *
203      * @property _currentSelection
204      * @type Range
205      * @private
206      */
207     _currentSelection: null,
209     /**
210      * The most recently selected image.
211      *
212      * @param _selectedImage
213      * @type Node
214      * @private
215      */
216     _selectedImage: null,
218     /**
219      * A reference to the currently open form.
220      *
221      * @param _form
222      * @type Node
223      * @private
224      */
225     _form: null,
227     /**
228      * The dimensions of the raw image before we manipulate it.
229      *
230      * @param _rawImageDimensions
231      * @type Object
232      * @private
233      */
234     _rawImageDimensions: null,
236     initializer: function() {
238         this.addButton({
239             icon: 'e/insert_edit_image',
240             callback: this._displayDialogue,
241             tags: 'img',
242             tagMatchRequiresAll: false
243         });
244         this.editor.delegate('dblclick', this._displayDialogue, 'img', this);
245         this.editor.delegate('click', this._handleClick, 'img', this);
246         this.editor.on('drop', this._handleDragDrop, this);
248         // e.preventDefault needed to stop the default event from clobbering the desired behaviour in some browsers.
249         this.editor.on('dragover', function(e) {
250             e.preventDefault();
251         }, this);
252         this.editor.on('dragenter', function(e) {
253             e.preventDefault();
254         }, this);
255     },
257     /**
258      * Handle a drag and drop event with an image.
259      *
260      * @method _handleDragDrop
261      * @param {EventFacade} e
262      * @return mixed
263      * @private
264      */
265     _handleDragDrop: function(e) {
267         var self = this,
268             host = this.get('host'),
269             template = Y.Handlebars.compile(IMAGETEMPLATE);
271         host.saveSelection();
272         e = e._event;
274         // Only handle the event if an image file was dropped in.
275         var handlesDataTransfer = (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length);
276         if (handlesDataTransfer && /^image\//.test(e.dataTransfer.files[0].type)) {
278             var options = host.get('filepickeroptions').image,
279                 savepath = (options.savepath === undefined) ? '/' : options.savepath,
280                 formData = new FormData(),
281                 timestamp = 0,
282                 uploadid = "",
283                 xhr = new XMLHttpRequest(),
284                 imagehtml = "",
285                 keys = Object.keys(options.repositories);
287             e.preventDefault();
288             e.stopPropagation();
289             formData.append('repo_upload_file', e.dataTransfer.files[0]);
290             formData.append('itemid', options.itemid);
292             // List of repositories is an object rather than an array.  This makes iteration more awkward.
293             for (var i = 0; i < keys.length; i++) {
294                 if (options.repositories[keys[i]].type === 'upload') {
295                     formData.append('repo_id', options.repositories[keys[i]].id);
296                     break;
297                 }
298             }
299             formData.append('env', options.env);
300             formData.append('sesskey', M.cfg.sesskey);
301             formData.append('client_id', options.client_id);
302             formData.append('savepath', savepath);
303             formData.append('ctx_id', options.context.id);
305             // Insert spinner as a placeholder.
306             timestamp = new Date().getTime();
307             uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
308             host.focus();
309             host.restoreSelection();
310             imagehtml = template({
311                 url: M.util.image_url("i/loading_small", 'moodle'),
312                 alt: M.util.get_string('uploading', COMPONENTNAME),
313                 id: uploadid
314             });
315             host.insertContentAtFocusPoint(imagehtml);
316             self.markUpdated();
318             // Kick off a XMLHttpRequest.
319             xhr.onreadystatechange = function() {
320                 var placeholder = self.editor.one('#' + uploadid),
321                     result,
322                     file,
323                     newhtml,
324                     newimage;
326                 if (xhr.readyState === 4) {
327                     if (xhr.status === 200) {
328                         result = JSON.parse(xhr.responseText);
329                         if (result) {
330                             if (result.error) {
331                                 if (placeholder) {
332                                     placeholder.remove(true);
333                                 }
334                                 return new M.core.ajaxException(result);
335                             }
337                             file = result;
338                             if (result.event && result.event === 'fileexists') {
339                                 // A file with this name is already in use here - rename to avoid conflict.
340                                 // Chances are, it's a different image (stored in a different folder on the user's computer).
341                                 // If the user wants to reuse an existing image, they can copy/paste it within the editor.
342                                 file = result.newfile;
343                             }
345                             // Replace placeholder with actual image.
346                             newhtml = template({
347                                 url: file.url,
348                                 presentation: true
349                             });
350                             newimage = Y.Node.create(newhtml);
351                             if (placeholder) {
352                                 placeholder.replace(newimage);
353                             } else {
354                                 self.editor.appendChild(newimage);
355                             }
356                             self.markUpdated();
357                         }
358                     } else {
359                         Y.use('moodle-core-notification-alert', function() {
360                             new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
361                         });
362                         if (placeholder) {
363                             placeholder.remove(true);
364                         }
365                     }
366                 }
367             };
368             xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
369             xhr.send(formData);
370             return false;
371         }
373 },
375     /**
376      * Handle a click on an image.
377      *
378      * @method _handleClick
379      * @param {EventFacade} e
380      * @private
381      */
382     _handleClick: function(e) {
383         var image = e.target;
385         var selection = this.get('host').getSelectionFromNode(image);
386         if (this.get('host').getSelection() !== selection) {
387             this.get('host').setSelection(selection);
388         }
389     },
391     /**
392      * Display the image editing tool.
393      *
394      * @method _displayDialogue
395      * @private
396      */
397     _displayDialogue: function() {
398         // Store the current selection.
399         this._currentSelection = this.get('host').getSelection();
400         if (this._currentSelection === false) {
401             return;
402         }
404         // Reset the image dimensions.
405         this._rawImageDimensions = null;
407         var dialogue = this.getDialogue({
408             headerContent: M.util.get_string('imageproperties', COMPONENTNAME),
409             width: 'auto',
410             focusAfterHide: true,
411             focusOnShowSelector: SELECTORS.INPUTURL
412         });
414         // Set the dialogue content, and then show the dialogue.
415         dialogue.set('bodyContent', this._getDialogueContent())
416                 .show();
417     },
419     /**
420      * Set the inputs for width and height if they are not set, and calculate
421      * if the constrain checkbox should be checked or not.
422      *
423      * @method _loadPreviewImage
424      * @param {String} url
425      * @private
426      */
427     _loadPreviewImage: function(url) {
428         var image = new Image();
429         var self = this;
431         image.onerror = function() {
432             var preview = self._form.one('.' + CSS.IMAGEPREVIEW);
433             preview.setStyles({
434                 'display': 'none'
435             });
437             // Centre the dialogue when clearing the image preview.
438             self.getDialogue().centerDialogue();
439         };
441         image.onload = function() {
442             var input, currentwidth, currentheight, widthRatio, heightRatio;
444             self._rawImageDimensions = {
445                 width: this.width,
446                 height: this.height
447             };
449             input = self._form.one('.' + CSS.INPUTWIDTH);
450             currentwidth = input.get('value');
451             if (currentwidth === '') {
452                 input.set('value', this.width);
453                 currentwidth = "" + this.width;
454             }
455             input = self._form.one('.' + CSS.INPUTHEIGHT);
456             currentheight = input.get('value');
457             if (currentheight === '') {
458                 input.set('value', this.height);
459                 currentheight = "" + this.height;
460             }
461             input = self._form.one('.' + CSS.IMAGEPREVIEW);
462             input.setAttribute('src', this.src);
463             input.setStyles({
464                 'display': 'inline'
465             });
467             input = self._form.one('.' + CSS.INPUTCONSTRAIN);
468             if (currentwidth.match(REGEX.ISPERCENT) && currentheight.match(REGEX.ISPERCENT)) {
469                 input.set('checked', currentwidth === currentheight);
470             } else {
471                 if (this.width === 0) {
472                     this.width = 1;
473                 }
474                 if (this.height === 0) {
475                     this.height = 1;
476                 }
477                 // This is the same as comparing to 3 decimal places.
478                 widthRatio = Math.round(1000 * parseInt(currentwidth, 10) / this.width);
479                 heightRatio = Math.round(1000 * parseInt(currentheight, 10) / this.height);
480                 input.set('checked', widthRatio === heightRatio);
481             }
483             // Apply the image sizing.
484             self._autoAdjustSize(self);
486             // Centre the dialogue once the preview image has loaded.
487             self.getDialogue().centerDialogue();
488         };
490         image.src = url;
491     },
493     /**
494      * Return the dialogue content for the tool, attaching any required
495      * events.
496      *
497      * @method _getDialogueContent
498      * @return {Node} The content to place in the dialogue.
499      * @private
500      */
501     _getDialogueContent: function() {
502         var template = Y.Handlebars.compile(TEMPLATE),
503             canShowFilepicker = this.get('host').canShowFilepicker('image'),
504             content = Y.Node.create(template({
505                 elementid: this.get('host').get('elementid'),
506                 CSS: CSS,
507                 component: COMPONENTNAME,
508                 showFilepicker: canShowFilepicker,
509                 alignments: ALIGNMENTS
510             }));
512         this._form = content;
514         // Configure the view of the current image.
515         this._applyImageProperties(this._form);
517         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
518         this._form.one('.' + CSS.IMAGEPRESENTATION).on('change', this._updateWarning, this);
519         this._form.one('.' + CSS.INPUTALT).on('change', this._updateWarning, this);
520         this._form.one('.' + CSS.INPUTWIDTH).on('blur', this._autoAdjustSize, this);
521         this._form.one('.' + CSS.INPUTHEIGHT).on('blur', this._autoAdjustSize, this, true);
522         this._form.one('.' + CSS.INPUTCONSTRAIN).on('change', function(event) {
523             if (event.target.get('checked')) {
524                 this._autoAdjustSize(event);
525             }
526         }, this);
527         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
528         this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setImage, this);
530         if (canShowFilepicker) {
531             this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {
532                     this.get('host').showFilepicker('image', this._filepickerCallback, this);
533             }, this);
534         }
536         return content;
537     },
539     _autoAdjustSize: function(e, forceHeight) {
540         forceHeight = forceHeight || false;
542         var keyField = this._form.one('.' + CSS.INPUTWIDTH),
543             keyFieldType = 'width',
544             subField = this._form.one('.' + CSS.INPUTHEIGHT),
545             subFieldType = 'height',
546             constrainField = this._form.one('.' + CSS.INPUTCONSTRAIN),
547             keyFieldValue = keyField.get('value'),
548             subFieldValue = subField.get('value'),
549             imagePreview = this._form.one('.' + CSS.IMAGEPREVIEW),
550             rawPercentage,
551             rawSize;
553         // If we do not know the image size, do not do anything.
554         if (!this._rawImageDimensions) {
555             return;
556         }
558         // Set the width back to default if it is empty.
559         if (keyFieldValue === '') {
560             keyFieldValue = this._rawImageDimensions[keyFieldType];
561             keyField.set('value', keyFieldValue);
562             keyFieldValue = keyField.get('value');
563         }
565         // Clear the existing preview sizes.
566         imagePreview.setStyles({
567             width: null,
568             height: null
569         });
571         // Now update with the new values.
572         if (!constrainField.get('checked')) {
573             // We are not keeping the image proportion - update the preview accordingly.
575             // Width.
576             if (keyFieldValue.match(REGEX.ISPERCENT)) {
577                 rawPercentage = parseInt(keyFieldValue, 10);
578                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
579                 imagePreview.setStyle('width', rawSize + 'px');
580             } else {
581                 imagePreview.setStyle('width', keyFieldValue + 'px');
582             }
584             // Height.
585             if (subFieldValue.match(REGEX.ISPERCENT)) {
586                 rawPercentage = parseInt(subFieldValue, 10);
587                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
588                 imagePreview.setStyle('height', rawSize + 'px');
589             } else {
590                 imagePreview.setStyle('height', subFieldValue + 'px');
591             }
592         } else {
593             // We are keeping the image in proportion.
594             if (forceHeight) {
595                 // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.
596                 var _temporaryValue;
597                 _temporaryValue = keyField;
598                 keyField = subField;
599                 subField = _temporaryValue;
601                 _temporaryValue = keyFieldType;
602                 keyFieldType = subFieldType;
603                 subFieldType = _temporaryValue;
605                 _temporaryValue = keyFieldValue;
606                 keyFieldValue = subFieldValue;
607                 subFieldValue = _temporaryValue;
608             }
610             if (keyFieldValue.match(REGEX.ISPERCENT)) {
611                 // This is a percentage based change. Copy it verbatim.
612                 subFieldValue = keyFieldValue;
614                 // Set the width to the calculated pixel width.
615                 rawPercentage = parseInt(keyFieldValue, 10);
616                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
618                 // And apply the width/height to the container.
619                 imagePreview.setStyle('width', rawSize);
620                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
621                 imagePreview.setStyle('height', rawSize);
622             } else {
623                 // Calculate the scaled subFieldValue from the keyFieldValue.
624                 subFieldValue = Math.round((keyFieldValue / this._rawImageDimensions[keyFieldType]) *
625                         this._rawImageDimensions[subFieldType]);
627                 if (forceHeight) {
628                     imagePreview.setStyles({
629                         'width': subFieldValue,
630                         'height': keyFieldValue
631                     });
632                 } else {
633                     imagePreview.setStyles({
634                         'width': keyFieldValue,
635                         'height': subFieldValue
636                     });
637                 }
638             }
640             // Update the subField's value within the form to reflect the changes.
641             subField.set('value', subFieldValue);
642         }
643     },
645     /**
646      * Update the dialogue after an image was selected in the File Picker.
647      *
648      * @method _filepickerCallback
649      * @param {object} params The parameters provided by the filepicker
650      * containing information about the image.
651      * @private
652      */
653     _filepickerCallback: function(params) {
654         if (params.url !== '') {
655             var input = this._form.one('.' + CSS.INPUTURL);
656             input.set('value', params.url);
658             // Auto set the width and height.
659             this._form.one('.' + CSS.INPUTWIDTH).set('value', '');
660             this._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
662             // Load the preview image.
663             this._loadPreviewImage(params.url);
664         }
665     },
667     /**
668      * Applies properties of an existing image to the image dialogue for editing.
669      *
670      * @method _applyImageProperties
671      * @param {Node} form
672      * @private
673      */
674     _applyImageProperties: function(form) {
675         var properties = this._getSelectedImageProperties(),
676             img = form.one('.' + CSS.IMAGEPREVIEW);
678         if (properties === false) {
679             img.setStyle('display', 'none');
680             // Set the default alignment.
681             ALIGNMENTS.some(function(alignment) {
682                 if (alignment.isDefault) {
683                     form.one('.' + CSS.INPUTALIGNMENT).set('value', alignment.value);
684                     return true;
685                 }
687                 return false;
688             }, this);
690             return;
691         }
693         if (properties.align) {
694             form.one('.' + CSS.INPUTALIGNMENT).set('value', properties.align);
695         }
696         if (properties.customstyle) {
697             form.one('.' + CSS.INPUTCUSTOMSTYLE).set('value', properties.customstyle);
698         }
699         if (properties.width) {
700             form.one('.' + CSS.INPUTWIDTH).set('value', properties.width);
701         }
702         if (properties.height) {
703             form.one('.' + CSS.INPUTHEIGHT).set('value', properties.height);
704         }
705         if (properties.alt) {
706             form.one('.' + CSS.INPUTALT).set('value', properties.alt);
707         }
708         if (properties.src) {
709             form.one('.' + CSS.INPUTURL).set('value', properties.src);
710             this._loadPreviewImage(properties.src);
711         }
712         if (properties.presentation) {
713             form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');
714         }
716         // Update the image preview based on the form properties.
717         this._autoAdjustSize();
718     },
720     /**
721      * Gets the properties of the currently selected image.
722      *
723      * The first image only if multiple images are selected.
724      *
725      * @method _getSelectedImageProperties
726      * @return {object}
727      * @private
728      */
729     _getSelectedImageProperties: function() {
730         var properties = {
731                 src: null,
732                 alt: null,
733                 width: null,
734                 height: null,
735                 align: '',
736                 presentation: false
737             },
739             // Get the current selection.
740             images = this.get('host').getSelectedNodes(),
741             width,
742             height,
743             style,
744             image;
746         if (images) {
747             images = images.filter('img');
748         }
750         if (images && images.size()) {
751             image = this._removeLegacyAlignment(images.item(0));
752             this._selectedImage = image;
754             style = image.getAttribute('style');
755             properties.customstyle = style;
757             width = image.getAttribute('width');
758             if (!width.match(REGEX.ISPERCENT)) {
759                 width = parseInt(width, 10);
760             }
761             height = image.getAttribute('height');
762             if (!height.match(REGEX.ISPERCENT)) {
763                 height = parseInt(height, 10);
764             }
766             if (width !== 0) {
767                 properties.width = width;
768             }
769             if (height !== 0) {
770                 properties.height = height;
771             }
772             this._getAlignmentPropeties(image, properties);
773             properties.src = image.getAttribute('src');
774             properties.alt = image.getAttribute('alt') || '';
775             properties.presentation = (image.get('role') === 'presentation');
776             return properties;
777         }
779         // No image selected - clean up.
780         this._selectedImage = null;
781         return false;
782     },
784     /**
785      * Sets the alignment of a properties object.
786      *
787      * @method _getAlignmentPropeties
788      * @param {Node} image The image that the alignment properties should be found for
789      * @param {Object} properties The properties object that is created in _getSelectedImageProperties()
790      * @private
791      */
792     _getAlignmentPropeties: function(image, properties) {
793         var complete = false,
794             defaultAlignment;
796         // Check for an alignment value.
797         complete = ALIGNMENTS.some(function(alignment) {
798             var classname = this._getAlignmentClass(alignment.value);
799             if (image.hasClass(classname)) {
800                 properties.align = alignment.value;
801                 Y.log('Found alignment ' + alignment.value, 'debug', 'atto_image-button');
803                 return true;
804             }
806             if (alignment.isDefault) {
807                 defaultAlignment = alignment.value;
808             }
810             return false;
811         }, this);
813         if (!complete && defaultAlignment) {
814             properties.align = defaultAlignment;
815         }
816     },
818     /**
819      * Update the form when the URL was changed. This includes updating the
820      * height, width, and image preview.
821      *
822      * @method _urlChanged
823      * @private
824      */
825     _urlChanged: function() {
826         var input = this._form.one('.' + CSS.INPUTURL);
828         if (input.get('value') !== '') {
829             // Load the preview image.
830             this._loadPreviewImage(input.get('value'));
831         }
832     },
834     /**
835      * Update the image in the contenteditable.
836      *
837      * @method _setImage
838      * @param {EventFacade} e
839      * @private
840      */
841     _setImage: function(e) {
842         var form = this._form,
843             url = form.one('.' + CSS.INPUTURL).get('value'),
844             alt = form.one('.' + CSS.INPUTALT).get('value'),
845             width = form.one('.' + CSS.INPUTWIDTH).get('value'),
846             height = form.one('.' + CSS.INPUTHEIGHT).get('value'),
847             alignment = this._getAlignmentClass(form.one('.' + CSS.INPUTALIGNMENT).get('value')),
848             presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked'),
849             constrain = form.one('.' + CSS.INPUTCONSTRAIN).get('checked'),
850             imagehtml,
851             customstyle = form.one('.' + CSS.INPUTCUSTOMSTYLE).get('value'),
852             classlist = [],
853             host = this.get('host');
855         e.preventDefault();
857         // Check if there are any accessibility issues.
858         if (this._updateWarning()) {
859             return;
860         }
862         // Focus on the editor in preparation for inserting the image.
863         host.focus();
864         if (url !== '') {
865             if (this._selectedImage) {
866                 host.setSelection(host.getSelectionFromNode(this._selectedImage));
867             } else {
868                 host.setSelection(this._currentSelection);
869             }
871             if (constrain) {
872                 classlist.push(CSS.RESPONSIVE);
873             }
875             // Add the alignment class for the image.
876             classlist.push(alignment);
878             if (!width.match(REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {
879                 form.one('.' + CSS.INPUTWIDTH).focus();
880                 return;
881             }
882             if (!height.match(REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {
883                 form.one('.' + CSS.INPUTHEIGHT).focus();
884                 return;
885             }
887             var template = Y.Handlebars.compile(IMAGETEMPLATE);
888             imagehtml = template({
889                 url: url,
890                 alt: alt,
891                 width: width,
892                 height: height,
893                 presentation: presentation,
894                 customstyle: customstyle,
895                 classlist: classlist.join(' ')
896             });
898             this.get('host').insertContentAtFocusPoint(imagehtml);
900             this.markUpdated();
901         }
903         this.getDialogue({
904             focusAfterHide: null
905         }).hide();
907     },
909     /**
910      * Removes any legacy styles added by previous versions of the atto image button.
911      *
912      * @method _removeLegacyAlignment
913      * @param {Y.Node} imageNode
914      * @return {Y.Node}
915      * @private
916      */
917     _removeLegacyAlignment: function(imageNode) {
918         if (!imageNode.getStyle('margin')) {
919             // There is no margin therefore this cannot match any known alignments.
920             return imageNode;
921         }
923         ALIGNMENTS.some(function(alignment) {
924             if (imageNode.getStyle(alignment.name) !== alignment.value) {
925                 // The name/value do not match. Skip.
926                 return false;
927             }
929             var normalisedNode = Y.Node.create('<div>');
930             normalisedNode.setStyle('margin', alignment.margin);
931             if (imageNode.getStyle('margin') !== normalisedNode.getStyle('margin')) {
932                 // The margin does not match.
933                 return false;
934             }
936             Y.log('Legacy alignment found and removed.', 'info', 'atto_image-button');
937             imageNode.addClass(this._getAlignmentClass(alignment.value));
938             imageNode.setStyle(alignment.name, null);
939             imageNode.setStyle('margin', null);
941             return true;
942         }, this);
944         return imageNode;
945     },
947     _getAlignmentClass: function(alignment) {
948         return CSS.ALIGNSETTINGS + '_' + alignment;
949     },
951     /**
952      * Update the alt text warning live.
953      *
954      * @method _updateWarning
955      * @return {boolean} whether a warning should be displayed.
956      * @private
957      */
958     _updateWarning: function() {
959         var form = this._form,
960             state = true,
961             alt = form.one('.' + CSS.INPUTALT).get('value'),
962             presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked');
963         if (alt === '' && !presentation) {
964             form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'block');
965             form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', true);
966             form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', true);
967             state = true;
968         } else {
969             form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'none');
970             form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', false);
971             form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', false);
972             state = false;
973         }
974         this.getDialogue().centerDialogue();
975         return state;
976     }
977 });
980 }, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});