Merge branch 'MDL-68615' of https://github.com/timhunt/moodle
[moodle.git] / lib / editor / atto / plugins / image / yui / build / moodle-atto_image-button / moodle-atto_image-button.js
1 YUI.add('moodle-atto_image-button', function (Y, NAME) {
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /*
19  * @package    atto_image
20  * @copyright  2013 Damyon Wiese  <damyon@moodle.com>
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  */
24 /**
25  * @module moodle-atto_image_alignment-button
26  */
28 /**
29  * Atto image selection tool.
30  *
31  * @namespace M.atto_image
32  * @class Button
33  * @extends M.editor_atto.EditorPlugin
34  */
36 var CSS = {
37         RESPONSIVE: 'img-responsive',
38         INPUTALIGNMENT: 'atto_image_alignment',
39         INPUTALT: 'atto_image_altentry',
40         INPUTHEIGHT: 'atto_image_heightentry',
41         INPUTSUBMIT: 'atto_image_urlentrysubmit',
42         INPUTURL: 'atto_image_urlentry',
43         INPUTSIZE: 'atto_image_size',
44         INPUTWIDTH: 'atto_image_widthentry',
45         IMAGEALTWARNING: 'atto_image_altwarning',
46         IMAGEBROWSER: 'openimagebrowser',
47         IMAGEPRESENTATION: 'atto_image_presentation',
48         INPUTCONSTRAIN: 'atto_image_constrain',
49         INPUTCUSTOMSTYLE: 'atto_image_customstyle',
50         IMAGEPREVIEW: 'atto_image_preview',
51         IMAGEPREVIEWBOX: 'atto_image_preview_box',
52         ALIGNSETTINGS: 'atto_image_button'
53     },
54     SELECTORS = {
55         INPUTURL: '.' + CSS.INPUTURL
56     },
57     ALIGNMENTS = [
58         // Vertical alignment.
59         {
60             name: 'verticalAlign',
61             str: 'alignment_top',
62             value: 'text-top',
63             margin: '0 0.5em'
64         }, {
65             name: 'verticalAlign',
66             str: 'alignment_middle',
67             value: 'middle',
68             margin: '0 0.5em'
69         }, {
70             name: 'verticalAlign',
71             str: 'alignment_bottom',
72             value: 'text-bottom',
73             margin: '0 0.5em',
74             isDefault: true
75         },
77         // Floats.
78         {
79             name: 'float',
80             str: 'alignment_left',
81             value: 'left',
82             margin: '0 0.5em 0 0'
83         }, {
84             name: 'float',
85             str: 'alignment_right',
86             value: 'right',
87             margin: '0 0 0 0.5em'
88         }
89     ],
91     REGEX = {
92         ISPERCENT: /\d+%/
93     },
95     COMPONENTNAME = 'atto_image',
97     TEMPLATE = '' +
98             '<form class="atto_form">' +
100                 // Add the repository browser button.
101                 '{{#if showFilepicker}}' +
102                     '<div class="mb-1">' +
103                         '<label for="{{elementid}}_{{CSS.INPUTURL}}">{{get_string "enterurl" component}}</label>' +
104                         '<div class="input-group input-append w-100">' +
105                             '<input class="form-control {{CSS.INPUTURL}}" type="url" ' +
106                             'id="{{elementid}}_{{CSS.INPUTURL}}" size="32"/>' +
107                             '<span class="input-group-append">' +
108                                 '<button class="btn btn-secondary {{CSS.IMAGEBROWSER}}" type="button">' +
109                                 '{{get_string "browserepositories" component}}</button>' +
110                             '</span>' +
111                         '</div>' +
112                     '</div>' +
113                 '{{else}}' +
114                     '<div class="mb-1">' +
115                         '<label for="{{elementid}}_{{CSS.INPUTURL}}">{{get_string "enterurl" component}}</label>' +
116                         '<input class="form-control fullwidth {{CSS.INPUTURL}}" type="url" ' +
117                         'id="{{elementid}}_{{CSS.INPUTURL}}" size="32"/>' +
118                     '</div>' +
119                 '{{/if}}' +
121                 // Add the Alt box.
122                 '<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.IMAGEALTWARNING}}">' +
123                     '{{get_string "presentationoraltrequired" component}}' +
124                 '</div>' +
125                 '<div class="mb-1">' +
126                 '<label for="{{elementid}}_{{CSS.INPUTALT}}">{{get_string "enteralt" component}}</label>' +
127                 '<input class="form-control fullwidth {{CSS.INPUTALT}}" type="text" value="" ' +
128                 'id="{{elementid}}_{{CSS.INPUTALT}}" size="32"/>' +
130                 // Add the presentation select box.
131                 '<div class="form-check">' +
132                 '<input type="checkbox" class="form-check-input {{CSS.IMAGEPRESENTATION}}" ' +
133                     'id="{{elementid}}_{{CSS.IMAGEPRESENTATION}}"/>' +
134                 '<label class="form-check-label" for="{{elementid}}_{{CSS.IMAGEPRESENTATION}}">' +
135                     '{{get_string "presentation" component}}' +
136                 '</label>' +
137                 '</div>' +
138                 '</div>' +
140                 // Add the size entry boxes.
141                 '<div class="mb-1">' +
142                 '<label class="" for="{{elementid}}_{{CSS.INPUTSIZE}}">{{get_string "size" component}}</label>' +
143                 '<div id="{{elementid}}_{{CSS.INPUTSIZE}}" class="form-inline {{CSS.INPUTSIZE}}">' +
144                 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTWIDTH}}">{{get_string "width" component}}</label>' +
145                 '<input type="text" class="form-control mr-1 input-mini {{CSS.INPUTWIDTH}}" ' +
146                 'id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="4"/> x' +
148                 // Add the height entry box.
149                 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTHEIGHT}}">{{get_string "height" component}}</label>' +
150                 '<input type="text" class="form-control ml-1 input-mini {{CSS.INPUTHEIGHT}}" ' +
151                 'id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +
153                 // Add the constrain checkbox.
154                 '<div class="form-check ml-2">' +
155                 '<input type="checkbox" class="form-check-input {{CSS.INPUTCONSTRAIN}}" ' +
156                 'id="{{elementid}}_{{CSS.INPUTCONSTRAIN}}"/>' +
157                 '<label class="form-check-label" for="{{elementid}}_{{CSS.INPUTCONSTRAIN}}">' +
158                 '{{get_string "constrain" component}}</label>' +
159                 '</div>' +
160                 '</div>' +
161                 '</div>' +
163                 // Add the alignment selector.
164                 '<div class="form-inline mb-1">' +
165                 '<label class="for="{{elementid}}_{{CSS.INPUTALIGNMENT}}">{{get_string "alignment" component}}</label>' +
166                 '<select class="custom-select {{CSS.INPUTALIGNMENT}}" id="{{elementid}}_{{CSS.INPUTALIGNMENT}}">' +
167                     '{{#each alignments}}' +
168                         '<option value="{{value}}">{{get_string str ../component}}</option>' +
169                     '{{/each}}' +
170                 '</select>' +
171                 '</div>' +
172                 // Hidden input to store custom styles.
173                 '<input type="hidden" class="{{CSS.INPUTCUSTOMSTYLE}}"/>' +
174                 '<br/>' +
176                 // Add the image preview.
177                 '<div class="mdl-align">' +
178                 '<div class="{{CSS.IMAGEPREVIEWBOX}}">' +
179                     '<img src="#" class="{{CSS.IMAGEPREVIEW}}" alt="" style="display: none;"/>' +
180                 '</div>' +
182                 // Add the submit button and close the form.
183                 '<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
184                     '{{get_string "saveimage" component}}</button>' +
185                 '</div>' +
186             '</form>',
188         IMAGETEMPLATE = '' +
189             '<img src="{{url}}" alt="{{alt}}" ' +
190                 '{{#if width}}width="{{width}}" {{/if}}' +
191                 '{{#if height}}height="{{height}}" {{/if}}' +
192                 '{{#if presentation}}role="presentation" {{/if}}' +
193                 '{{#if customstyle}}style="{{customstyle}}" {{/if}}' +
194                 '{{#if classlist}}class="{{classlist}}" {{/if}}' +
195                 '{{#if id}}id="{{id}}" {{/if}}' +
196                 '/>';
198 Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
199     /**
200      * A reference to the current selection at the time that the dialogue
201      * was opened.
202      *
203      * @property _currentSelection
204      * @type Range
205      * @private
206      */
207     _currentSelection: null,
209     /**
210      * The most recently selected image.
211      *
212      * @param _selectedImage
213      * @type Node
214      * @private
215      */
216     _selectedImage: null,
218     /**
219      * A reference to the currently open form.
220      *
221      * @param _form
222      * @type Node
223      * @private
224      */
225     _form: null,
227     /**
228      * The dimensions of the raw image before we manipulate it.
229      *
230      * @param _rawImageDimensions
231      * @type Object
232      * @private
233      */
234     _rawImageDimensions: null,
236     initializer: function() {
238         this.addButton({
239             icon: 'e/insert_edit_image',
240             callback: this._displayDialogue,
241             tags: 'img',
242             tagMatchRequiresAll: false
243         });
244         this.editor.delegate('dblclick', this._displayDialogue, 'img', this);
245         this.editor.delegate('click', this._handleClick, 'img', this);
246         this.editor.on('paste', this._handlePaste, this);
247         this.editor.on('drop', this._handleDragDrop, this);
249         // e.preventDefault needed to stop the default event from clobbering the desired behaviour in some browsers.
250         this.editor.on('dragover', function(e) {
251             e.preventDefault();
252         }, this);
253         this.editor.on('dragenter', function(e) {
254             e.preventDefault();
255         }, this);
256     },
258     /**
259      * Handle a drag and drop event with an image.
260      *
261      * @method _handleDragDrop
262      * @param {EventFacade} e
263      * @return {boolean} false if we handled the event, else true.
264      * @private
265      */
266     _handleDragDrop: function(e) {
267         if (!e._event || !e._event.dataTransfer) {
268             // Drop not fully supported in this browser.
269             return true;
270         }
272         return this._handlePasteOrDropHelper(e, e._event.dataTransfer);
273     },
275     /**
276      * Handles paste events where - if the thing being pasted is an image.
277      *
278      * @method _handlePaste
279      * @param {EventFacade} e
280      * @return {boolean} false if we handled the event, else true.
281      * @private
282      */
283     _handlePaste: function(e) {
284         if (!e._event || !e._event.clipboardData) {
285             // Paste not fully supported in this browser.
286             return true;
287         }
289         return this._handlePasteOrDropHelper(e, e._event.clipboardData);
290     },
292     /**
293      * Handle a drag and drop event with an image.
294      *
295      * @method _handleDragDrop
296      * @param {EventFacade} e
297      * @param {DataTransfer} dataTransfer
298      * @return {boolean} false if we handled the event, else true.
299      * @private
300      */
301     _handlePasteOrDropHelper: function(e, dataTransfer) {
303         var items = dataTransfer.items,
304             didUpload = false;
305         for (var i = 0; i < items.length; i++) {
306             var item = items[i];
307             if (item.kind !== 'file') {
308                 continue;
309             }
310             if (!this._isImage(item.type)) {
311                 continue;
312             }
313             this._uploadImage(item.getAsFile());
314             didUpload = true;
315         }
317         if (didUpload) {
318             // We handled this.
319             e.preventDefault();
320             e.stopPropagation();
321             return false;
322         } else {
323             // Let someone else try to handle it.
324             return true;
325         }
326     },
328     /**
329      * Is this file an image?
330      *
331      * @method _isImage
332      * @param {string} mimeType the file's mime type.
333      * @return {boolean} true if the file has an image mimeType.
334      * @private
335      */
336     _isImage: function(mimeType) {
337         return mimeType.indexOf('image/') === 0;
338     },
340     /**
341      * Used by _handleDragDrop and _handlePaste to upload an image and insert it.
342      *
343      * @method _uploadImage
344      * @param {File} fileToSave
345      * @private
346      */
347     _uploadImage: function(fileToSave) {
349         var self = this,
350             host = this.get('host'),
351             template = Y.Handlebars.compile(IMAGETEMPLATE);
353         host.saveSelection();
355         var options = host.get('filepickeroptions').image,
356             savepath = (options.savepath === undefined) ? '/' : options.savepath,
357             formData = new FormData(),
358             timestamp = 0,
359             uploadid = "",
360             xhr = new XMLHttpRequest(),
361             imagehtml = "",
362             keys = Object.keys(options.repositories);
364         formData.append('repo_upload_file', fileToSave);
365         formData.append('itemid', options.itemid);
367         // List of repositories is an object rather than an array.  This makes iteration more awkward.
368         for (var i = 0; i < keys.length; i++) {
369             if (options.repositories[keys[i]].type === 'upload') {
370                 formData.append('repo_id', options.repositories[keys[i]].id);
371                 break;
372             }
373         }
374         formData.append('env', options.env);
375         formData.append('sesskey', M.cfg.sesskey);
376         formData.append('client_id', options.client_id);
377         formData.append('savepath', savepath);
378         formData.append('ctx_id', options.context.id);
380         // Insert spinner as a placeholder.
381         timestamp = new Date().getTime();
382         uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
383         host.focus();
384         host.restoreSelection();
385         imagehtml = template({
386             url: M.util.image_url("i/loading_small", 'moodle'),
387             alt: M.util.get_string('uploading', COMPONENTNAME),
388             id: uploadid
389         });
390         host.insertContentAtFocusPoint(imagehtml);
391         self.markUpdated();
393         // Kick off a XMLHttpRequest.
394         xhr.onreadystatechange = function() {
395             var placeholder = self.editor.one('#' + uploadid),
396                 result,
397                 file,
398                 newhtml,
399                 newimage;
401             if (xhr.readyState === 4) {
402                 if (xhr.status === 200) {
403                     result = JSON.parse(xhr.responseText);
404                     if (result) {
405                         if (result.error) {
406                             if (placeholder) {
407                                 placeholder.remove(true);
408                             }
409                             throw new M.core.ajaxException(result);
410                         }
412                         file = result;
413                         if (result.event && result.event === 'fileexists') {
414                             // A file with this name is already in use here - rename to avoid conflict.
415                             // Chances are, it's a different image (stored in a different folder on the user's computer).
416                             // If the user wants to reuse an existing image, they can copy/paste it within the editor.
417                             file = result.newfile;
418                         }
420                         // Replace placeholder with actual image.
421                         newhtml = template({
422                             url: file.url,
423                             presentation: true
424                         });
425                         newimage = Y.Node.create(newhtml);
426                         if (placeholder) {
427                             placeholder.replace(newimage);
428                         } else {
429                             self.editor.appendChild(newimage);
430                         }
431                         self.markUpdated();
432                     }
433                 } else {
434                     Y.use('moodle-core-notification-alert', function() {
435                         new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
436                     });
437                     if (placeholder) {
438                         placeholder.remove(true);
439                     }
440                 }
441             }
442         };
443         xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
444         xhr.send(formData);
445     },
447     /**
448      * Handle a click on an image.
449      *
450      * @method _handleClick
451      * @param {EventFacade} e
452      * @private
453      */
454     _handleClick: function(e) {
455         var image = e.target;
457         var selection = this.get('host').getSelectionFromNode(image);
458         if (this.get('host').getSelection() !== selection) {
459             this.get('host').setSelection(selection);
460         }
461     },
463     /**
464      * Display the image editing tool.
465      *
466      * @method _displayDialogue
467      * @private
468      */
469     _displayDialogue: function() {
470         // Store the current selection.
471         this._currentSelection = this.get('host').getSelection();
472         if (this._currentSelection === false) {
473             return;
474         }
476         // Reset the image dimensions.
477         this._rawImageDimensions = null;
479         var dialogue = this.getDialogue({
480             headerContent: M.util.get_string('imageproperties', COMPONENTNAME),
481             width: 'auto',
482             focusAfterHide: true,
483             focusOnShowSelector: SELECTORS.INPUTURL
484         });
486         // Set the dialogue content, and then show the dialogue.
487         dialogue.set('bodyContent', this._getDialogueContent())
488                 .show();
489     },
491     /**
492      * Set the inputs for width and height if they are not set, and calculate
493      * if the constrain checkbox should be checked or not.
494      *
495      * @method _loadPreviewImage
496      * @param {String} url
497      * @private
498      */
499     _loadPreviewImage: function(url) {
500         var image = new Image();
501         var self = this;
503         image.onerror = function() {
504             var preview = self._form.one('.' + CSS.IMAGEPREVIEW);
505             preview.setStyles({
506                 'display': 'none'
507             });
509             // Centre the dialogue when clearing the image preview.
510             self.getDialogue().centerDialogue();
511         };
513         image.onload = function() {
514             var input, currentwidth, currentheight, widthRatio, heightRatio;
516             self._rawImageDimensions = {
517                 width: this.width,
518                 height: this.height
519             };
521             input = self._form.one('.' + CSS.INPUTWIDTH);
522             currentwidth = input.get('value');
523             if (currentwidth === '') {
524                 input.set('value', this.width);
525                 currentwidth = "" + this.width;
526             }
527             input = self._form.one('.' + CSS.INPUTHEIGHT);
528             currentheight = input.get('value');
529             if (currentheight === '') {
530                 input.set('value', this.height);
531                 currentheight = "" + this.height;
532             }
533             input = self._form.one('.' + CSS.IMAGEPREVIEW);
534             input.setAttribute('src', this.src);
535             input.setStyles({
536                 'display': 'inline'
537             });
539             input = self._form.one('.' + CSS.INPUTCONSTRAIN);
540             if (currentwidth.match(REGEX.ISPERCENT) && currentheight.match(REGEX.ISPERCENT)) {
541                 input.set('checked', currentwidth === currentheight);
542             } else {
543                 if (this.width === 0) {
544                     this.width = 1;
545                 }
546                 if (this.height === 0) {
547                     this.height = 1;
548                 }
549                 // This is the same as comparing to 3 decimal places.
550                 widthRatio = Math.round(1000 * parseInt(currentwidth, 10) / this.width);
551                 heightRatio = Math.round(1000 * parseInt(currentheight, 10) / this.height);
552                 input.set('checked', widthRatio === heightRatio);
553             }
555             // Apply the image sizing.
556             self._autoAdjustSize(self);
558             // Centre the dialogue once the preview image has loaded.
559             self.getDialogue().centerDialogue();
560         };
562         image.src = url;
563     },
565     /**
566      * Return the dialogue content for the tool, attaching any required
567      * events.
568      *
569      * @method _getDialogueContent
570      * @return {Node} The content to place in the dialogue.
571      * @private
572      */
573     _getDialogueContent: function() {
574         var template = Y.Handlebars.compile(TEMPLATE),
575             canShowFilepicker = this.get('host').canShowFilepicker('image'),
576             content = Y.Node.create(template({
577                 elementid: this.get('host').get('elementid'),
578                 CSS: CSS,
579                 component: COMPONENTNAME,
580                 showFilepicker: canShowFilepicker,
581                 alignments: ALIGNMENTS
582             }));
584         this._form = content;
586         // Configure the view of the current image.
587         this._applyImageProperties(this._form);
589         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
590         this._form.one('.' + CSS.IMAGEPRESENTATION).on('change', this._updateWarning, this);
591         this._form.one('.' + CSS.INPUTALT).on('change', this._updateWarning, this);
592         this._form.one('.' + CSS.INPUTWIDTH).on('blur', this._autoAdjustSize, this);
593         this._form.one('.' + CSS.INPUTHEIGHT).on('blur', this._autoAdjustSize, this, true);
594         this._form.one('.' + CSS.INPUTCONSTRAIN).on('change', function(event) {
595             if (event.target.get('checked')) {
596                 this._autoAdjustSize(event);
597             }
598         }, this);
599         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
600         this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setImage, this);
602         if (canShowFilepicker) {
603             this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {
604                     this.get('host').showFilepicker('image', this._filepickerCallback, this);
605             }, this);
606         }
608         return content;
609     },
611     _autoAdjustSize: function(e, forceHeight) {
612         forceHeight = forceHeight || false;
614         var keyField = this._form.one('.' + CSS.INPUTWIDTH),
615             keyFieldType = 'width',
616             subField = this._form.one('.' + CSS.INPUTHEIGHT),
617             subFieldType = 'height',
618             constrainField = this._form.one('.' + CSS.INPUTCONSTRAIN),
619             keyFieldValue = keyField.get('value'),
620             subFieldValue = subField.get('value'),
621             imagePreview = this._form.one('.' + CSS.IMAGEPREVIEW),
622             rawPercentage,
623             rawSize;
625         // If we do not know the image size, do not do anything.
626         if (!this._rawImageDimensions) {
627             return;
628         }
630         // Set the width back to default if it is empty.
631         if (keyFieldValue === '') {
632             keyFieldValue = this._rawImageDimensions[keyFieldType];
633             keyField.set('value', keyFieldValue);
634             keyFieldValue = keyField.get('value');
635         }
637         // Clear the existing preview sizes.
638         imagePreview.setStyles({
639             width: null,
640             height: null
641         });
643         // Now update with the new values.
644         if (!constrainField.get('checked')) {
645             // We are not keeping the image proportion - update the preview accordingly.
647             // Width.
648             if (keyFieldValue.match(REGEX.ISPERCENT)) {
649                 rawPercentage = parseInt(keyFieldValue, 10);
650                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
651                 imagePreview.setStyle('width', rawSize + 'px');
652             } else {
653                 imagePreview.setStyle('width', keyFieldValue + 'px');
654             }
656             // Height.
657             if (subFieldValue.match(REGEX.ISPERCENT)) {
658                 rawPercentage = parseInt(subFieldValue, 10);
659                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
660                 imagePreview.setStyle('height', rawSize + 'px');
661             } else {
662                 imagePreview.setStyle('height', subFieldValue + 'px');
663             }
664         } else {
665             // We are keeping the image in proportion.
666             if (forceHeight) {
667                 // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.
668                 var _temporaryValue;
669                 _temporaryValue = keyField;
670                 keyField = subField;
671                 subField = _temporaryValue;
673                 _temporaryValue = keyFieldType;
674                 keyFieldType = subFieldType;
675                 subFieldType = _temporaryValue;
677                 _temporaryValue = keyFieldValue;
678                 keyFieldValue = subFieldValue;
679                 subFieldValue = _temporaryValue;
680             }
682             if (keyFieldValue.match(REGEX.ISPERCENT)) {
683                 // This is a percentage based change. Copy it verbatim.
684                 subFieldValue = keyFieldValue;
686                 // Set the width to the calculated pixel width.
687                 rawPercentage = parseInt(keyFieldValue, 10);
688                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
690                 // And apply the width/height to the container.
691                 imagePreview.setStyle('width', rawSize);
692                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
693                 imagePreview.setStyle('height', rawSize);
694             } else {
695                 // Calculate the scaled subFieldValue from the keyFieldValue.
696                 subFieldValue = Math.round((keyFieldValue / this._rawImageDimensions[keyFieldType]) *
697                         this._rawImageDimensions[subFieldType]);
699                 if (forceHeight) {
700                     imagePreview.setStyles({
701                         'width': subFieldValue,
702                         'height': keyFieldValue
703                     });
704                 } else {
705                     imagePreview.setStyles({
706                         'width': keyFieldValue,
707                         'height': subFieldValue
708                     });
709                 }
710             }
712             // Update the subField's value within the form to reflect the changes.
713             subField.set('value', subFieldValue);
714         }
715     },
717     /**
718      * Update the dialogue after an image was selected in the File Picker.
719      *
720      * @method _filepickerCallback
721      * @param {object} params The parameters provided by the filepicker
722      * containing information about the image.
723      * @private
724      */
725     _filepickerCallback: function(params) {
726         if (params.url !== '') {
727             var input = this._form.one('.' + CSS.INPUTURL);
728             input.set('value', params.url);
730             // Auto set the width and height.
731             this._form.one('.' + CSS.INPUTWIDTH).set('value', '');
732             this._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
734             // Load the preview image.
735             this._loadPreviewImage(params.url);
736         }
737     },
739     /**
740      * Applies properties of an existing image to the image dialogue for editing.
741      *
742      * @method _applyImageProperties
743      * @param {Node} form
744      * @private
745      */
746     _applyImageProperties: function(form) {
747         var properties = this._getSelectedImageProperties(),
748             img = form.one('.' + CSS.IMAGEPREVIEW);
750         if (properties === false) {
751             img.setStyle('display', 'none');
752             // Set the default alignment.
753             ALIGNMENTS.some(function(alignment) {
754                 if (alignment.isDefault) {
755                     form.one('.' + CSS.INPUTALIGNMENT).set('value', alignment.value);
756                     return true;
757                 }
759                 return false;
760             }, this);
762             return;
763         }
765         if (properties.align) {
766             form.one('.' + CSS.INPUTALIGNMENT).set('value', properties.align);
767         }
768         if (properties.customstyle) {
769             form.one('.' + CSS.INPUTCUSTOMSTYLE).set('value', properties.customstyle);
770         }
771         if (properties.width) {
772             form.one('.' + CSS.INPUTWIDTH).set('value', properties.width);
773         }
774         if (properties.height) {
775             form.one('.' + CSS.INPUTHEIGHT).set('value', properties.height);
776         }
777         if (properties.alt) {
778             form.one('.' + CSS.INPUTALT).set('value', properties.alt);
779         }
780         if (properties.src) {
781             form.one('.' + CSS.INPUTURL).set('value', properties.src);
782             this._loadPreviewImage(properties.src);
783         }
784         if (properties.presentation) {
785             form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');
786         }
788         // Update the image preview based on the form properties.
789         this._autoAdjustSize();
790     },
792     /**
793      * Gets the properties of the currently selected image.
794      *
795      * The first image only if multiple images are selected.
796      *
797      * @method _getSelectedImageProperties
798      * @return {object}
799      * @private
800      */
801     _getSelectedImageProperties: function() {
802         var properties = {
803                 src: null,
804                 alt: null,
805                 width: null,
806                 height: null,
807                 align: '',
808                 presentation: false
809             },
811             // Get the current selection.
812             images = this.get('host').getSelectedNodes(),
813             width,
814             height,
815             style,
816             image;
818         if (images) {
819             images = images.filter('img');
820         }
822         if (images && images.size()) {
823             image = this._removeLegacyAlignment(images.item(0));
824             this._selectedImage = image;
826             style = image.getAttribute('style');
827             properties.customstyle = style;
829             width = image.getAttribute('width');
830             if (!width.match(REGEX.ISPERCENT)) {
831                 width = parseInt(width, 10);
832             }
833             height = image.getAttribute('height');
834             if (!height.match(REGEX.ISPERCENT)) {
835                 height = parseInt(height, 10);
836             }
838             if (width !== 0) {
839                 properties.width = width;
840             }
841             if (height !== 0) {
842                 properties.height = height;
843             }
844             this._getAlignmentPropeties(image, properties);
845             properties.src = image.getAttribute('src');
846             properties.alt = image.getAttribute('alt') || '';
847             properties.presentation = (image.get('role') === 'presentation');
848             return properties;
849         }
851         // No image selected - clean up.
852         this._selectedImage = null;
853         return false;
854     },
856     /**
857      * Sets the alignment of a properties object.
858      *
859      * @method _getAlignmentPropeties
860      * @param {Node} image The image that the alignment properties should be found for
861      * @param {Object} properties The properties object that is created in _getSelectedImageProperties()
862      * @private
863      */
864     _getAlignmentPropeties: function(image, properties) {
865         var complete = false,
866             defaultAlignment;
868         // Check for an alignment value.
869         complete = ALIGNMENTS.some(function(alignment) {
870             var classname = this._getAlignmentClass(alignment.value);
871             if (image.hasClass(classname)) {
872                 properties.align = alignment.value;
874                 return true;
875             }
877             if (alignment.isDefault) {
878                 defaultAlignment = alignment.value;
879             }
881             return false;
882         }, this);
884         if (!complete && defaultAlignment) {
885             properties.align = defaultAlignment;
886         }
887     },
889     /**
890      * Update the form when the URL was changed. This includes updating the
891      * height, width, and image preview.
892      *
893      * @method _urlChanged
894      * @private
895      */
896     _urlChanged: function() {
897         var input = this._form.one('.' + CSS.INPUTURL);
899         if (input.get('value') !== '') {
900             // Load the preview image.
901             this._loadPreviewImage(input.get('value'));
902         }
903     },
905     /**
906      * Update the image in the contenteditable.
907      *
908      * @method _setImage
909      * @param {EventFacade} e
910      * @private
911      */
912     _setImage: function(e) {
913         var form = this._form,
914             url = form.one('.' + CSS.INPUTURL).get('value'),
915             alt = form.one('.' + CSS.INPUTALT).get('value'),
916             width = form.one('.' + CSS.INPUTWIDTH).get('value'),
917             height = form.one('.' + CSS.INPUTHEIGHT).get('value'),
918             alignment = this._getAlignmentClass(form.one('.' + CSS.INPUTALIGNMENT).get('value')),
919             presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked'),
920             constrain = form.one('.' + CSS.INPUTCONSTRAIN).get('checked'),
921             imagehtml,
922             customstyle = form.one('.' + CSS.INPUTCUSTOMSTYLE).get('value'),
923             classlist = [],
924             host = this.get('host');
926         e.preventDefault();
928         // Check if there are any accessibility issues.
929         if (this._updateWarning()) {
930             return;
931         }
933         // Focus on the editor in preparation for inserting the image.
934         host.focus();
935         if (url !== '') {
936             if (this._selectedImage) {
937                 host.setSelection(host.getSelectionFromNode(this._selectedImage));
938             } else {
939                 host.setSelection(this._currentSelection);
940             }
942             if (constrain) {
943                 classlist.push(CSS.RESPONSIVE);
944             }
946             // Add the alignment class for the image.
947             classlist.push(alignment);
949             if (!width.match(REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {
950                 form.one('.' + CSS.INPUTWIDTH).focus();
951                 return;
952             }
953             if (!height.match(REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {
954                 form.one('.' + CSS.INPUTHEIGHT).focus();
955                 return;
956             }
958             var template = Y.Handlebars.compile(IMAGETEMPLATE);
959             imagehtml = template({
960                 url: url,
961                 alt: alt,
962                 width: width,
963                 height: height,
964                 presentation: presentation,
965                 customstyle: customstyle,
966                 classlist: classlist.join(' ')
967             });
969             this.get('host').insertContentAtFocusPoint(imagehtml);
971             this.markUpdated();
972         }
974         this.getDialogue({
975             focusAfterHide: null
976         }).hide();
978     },
980     /**
981      * Removes any legacy styles added by previous versions of the atto image button.
982      *
983      * @method _removeLegacyAlignment
984      * @param {Y.Node} imageNode
985      * @return {Y.Node}
986      * @private
987      */
988     _removeLegacyAlignment: function(imageNode) {
989         if (!imageNode.getStyle('margin')) {
990             // There is no margin therefore this cannot match any known alignments.
991             return imageNode;
992         }
994         ALIGNMENTS.some(function(alignment) {
995             if (imageNode.getStyle(alignment.name) !== alignment.value) {
996                 // The name/value do not match. Skip.
997                 return false;
998             }
1000             var normalisedNode = Y.Node.create('<div>');
1001             normalisedNode.setStyle('margin', alignment.margin);
1002             if (imageNode.getStyle('margin') !== normalisedNode.getStyle('margin')) {
1003                 // The margin does not match.
1004                 return false;
1005             }
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 });
1050 }, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});