MDL-68541 editor-atto: Limiting alt text length, changing ignore text.
[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="mb-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="mb-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 mb-1 {{CSS.IMAGEALTWARNING}}">' +
121                     '{{get_string "presentationoraltrequired" component}}' +
122                 '</div>' +
123                 '<div class="mb-1">' +
124                 '<label for="{{elementid}}_{{CSS.INPUTALT}}">{{get_string "enteralt" component}}</label>' +
125                 '<textarea class="form-control fullwidth {{CSS.INPUTALT}}" ' +
126                 'id="{{elementid}}_{{CSS.INPUTALT}}" maxlength="125"></textarea>' +
128                 // Add the character count.
129                 '<div id="the-count" class="d-flex justify-content-end small">' +
130                 '<span id="currentcount">0</span>' +
131                 '<span id="maximumcount"> / 125</span>' +
132                 '</div>' +
134                 // Add the presentation select box.
135                 '<div class="form-check">' +
136                 '<input type="checkbox" class="form-check-input {{CSS.IMAGEPRESENTATION}}" ' +
137                     'id="{{elementid}}_{{CSS.IMAGEPRESENTATION}}"/>' +
138                 '<label class="form-check-label" for="{{elementid}}_{{CSS.IMAGEPRESENTATION}}">' +
139                     '{{get_string "presentation" component}}' +
140                 '</label>' +
141                 '</div>' +
142                 '</div>' +
144                 // Add the size entry boxes.
145                 '<div class="mb-1">' +
146                 '<label class="" for="{{elementid}}_{{CSS.INPUTSIZE}}">{{get_string "size" component}}</label>' +
147                 '<div id="{{elementid}}_{{CSS.INPUTSIZE}}" class="form-inline {{CSS.INPUTSIZE}}">' +
148                 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTWIDTH}}">{{get_string "width" component}}</label>' +
149                 '<input type="text" class="form-control mr-1 input-mini {{CSS.INPUTWIDTH}}" ' +
150                 'id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="4"/> x' +
152                 // Add the height entry box.
153                 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTHEIGHT}}">{{get_string "height" component}}</label>' +
154                 '<input type="text" class="form-control ml-1 input-mini {{CSS.INPUTHEIGHT}}" ' +
155                 'id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +
157                 // Add the constrain checkbox.
158                 '<div class="form-check ml-2">' +
159                 '<input type="checkbox" class="form-check-input {{CSS.INPUTCONSTRAIN}}" ' +
160                 'id="{{elementid}}_{{CSS.INPUTCONSTRAIN}}"/>' +
161                 '<label class="form-check-label" for="{{elementid}}_{{CSS.INPUTCONSTRAIN}}">' +
162                 '{{get_string "constrain" component}}</label>' +
163                 '</div>' +
164                 '</div>' +
165                 '</div>' +
167                 // Add the alignment selector.
168                 '<div class="form-inline mb-1">' +
169                 '<label class="for="{{elementid}}_{{CSS.INPUTALIGNMENT}}">{{get_string "alignment" component}}</label>' +
170                 '<select class="custom-select {{CSS.INPUTALIGNMENT}}" id="{{elementid}}_{{CSS.INPUTALIGNMENT}}">' +
171                     '{{#each alignments}}' +
172                         '<option value="{{value}}">{{get_string str ../component}}</option>' +
173                     '{{/each}}' +
174                 '</select>' +
175                 '</div>' +
176                 // Hidden input to store custom styles.
177                 '<input type="hidden" class="{{CSS.INPUTCUSTOMSTYLE}}"/>' +
178                 '<br/>' +
180                 // Add the image preview.
181                 '<div class="mdl-align">' +
182                 '<div class="{{CSS.IMAGEPREVIEWBOX}}">' +
183                     '<img src="#" class="{{CSS.IMAGEPREVIEW}}" alt="" style="display: none;"/>' +
184                 '</div>' +
186                 // Add the submit button and close the form.
187                 '<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
188                     '{{get_string "saveimage" component}}</button>' +
189                 '</div>' +
190             '</form>',
192         IMAGETEMPLATE = '' +
193             '<img src="{{url}}" alt="{{alt}}" ' +
194                 '{{#if width}}width="{{width}}" {{/if}}' +
195                 '{{#if height}}height="{{height}}" {{/if}}' +
196                 '{{#if presentation}}role="presentation" {{/if}}' +
197                 '{{#if customstyle}}style="{{customstyle}}" {{/if}}' +
198                 '{{#if classlist}}class="{{classlist}}" {{/if}}' +
199                 '{{#if id}}id="{{id}}" {{/if}}' +
200                 '/>';
202 Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
203     /**
204      * A reference to the current selection at the time that the dialogue
205      * was opened.
206      *
207      * @property _currentSelection
208      * @type Range
209      * @private
210      */
211     _currentSelection: null,
213     /**
214      * The most recently selected image.
215      *
216      * @param _selectedImage
217      * @type Node
218      * @private
219      */
220     _selectedImage: null,
222     /**
223      * A reference to the currently open form.
224      *
225      * @param _form
226      * @type Node
227      * @private
228      */
229     _form: null,
231     /**
232      * The dimensions of the raw image before we manipulate it.
233      *
234      * @param _rawImageDimensions
235      * @type Object
236      * @private
237      */
238     _rawImageDimensions: null,
240     initializer: function() {
242         this.addButton({
243             icon: 'e/insert_edit_image',
244             callback: this._displayDialogue,
245             tags: 'img',
246             tagMatchRequiresAll: false
247         });
248         this.editor.delegate('dblclick', this._displayDialogue, 'img', this);
249         this.editor.delegate('click', this._handleClick, 'img', this);
250         this.editor.on('drop', this._handleDragDrop, this);
252         // e.preventDefault needed to stop the default event from clobbering the desired behaviour in some browsers.
253         this.editor.on('dragover', function(e) {
254             e.preventDefault();
255         }, this);
256         this.editor.on('dragenter', function(e) {
257             e.preventDefault();
258         }, this);
259     },
261     /**
262      * Handle a drag and drop event with an image.
263      *
264      * @method _handleDragDrop
265      * @param {EventFacade} e
266      * @return mixed
267      * @private
268      */
269     _handleDragDrop: function(e) {
271         var self = this,
272             host = this.get('host'),
273             template = Y.Handlebars.compile(IMAGETEMPLATE);
275         host.saveSelection();
276         e = e._event;
278         // Only handle the event if an image file was dropped in.
279         var handlesDataTransfer = (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length);
280         if (handlesDataTransfer && /^image\//.test(e.dataTransfer.files[0].type)) {
282             var options = host.get('filepickeroptions').image,
283                 savepath = (options.savepath === undefined) ? '/' : options.savepath,
284                 formData = new FormData(),
285                 timestamp = 0,
286                 uploadid = "",
287                 xhr = new XMLHttpRequest(),
288                 imagehtml = "",
289                 keys = Object.keys(options.repositories);
291             e.preventDefault();
292             e.stopPropagation();
293             formData.append('repo_upload_file', e.dataTransfer.files[0]);
294             formData.append('itemid', options.itemid);
296             // List of repositories is an object rather than an array.  This makes iteration more awkward.
297             for (var i = 0; i < keys.length; i++) {
298                 if (options.repositories[keys[i]].type === 'upload') {
299                     formData.append('repo_id', options.repositories[keys[i]].id);
300                     break;
301                 }
302             }
303             formData.append('env', options.env);
304             formData.append('sesskey', M.cfg.sesskey);
305             formData.append('client_id', options.client_id);
306             formData.append('savepath', savepath);
307             formData.append('ctx_id', options.context.id);
309             // Insert spinner as a placeholder.
310             timestamp = new Date().getTime();
311             uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
312             host.focus();
313             host.restoreSelection();
314             imagehtml = template({
315                 url: M.util.image_url("i/loading_small", 'moodle'),
316                 alt: M.util.get_string('uploading', COMPONENTNAME),
317                 id: uploadid
318             });
319             host.insertContentAtFocusPoint(imagehtml);
320             self.markUpdated();
322             // Kick off a XMLHttpRequest.
323             xhr.onreadystatechange = function() {
324                 var placeholder = self.editor.one('#' + uploadid),
325                     result,
326                     file,
327                     newhtml,
328                     newimage;
330                 if (xhr.readyState === 4) {
331                     if (xhr.status === 200) {
332                         result = JSON.parse(xhr.responseText);
333                         if (result) {
334                             if (result.error) {
335                                 if (placeholder) {
336                                     placeholder.remove(true);
337                                 }
338                                 return new M.core.ajaxException(result);
339                             }
341                             file = result;
342                             if (result.event && result.event === 'fileexists') {
343                                 // A file with this name is already in use here - rename to avoid conflict.
344                                 // Chances are, it's a different image (stored in a different folder on the user's computer).
345                                 // If the user wants to reuse an existing image, they can copy/paste it within the editor.
346                                 file = result.newfile;
347                             }
349                             // Replace placeholder with actual image.
350                             newhtml = template({
351                                 url: file.url,
352                                 presentation: true
353                             });
354                             newimage = Y.Node.create(newhtml);
355                             if (placeholder) {
356                                 placeholder.replace(newimage);
357                             } else {
358                                 self.editor.appendChild(newimage);
359                             }
360                             self.markUpdated();
361                         }
362                     } else {
363                         Y.use('moodle-core-notification-alert', function() {
364                             new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
365                         });
366                         if (placeholder) {
367                             placeholder.remove(true);
368                         }
369                     }
370                 }
371             };
372             xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
373             xhr.send(formData);
374             return false;
375         }
377 },
379     /**
380      * Handle a click on an image.
381      *
382      * @method _handleClick
383      * @param {EventFacade} e
384      * @private
385      */
386     _handleClick: function(e) {
387         var image = e.target;
389         var selection = this.get('host').getSelectionFromNode(image);
390         if (this.get('host').getSelection() !== selection) {
391             this.get('host').setSelection(selection);
392         }
393     },
395     /**
396      * Display the image editing tool.
397      *
398      * @method _displayDialogue
399      * @private
400      */
401     _displayDialogue: function() {
402         // Store the current selection.
403         this._currentSelection = this.get('host').getSelection();
404         if (this._currentSelection === false) {
405             return;
406         }
408         // Reset the image dimensions.
409         this._rawImageDimensions = null;
411         var dialogue = this.getDialogue({
412             headerContent: M.util.get_string('imageproperties', COMPONENTNAME),
413             width: 'auto',
414             focusAfterHide: true,
415             focusOnShowSelector: SELECTORS.INPUTURL
416         });
418         // Set the dialogue content, and then show the dialogue.
419         dialogue.set('bodyContent', this._getDialogueContent())
420                 .show();
421     },
423     /**
424      * Set the inputs for width and height if they are not set, and calculate
425      * if the constrain checkbox should be checked or not.
426      *
427      * @method _loadPreviewImage
428      * @param {String} url
429      * @private
430      */
431     _loadPreviewImage: function(url) {
432         var image = new Image();
433         var self = this;
435         image.onerror = function() {
436             var preview = self._form.one('.' + CSS.IMAGEPREVIEW);
437             preview.setStyles({
438                 'display': 'none'
439             });
441             // Centre the dialogue when clearing the image preview.
442             self.getDialogue().centerDialogue();
443         };
445         image.onload = function() {
446             var input, currentwidth, currentheight, widthRatio, heightRatio;
448             self._rawImageDimensions = {
449                 width: this.width,
450                 height: this.height
451             };
453             input = self._form.one('.' + CSS.INPUTWIDTH);
454             currentwidth = input.get('value');
455             if (currentwidth === '') {
456                 input.set('value', this.width);
457                 currentwidth = "" + this.width;
458             }
459             input = self._form.one('.' + CSS.INPUTHEIGHT);
460             currentheight = input.get('value');
461             if (currentheight === '') {
462                 input.set('value', this.height);
463                 currentheight = "" + this.height;
464             }
465             input = self._form.one('.' + CSS.IMAGEPREVIEW);
466             input.setAttribute('src', this.src);
467             input.setStyles({
468                 'display': 'inline'
469             });
471             input = self._form.one('.' + CSS.INPUTCONSTRAIN);
472             if (currentwidth.match(REGEX.ISPERCENT) && currentheight.match(REGEX.ISPERCENT)) {
473                 input.set('checked', currentwidth === currentheight);
474             } else {
475                 if (this.width === 0) {
476                     this.width = 1;
477                 }
478                 if (this.height === 0) {
479                     this.height = 1;
480                 }
481                 // This is the same as comparing to 3 decimal places.
482                 widthRatio = Math.round(1000 * parseInt(currentwidth, 10) / this.width);
483                 heightRatio = Math.round(1000 * parseInt(currentheight, 10) / this.height);
484                 input.set('checked', widthRatio === heightRatio);
485             }
487             // Apply the image sizing.
488             self._autoAdjustSize(self);
490             // Centre the dialogue once the preview image has loaded.
491             self.getDialogue().centerDialogue();
492         };
494         image.src = url;
495     },
497     /**
498      * Return the dialogue content for the tool, attaching any required
499      * events.
500      *
501      * @method _getDialogueContent
502      * @return {Node} The content to place in the dialogue.
503      * @private
504      */
505     _getDialogueContent: function() {
506         var template = Y.Handlebars.compile(TEMPLATE),
507             canShowFilepicker = this.get('host').canShowFilepicker('image'),
508             content = Y.Node.create(template({
509                 elementid: this.get('host').get('elementid'),
510                 CSS: CSS,
511                 component: COMPONENTNAME,
512                 showFilepicker: canShowFilepicker,
513                 alignments: ALIGNMENTS
514             }));
516         this._form = content;
518         // Configure the view of the current image.
519         this._applyImageProperties(this._form);
521         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
522         this._form.one('.' + CSS.IMAGEPRESENTATION).on('change', this._updateWarning, this);
523         this._form.one('.' + CSS.INPUTALT).on('change', this._updateWarning, this);
524         this._form.one('.' + CSS.INPUTWIDTH).on('blur', this._autoAdjustSize, this);
525         this._form.one('.' + CSS.INPUTHEIGHT).on('blur', this._autoAdjustSize, this, true);
526         this._form.one('.' + CSS.INPUTCONSTRAIN).on('change', function(event) {
527             if (event.target.get('checked')) {
528                 this._autoAdjustSize(event);
529             }
530         }, this);
531         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
532         this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setImage, this);
534         if (canShowFilepicker) {
535             this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {
536                     this.get('host').showFilepicker('image', this._filepickerCallback, this);
537             }, this);
538         }
540         // Character count.
541         this._form.one('.' + CSS.INPUTALT).on('keyup', this._handleKeyup, this);
543         return content;
544     },
546     _autoAdjustSize: function(e, forceHeight) {
547         forceHeight = forceHeight || false;
549         var keyField = this._form.one('.' + CSS.INPUTWIDTH),
550             keyFieldType = 'width',
551             subField = this._form.one('.' + CSS.INPUTHEIGHT),
552             subFieldType = 'height',
553             constrainField = this._form.one('.' + CSS.INPUTCONSTRAIN),
554             keyFieldValue = keyField.get('value'),
555             subFieldValue = subField.get('value'),
556             imagePreview = this._form.one('.' + CSS.IMAGEPREVIEW),
557             rawPercentage,
558             rawSize;
560         // If we do not know the image size, do not do anything.
561         if (!this._rawImageDimensions) {
562             return;
563         }
565         // Set the width back to default if it is empty.
566         if (keyFieldValue === '') {
567             keyFieldValue = this._rawImageDimensions[keyFieldType];
568             keyField.set('value', keyFieldValue);
569             keyFieldValue = keyField.get('value');
570         }
572         // Clear the existing preview sizes.
573         imagePreview.setStyles({
574             width: null,
575             height: null
576         });
578         // Now update with the new values.
579         if (!constrainField.get('checked')) {
580             // We are not keeping the image proportion - update the preview accordingly.
582             // Width.
583             if (keyFieldValue.match(REGEX.ISPERCENT)) {
584                 rawPercentage = parseInt(keyFieldValue, 10);
585                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
586                 imagePreview.setStyle('width', rawSize + 'px');
587             } else {
588                 imagePreview.setStyle('width', keyFieldValue + 'px');
589             }
591             // Height.
592             if (subFieldValue.match(REGEX.ISPERCENT)) {
593                 rawPercentage = parseInt(subFieldValue, 10);
594                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
595                 imagePreview.setStyle('height', rawSize + 'px');
596             } else {
597                 imagePreview.setStyle('height', subFieldValue + 'px');
598             }
599         } else {
600             // We are keeping the image in proportion.
601             if (forceHeight) {
602                 // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.
603                 var _temporaryValue;
604                 _temporaryValue = keyField;
605                 keyField = subField;
606                 subField = _temporaryValue;
608                 _temporaryValue = keyFieldType;
609                 keyFieldType = subFieldType;
610                 subFieldType = _temporaryValue;
612                 _temporaryValue = keyFieldValue;
613                 keyFieldValue = subFieldValue;
614                 subFieldValue = _temporaryValue;
615             }
617             if (keyFieldValue.match(REGEX.ISPERCENT)) {
618                 // This is a percentage based change. Copy it verbatim.
619                 subFieldValue = keyFieldValue;
621                 // Set the width to the calculated pixel width.
622                 rawPercentage = parseInt(keyFieldValue, 10);
623                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
625                 // And apply the width/height to the container.
626                 imagePreview.setStyle('width', rawSize);
627                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
628                 imagePreview.setStyle('height', rawSize);
629             } else {
630                 // Calculate the scaled subFieldValue from the keyFieldValue.
631                 subFieldValue = Math.round((keyFieldValue / this._rawImageDimensions[keyFieldType]) *
632                         this._rawImageDimensions[subFieldType]);
634                 if (forceHeight) {
635                     imagePreview.setStyles({
636                         'width': subFieldValue,
637                         'height': keyFieldValue
638                     });
639                 } else {
640                     imagePreview.setStyles({
641                         'width': keyFieldValue,
642                         'height': subFieldValue
643                     });
644                 }
645             }
647             // Update the subField's value within the form to reflect the changes.
648             subField.set('value', subFieldValue);
649         }
650     },
652     /**
653      * Update the dialogue after an image was selected in the File Picker.
654      *
655      * @method _filepickerCallback
656      * @param {object} params The parameters provided by the filepicker
657      * containing information about the image.
658      * @private
659      */
660     _filepickerCallback: function(params) {
661         if (params.url !== '') {
662             var input = this._form.one('.' + CSS.INPUTURL);
663             input.set('value', params.url);
665             // Auto set the width and height.
666             this._form.one('.' + CSS.INPUTWIDTH).set('value', '');
667             this._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
669             // Load the preview image.
670             this._loadPreviewImage(params.url);
671         }
672     },
674     /**
675      * Applies properties of an existing image to the image dialogue for editing.
676      *
677      * @method _applyImageProperties
678      * @param {Node} form
679      * @private
680      */
681     _applyImageProperties: function(form) {
682         var properties = this._getSelectedImageProperties(),
683             img = form.one('.' + CSS.IMAGEPREVIEW);
685         if (properties === false) {
686             img.setStyle('display', 'none');
687             // Set the default alignment.
688             ALIGNMENTS.some(function(alignment) {
689                 if (alignment.isDefault) {
690                     form.one('.' + CSS.INPUTALIGNMENT).set('value', alignment.value);
691                     return true;
692                 }
694                 return false;
695             }, this);
697             return;
698         }
700         if (properties.align) {
701             form.one('.' + CSS.INPUTALIGNMENT).set('value', properties.align);
702         }
703         if (properties.customstyle) {
704             form.one('.' + CSS.INPUTCUSTOMSTYLE).set('value', properties.customstyle);
705         }
706         if (properties.width) {
707             form.one('.' + CSS.INPUTWIDTH).set('value', properties.width);
708         }
709         if (properties.height) {
710             form.one('.' + CSS.INPUTHEIGHT).set('value', properties.height);
711         }
712         if (properties.alt) {
713             form.one('.' + CSS.INPUTALT).set('value', properties.alt);
714         }
715         if (properties.src) {
716             form.one('.' + CSS.INPUTURL).set('value', properties.src);
717             this._loadPreviewImage(properties.src);
718         }
719         if (properties.presentation) {
720             form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');
721         }
723         // Update the image preview based on the form properties.
724         this._autoAdjustSize();
725     },
727     /**
728      * Gets the properties of the currently selected image.
729      *
730      * The first image only if multiple images are selected.
731      *
732      * @method _getSelectedImageProperties
733      * @return {object}
734      * @private
735      */
736     _getSelectedImageProperties: function() {
737         var properties = {
738                 src: null,
739                 alt: null,
740                 width: null,
741                 height: null,
742                 align: '',
743                 presentation: false
744             },
746             // Get the current selection.
747             images = this.get('host').getSelectedNodes(),
748             width,
749             height,
750             style,
751             image;
753         if (images) {
754             images = images.filter('img');
755         }
757         if (images && images.size()) {
758             image = this._removeLegacyAlignment(images.item(0));
759             this._selectedImage = image;
761             style = image.getAttribute('style');
762             properties.customstyle = style;
764             width = image.getAttribute('width');
765             if (!width.match(REGEX.ISPERCENT)) {
766                 width = parseInt(width, 10);
767             }
768             height = image.getAttribute('height');
769             if (!height.match(REGEX.ISPERCENT)) {
770                 height = parseInt(height, 10);
771             }
773             if (width !== 0) {
774                 properties.width = width;
775             }
776             if (height !== 0) {
777                 properties.height = height;
778             }
779             this._getAlignmentPropeties(image, properties);
780             properties.src = image.getAttribute('src');
781             properties.alt = image.getAttribute('alt') || '';
782             properties.presentation = (image.get('role') === 'presentation');
783             return properties;
784         }
786         // No image selected - clean up.
787         this._selectedImage = null;
788         return false;
789     },
791     /**
792      * Sets the alignment of a properties object.
793      *
794      * @method _getAlignmentPropeties
795      * @param {Node} image The image that the alignment properties should be found for
796      * @param {Object} properties The properties object that is created in _getSelectedImageProperties()
797      * @private
798      */
799     _getAlignmentPropeties: function(image, properties) {
800         var complete = false,
801             defaultAlignment;
803         // Check for an alignment value.
804         complete = ALIGNMENTS.some(function(alignment) {
805             var classname = this._getAlignmentClass(alignment.value);
806             if (image.hasClass(classname)) {
807                 properties.align = alignment.value;
808                 Y.log('Found alignment ' + alignment.value, 'debug', 'atto_image-button');
810                 return true;
811             }
813             if (alignment.isDefault) {
814                 defaultAlignment = alignment.value;
815             }
817             return false;
818         }, this);
820         if (!complete && defaultAlignment) {
821             properties.align = defaultAlignment;
822         }
823     },
825     /**
826      * Update the form when the URL was changed. This includes updating the
827      * height, width, and image preview.
828      *
829      * @method _urlChanged
830      * @private
831      */
832     _urlChanged: function() {
833         var input = this._form.one('.' + CSS.INPUTURL);
835         if (input.get('value') !== '') {
836             // Load the preview image.
837             this._loadPreviewImage(input.get('value'));
838         }
839     },
841     /**
842      * Update the image in the contenteditable.
843      *
844      * @method _setImage
845      * @param {EventFacade} e
846      * @private
847      */
848     _setImage: function(e) {
849         var form = this._form,
850             url = form.one('.' + CSS.INPUTURL).get('value'),
851             alt = form.one('.' + CSS.INPUTALT).get('value'),
852             width = form.one('.' + CSS.INPUTWIDTH).get('value'),
853             height = form.one('.' + CSS.INPUTHEIGHT).get('value'),
854             alignment = this._getAlignmentClass(form.one('.' + CSS.INPUTALIGNMENT).get('value')),
855             presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked'),
856             constrain = form.one('.' + CSS.INPUTCONSTRAIN).get('checked'),
857             imagehtml,
858             customstyle = form.one('.' + CSS.INPUTCUSTOMSTYLE).get('value'),
859             classlist = [],
860             host = this.get('host');
862         e.preventDefault();
864         // Check if there are any accessibility issues.
865         if (this._updateWarning()) {
866             return;
867         }
869         // Focus on the editor in preparation for inserting the image.
870         host.focus();
871         if (url !== '') {
872             if (this._selectedImage) {
873                 host.setSelection(host.getSelectionFromNode(this._selectedImage));
874             } else {
875                 host.setSelection(this._currentSelection);
876             }
878             if (constrain) {
879                 classlist.push(CSS.RESPONSIVE);
880             }
882             // Add the alignment class for the image.
883             classlist.push(alignment);
885             if (!width.match(REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {
886                 form.one('.' + CSS.INPUTWIDTH).focus();
887                 return;
888             }
889             if (!height.match(REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {
890                 form.one('.' + CSS.INPUTHEIGHT).focus();
891                 return;
892             }
894             var template = Y.Handlebars.compile(IMAGETEMPLATE);
895             imagehtml = template({
896                 url: url,
897                 alt: alt,
898                 width: width,
899                 height: height,
900                 presentation: presentation,
901                 customstyle: customstyle,
902                 classlist: classlist.join(' ')
903             });
905             this.get('host').insertContentAtFocusPoint(imagehtml);
907             this.markUpdated();
908         }
910         this.getDialogue({
911             focusAfterHide: null
912         }).hide();
914     },
916     /**
917      * Removes any legacy styles added by previous versions of the atto image button.
918      *
919      * @method _removeLegacyAlignment
920      * @param {Y.Node} imageNode
921      * @return {Y.Node}
922      * @private
923      */
924     _removeLegacyAlignment: function(imageNode) {
925         if (!imageNode.getStyle('margin')) {
926             // There is no margin therefore this cannot match any known alignments.
927             return imageNode;
928         }
930         ALIGNMENTS.some(function(alignment) {
931             if (imageNode.getStyle(alignment.name) !== alignment.value) {
932                 // The name/value do not match. Skip.
933                 return false;
934             }
936             var normalisedNode = Y.Node.create('<div>');
937             normalisedNode.setStyle('margin', alignment.margin);
938             if (imageNode.getStyle('margin') !== normalisedNode.getStyle('margin')) {
939                 // The margin does not match.
940                 return false;
941             }
943             Y.log('Legacy alignment found and removed.', 'info', 'atto_image-button');
944             imageNode.addClass(this._getAlignmentClass(alignment.value));
945             imageNode.setStyle(alignment.name, null);
946             imageNode.setStyle('margin', null);
948             return true;
949         }, this);
951         return imageNode;
952     },
954     _getAlignmentClass: function(alignment) {
955         return CSS.ALIGNSETTINGS + '_' + alignment;
956     },
958     /**
959      * Update the alt text warning live.
960      *
961      * @method _updateWarning
962      * @return {boolean} whether a warning should be displayed.
963      * @private
964      */
965     _updateWarning: function() {
966         var form = this._form,
967             state = true,
968             alt = form.one('.' + CSS.INPUTALT).get('value'),
969             presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked');
970         if (alt === '' && !presentation) {
971             form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'block');
972             form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', true);
973             form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', true);
974             state = true;
975         } else {
976             form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'none');
977             form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', false);
978             form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', false);
979             state = false;
980         }
981         this.getDialogue().centerDialogue();
982         return state;
983     },
985     /**
986      * Handle the keyup to update the character count.
987      */
988     _handleKeyup: function() {
989         var form = this._form,
990             alt = form.one('.' + CSS.INPUTALT).get('value'),
991             characterCount = alt.length,
992             current = form.one('#currentcount');
993         current.setHTML(characterCount);
994     }
995 });