Merge branch 'MDL-68615' of https://github.com/timhunt/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         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                 '<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="mb-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 mr-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 ml-1 input-mini {{CSS.INPUTHEIGHT}}" ' +
149                 'id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +
151                 // Add the constrain checkbox.
152                 '<div class="form-check ml-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 mb-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('paste', this._handlePaste, this);
245         this.editor.on('drop', this._handleDragDrop, this);
247         // e.preventDefault needed to stop the default event from clobbering the desired behaviour in some browsers.
248         this.editor.on('dragover', function(e) {
249             e.preventDefault();
250         }, this);
251         this.editor.on('dragenter', function(e) {
252             e.preventDefault();
253         }, this);
254     },
256     /**
257      * Handle a drag and drop event with an image.
258      *
259      * @method _handleDragDrop
260      * @param {EventFacade} e
261      * @return {boolean} false if we handled the event, else true.
262      * @private
263      */
264     _handleDragDrop: function(e) {
265         if (!e._event || !e._event.dataTransfer) {
266             // Drop not fully supported in this browser.
267             return true;
268         }
270         return this._handlePasteOrDropHelper(e, e._event.dataTransfer);
271     },
273     /**
274      * Handles paste events where - if the thing being pasted is an image.
275      *
276      * @method _handlePaste
277      * @param {EventFacade} e
278      * @return {boolean} false if we handled the event, else true.
279      * @private
280      */
281     _handlePaste: function(e) {
282         if (!e._event || !e._event.clipboardData) {
283             // Paste not fully supported in this browser.
284             return true;
285         }
287         return this._handlePasteOrDropHelper(e, e._event.clipboardData);
288     },
290     /**
291      * Handle a drag and drop event with an image.
292      *
293      * @method _handleDragDrop
294      * @param {EventFacade} e
295      * @param {DataTransfer} dataTransfer
296      * @return {boolean} false if we handled the event, else true.
297      * @private
298      */
299     _handlePasteOrDropHelper: function(e, dataTransfer) {
301         var items = dataTransfer.items,
302             didUpload = false;
303         for (var i = 0; i < items.length; i++) {
304             var item = items[i];
305             if (item.kind !== 'file') {
306                 continue;
307             }
308             if (!this._isImage(item.type)) {
309                 continue;
310             }
311             this._uploadImage(item.getAsFile());
312             didUpload = true;
313         }
315         if (didUpload) {
316             // We handled this.
317             e.preventDefault();
318             e.stopPropagation();
319             return false;
320         } else {
321             // Let someone else try to handle it.
322             return true;
323         }
324     },
326     /**
327      * Is this file an image?
328      *
329      * @method _isImage
330      * @param {string} mimeType the file's mime type.
331      * @return {boolean} true if the file has an image mimeType.
332      * @private
333      */
334     _isImage: function(mimeType) {
335         return mimeType.indexOf('image/') === 0;
336     },
338     /**
339      * Used by _handleDragDrop and _handlePaste to upload an image and insert it.
340      *
341      * @method _uploadImage
342      * @param {File} fileToSave
343      * @private
344      */
345     _uploadImage: function(fileToSave) {
347         var self = this,
348             host = this.get('host'),
349             template = Y.Handlebars.compile(IMAGETEMPLATE);
351         host.saveSelection();
353         var options = host.get('filepickeroptions').image,
354             savepath = (options.savepath === undefined) ? '/' : options.savepath,
355             formData = new FormData(),
356             timestamp = 0,
357             uploadid = "",
358             xhr = new XMLHttpRequest(),
359             imagehtml = "",
360             keys = Object.keys(options.repositories);
362         formData.append('repo_upload_file', fileToSave);
363         formData.append('itemid', options.itemid);
365         // List of repositories is an object rather than an array.  This makes iteration more awkward.
366         for (var i = 0; i < keys.length; i++) {
367             if (options.repositories[keys[i]].type === 'upload') {
368                 formData.append('repo_id', options.repositories[keys[i]].id);
369                 break;
370             }
371         }
372         formData.append('env', options.env);
373         formData.append('sesskey', M.cfg.sesskey);
374         formData.append('client_id', options.client_id);
375         formData.append('savepath', savepath);
376         formData.append('ctx_id', options.context.id);
378         // Insert spinner as a placeholder.
379         timestamp = new Date().getTime();
380         uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
381         host.focus();
382         host.restoreSelection();
383         imagehtml = template({
384             url: M.util.image_url("i/loading_small", 'moodle'),
385             alt: M.util.get_string('uploading', COMPONENTNAME),
386             id: uploadid
387         });
388         host.insertContentAtFocusPoint(imagehtml);
389         self.markUpdated();
391         // Kick off a XMLHttpRequest.
392         xhr.onreadystatechange = function() {
393             var placeholder = self.editor.one('#' + uploadid),
394                 result,
395                 file,
396                 newhtml,
397                 newimage;
399             if (xhr.readyState === 4) {
400                 if (xhr.status === 200) {
401                     result = JSON.parse(xhr.responseText);
402                     if (result) {
403                         if (result.error) {
404                             if (placeholder) {
405                                 placeholder.remove(true);
406                             }
407                             throw new M.core.ajaxException(result);
408                         }
410                         file = result;
411                         if (result.event && result.event === 'fileexists') {
412                             // A file with this name is already in use here - rename to avoid conflict.
413                             // Chances are, it's a different image (stored in a different folder on the user's computer).
414                             // If the user wants to reuse an existing image, they can copy/paste it within the editor.
415                             file = result.newfile;
416                         }
418                         // Replace placeholder with actual image.
419                         newhtml = template({
420                             url: file.url,
421                             presentation: true
422                         });
423                         newimage = Y.Node.create(newhtml);
424                         if (placeholder) {
425                             placeholder.replace(newimage);
426                         } else {
427                             self.editor.appendChild(newimage);
428                         }
429                         self.markUpdated();
430                     }
431                 } else {
432                     Y.use('moodle-core-notification-alert', function() {
433                         new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
434                     });
435                     if (placeholder) {
436                         placeholder.remove(true);
437                     }
438                 }
439             }
440         };
441         xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
442         xhr.send(formData);
443     },
445     /**
446      * Handle a click on an image.
447      *
448      * @method _handleClick
449      * @param {EventFacade} e
450      * @private
451      */
452     _handleClick: function(e) {
453         var image = e.target;
455         var selection = this.get('host').getSelectionFromNode(image);
456         if (this.get('host').getSelection() !== selection) {
457             this.get('host').setSelection(selection);
458         }
459     },
461     /**
462      * Display the image editing tool.
463      *
464      * @method _displayDialogue
465      * @private
466      */
467     _displayDialogue: function() {
468         // Store the current selection.
469         this._currentSelection = this.get('host').getSelection();
470         if (this._currentSelection === false) {
471             return;
472         }
474         // Reset the image dimensions.
475         this._rawImageDimensions = null;
477         var dialogue = this.getDialogue({
478             headerContent: M.util.get_string('imageproperties', COMPONENTNAME),
479             width: 'auto',
480             focusAfterHide: true,
481             focusOnShowSelector: SELECTORS.INPUTURL
482         });
484         // Set the dialogue content, and then show the dialogue.
485         dialogue.set('bodyContent', this._getDialogueContent())
486                 .show();
487     },
489     /**
490      * Set the inputs for width and height if they are not set, and calculate
491      * if the constrain checkbox should be checked or not.
492      *
493      * @method _loadPreviewImage
494      * @param {String} url
495      * @private
496      */
497     _loadPreviewImage: function(url) {
498         var image = new Image();
499         var self = this;
501         image.onerror = function() {
502             var preview = self._form.one('.' + CSS.IMAGEPREVIEW);
503             preview.setStyles({
504                 'display': 'none'
505             });
507             // Centre the dialogue when clearing the image preview.
508             self.getDialogue().centerDialogue();
509         };
511         image.onload = function() {
512             var input, currentwidth, currentheight, widthRatio, heightRatio;
514             self._rawImageDimensions = {
515                 width: this.width,
516                 height: this.height
517             };
519             input = self._form.one('.' + CSS.INPUTWIDTH);
520             currentwidth = input.get('value');
521             if (currentwidth === '') {
522                 input.set('value', this.width);
523                 currentwidth = "" + this.width;
524             }
525             input = self._form.one('.' + CSS.INPUTHEIGHT);
526             currentheight = input.get('value');
527             if (currentheight === '') {
528                 input.set('value', this.height);
529                 currentheight = "" + this.height;
530             }
531             input = self._form.one('.' + CSS.IMAGEPREVIEW);
532             input.setAttribute('src', this.src);
533             input.setStyles({
534                 'display': 'inline'
535             });
537             input = self._form.one('.' + CSS.INPUTCONSTRAIN);
538             if (currentwidth.match(REGEX.ISPERCENT) && currentheight.match(REGEX.ISPERCENT)) {
539                 input.set('checked', currentwidth === currentheight);
540             } else {
541                 if (this.width === 0) {
542                     this.width = 1;
543                 }
544                 if (this.height === 0) {
545                     this.height = 1;
546                 }
547                 // This is the same as comparing to 3 decimal places.
548                 widthRatio = Math.round(1000 * parseInt(currentwidth, 10) / this.width);
549                 heightRatio = Math.round(1000 * parseInt(currentheight, 10) / this.height);
550                 input.set('checked', widthRatio === heightRatio);
551             }
553             // Apply the image sizing.
554             self._autoAdjustSize(self);
556             // Centre the dialogue once the preview image has loaded.
557             self.getDialogue().centerDialogue();
558         };
560         image.src = url;
561     },
563     /**
564      * Return the dialogue content for the tool, attaching any required
565      * events.
566      *
567      * @method _getDialogueContent
568      * @return {Node} The content to place in the dialogue.
569      * @private
570      */
571     _getDialogueContent: function() {
572         var template = Y.Handlebars.compile(TEMPLATE),
573             canShowFilepicker = this.get('host').canShowFilepicker('image'),
574             content = Y.Node.create(template({
575                 elementid: this.get('host').get('elementid'),
576                 CSS: CSS,
577                 component: COMPONENTNAME,
578                 showFilepicker: canShowFilepicker,
579                 alignments: ALIGNMENTS
580             }));
582         this._form = content;
584         // Configure the view of the current image.
585         this._applyImageProperties(this._form);
587         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
588         this._form.one('.' + CSS.IMAGEPRESENTATION).on('change', this._updateWarning, this);
589         this._form.one('.' + CSS.INPUTALT).on('change', this._updateWarning, this);
590         this._form.one('.' + CSS.INPUTWIDTH).on('blur', this._autoAdjustSize, this);
591         this._form.one('.' + CSS.INPUTHEIGHT).on('blur', this._autoAdjustSize, this, true);
592         this._form.one('.' + CSS.INPUTCONSTRAIN).on('change', function(event) {
593             if (event.target.get('checked')) {
594                 this._autoAdjustSize(event);
595             }
596         }, this);
597         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
598         this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setImage, this);
600         if (canShowFilepicker) {
601             this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {
602                     this.get('host').showFilepicker('image', this._filepickerCallback, this);
603             }, this);
604         }
606         return content;
607     },
609     _autoAdjustSize: function(e, forceHeight) {
610         forceHeight = forceHeight || false;
612         var keyField = this._form.one('.' + CSS.INPUTWIDTH),
613             keyFieldType = 'width',
614             subField = this._form.one('.' + CSS.INPUTHEIGHT),
615             subFieldType = 'height',
616             constrainField = this._form.one('.' + CSS.INPUTCONSTRAIN),
617             keyFieldValue = keyField.get('value'),
618             subFieldValue = subField.get('value'),
619             imagePreview = this._form.one('.' + CSS.IMAGEPREVIEW),
620             rawPercentage,
621             rawSize;
623         // If we do not know the image size, do not do anything.
624         if (!this._rawImageDimensions) {
625             return;
626         }
628         // Set the width back to default if it is empty.
629         if (keyFieldValue === '') {
630             keyFieldValue = this._rawImageDimensions[keyFieldType];
631             keyField.set('value', keyFieldValue);
632             keyFieldValue = keyField.get('value');
633         }
635         // Clear the existing preview sizes.
636         imagePreview.setStyles({
637             width: null,
638             height: null
639         });
641         // Now update with the new values.
642         if (!constrainField.get('checked')) {
643             // We are not keeping the image proportion - update the preview accordingly.
645             // Width.
646             if (keyFieldValue.match(REGEX.ISPERCENT)) {
647                 rawPercentage = parseInt(keyFieldValue, 10);
648                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
649                 imagePreview.setStyle('width', rawSize + 'px');
650             } else {
651                 imagePreview.setStyle('width', keyFieldValue + 'px');
652             }
654             // Height.
655             if (subFieldValue.match(REGEX.ISPERCENT)) {
656                 rawPercentage = parseInt(subFieldValue, 10);
657                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
658                 imagePreview.setStyle('height', rawSize + 'px');
659             } else {
660                 imagePreview.setStyle('height', subFieldValue + 'px');
661             }
662         } else {
663             // We are keeping the image in proportion.
664             if (forceHeight) {
665                 // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.
666                 var _temporaryValue;
667                 _temporaryValue = keyField;
668                 keyField = subField;
669                 subField = _temporaryValue;
671                 _temporaryValue = keyFieldType;
672                 keyFieldType = subFieldType;
673                 subFieldType = _temporaryValue;
675                 _temporaryValue = keyFieldValue;
676                 keyFieldValue = subFieldValue;
677                 subFieldValue = _temporaryValue;
678             }
680             if (keyFieldValue.match(REGEX.ISPERCENT)) {
681                 // This is a percentage based change. Copy it verbatim.
682                 subFieldValue = keyFieldValue;
684                 // Set the width to the calculated pixel width.
685                 rawPercentage = parseInt(keyFieldValue, 10);
686                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
688                 // And apply the width/height to the container.
689                 imagePreview.setStyle('width', rawSize);
690                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
691                 imagePreview.setStyle('height', rawSize);
692             } else {
693                 // Calculate the scaled subFieldValue from the keyFieldValue.
694                 subFieldValue = Math.round((keyFieldValue / this._rawImageDimensions[keyFieldType]) *
695                         this._rawImageDimensions[subFieldType]);
697                 if (forceHeight) {
698                     imagePreview.setStyles({
699                         'width': subFieldValue,
700                         'height': keyFieldValue
701                     });
702                 } else {
703                     imagePreview.setStyles({
704                         'width': keyFieldValue,
705                         'height': subFieldValue
706                     });
707                 }
708             }
710             // Update the subField's value within the form to reflect the changes.
711             subField.set('value', subFieldValue);
712         }
713     },
715     /**
716      * Update the dialogue after an image was selected in the File Picker.
717      *
718      * @method _filepickerCallback
719      * @param {object} params The parameters provided by the filepicker
720      * containing information about the image.
721      * @private
722      */
723     _filepickerCallback: function(params) {
724         if (params.url !== '') {
725             var input = this._form.one('.' + CSS.INPUTURL);
726             input.set('value', params.url);
728             // Auto set the width and height.
729             this._form.one('.' + CSS.INPUTWIDTH).set('value', '');
730             this._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
732             // Load the preview image.
733             this._loadPreviewImage(params.url);
734         }
735     },
737     /**
738      * Applies properties of an existing image to the image dialogue for editing.
739      *
740      * @method _applyImageProperties
741      * @param {Node} form
742      * @private
743      */
744     _applyImageProperties: function(form) {
745         var properties = this._getSelectedImageProperties(),
746             img = form.one('.' + CSS.IMAGEPREVIEW);
748         if (properties === false) {
749             img.setStyle('display', 'none');
750             // Set the default alignment.
751             ALIGNMENTS.some(function(alignment) {
752                 if (alignment.isDefault) {
753                     form.one('.' + CSS.INPUTALIGNMENT).set('value', alignment.value);
754                     return true;
755                 }
757                 return false;
758             }, this);
760             return;
761         }
763         if (properties.align) {
764             form.one('.' + CSS.INPUTALIGNMENT).set('value', properties.align);
765         }
766         if (properties.customstyle) {
767             form.one('.' + CSS.INPUTCUSTOMSTYLE).set('value', properties.customstyle);
768         }
769         if (properties.width) {
770             form.one('.' + CSS.INPUTWIDTH).set('value', properties.width);
771         }
772         if (properties.height) {
773             form.one('.' + CSS.INPUTHEIGHT).set('value', properties.height);
774         }
775         if (properties.alt) {
776             form.one('.' + CSS.INPUTALT).set('value', properties.alt);
777         }
778         if (properties.src) {
779             form.one('.' + CSS.INPUTURL).set('value', properties.src);
780             this._loadPreviewImage(properties.src);
781         }
782         if (properties.presentation) {
783             form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');
784         }
786         // Update the image preview based on the form properties.
787         this._autoAdjustSize();
788     },
790     /**
791      * Gets the properties of the currently selected image.
792      *
793      * The first image only if multiple images are selected.
794      *
795      * @method _getSelectedImageProperties
796      * @return {object}
797      * @private
798      */
799     _getSelectedImageProperties: function() {
800         var properties = {
801                 src: null,
802                 alt: null,
803                 width: null,
804                 height: null,
805                 align: '',
806                 presentation: false
807             },
809             // Get the current selection.
810             images = this.get('host').getSelectedNodes(),
811             width,
812             height,
813             style,
814             image;
816         if (images) {
817             images = images.filter('img');
818         }
820         if (images && images.size()) {
821             image = this._removeLegacyAlignment(images.item(0));
822             this._selectedImage = image;
824             style = image.getAttribute('style');
825             properties.customstyle = style;
827             width = image.getAttribute('width');
828             if (!width.match(REGEX.ISPERCENT)) {
829                 width = parseInt(width, 10);
830             }
831             height = image.getAttribute('height');
832             if (!height.match(REGEX.ISPERCENT)) {
833                 height = parseInt(height, 10);
834             }
836             if (width !== 0) {
837                 properties.width = width;
838             }
839             if (height !== 0) {
840                 properties.height = height;
841             }
842             this._getAlignmentPropeties(image, properties);
843             properties.src = image.getAttribute('src');
844             properties.alt = image.getAttribute('alt') || '';
845             properties.presentation = (image.get('role') === 'presentation');
846             return properties;
847         }
849         // No image selected - clean up.
850         this._selectedImage = null;
851         return false;
852     },
854     /**
855      * Sets the alignment of a properties object.
856      *
857      * @method _getAlignmentPropeties
858      * @param {Node} image The image that the alignment properties should be found for
859      * @param {Object} properties The properties object that is created in _getSelectedImageProperties()
860      * @private
861      */
862     _getAlignmentPropeties: function(image, properties) {
863         var complete = false,
864             defaultAlignment;
866         // Check for an alignment value.
867         complete = ALIGNMENTS.some(function(alignment) {
868             var classname = this._getAlignmentClass(alignment.value);
869             if (image.hasClass(classname)) {
870                 properties.align = alignment.value;
871                 Y.log('Found alignment ' + alignment.value, 'debug', 'atto_image-button');
873                 return true;
874             }
876             if (alignment.isDefault) {
877                 defaultAlignment = alignment.value;
878             }
880             return false;
881         }, this);
883         if (!complete && defaultAlignment) {
884             properties.align = defaultAlignment;
885         }
886     },
888     /**
889      * Update the form when the URL was changed. This includes updating the
890      * height, width, and image preview.
891      *
892      * @method _urlChanged
893      * @private
894      */
895     _urlChanged: function() {
896         var input = this._form.one('.' + CSS.INPUTURL);
898         if (input.get('value') !== '') {
899             // Load the preview image.
900             this._loadPreviewImage(input.get('value'));
901         }
902     },
904     /**
905      * Update the image in the contenteditable.
906      *
907      * @method _setImage
908      * @param {EventFacade} e
909      * @private
910      */
911     _setImage: function(e) {
912         var form = this._form,
913             url = form.one('.' + CSS.INPUTURL).get('value'),
914             alt = form.one('.' + CSS.INPUTALT).get('value'),
915             width = form.one('.' + CSS.INPUTWIDTH).get('value'),
916             height = form.one('.' + CSS.INPUTHEIGHT).get('value'),
917             alignment = this._getAlignmentClass(form.one('.' + CSS.INPUTALIGNMENT).get('value')),
918             presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked'),
919             constrain = form.one('.' + CSS.INPUTCONSTRAIN).get('checked'),
920             imagehtml,
921             customstyle = form.one('.' + CSS.INPUTCUSTOMSTYLE).get('value'),
922             classlist = [],
923             host = this.get('host');
925         e.preventDefault();
927         // Check if there are any accessibility issues.
928         if (this._updateWarning()) {
929             return;
930         }
932         // Focus on the editor in preparation for inserting the image.
933         host.focus();
934         if (url !== '') {
935             if (this._selectedImage) {
936                 host.setSelection(host.getSelectionFromNode(this._selectedImage));
937             } else {
938                 host.setSelection(this._currentSelection);
939             }
941             if (constrain) {
942                 classlist.push(CSS.RESPONSIVE);
943             }
945             // Add the alignment class for the image.
946             classlist.push(alignment);
948             if (!width.match(REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {
949                 form.one('.' + CSS.INPUTWIDTH).focus();
950                 return;
951             }
952             if (!height.match(REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {
953                 form.one('.' + CSS.INPUTHEIGHT).focus();
954                 return;
955             }
957             var template = Y.Handlebars.compile(IMAGETEMPLATE);
958             imagehtml = template({
959                 url: url,
960                 alt: alt,
961                 width: width,
962                 height: height,
963                 presentation: presentation,
964                 customstyle: customstyle,
965                 classlist: classlist.join(' ')
966             });
968             this.get('host').insertContentAtFocusPoint(imagehtml);
970             this.markUpdated();
971         }
973         this.getDialogue({
974             focusAfterHide: null
975         }).hide();
977     },
979     /**
980      * Removes any legacy styles added by previous versions of the atto image button.
981      *
982      * @method _removeLegacyAlignment
983      * @param {Y.Node} imageNode
984      * @return {Y.Node}
985      * @private
986      */
987     _removeLegacyAlignment: function(imageNode) {
988         if (!imageNode.getStyle('margin')) {
989             // There is no margin therefore this cannot match any known alignments.
990             return imageNode;
991         }
993         ALIGNMENTS.some(function(alignment) {
994             if (imageNode.getStyle(alignment.name) !== alignment.value) {
995                 // The name/value do not match. Skip.
996                 return false;
997             }
999             var normalisedNode = Y.Node.create('<div>');
1000             normalisedNode.setStyle('margin', alignment.margin);
1001             if (imageNode.getStyle('margin') !== normalisedNode.getStyle('margin')) {
1002                 // The margin does not match.
1003                 return false;
1004             }
1006             Y.log('Legacy alignment found and removed.', 'info', 'atto_image-button');
1007             imageNode.addClass(this._getAlignmentClass(alignment.value));
1008             imageNode.setStyle(alignment.name, null);
1009             imageNode.setStyle('margin', null);
1011             return true;
1012         }, this);
1014         return imageNode;
1015     },
1017     _getAlignmentClass: function(alignment) {
1018         return CSS.ALIGNSETTINGS + '_' + alignment;
1019     },
1021     /**
1022      * Update the alt text warning live.
1023      *
1024      * @method _updateWarning
1025      * @return {boolean} whether a warning should be displayed.
1026      * @private
1027      */
1028     _updateWarning: function() {
1029         var form = this._form,
1030             state = true,
1031             alt = form.one('.' + CSS.INPUTALT).get('value'),
1032             presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked');
1033         if (alt === '' && !presentation) {
1034             form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'block');
1035             form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', true);
1036             form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', true);
1037             state = true;
1038         } else {
1039             form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'none');
1040             form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', false);
1041             form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', false);
1042             state = false;
1043         }
1044         this.getDialogue().centerDialogue();
1045         return state;
1046     }
1047 });