Merge branch 'MDL-68541-39' of https://github.com/PoetOS/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                 '<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 src="#" 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         var options = host.get('filepickeroptions').image,
362             savepath = (options.savepath === undefined) ? '/' : options.savepath,
363             formData = new FormData(),
364             timestamp = 0,
365             uploadid = "",
366             xhr = new XMLHttpRequest(),
367             imagehtml = "",
368             keys = Object.keys(options.repositories);
370         formData.append('repo_upload_file', fileToSave);
371         formData.append('itemid', options.itemid);
373         // List of repositories is an object rather than an array.  This makes iteration more awkward.
374         for (var i = 0; i < keys.length; i++) {
375             if (options.repositories[keys[i]].type === 'upload') {
376                 formData.append('repo_id', options.repositories[keys[i]].id);
377                 break;
378             }
379         }
380         formData.append('env', options.env);
381         formData.append('sesskey', M.cfg.sesskey);
382         formData.append('client_id', options.client_id);
383         formData.append('savepath', savepath);
384         formData.append('ctx_id', options.context.id);
386         // Insert spinner as a placeholder.
387         timestamp = new Date().getTime();
388         uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
389         host.focus();
390         host.restoreSelection();
391         imagehtml = template({
392             url: M.util.image_url("i/loading_small", 'moodle'),
393             alt: M.util.get_string('uploading', COMPONENTNAME),
394             id: uploadid
395         });
396         host.insertContentAtFocusPoint(imagehtml);
397         self.markUpdated();
399         // Kick off a XMLHttpRequest.
400         xhr.onreadystatechange = function() {
401             var placeholder = self.editor.one('#' + uploadid),
402                 result,
403                 file,
404                 newhtml,
405                 newimage;
407             if (xhr.readyState === 4) {
408                 if (xhr.status === 200) {
409                     result = JSON.parse(xhr.responseText);
410                     if (result) {
411                         if (result.error) {
412                             if (placeholder) {
413                                 placeholder.remove(true);
414                             }
415                             throw new M.core.ajaxException(result);
416                         }
418                         file = result;
419                         if (result.event && result.event === 'fileexists') {
420                             // A file with this name is already in use here - rename to avoid conflict.
421                             // Chances are, it's a different image (stored in a different folder on the user's computer).
422                             // If the user wants to reuse an existing image, they can copy/paste it within the editor.
423                             file = result.newfile;
424                         }
426                         // Replace placeholder with actual image.
427                         newhtml = template({
428                             url: file.url,
429                             presentation: true
430                         });
431                         newimage = Y.Node.create(newhtml);
432                         if (placeholder) {
433                             placeholder.replace(newimage);
434                         } else {
435                             self.editor.appendChild(newimage);
436                         }
437                         self.markUpdated();
438                     }
439                 } else {
440                     Y.use('moodle-core-notification-alert', function() {
441                         new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
442                     });
443                     if (placeholder) {
444                         placeholder.remove(true);
445                     }
446                 }
447             }
448         };
449         xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
450         xhr.send(formData);
451     },
453     /**
454      * Handle a click on an image.
455      *
456      * @method _handleClick
457      * @param {EventFacade} e
458      * @private
459      */
460     _handleClick: function(e) {
461         var image = e.target;
463         var selection = this.get('host').getSelectionFromNode(image);
464         if (this.get('host').getSelection() !== selection) {
465             this.get('host').setSelection(selection);
466         }
467     },
469     /**
470      * Display the image editing tool.
471      *
472      * @method _displayDialogue
473      * @private
474      */
475     _displayDialogue: function() {
476         // Store the current selection.
477         this._currentSelection = this.get('host').getSelection();
478         if (this._currentSelection === false) {
479             return;
480         }
482         // Reset the image dimensions.
483         this._rawImageDimensions = null;
485         var dialogue = this.getDialogue({
486             headerContent: M.util.get_string('imageproperties', COMPONENTNAME),
487             width: 'auto',
488             focusAfterHide: true,
489             focusOnShowSelector: SELECTORS.INPUTURL
490         });
492         // Set the dialogue content, and then show the dialogue.
493         dialogue.set('bodyContent', this._getDialogueContent())
494                 .show();
495     },
497     /**
498      * Set the inputs for width and height if they are not set, and calculate
499      * if the constrain checkbox should be checked or not.
500      *
501      * @method _loadPreviewImage
502      * @param {String} url
503      * @private
504      */
505     _loadPreviewImage: function(url) {
506         var image = new Image();
507         var self = this;
509         image.onerror = function() {
510             var preview = self._form.one('.' + CSS.IMAGEPREVIEW);
511             preview.setStyles({
512                 'display': 'none'
513             });
515             // Centre the dialogue when clearing the image preview.
516             self.getDialogue().centerDialogue();
517         };
519         image.onload = function() {
520             var input, currentwidth, currentheight, widthRatio, heightRatio;
522             self._rawImageDimensions = {
523                 width: this.width,
524                 height: this.height
525             };
527             input = self._form.one('.' + CSS.INPUTWIDTH);
528             currentwidth = input.get('value');
529             if (currentwidth === '') {
530                 input.set('value', this.width);
531                 currentwidth = "" + this.width;
532             }
533             input = self._form.one('.' + CSS.INPUTHEIGHT);
534             currentheight = input.get('value');
535             if (currentheight === '') {
536                 input.set('value', this.height);
537                 currentheight = "" + this.height;
538             }
539             input = self._form.one('.' + CSS.IMAGEPREVIEW);
540             input.setAttribute('src', this.src);
541             input.setStyles({
542                 'display': 'inline'
543             });
545             input = self._form.one('.' + CSS.INPUTCONSTRAIN);
546             if (currentwidth.match(REGEX.ISPERCENT) && currentheight.match(REGEX.ISPERCENT)) {
547                 input.set('checked', currentwidth === currentheight);
548             } else {
549                 if (this.width === 0) {
550                     this.width = 1;
551                 }
552                 if (this.height === 0) {
553                     this.height = 1;
554                 }
555                 // This is the same as comparing to 3 decimal places.
556                 widthRatio = Math.round(1000 * parseInt(currentwidth, 10) / this.width);
557                 heightRatio = Math.round(1000 * parseInt(currentheight, 10) / this.height);
558                 input.set('checked', widthRatio === heightRatio);
559             }
561             // Apply the image sizing.
562             self._autoAdjustSize(self);
564             // Centre the dialogue once the preview image has loaded.
565             self.getDialogue().centerDialogue();
566         };
568         image.src = url;
569     },
571     /**
572      * Return the dialogue content for the tool, attaching any required
573      * events.
574      *
575      * @method _getDialogueContent
576      * @return {Node} The content to place in the dialogue.
577      * @private
578      */
579     _getDialogueContent: function() {
580         var template = Y.Handlebars.compile(TEMPLATE),
581             canShowFilepicker = this.get('host').canShowFilepicker('image'),
582             content = Y.Node.create(template({
583                 elementid: this.get('host').get('elementid'),
584                 CSS: CSS,
585                 component: COMPONENTNAME,
586                 showFilepicker: canShowFilepicker,
587                 alignments: ALIGNMENTS
588             }));
590         this._form = content;
592         // Configure the view of the current image.
593         this._applyImageProperties(this._form);
595         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
596         this._form.one('.' + CSS.IMAGEPRESENTATION).on('change', this._updateWarning, this);
597         this._form.one('.' + CSS.INPUTALT).on('change', this._updateWarning, this);
598         this._form.one('.' + CSS.INPUTWIDTH).on('blur', this._autoAdjustSize, this);
599         this._form.one('.' + CSS.INPUTHEIGHT).on('blur', this._autoAdjustSize, this, true);
600         this._form.one('.' + CSS.INPUTCONSTRAIN).on('change', function(event) {
601             if (event.target.get('checked')) {
602                 this._autoAdjustSize(event);
603             }
604         }, this);
605         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
606         this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setImage, this);
608         if (canShowFilepicker) {
609             this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {
610                     this.get('host').showFilepicker('image', this._filepickerCallback, this);
611             }, this);
612         }
614         // Character count.
615         this._form.one('.' + CSS.INPUTALT).on('keyup', this._handleKeyup, this);
617         return content;
618     },
620     _autoAdjustSize: function(e, forceHeight) {
621         forceHeight = forceHeight || false;
623         var keyField = this._form.one('.' + CSS.INPUTWIDTH),
624             keyFieldType = 'width',
625             subField = this._form.one('.' + CSS.INPUTHEIGHT),
626             subFieldType = 'height',
627             constrainField = this._form.one('.' + CSS.INPUTCONSTRAIN),
628             keyFieldValue = keyField.get('value'),
629             subFieldValue = subField.get('value'),
630             imagePreview = this._form.one('.' + CSS.IMAGEPREVIEW),
631             rawPercentage,
632             rawSize;
634         // If we do not know the image size, do not do anything.
635         if (!this._rawImageDimensions) {
636             return;
637         }
639         // Set the width back to default if it is empty.
640         if (keyFieldValue === '') {
641             keyFieldValue = this._rawImageDimensions[keyFieldType];
642             keyField.set('value', keyFieldValue);
643             keyFieldValue = keyField.get('value');
644         }
646         // Clear the existing preview sizes.
647         imagePreview.setStyles({
648             width: null,
649             height: null
650         });
652         // Now update with the new values.
653         if (!constrainField.get('checked')) {
654             // We are not keeping the image proportion - update the preview accordingly.
656             // Width.
657             if (keyFieldValue.match(REGEX.ISPERCENT)) {
658                 rawPercentage = parseInt(keyFieldValue, 10);
659                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
660                 imagePreview.setStyle('width', rawSize + 'px');
661             } else {
662                 imagePreview.setStyle('width', keyFieldValue + 'px');
663             }
665             // Height.
666             if (subFieldValue.match(REGEX.ISPERCENT)) {
667                 rawPercentage = parseInt(subFieldValue, 10);
668                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
669                 imagePreview.setStyle('height', rawSize + 'px');
670             } else {
671                 imagePreview.setStyle('height', subFieldValue + 'px');
672             }
673         } else {
674             // We are keeping the image in proportion.
675             if (forceHeight) {
676                 // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.
677                 var _temporaryValue;
678                 _temporaryValue = keyField;
679                 keyField = subField;
680                 subField = _temporaryValue;
682                 _temporaryValue = keyFieldType;
683                 keyFieldType = subFieldType;
684                 subFieldType = _temporaryValue;
686                 _temporaryValue = keyFieldValue;
687                 keyFieldValue = subFieldValue;
688                 subFieldValue = _temporaryValue;
689             }
691             if (keyFieldValue.match(REGEX.ISPERCENT)) {
692                 // This is a percentage based change. Copy it verbatim.
693                 subFieldValue = keyFieldValue;
695                 // Set the width to the calculated pixel width.
696                 rawPercentage = parseInt(keyFieldValue, 10);
697                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
699                 // And apply the width/height to the container.
700                 imagePreview.setStyle('width', rawSize);
701                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
702                 imagePreview.setStyle('height', rawSize);
703             } else {
704                 // Calculate the scaled subFieldValue from the keyFieldValue.
705                 subFieldValue = Math.round((keyFieldValue / this._rawImageDimensions[keyFieldType]) *
706                         this._rawImageDimensions[subFieldType]);
708                 if (forceHeight) {
709                     imagePreview.setStyles({
710                         'width': subFieldValue,
711                         'height': keyFieldValue
712                     });
713                 } else {
714                     imagePreview.setStyles({
715                         'width': keyFieldValue,
716                         'height': subFieldValue
717                     });
718                 }
719             }
721             // Update the subField's value within the form to reflect the changes.
722             subField.set('value', subFieldValue);
723         }
724     },
726     /**
727      * Update the dialogue after an image was selected in the File Picker.
728      *
729      * @method _filepickerCallback
730      * @param {object} params The parameters provided by the filepicker
731      * containing information about the image.
732      * @private
733      */
734     _filepickerCallback: function(params) {
735         if (params.url !== '') {
736             var input = this._form.one('.' + CSS.INPUTURL);
737             input.set('value', params.url);
739             // Auto set the width and height.
740             this._form.one('.' + CSS.INPUTWIDTH).set('value', '');
741             this._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
743             // Load the preview image.
744             this._loadPreviewImage(params.url);
745         }
746     },
748     /**
749      * Applies properties of an existing image to the image dialogue for editing.
750      *
751      * @method _applyImageProperties
752      * @param {Node} form
753      * @private
754      */
755     _applyImageProperties: function(form) {
756         var properties = this._getSelectedImageProperties(),
757             img = form.one('.' + CSS.IMAGEPREVIEW);
759         if (properties === false) {
760             img.setStyle('display', 'none');
761             // Set the default alignment.
762             ALIGNMENTS.some(function(alignment) {
763                 if (alignment.isDefault) {
764                     form.one('.' + CSS.INPUTALIGNMENT).set('value', alignment.value);
765                     return true;
766                 }
768                 return false;
769             }, this);
771             return;
772         }
774         if (properties.align) {
775             form.one('.' + CSS.INPUTALIGNMENT).set('value', properties.align);
776         }
777         if (properties.customstyle) {
778             form.one('.' + CSS.INPUTCUSTOMSTYLE).set('value', properties.customstyle);
779         }
780         if (properties.width) {
781             form.one('.' + CSS.INPUTWIDTH).set('value', properties.width);
782         }
783         if (properties.height) {
784             form.one('.' + CSS.INPUTHEIGHT).set('value', properties.height);
785         }
786         if (properties.alt) {
787             form.one('.' + CSS.INPUTALT).set('value', properties.alt);
788         }
789         if (properties.src) {
790             form.one('.' + CSS.INPUTURL).set('value', properties.src);
791             this._loadPreviewImage(properties.src);
792         }
793         if (properties.presentation) {
794             form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');
795         }
797         // Update the image preview based on the form properties.
798         this._autoAdjustSize();
799     },
801     /**
802      * Gets the properties of the currently selected image.
803      *
804      * The first image only if multiple images are selected.
805      *
806      * @method _getSelectedImageProperties
807      * @return {object}
808      * @private
809      */
810     _getSelectedImageProperties: function() {
811         var properties = {
812                 src: null,
813                 alt: null,
814                 width: null,
815                 height: null,
816                 align: '',
817                 presentation: false
818             },
820             // Get the current selection.
821             images = this.get('host').getSelectedNodes(),
822             width,
823             height,
824             style,
825             image;
827         if (images) {
828             images = images.filter('img');
829         }
831         if (images && images.size()) {
832             image = this._removeLegacyAlignment(images.item(0));
833             this._selectedImage = image;
835             style = image.getAttribute('style');
836             properties.customstyle = style;
838             width = image.getAttribute('width');
839             if (!width.match(REGEX.ISPERCENT)) {
840                 width = parseInt(width, 10);
841             }
842             height = image.getAttribute('height');
843             if (!height.match(REGEX.ISPERCENT)) {
844                 height = parseInt(height, 10);
845             }
847             if (width !== 0) {
848                 properties.width = width;
849             }
850             if (height !== 0) {
851                 properties.height = height;
852             }
853             this._getAlignmentPropeties(image, properties);
854             properties.src = image.getAttribute('src');
855             properties.alt = image.getAttribute('alt') || '';
856             properties.presentation = (image.get('role') === 'presentation');
857             return properties;
858         }
860         // No image selected - clean up.
861         this._selectedImage = null;
862         return false;
863     },
865     /**
866      * Sets the alignment of a properties object.
867      *
868      * @method _getAlignmentPropeties
869      * @param {Node} image The image that the alignment properties should be found for
870      * @param {Object} properties The properties object that is created in _getSelectedImageProperties()
871      * @private
872      */
873     _getAlignmentPropeties: function(image, properties) {
874         var complete = false,
875             defaultAlignment;
877         // Check for an alignment value.
878         complete = ALIGNMENTS.some(function(alignment) {
879             var classname = this._getAlignmentClass(alignment.value);
880             if (image.hasClass(classname)) {
881                 properties.align = alignment.value;
883                 return true;
884             }
886             if (alignment.isDefault) {
887                 defaultAlignment = alignment.value;
888             }
890             return false;
891         }, this);
893         if (!complete && defaultAlignment) {
894             properties.align = defaultAlignment;
895         }
896     },
898     /**
899      * Update the form when the URL was changed. This includes updating the
900      * height, width, and image preview.
901      *
902      * @method _urlChanged
903      * @private
904      */
905     _urlChanged: function() {
906         var input = this._form.one('.' + CSS.INPUTURL);
908         if (input.get('value') !== '') {
909             // Load the preview image.
910             this._loadPreviewImage(input.get('value'));
911         }
912     },
914     /**
915      * Update the image in the contenteditable.
916      *
917      * @method _setImage
918      * @param {EventFacade} e
919      * @private
920      */
921     _setImage: function(e) {
922         var form = this._form,
923             url = form.one('.' + CSS.INPUTURL).get('value'),
924             alt = form.one('.' + CSS.INPUTALT).get('value'),
925             width = form.one('.' + CSS.INPUTWIDTH).get('value'),
926             height = form.one('.' + CSS.INPUTHEIGHT).get('value'),
927             alignment = this._getAlignmentClass(form.one('.' + CSS.INPUTALIGNMENT).get('value')),
928             presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked'),
929             constrain = form.one('.' + CSS.INPUTCONSTRAIN).get('checked'),
930             imagehtml,
931             customstyle = form.one('.' + CSS.INPUTCUSTOMSTYLE).get('value'),
932             classlist = [],
933             host = this.get('host');
935         e.preventDefault();
937         // Check if there are any accessibility issues.
938         if (this._updateWarning()) {
939             return;
940         }
942         // Focus on the editor in preparation for inserting the image.
943         host.focus();
944         if (url !== '') {
945             if (this._selectedImage) {
946                 host.setSelection(host.getSelectionFromNode(this._selectedImage));
947             } else {
948                 host.setSelection(this._currentSelection);
949             }
951             if (constrain) {
952                 classlist.push(CSS.RESPONSIVE);
953             }
955             // Add the alignment class for the image.
956             classlist.push(alignment);
958             if (!width.match(REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {
959                 form.one('.' + CSS.INPUTWIDTH).focus();
960                 return;
961             }
962             if (!height.match(REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {
963                 form.one('.' + CSS.INPUTHEIGHT).focus();
964                 return;
965             }
967             var template = Y.Handlebars.compile(IMAGETEMPLATE);
968             imagehtml = template({
969                 url: url,
970                 alt: alt,
971                 width: width,
972                 height: height,
973                 presentation: presentation,
974                 customstyle: customstyle,
975                 classlist: classlist.join(' ')
976             });
978             this.get('host').insertContentAtFocusPoint(imagehtml);
980             this.markUpdated();
981         }
983         this.getDialogue({
984             focusAfterHide: null
985         }).hide();
987     },
989     /**
990      * Removes any legacy styles added by previous versions of the atto image button.
991      *
992      * @method _removeLegacyAlignment
993      * @param {Y.Node} imageNode
994      * @return {Y.Node}
995      * @private
996      */
997     _removeLegacyAlignment: function(imageNode) {
998         if (!imageNode.getStyle('margin')) {
999             // There is no margin therefore this cannot match any known alignments.
1000             return imageNode;
1001         }
1003         ALIGNMENTS.some(function(alignment) {
1004             if (imageNode.getStyle(alignment.name) !== alignment.value) {
1005                 // The name/value do not match. Skip.
1006                 return false;
1007             }
1009             var normalisedNode = Y.Node.create('<div>');
1010             normalisedNode.setStyle('margin', alignment.margin);
1011             if (imageNode.getStyle('margin') !== normalisedNode.getStyle('margin')) {
1012                 // The margin does not match.
1013                 return false;
1014             }
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 });
1070 }, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});