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