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