Merge branch 'MDL-43996-master' of git://github.com/jethac/moodle
[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     },
51     SELECTORS = {
52         INPUTURL: '.' + CSS.INPUTURL
53     },
54     ALIGNMENTS = [
55         // Vertical alignment.
56         {
57             name: 'text-top',
58             str: 'alignment_top',
59             value: 'vertical-align',
60             margin: '0 .5em'
61         }, {
62             name: 'middle',
63             str: 'alignment_middle',
64             value: 'vertical-align',
65             margin: '0 .5em'
66         }, {
67             name: 'text-bottom',
68             str: 'alignment_bottom',
69             value: 'vertical-align',
70             margin: '0 .5em',
71             isDefault: true
72         },
74         // Floats.
75         {
76             name: 'left',
77             str: 'alignment_left',
78             value: 'float',
79             margin: '0 .5em 0 0'
80         }, {
81             name: 'right',
82             str: 'alignment_right',
83             value: 'float',
84             margin: '0 0 0 .5em'
85         }, {
86             name: 'customstyle',
87             str: 'customstyle',
88             value: 'style'
89         }
90     ],
92     REGEX = {
93         ISPERCENT: /\d+%/
94     },
96     COMPONENTNAME = 'atto_image',
98     TEMPLATE = '' +
99             '<form class="atto_form">' +
100                 '<label for="{{elementid}}_{{CSS.INPUTURL}}">{{get_string "enterurl" component}}</label>' +
101                 '<input class="fullwidth {{CSS.INPUTURL}}" type="url" id="{{elementid}}_{{CSS.INPUTURL}}" size="32"/>' +
102                 '<br/>' +
104                 // Add the repository browser button.
105                 '{{#if showFilepicker}}' +
106                     '<button class="{{CSS.IMAGEBROWSER}}" type="button">{{get_string "browserepositories" component}}</button>' +
107                 '{{/if}}' +
109                 // Add the Alt box.
110                 '<div style="display:none" role="alert" class="warning {{CSS.IMAGEALTWARNING}}">' +
111                     '{{get_string "presentationoraltrequired" component}}' +
112                 '</div>' +
113                 '<label for="{{elementid}}_{{CSS.INPUTALT}}">{{get_string "enteralt" component}}</label>' +
114                 '<input class="fullwidth {{CSS.INPUTALT}}" type="text" value="" id="{{elementid}}_{{CSS.INPUTALT}}" size="32"/>' +
115                 '<br/>' +
117                 // Add the presentation select box.
118                 '<input type="checkbox" class="{{CSS.IMAGEPRESENTATION}}" id="{{elementid}}_{{CSS.IMAGEPRESENTATION}}"/>' +
119                 '<label class="sameline" for="{{elementid}}_{{CSS.IMAGEPRESENTATION}}">{{get_string "presentation" component}}</label>' +
120                 '<br/>' +
122                 // Add the size entry boxes.
123                 '<label class="sameline" for="{{elementid}}_{{CSS.INPUTSIZE}}">{{get_string "size" component}}</label>' +
124                 '<div id="{{elementid}}_{{CSS.INPUTSIZE}}" class="{{CSS.INPUTSIZE}}">' +
125                 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTWIDTH}}">{{get_string "width" component}}</label>' +
126                 '<input type="text" class="{{CSS.INPUTWIDTH}} input-mini" id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="4"/> x ' +
128                 // Add the height entry box.
129                 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTHEIGHT}}">{{get_string "height" component}}</label>' +
130                 '<input type="text" class="{{CSS.INPUTHEIGHT}} input-mini" id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +
132                 // Add the constrain checkbox.
133                 '<input type="checkbox" class="{{CSS.INPUTCONSTRAIN}} sameline" id="{{elementid}}_{{CSS.INPUTCONSTRAIN}}"/>' +
134                 '<label for="{{elementid}}_{{CSS.INPUTCONSTRAIN}}">{{get_string "constrain" component}}</label>' +
135                 '</div>' +
137                 // Add the alignment selector.
138                 '<label class="sameline" for="{{elementid}}_{{CSS.INPUTALIGNMENT}}">{{get_string "alignment" component}}</label>' +
139                 '<select class="{{CSS.INPUTALIGNMENT}}" id="{{elementid}}_{{CSS.INPUTALIGNMENT}}">' +
140                     '{{#each alignments}}' +
141                         '<option value="{{value}}:{{name}};">{{get_string str ../component}}</option>' +
142                     '{{/each}}' +
143                 '</select>' +
144                 // Hidden input to store custom styles.
145                 '<input type="hidden" class="{{CSS.INPUTCUSTOMSTYLE}}"/>' +
146                 '<br/>' +
148                 // Add the image preview.
149                 '<div class="mdl-align">' +
150                 '<div class="{{CSS.IMAGEPREVIEWBOX}}">' +
151                     '<img src="#" class="{{CSS.IMAGEPREVIEW}}" alt="" style="display: none;"/>' +
152                 '</div>' +
154                 // Add the submit button and close the form.
155                 '<button class="{{CSS.INPUTSUBMIT}}" type="submit">{{get_string "saveimage" component}}</button>' +
156                 '</div>' +
157             '</form>',
159         IMAGETEMPLATE = '' +
160             '<img src="{{url}}" alt="{{alt}}" ' +
161                 '{{#if width}}width="{{width}}" {{/if}}' +
162                 '{{#if height}}height="{{height}}" {{/if}}' +
163                 '{{#if presentation}}role="presentation" {{/if}}' +
164                 'style="{{alignment}}{{margin}}{{customstyle}}"' +
165                 '{{#if classlist}}class="{{classlist}}" {{/if}}' +
166                 '{{#if id}}id="{{id}}" {{/if}}' +
167                 '/>';
169 Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
170     /**
171      * A reference to the current selection at the time that the dialogue
172      * was opened.
173      *
174      * @property _currentSelection
175      * @type Range
176      * @private
177      */
178     _currentSelection: null,
180     /**
181      * The most recently selected image.
182      *
183      * @param _selectedImage
184      * @type Node
185      * @private
186      */
187     _selectedImage: null,
189     /**
190      * A reference to the currently open form.
191      *
192      * @param _form
193      * @type Node
194      * @private
195      */
196     _form: null,
198     /**
199      * The dimensions of the raw image before we manipulate it.
200      *
201      * @param _rawImageDimensions
202      * @type Object
203      * @private
204      */
205     _rawImageDimensions: null,
207     initializer: function() {
209         this.addButton({
210             icon: 'e/insert_edit_image',
211             callback: this._displayDialogue,
212             tags: 'img',
213             tagMatchRequiresAll: false
214         });
215         this.editor.delegate('dblclick', this._handleDoubleClick, 'img', this);
216         this.editor.on('drop', this._handleDragDrop, this);
217     },
219     /**
220      * Handle a drag and drop event with an image.
221      *
222      * @method _handleDragDrop
223      * @param {EventFacade} e
224      * @private
225      */
226     _handleDragDrop: function(e) {
228         var self = this,
229             host = this.get('host'),
230             template = Y.Handlebars.compile(IMAGETEMPLATE);
232         host.saveSelection();
233         e = e._event;
235         // Only handle the event if an image file was dropped in.
236         if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length && /^image\//.test(e.dataTransfer.files[0].type)) {
238             var options = host.get('filepickeroptions').image,
239                 savepath = (options.savepath === undefined) ? '/' : options.savepath,
240                 formData = new FormData(),
241                 timestamp = 0,
242                 uploadid = "",
243                 xhr = new XMLHttpRequest(),
244                 imagehtml = "",
245                 keys = Object.keys(options.repositories);
247             e.preventDefault();
248             e.stopPropagation();
249             formData.append('repo_upload_file', e.dataTransfer.files[0]);
250             formData.append('itemid', options.itemid);
252             // List of repositories is an object rather than an array.  This makes iteration more awkward.
253             for (var i = 0; i < keys.length; i++) {
254                 if (options.repositories[keys[i]].type === 'upload') {
255                     formData.append('repo_id', options.repositories[keys[i]].id);
256                     break;
257                 }
258             }
259             formData.append('env', options.env);
260             formData.append('sesskey', M.cfg.sesskey);
261             formData.append('client_id', options.client_id);
262             formData.append('savepath', savepath);
263             formData.append('ctx_id', options.context.id);
265             // Insert spinner as a placeholder.
266             timestamp = new Date().getTime();
267             uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
268             host.focus();
269             host.restoreSelection();
270             imagehtml = template({
271                 url: M.util.image_url("i/loading_small", 'moodle'),
272                 alt: M.util.get_string('uploading', COMPONENTNAME),
273                 id: uploadid
274             });
275             host.insertContentAtFocusPoint(imagehtml);
276             self.markUpdated();
278             // Kick off a XMLHttpRequest.
279             xhr.onreadystatechange = function() {
280                 var placeholder = self.editor.one('#' + uploadid),
281                     result,
282                     file,
283                     newhtml,
284                     newimage;
286                 if (xhr.readyState === 4) {
287                     if (xhr.status === 200) {
288                         result = JSON.parse(xhr.responseText);
289                         if (result) {
290                             if (result.error) {
291                                 if (placeholder) {
292                                     placeholder.remove(true);
293                                 }
294                                 return new M.core.ajaxException(result);
295                             }
297                             file = result;
298                             if (result.event && result.event === 'fileexists') {
299                                 // A file with this name is already in use here - rename to avoid conflict.
300                                 // Chances are, it's a different image (stored in a different folder on the user's computer).
301                                 // If the user wants to reuse an existing image, they can copy/paste it within the editor.
302                                 file = result.newfile;
303                             }
305                             // Replace placeholder with actual image.
306                             newhtml = template({
307                                 url: file.url,
308                                 presentation: true
309                             });
310                             newimage = Y.Node.create(newhtml);
311                             if (placeholder) {
312                                 placeholder.replace(newimage);
313                             } else {
314                                 self.editor.appendChild(newimage);
315                             }
316                             self.markUpdated();
317                         }
318                     } else {
319                         alert(M.util.get_string('servererror', 'moodle'));
320                         if (placeholder) {
321                             placeholder.remove(true);
322                         }
323                     }
324                 }
325             };
326             xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
327             xhr.send(formData);
328         }
329         return false;
331     },
333     /**
334      * Handle a double click on an image.
335      *
336      * @method _handleDoubleClick
337      * @param {EventFacade} e
338      * @private
339      */
340     _handleDoubleClick: function(e) {
341         var image = e.target;
343         var selection = this.get('host').getSelectionFromNode(image);
344         this.get('host').setSelection(selection);
345         this._displayDialogue();
346     },
348     /**
349      * Display the image editing tool.
350      *
351      * @method _displayDialogue
352      * @private
353      */
354     _displayDialogue: function() {
355         // Store the current selection.
356         this._currentSelection = this.get('host').getSelection();
357         if (this._currentSelection === false) {
358             return;
359         }
361         // Reset the image dimensions.
362         this._rawImageDimensions = null;
364         var dialogue = this.getDialogue({
365             headerContent: M.util.get_string('imageproperties', COMPONENTNAME),
366             width: '480px',
367             focusAfterHide: true,
368             focusOnShowSelector: SELECTORS.INPUTURL
369         });
371         // Set the dialogue content, and then show the dialogue.
372         dialogue.set('bodyContent', this._getDialogueContent())
373                 .show();
374     },
376     /**
377      * Set the inputs for width and height if they are not set, and calculate
378      * if the constrain checkbox should be checked or not.
379      *
380      * @method _loadPreviewImage
381      * @param {String} url
382      * @private
383      */
384     _loadPreviewImage: function(url) {
385         var image = new Image(), self = this;
387         image.onerror = function() {
388             var preview = self._form.one('.' + CSS.IMAGEPREVIEW);
389             preview.setStyles({
390                 'display': 'none'
391             });
393             // Centre the dialogue when clearing the image preview.
394             self.getDialogue().centerDialogue();
395         };
397         image.onload = function() {
398             var input, currentwidth, currentheight, widthRatio, heightRatio;
400             self._rawImageDimensions = {
401                 width: this.width,
402                 height: this.height
403             };
405             input = self._form.one('.' + CSS.INPUTWIDTH);
406             currentwidth = input.get('value');
407             if (currentwidth === '') {
408                 input.set('value', this.width);
409                 currentwidth = "" + this.width;
410             }
411             input = self._form.one('.' + CSS.INPUTHEIGHT);
412             currentheight = input.get('value');
413             if (currentheight === '') {
414                 input.set('value', this.height);
415                 currentheight = "" + this.height;
416             }
417             input = self._form.one('.' + CSS.IMAGEPREVIEW);
418             input.setAttribute('src', this.src);
419             input.setStyles({
420                 'display': 'inline'
421             });
423             input = self._form.one('.' + CSS.INPUTCONSTRAIN);
424             if (currentwidth.match(REGEX.ISPERCENT) && currentheight.match(REGEX.ISPERCENT)) {
425                 input.set('checked', currentwidth === currentheight);
426             } else {
427                 if (this.width === 0) {
428                     this.width = 1;
429                 }
430                 if (this.height === 0) {
431                     this.height = 1;
432                 }
433                 // This is the same as comparing to 3 decimal places.
434                 widthRatio = Math.round(1000*parseInt(currentwidth, 10) / this.width);
435                 heightRatio = Math.round(1000*parseInt(currentheight, 10) / this.height);
436                 input.set('checked', widthRatio === heightRatio);
437             }
439             // Apply the image sizing.
440             self._autoAdjustSize(self);
442             // Centre the dialogue once the preview image has loaded.
443             self.getDialogue().centerDialogue();
444         };
446         image.src = url;
447     },
449     /**
450      * Return the dialogue content for the tool, attaching any required
451      * events.
452      *
453      * @method _getDialogueContent
454      * @return {Node} The content to place in the dialogue.
455      * @private
456      */
457     _getDialogueContent: function() {
458         var template = Y.Handlebars.compile(TEMPLATE),
459             canShowFilepicker = this.get('host').canShowFilepicker('image'),
460             content = Y.Node.create(template({
461                 elementid: this.get('host').get('elementid'),
462                 CSS: CSS,
463                 component: COMPONENTNAME,
464                 showFilepicker: canShowFilepicker,
465                 alignments: ALIGNMENTS
466             }));
468         this._form = content;
470         // Configure the view of the current image.
471         this._applyImageProperties(this._form);
473         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
474         this._form.one('.' + CSS.IMAGEPRESENTATION).on('change', this._updateWarning, this);
475         this._form.one('.' + CSS.INPUTALT).on('change', this._updateWarning, this);
476         this._form.one('.' + CSS.INPUTWIDTH).on('blur', this._autoAdjustSize, this);
477         this._form.one('.' + CSS.INPUTHEIGHT).on('blur', this._autoAdjustSize, this, true);
478         this._form.one('.' + CSS.INPUTCONSTRAIN).on('change', function(event) {
479             if (event.target.get('checked')) {
480                 this._autoAdjustSize(event);
481             }
482         }, this);
483         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
484         this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setImage, this);
486         if (canShowFilepicker) {
487             this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {
488                     this.get('host').showFilepicker('image', this._filepickerCallback, this);
489             }, this);
490         }
492         return content;
493     },
495     _autoAdjustSize: function(e, forceHeight) {
496         forceHeight = forceHeight || false;
498         var keyField = this._form.one('.' + CSS.INPUTWIDTH),
499             keyFieldType = 'width',
500             subField = this._form.one('.' + CSS.INPUTHEIGHT),
501             subFieldType = 'height',
502             constrainField = this._form.one('.' + CSS.INPUTCONSTRAIN),
503             keyFieldValue = keyField.get('value'),
504             subFieldValue = subField.get('value'),
505             imagePreview = this._form.one('.' + CSS.IMAGEPREVIEW),
506             rawPercentage,
507             rawSize;
509         // If we do not know the image size, do not do anything.
510         if (!this._rawImageDimensions) {
511             return;
512         }
514         // Set the width back to default if it is empty.
515         if (keyFieldValue === '') {
516             keyFieldValue = this._rawImageDimensions[keyFieldType];
517             keyField.set('value', keyFieldValue);
518             keyFieldValue = keyField.get('value');
519         }
521         // Clear the existing preview sizes.
522         imagePreview.setStyles({
523             width: null,
524             height: null
525         });
527         // Now update with the new values.
528         if (!constrainField.get('checked')) {
529             // We are not keeping the image proportion - update the preview accordingly.
531             // Width.
532             if (keyFieldValue.match(REGEX.ISPERCENT)) {
533                 rawPercentage = parseInt(keyFieldValue, 10);
534                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
535                 imagePreview.setStyle('width', rawSize + 'px');
536             } else {
537                 imagePreview.setStyle('width', keyFieldValue + 'px');
538             }
540             // Height.
541             if (subFieldValue.match(REGEX.ISPERCENT)) {
542                 rawPercentage = parseInt(subFieldValue, 10);
543                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
544                 imagePreview.setStyle('height', rawSize + 'px');
545             } else {
546                 imagePreview.setStyle('height', subFieldValue + 'px');
547             }
548         } else {
549             // We are keeping the image in proportion.
550             if (forceHeight) {
551                 // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.
552                 var _temporaryValue;
553                 _temporaryValue = keyField;
554                 keyField = subField;
555                 subField = _temporaryValue;
557                 _temporaryValue = keyFieldType;
558                 keyFieldType = subFieldType;
559                 subFieldType = _temporaryValue;
561                 _temporaryValue = keyFieldValue;
562                 keyFieldValue = subFieldValue;
563                 subFieldValue = _temporaryValue;
564             }
566             if (keyFieldValue.match(REGEX.ISPERCENT)) {
567                 // This is a percentage based change. Copy it verbatim.
568                 subFieldValue = keyFieldValue;
570                 // Set the width to the calculated pixel width.
571                 rawPercentage = parseInt(keyFieldValue, 10);
572                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
574                 // And apply the width/height to the container.
575                 imagePreview.setStyle('width', rawSize);
576                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
577                 imagePreview.setStyle('height', rawSize);
578             } else {
579                 // Calculate the scaled subFieldValue from the keyFieldValue.
580                 subFieldValue = Math.round((keyFieldValue / this._rawImageDimensions[keyFieldType]) *
581                         this._rawImageDimensions[subFieldType]);
583                 if (forceHeight) {
584                     imagePreview.setStyles({
585                         'width': subFieldValue,
586                         'height': keyFieldValue
587                     });
588                 } else {
589                     imagePreview.setStyles({
590                         'width': keyFieldValue,
591                         'height': subFieldValue
592                     });
593                 }
594             }
596             // Update the subField's value within the form to reflect the changes.
597             subField.set('value', subFieldValue);
598         }
599     },
601     /**
602      * Update the dialogue after an image was selected in the File Picker.
603      *
604      * @method _filepickerCallback
605      * @param {object} params The parameters provided by the filepicker
606      * containing information about the image.
607      * @private
608      */
609     _filepickerCallback: function(params) {
610         if (params.url !== '') {
611             var input = this._form.one('.' + CSS.INPUTURL);
612             input.set('value', params.url);
614             // Auto set the width and height.
615             this._form.one('.' + CSS.INPUTWIDTH).set('value', '');
616             this._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
618             // Load the preview image.
619             this._loadPreviewImage(params.url);
620         }
621     },
623     /**
624      * Applies properties of an existing image to the image dialogue for editing.
625      *
626      * @method _applyImageProperties
627      * @param {Node} form
628      * @private
629      */
630     _applyImageProperties: function(form) {
631         var properties = this._getSelectedImageProperties(),
632             img = form.one('.' + CSS.IMAGEPREVIEW),
633             i;
635         if (properties === false) {
636             img.setStyle('display', 'none');
637             // Set the default alignment.
638             for (i in ALIGNMENTS) {
639                 if (ALIGNMENTS[i].isDefault === true) {
640                     css = ALIGNMENTS[i].value + ':' + ALIGNMENTS[i].name + ';';
641                     form.one('.' + CSS.INPUTALIGNMENT).set('value', css);
642                 }
643             }
644             // Remove the custom style option if this is a new image.
645             form.one('.' + CSS.INPUTALIGNMENT).getDOMNode().options.remove(ALIGNMENTS.length - 1);
646             return;
647         }
649         if (properties.align) {
650             form.one('.' + CSS.INPUTALIGNMENT).set('value', properties.align);
651             // Remove the custom style option if we have a standard alignment.
652             form.one('.' + CSS.INPUTALIGNMENT).getDOMNode().options.remove(ALIGNMENTS.length - 1);
653         } else {
654             form.one('.' + CSS.INPUTALIGNMENT).set('value', 'style:customstyle;');
655         }
656         if (properties.customstyle) {
657             form.one('.' + CSS.INPUTCUSTOMSTYLE).set('value', properties.customstyle);
658         }
659         if (properties.width) {
660             form.one('.' + CSS.INPUTWIDTH).set('value', properties.width);
661         }
662         if (properties.height) {
663             form.one('.' + CSS.INPUTHEIGHT).set('value', properties.height);
664         }
665         if (properties.alt) {
666             form.one('.' + CSS.INPUTALT).set('value', properties.alt);
667         }
668         if (properties.src) {
669             form.one('.' + CSS.INPUTURL).set('value', properties.src);
670             this._loadPreviewImage(properties.src);
671         }
672         if (properties.presentation) {
673             form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');
674         }
676         // Update the image preview based on the form properties.
677         this._autoAdjustSize();
678     },
680     /**
681      * Gets the properties of the currently selected image.
682      *
683      * The first image only if multiple images are selected.
684      *
685      * @method _getSelectedImageProperties
686      * @return {object}
687      * @private
688      */
689     _getSelectedImageProperties: function() {
690         var properties = {
691                 src: null,
692                 alt :null,
693                 width: null,
694                 height: null,
695                 align: '',
696                 presentation: false
697             },
699             // Get the current selection.
700             images = this.get('host').getSelectedNodes(),
701             i, width, height, style, css;
703         if (images) {
704             images = images.filter('img');
705         }
707         if (images && images.size()) {
708             image = images.item(0);
709             this._selectedImage = image;
711             style = image.getAttribute('style');
712             properties.customstyle = style;
713             style = style.replace(/ /g, '');
715             width = image.getAttribute('width');
716             if (!width.match(REGEX.ISPERCENT)) {
717                 width = parseInt(width, 10);
718             }
719             height = image.getAttribute('height');
720             if (!height.match(REGEX.ISPERCENT)) {
721                 height = parseInt(height, 10);
722             }
724             if (width !== 0) {
725                 properties.width = width;
726             }
727             if (height !== 0) {
728                 properties.height = height;
729             }
730             for (i in ALIGNMENTS) {
731                 css = ALIGNMENTS[i].value + ':' + ALIGNMENTS[i].name + ';';
732                 if (style.indexOf(css) !== -1) {
733                     margin = 'margin:' + ALIGNMENTS[i].margin + ';';
734                     margin = margin.replace(/ /g, '');
735                     // Must match alignment and margins - otherwise custom style is selected.
736                     if (style.indexOf(margin) !== -1) {
737                         properties.align = css;
738                         break;
739                     }
740                 }
741             }
742             properties.src = image.getAttribute('src');
743             properties.alt = image.getAttribute('alt') || '';
744             properties.presentation = (image.get('role') === 'presentation');
745             return properties;
746         }
748         // No image selected - clean up.
749         this._selectedImage = null;
750         return false;
751     },
753     /**
754      * Update the form when the URL was changed. This includes updating the
755      * height, width, and image preview.
756      *
757      * @method _urlChanged
758      * @private
759      */
760     _urlChanged: function() {
761         var input = this._form.one('.' + CSS.INPUTURL);
763         if (input.get('value') !== '') {
764             // Load the preview image.
765             this._loadPreviewImage(input.get('value'));
766         }
767     },
769     /**
770      * Update the image in the contenteditable.
771      *
772      * @method _setImage
773      * @param {EventFacade} e
774      * @private
775      */
776     _setImage: function(e) {
777         var form = this._form,
778             url = form.one('.' + CSS.INPUTURL).get('value'),
779             alt = form.one('.' + CSS.INPUTALT).get('value'),
780             width = form.one('.' + CSS.INPUTWIDTH).get('value'),
781             height = form.one('.' + CSS.INPUTHEIGHT).get('value'),
782             alignment = form.one('.' + CSS.INPUTALIGNMENT).get('value'),
783             margin = '',
784             presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked'),
785             constrain = form.one('.' + CSS.INPUTCONSTRAIN).get('checked'),
786             imagehtml,
787             customstyle = '',
788             i,
789             classlist = [],
790             host = this.get('host');
792         e.preventDefault();
794         // Check if there are any accessibility issues.
795         if (this._updateWarning()) {
796             return;
797         }
799         // Focus on the editor in preparation for inserting the image.
800         host.focus();
801         if (url !== '') {
802             if (this._selectedImage) {
803                 host.setSelection(host.getSelectionFromNode(this._selectedImage));
804             } else {
805                 host.setSelection(this._currentSelection);
806             }
808             if (alignment === 'style:customstyle;') {
809                 alignment = '';
810                 customstyle = form.one('.' + CSS.INPUTCUSTOMSTYLE).get('value');
811             } else {
812                 for (i in ALIGNMENTS) {
813                     css = ALIGNMENTS[i].value + ':' + ALIGNMENTS[i].name + ';';
814                     if (alignment === css) {
815                         margin = ' margin: ' + ALIGNMENTS[i].margin + ';';
816                     }
817                 }
818             }
820             if (constrain) {
821                 classlist.push(CSS.RESPONSIVE);
822             }
824             if (!width.match(REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {
825                 form.one('.' + CSS.INPUTWIDTH).focus();
826                 return;
827             }
828             if (!height.match(REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {
829                 form.one('.' + CSS.INPUTHEIGHT).focus();
830                 return;
831             }
833             template = Y.Handlebars.compile(IMAGETEMPLATE);
834             imagehtml = template({
835                 url: url,
836                 alt: alt,
837                 width: width,
838                 height: height,
839                 presentation: presentation,
840                 alignment: alignment,
841                 margin: margin,
842                 customstyle: customstyle,
843                 classlist: classlist.join(' ')
844             });
846             this.get('host').insertContentAtFocusPoint(imagehtml);
848             this.markUpdated();
849         }
851         this.getDialogue({
852             focusAfterHide: null
853         }).hide();
855     },
857     /**
858      * Update the alt text warning live.
859      *
860      * @method _updateWarning
861      * @return {boolean} whether a warning should be displayed.
862      * @private
863      */
864     _updateWarning: function() {
865         var form = this._form,
866             state = true,
867             alt = form.one('.' + CSS.INPUTALT).get('value'),
868             presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked');
869         if (alt === '' && !presentation) {
870             form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'block');
871             form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', true);
872             form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', true);
873             state = true;
874         } else {
875             form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'none');
876             form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', false);
877             form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', false);
878             state = false;
879         }
880         this.getDialogue().centerDialogue();
881         return state;
882     }
883 });