weekly release 4.0dev
[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-fluid',
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                 '<textarea class="form-control fullwidth {{CSS.INPUTALT}}" ' +
128                 'id="{{elementid}}_{{CSS.INPUTALT}}" maxlength="125"></textarea>' +
130                 // Add the character count.
131                 '<div id="the-count" class="d-flex justify-content-end small">' +
132                 '<span id="currentcount">0</span>' +
133                 '<span id="maximumcount"> / 125</span>' +
134                 '</div>' +
136                 // Add the presentation select box.
137                 '<div class="form-check">' +
138                 '<input type="checkbox" class="form-check-input {{CSS.IMAGEPRESENTATION}}" ' +
139                     'id="{{elementid}}_{{CSS.IMAGEPRESENTATION}}"/>' +
140                 '<label class="form-check-label" for="{{elementid}}_{{CSS.IMAGEPRESENTATION}}">' +
141                     '{{get_string "presentation" component}}' +
142                 '</label>' +
143                 '</div>' +
144                 '</div>' +
146                 // Add the size entry boxes.
147                 '<div class="mb-1">' +
148                 '<label class="" for="{{elementid}}_{{CSS.INPUTSIZE}}">{{get_string "size" component}}</label>' +
149                 '<div id="{{elementid}}_{{CSS.INPUTSIZE}}" class="form-inline {{CSS.INPUTSIZE}}">' +
150                 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTWIDTH}}">{{get_string "width" component}}</label>' +
151                 '<input type="text" class="form-control mr-1 input-mini {{CSS.INPUTWIDTH}}" ' +
152                 'id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="4"/> x' +
154                 // Add the height entry box.
155                 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTHEIGHT}}">{{get_string "height" component}}</label>' +
156                 '<input type="text" class="form-control ml-1 input-mini {{CSS.INPUTHEIGHT}}" ' +
157                 'id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +
159                 // Add the constrain checkbox.
160                 '<div class="form-check ml-2">' +
161                 '<input type="checkbox" class="form-check-input {{CSS.INPUTCONSTRAIN}}" ' +
162                 'id="{{elementid}}_{{CSS.INPUTCONSTRAIN}}"/>' +
163                 '<label class="form-check-label" for="{{elementid}}_{{CSS.INPUTCONSTRAIN}}">' +
164                 '{{get_string "constrain" component}}</label>' +
165                 '</div>' +
166                 '</div>' +
167                 '</div>' +
169                 // Add the alignment selector.
170                 '<div class="form-inline mb-1">' +
171                 '<label class="for="{{elementid}}_{{CSS.INPUTALIGNMENT}}">{{get_string "alignment" component}}</label>' +
172                 '<select class="custom-select {{CSS.INPUTALIGNMENT}}" id="{{elementid}}_{{CSS.INPUTALIGNMENT}}">' +
173                     '{{#each alignments}}' +
174                         '<option value="{{value}}">{{get_string str ../component}}</option>' +
175                     '{{/each}}' +
176                 '</select>' +
177                 '</div>' +
178                 // Hidden input to store custom styles.
179                 '<input type="hidden" class="{{CSS.INPUTCUSTOMSTYLE}}"/>' +
180                 '<br/>' +
182                 // Add the image preview.
183                 '<div class="mdl-align">' +
184                 '<div class="{{CSS.IMAGEPREVIEWBOX}}">' +
185                     '<img class="{{CSS.IMAGEPREVIEW}}" alt="" style="display: none;"/>' +
186                 '</div>' +
188                 // Add the submit button and close the form.
189                 '<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
190                     '{{get_string "saveimage" component}}</button>' +
191                 '</div>' +
192             '</form>',
194         IMAGETEMPLATE = '' +
195             '<img src="{{url}}" alt="{{alt}}" ' +
196                 '{{#if width}}width="{{width}}" {{/if}}' +
197                 '{{#if height}}height="{{height}}" {{/if}}' +
198                 '{{#if presentation}}role="presentation" {{/if}}' +
199                 '{{#if customstyle}}style="{{customstyle}}" {{/if}}' +
200                 '{{#if classlist}}class="{{classlist}}" {{/if}}' +
201                 '{{#if id}}id="{{id}}" {{/if}}' +
202                 '/>';
204 Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
205     /**
206      * A reference to the current selection at the time that the dialogue
207      * was opened.
208      *
209      * @property _currentSelection
210      * @type Range
211      * @private
212      */
213     _currentSelection: null,
215     /**
216      * The most recently selected image.
217      *
218      * @param _selectedImage
219      * @type Node
220      * @private
221      */
222     _selectedImage: null,
224     /**
225      * A reference to the currently open form.
226      *
227      * @param _form
228      * @type Node
229      * @private
230      */
231     _form: null,
233     /**
234      * The dimensions of the raw image before we manipulate it.
235      *
236      * @param _rawImageDimensions
237      * @type Object
238      * @private
239      */
240     _rawImageDimensions: null,
242     initializer: function() {
244         this.addButton({
245             icon: 'e/insert_edit_image',
246             callback: this._displayDialogue,
247             tags: 'img',
248             tagMatchRequiresAll: false
249         });
250         this.editor.delegate('dblclick', this._displayDialogue, 'img', this);
251         this.editor.delegate('click', this._handleClick, 'img', this);
252         this.editor.on('paste', this._handlePaste, this);
253         this.editor.on('drop', this._handleDragDrop, this);
255         // e.preventDefault needed to stop the default event from clobbering the desired behaviour in some browsers.
256         this.editor.on('dragover', function(e) {
257             e.preventDefault();
258         }, this);
259         this.editor.on('dragenter', function(e) {
260             e.preventDefault();
261         }, this);
262     },
264     /**
265      * Handle a drag and drop event with an image.
266      *
267      * @method _handleDragDrop
268      * @param {EventFacade} e
269      * @return {boolean} false if we handled the event, else true.
270      * @private
271      */
272     _handleDragDrop: function(e) {
273         if (!e._event || !e._event.dataTransfer) {
274             // Drop not fully supported in this browser.
275             return true;
276         }
278         return this._handlePasteOrDropHelper(e, e._event.dataTransfer);
279     },
281     /**
282      * Handles paste events where - if the thing being pasted is an image.
283      *
284      * @method _handlePaste
285      * @param {EventFacade} e
286      * @return {boolean} false if we handled the event, else true.
287      * @private
288      */
289     _handlePaste: function(e) {
290         if (!e._event || !e._event.clipboardData) {
291             // Paste not fully supported in this browser.
292             return true;
293         }
295         return this._handlePasteOrDropHelper(e, e._event.clipboardData);
296     },
298     /**
299      * Handle a drag and drop event with an image.
300      *
301      * @method _handleDragDrop
302      * @param {EventFacade} e
303      * @param {DataTransfer} dataTransfer
304      * @return {boolean} false if we handled the event, else true.
305      * @private
306      */
307     _handlePasteOrDropHelper: function(e, dataTransfer) {
309         var items = dataTransfer.items,
310             didUpload = false;
311         for (var i = 0; i < items.length; i++) {
312             var item = items[i];
313             if (item.kind !== 'file') {
314                 continue;
315             }
316             if (!this._isImage(item.type)) {
317                 continue;
318             }
319             this._uploadImage(item.getAsFile());
320             didUpload = true;
321         }
323         if (didUpload) {
324             // We handled this.
325             e.preventDefault();
326             e.stopPropagation();
327             return false;
328         } else {
329             // Let someone else try to handle it.
330             return true;
331         }
332     },
334     /**
335      * Is this file an image?
336      *
337      * @method _isImage
338      * @param {string} mimeType the file's mime type.
339      * @return {boolean} true if the file has an image mimeType.
340      * @private
341      */
342     _isImage: function(mimeType) {
343         return mimeType.indexOf('image/') === 0;
344     },
346     /**
347      * Used by _handleDragDrop and _handlePaste to upload an image and insert it.
348      *
349      * @method _uploadImage
350      * @param {File} fileToSave
351      * @private
352      */
353     _uploadImage: function(fileToSave) {
355         var self = this,
356             host = this.get('host'),
357             template = Y.Handlebars.compile(IMAGETEMPLATE);
359         host.saveSelection();
361         // Trigger form upload start events.
362         require(['core_form/events'], function(FormEvent) {
363             FormEvent.notifyUploadStarted(self.editor.get('id'));
364         });
366         var options = host.get('filepickeroptions').image,
367             savepath = (options.savepath === undefined) ? '/' : options.savepath,
368             formData = new FormData(),
369             timestamp = 0,
370             uploadid = "",
371             xhr = new XMLHttpRequest(),
372             imagehtml = "",
373             keys = Object.keys(options.repositories);
375         formData.append('repo_upload_file', fileToSave);
376         formData.append('itemid', options.itemid);
378         // List of repositories is an object rather than an array.  This makes iteration more awkward.
379         for (var i = 0; i < keys.length; i++) {
380             if (options.repositories[keys[i]].type === 'upload') {
381                 formData.append('repo_id', options.repositories[keys[i]].id);
382                 break;
383             }
384         }
385         formData.append('env', options.env);
386         formData.append('sesskey', M.cfg.sesskey);
387         formData.append('client_id', options.client_id);
388         formData.append('savepath', savepath);
389         formData.append('ctx_id', options.context.id);
391         // Insert spinner as a placeholder.
392         timestamp = new Date().getTime();
393         uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
394         host.focus();
395         host.restoreSelection();
396         imagehtml = template({
397             url: M.util.image_url("i/loading_small", 'moodle'),
398             alt: M.util.get_string('uploading', COMPONENTNAME),
399             id: uploadid
400         });
401         host.insertContentAtFocusPoint(imagehtml);
402         self.markUpdated();
404         // Kick off a XMLHttpRequest.
405         xhr.onreadystatechange = function() {
406             var placeholder = self.editor.one('#' + uploadid),
407                 result,
408                 file,
409                 newhtml,
410                 newimage;
412             if (xhr.readyState === 4) {
413                 if (xhr.status === 200) {
414                     result = JSON.parse(xhr.responseText);
415                     if (result) {
416                         if (result.error) {
417                             if (placeholder) {
418                                 placeholder.remove(true);
419                             }
420                             // Trigger form upload complete events.
421                             require(['core_form/events'], function(FormEvent) {
422                                 FormEvent.notifyUploadCompleted(self.editor.get('id'));
423                             });
424                             throw new M.core.ajaxException(result);
425                         }
427                         file = result;
428                         if (result.event && result.event === 'fileexists') {
429                             // A file with this name is already in use here - rename to avoid conflict.
430                             // Chances are, it's a different image (stored in a different folder on the user's computer).
431                             // If the user wants to reuse an existing image, they can copy/paste it within the editor.
432                             file = result.newfile;
433                         }
435                         // Replace placeholder with actual image.
436                         newhtml = template({
437                             url: file.url,
438                             presentation: true,
439                             classlist: CSS.RESPONSIVE
440                         });
441                         newimage = Y.Node.create(newhtml);
442                         if (placeholder) {
443                             placeholder.replace(newimage);
444                         } else {
445                             self.editor.appendChild(newimage);
446                         }
447                         self.markUpdated();
448                     }
449                 } else {
450                     Y.use('moodle-core-notification-alert', function() {
451                         // Trigger form upload complete events.
452                         require(['core_form/events'], function(FormEvent) {
453                             FormEvent.notifyUploadCompleted(self.editor.get('id'));
454                         });
455                         new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
456                     });
457                     if (placeholder) {
458                         placeholder.remove(true);
459                     }
460                 }
461                 // Trigger form upload complete events.
462                 require(['core_form/events'], function(FormEvent) {
463                     FormEvent.notifyUploadCompleted(self.editor.get('id'));
464                 });
465             }
466         };
467         xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
468         xhr.send(formData);
469     },
471     /**
472      * Handle a click on an image.
473      *
474      * @method _handleClick
475      * @param {EventFacade} e
476      * @private
477      */
478     _handleClick: function(e) {
479         var image = e.target;
481         var selection = this.get('host').getSelectionFromNode(image);
482         if (this.get('host').getSelection() !== selection) {
483             this.get('host').setSelection(selection);
484         }
485     },
487     /**
488      * Display the image editing tool.
489      *
490      * @method _displayDialogue
491      * @private
492      */
493     _displayDialogue: function() {
494         // Store the current selection.
495         this._currentSelection = this.get('host').getSelection();
496         if (this._currentSelection === false) {
497             return;
498         }
500         // Reset the image dimensions.
501         this._rawImageDimensions = null;
503         var dialogue = this.getDialogue({
504             headerContent: M.util.get_string('imageproperties', COMPONENTNAME),
505             width: 'auto',
506             focusAfterHide: true,
507             focusOnShowSelector: SELECTORS.INPUTURL
508         });
509         // Set a maximum width for the dialog. This will prevent the dialog width to extend beyond the screen width
510         // in cases when the uploaded image has larger width.
511         dialogue.get('boundingBox').setStyle('maxWidth', '90%');
512         // Set the dialogue content, and then show the dialogue.
513         dialogue.set('bodyContent', this._getDialogueContent())
514                 .show();
515     },
517     /**
518      * Set the inputs for width and height if they are not set, and calculate
519      * if the constrain checkbox should be checked or not.
520      *
521      * @method _loadPreviewImage
522      * @param {String} url
523      * @private
524      */
525     _loadPreviewImage: function(url) {
526         var image = new Image();
527         var self = this;
529         image.onerror = function() {
530             var preview = self._form.one('.' + CSS.IMAGEPREVIEW);
531             preview.setStyles({
532                 'display': 'none'
533             });
535             // Centre the dialogue when clearing the image preview.
536             self.getDialogue().centerDialogue();
537         };
539         image.onload = function() {
540             var input, currentwidth, currentheight, widthRatio, heightRatio;
542             self._rawImageDimensions = {
543                 width: this.width,
544                 height: this.height
545             };
547             input = self._form.one('.' + CSS.INPUTWIDTH);
548             currentwidth = input.get('value');
549             if (currentwidth === '') {
550                 input.set('value', this.width);
551                 currentwidth = "" + this.width;
552             }
553             input = self._form.one('.' + CSS.INPUTHEIGHT);
554             currentheight = input.get('value');
555             if (currentheight === '') {
556                 input.set('value', this.height);
557                 currentheight = "" + this.height;
558             }
559             input = self._form.one('.' + CSS.IMAGEPREVIEW);
560             input.setAttribute('src', this.src);
561             input.setStyles({
562                 'display': 'inline'
563             });
565             input = self._form.one('.' + CSS.INPUTCONSTRAIN);
566             if (currentwidth.match(REGEX.ISPERCENT) && currentheight.match(REGEX.ISPERCENT)) {
567                 input.set('checked', currentwidth === currentheight);
568             } else {
569                 if (this.width === 0) {
570                     this.width = 1;
571                 }
572                 if (this.height === 0) {
573                     this.height = 1;
574                 }
575                 // This is the same as comparing to 3 decimal places.
576                 widthRatio = Math.round(1000 * parseInt(currentwidth, 10) / this.width);
577                 heightRatio = Math.round(1000 * parseInt(currentheight, 10) / this.height);
578                 input.set('checked', widthRatio === heightRatio);
579             }
581             // Apply the image sizing.
582             self._autoAdjustSize(self);
584             // Centre the dialogue once the preview image has loaded.
585             self.getDialogue().centerDialogue();
586         };
588         image.src = url;
589     },
591     /**
592      * Return the dialogue content for the tool, attaching any required
593      * events.
594      *
595      * @method _getDialogueContent
596      * @return {Node} The content to place in the dialogue.
597      * @private
598      */
599     _getDialogueContent: function() {
600         var template = Y.Handlebars.compile(TEMPLATE),
601             canShowFilepicker = this.get('host').canShowFilepicker('image'),
602             content = Y.Node.create(template({
603                 elementid: this.get('host').get('elementid'),
604                 CSS: CSS,
605                 component: COMPONENTNAME,
606                 showFilepicker: canShowFilepicker,
607                 alignments: ALIGNMENTS
608             }));
610         this._form = content;
612         // Configure the view of the current image.
613         this._applyImageProperties(this._form);
615         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
616         this._form.one('.' + CSS.IMAGEPRESENTATION).on('change', this._updateWarning, this);
617         this._form.one('.' + CSS.INPUTALT).on('change', this._updateWarning, this);
618         this._form.one('.' + CSS.INPUTWIDTH).on('blur', this._autoAdjustSize, this);
619         this._form.one('.' + CSS.INPUTHEIGHT).on('blur', this._autoAdjustSize, this, true);
620         this._form.one('.' + CSS.INPUTCONSTRAIN).on('change', function(event) {
621             if (event.target.get('checked')) {
622                 this._autoAdjustSize(event);
623             }
624         }, this);
625         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
626         this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setImage, this);
628         if (canShowFilepicker) {
629             this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {
630                     this.get('host').showFilepicker('image', this._filepickerCallback, this);
631             }, this);
632         }
634         // Character count.
635         this._form.one('.' + CSS.INPUTALT).on('keyup', this._handleKeyup, this);
637         return content;
638     },
640     _autoAdjustSize: function(e, forceHeight) {
641         forceHeight = forceHeight || false;
643         var keyField = this._form.one('.' + CSS.INPUTWIDTH),
644             keyFieldType = 'width',
645             subField = this._form.one('.' + CSS.INPUTHEIGHT),
646             subFieldType = 'height',
647             constrainField = this._form.one('.' + CSS.INPUTCONSTRAIN),
648             keyFieldValue = keyField.get('value'),
649             subFieldValue = subField.get('value'),
650             imagePreview = this._form.one('.' + CSS.IMAGEPREVIEW),
651             rawPercentage,
652             rawSize;
654         // If we do not know the image size, do not do anything.
655         if (!this._rawImageDimensions) {
656             return;
657         }
659         // Set the width back to default if it is empty.
660         if (keyFieldValue === '') {
661             keyFieldValue = this._rawImageDimensions[keyFieldType];
662             keyField.set('value', keyFieldValue);
663             keyFieldValue = keyField.get('value');
664         }
666         // Clear the existing preview sizes.
667         imagePreview.setStyles({
668             width: null,
669             height: null
670         });
672         // Now update with the new values.
673         if (!constrainField.get('checked')) {
674             // We are not keeping the image proportion - update the preview accordingly.
676             // Width.
677             if (keyFieldValue.match(REGEX.ISPERCENT)) {
678                 rawPercentage = parseInt(keyFieldValue, 10);
679                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
680                 imagePreview.setStyle('width', rawSize + 'px');
681             } else {
682                 imagePreview.setStyle('width', keyFieldValue + 'px');
683             }
685             // Height.
686             if (subFieldValue.match(REGEX.ISPERCENT)) {
687                 rawPercentage = parseInt(subFieldValue, 10);
688                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
689                 imagePreview.setStyle('height', rawSize + 'px');
690             } else {
691                 imagePreview.setStyle('height', subFieldValue + 'px');
692             }
693         } else {
694             // We are keeping the image in proportion.
695             if (forceHeight) {
696                 // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.
697                 var _temporaryValue;
698                 _temporaryValue = keyField;
699                 keyField = subField;
700                 subField = _temporaryValue;
702                 _temporaryValue = keyFieldType;
703                 keyFieldType = subFieldType;
704                 subFieldType = _temporaryValue;
706                 _temporaryValue = keyFieldValue;
707                 keyFieldValue = subFieldValue;
708                 subFieldValue = _temporaryValue;
709             }
711             if (keyFieldValue.match(REGEX.ISPERCENT)) {
712                 // This is a percentage based change. Copy it verbatim.
713                 subFieldValue = keyFieldValue;
715                 // Set the width to the calculated pixel width.
716                 rawPercentage = parseInt(keyFieldValue, 10);
717                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
719                 // And apply the width/height to the container.
720                 imagePreview.setStyle('width', rawSize);
721                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
722                 imagePreview.setStyle('height', rawSize);
723             } else {
724                 // Calculate the scaled subFieldValue from the keyFieldValue.
725                 subFieldValue = Math.round((keyFieldValue / this._rawImageDimensions[keyFieldType]) *
726                         this._rawImageDimensions[subFieldType]);
728                 if (forceHeight) {
729                     imagePreview.setStyles({
730                         'width': subFieldValue,
731                         'height': keyFieldValue
732                     });
733                 } else {
734                     imagePreview.setStyles({
735                         'width': keyFieldValue,
736                         'height': subFieldValue
737                     });
738                 }
739             }
741             // Update the subField's value within the form to reflect the changes.
742             subField.set('value', subFieldValue);
743         }
744     },
746     /**
747      * Update the dialogue after an image was selected in the File Picker.
748      *
749      * @method _filepickerCallback
750      * @param {object} params The parameters provided by the filepicker
751      * containing information about the image.
752      * @private
753      */
754     _filepickerCallback: function(params) {
755         if (params.url !== '') {
756             var input = this._form.one('.' + CSS.INPUTURL);
757             input.set('value', params.url);
759             // Auto set the width and height.
760             this._form.one('.' + CSS.INPUTWIDTH).set('value', '');
761             this._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
763             // Load the preview image.
764             this._loadPreviewImage(params.url);
765         }
766     },
768     /**
769      * Applies properties of an existing image to the image dialogue for editing.
770      *
771      * @method _applyImageProperties
772      * @param {Node} form
773      * @private
774      */
775     _applyImageProperties: function(form) {
776         var properties = this._getSelectedImageProperties(),
777             img = form.one('.' + CSS.IMAGEPREVIEW);
779         if (properties === false) {
780             img.setStyle('display', 'none');
781             // Set the default alignment.
782             ALIGNMENTS.some(function(alignment) {
783                 if (alignment.isDefault) {
784                     form.one('.' + CSS.INPUTALIGNMENT).set('value', alignment.value);
785                     return true;
786                 }
788                 return false;
789             }, this);
791             return;
792         }
794         if (properties.align) {
795             form.one('.' + CSS.INPUTALIGNMENT).set('value', properties.align);
796         }
797         if (properties.customstyle) {
798             form.one('.' + CSS.INPUTCUSTOMSTYLE).set('value', properties.customstyle);
799         }
800         if (properties.width) {
801             form.one('.' + CSS.INPUTWIDTH).set('value', properties.width);
802         }
803         if (properties.height) {
804             form.one('.' + CSS.INPUTHEIGHT).set('value', properties.height);
805         }
806         if (properties.alt) {
807             form.one('.' + CSS.INPUTALT).set('value', properties.alt);
808         }
809         if (properties.src) {
810             form.one('.' + CSS.INPUTURL).set('value', properties.src);
811             this._loadPreviewImage(properties.src);
812         }
813         if (properties.presentation) {
814             form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');
815         }
817         // Update the image preview based on the form properties.
818         this._autoAdjustSize();
819     },
821     /**
822      * Gets the properties of the currently selected image.
823      *
824      * The first image only if multiple images are selected.
825      *
826      * @method _getSelectedImageProperties
827      * @return {object}
828      * @private
829      */
830     _getSelectedImageProperties: function() {
831         var properties = {
832                 src: null,
833                 alt: null,
834                 width: null,
835                 height: null,
836                 align: '',
837                 presentation: false
838             },
840             // Get the current selection.
841             images = this.get('host').getSelectedNodes(),
842             width,
843             height,
844             style,
845             image;
847         if (images) {
848             images = images.filter('img');
849         }
851         if (images && images.size()) {
852             image = this._removeLegacyAlignment(images.item(0));
853             this._selectedImage = image;
855             style = image.getAttribute('style');
856             properties.customstyle = style;
858             width = image.getAttribute('width');
859             if (!width.match(REGEX.ISPERCENT)) {
860                 width = parseInt(width, 10);
861             }
862             height = image.getAttribute('height');
863             if (!height.match(REGEX.ISPERCENT)) {
864                 height = parseInt(height, 10);
865             }
867             if (width !== 0) {
868                 properties.width = width;
869             }
870             if (height !== 0) {
871                 properties.height = height;
872             }
873             this._getAlignmentPropeties(image, properties);
874             properties.src = image.getAttribute('src');
875             properties.alt = image.getAttribute('alt') || '';
876             properties.presentation = (image.get('role') === 'presentation');
877             return properties;
878         }
880         // No image selected - clean up.
881         this._selectedImage = null;
882         return false;
883     },
885     /**
886      * Sets the alignment of a properties object.
887      *
888      * @method _getAlignmentPropeties
889      * @param {Node} image The image that the alignment properties should be found for
890      * @param {Object} properties The properties object that is created in _getSelectedImageProperties()
891      * @private
892      */
893     _getAlignmentPropeties: function(image, properties) {
894         var complete = false,
895             defaultAlignment;
897         // Check for an alignment value.
898         complete = ALIGNMENTS.some(function(alignment) {
899             var classname = this._getAlignmentClass(alignment.value);
900             if (image.hasClass(classname)) {
901                 properties.align = alignment.value;
903                 return true;
904             }
906             if (alignment.isDefault) {
907                 defaultAlignment = alignment.value;
908             }
910             return false;
911         }, this);
913         if (!complete && defaultAlignment) {
914             properties.align = defaultAlignment;
915         }
916     },
918     /**
919      * Update the form when the URL was changed. This includes updating the
920      * height, width, and image preview.
921      *
922      * @method _urlChanged
923      * @private
924      */
925     _urlChanged: function() {
926         var input = this._form.one('.' + CSS.INPUTURL);
928         if (input.get('value') !== '') {
929             // Load the preview image.
930             this._loadPreviewImage(input.get('value'));
931         }
932     },
934     /**
935      * Update the image in the contenteditable.
936      *
937      * @method _setImage
938      * @param {EventFacade} e
939      * @private
940      */
941     _setImage: function(e) {
942         var form = this._form,
943             url = form.one('.' + CSS.INPUTURL).get('value'),
944             alt = form.one('.' + CSS.INPUTALT).get('value'),
945             width = form.one('.' + CSS.INPUTWIDTH).get('value'),
946             height = form.one('.' + CSS.INPUTHEIGHT).get('value'),
947             alignment = this._getAlignmentClass(form.one('.' + CSS.INPUTALIGNMENT).get('value')),
948             presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked'),
949             constrain = form.one('.' + CSS.INPUTCONSTRAIN).get('checked'),
950             imagehtml,
951             customstyle = form.one('.' + CSS.INPUTCUSTOMSTYLE).get('value'),
952             classlist = [],
953             host = this.get('host');
955         e.preventDefault();
957         // Check if there are any accessibility issues.
958         if (this._updateWarning()) {
959             return;
960         }
962         // Focus on the editor in preparation for inserting the image.
963         host.focus();
964         if (url !== '') {
965             if (this._selectedImage) {
966                 host.setSelection(host.getSelectionFromNode(this._selectedImage));
967             } else {
968                 host.setSelection(this._currentSelection);
969             }
971             if (constrain) {
972                 classlist.push(CSS.RESPONSIVE);
973             }
975             // Add the alignment class for the image.
976             classlist.push(alignment);
978             if (!width.match(REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {
979                 form.one('.' + CSS.INPUTWIDTH).focus();
980                 return;
981             }
982             if (!height.match(REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {
983                 form.one('.' + CSS.INPUTHEIGHT).focus();
984                 return;
985             }
987             var template = Y.Handlebars.compile(IMAGETEMPLATE);
988             imagehtml = template({
989                 url: url,
990                 alt: alt,
991                 width: width,
992                 height: height,
993                 presentation: presentation,
994                 customstyle: customstyle,
995                 classlist: classlist.join(' ')
996             });
998             this.get('host').insertContentAtFocusPoint(imagehtml);
1000             this.markUpdated();
1001         }
1003         this.getDialogue({
1004             focusAfterHide: null
1005         }).hide();
1007     },
1009     /**
1010      * Removes any legacy styles added by previous versions of the atto image button.
1011      *
1012      * @method _removeLegacyAlignment
1013      * @param {Y.Node} imageNode
1014      * @return {Y.Node}
1015      * @private
1016      */
1017     _removeLegacyAlignment: function(imageNode) {
1018         if (!imageNode.getStyle('margin')) {
1019             // There is no margin therefore this cannot match any known alignments.
1020             return imageNode;
1021         }
1023         ALIGNMENTS.some(function(alignment) {
1024             if (imageNode.getStyle(alignment.name) !== alignment.value) {
1025                 // The name/value do not match. Skip.
1026                 return false;
1027             }
1029             var normalisedNode = Y.Node.create('<div>');
1030             normalisedNode.setStyle('margin', alignment.margin);
1031             if (imageNode.getStyle('margin') !== normalisedNode.getStyle('margin')) {
1032                 // The margin does not match.
1033                 return false;
1034             }
1036             imageNode.addClass(this._getAlignmentClass(alignment.value));
1037             imageNode.setStyle(alignment.name, null);
1038             imageNode.setStyle('margin', null);
1040             return true;
1041         }, this);
1043         return imageNode;
1044     },
1046     _getAlignmentClass: function(alignment) {
1047         return CSS.ALIGNSETTINGS + '_' + alignment;
1048     },
1050     /**
1051      * Update the alt text warning live.
1052      *
1053      * @method _updateWarning
1054      * @return {boolean} whether a warning should be displayed.
1055      * @private
1056      */
1057     _updateWarning: function() {
1058         var form = this._form,
1059             state = true,
1060             alt = form.one('.' + CSS.INPUTALT).get('value'),
1061             presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked');
1062         if (alt === '' && !presentation) {
1063             form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'block');
1064             form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', true);
1065             form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', true);
1066             state = true;
1067         } else {
1068             form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'none');
1069             form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', false);
1070             form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', false);
1071             state = false;
1072         }
1073         this.getDialogue().centerDialogue();
1074         return state;
1075     },
1077     /**
1078      * Handle the keyup to update the character count.
1079      */
1080     _handleKeyup: function() {
1081         var form = this._form,
1082             alt = form.one('.' + CSS.INPUTALT).get('value'),
1083             characterCount = alt.length,
1084             current = form.one('#currentcount');
1085         current.setHTML(characterCount);
1086     }
1087 });
1090 }, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});