MDL-68541 editor-atto: Limiting alt text length, changing ignore text.
[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('drop', this._handleDragDrop, this);
254         // e.preventDefault needed to stop the default event from clobbering the desired behaviour in some browsers.
255         this.editor.on('dragover', function(e) {
256             e.preventDefault();
257         }, this);
258         this.editor.on('dragenter', function(e) {
259             e.preventDefault();
260         }, this);
261     },
263     /**
264      * Handle a drag and drop event with an image.
265      *
266      * @method _handleDragDrop
267      * @param {EventFacade} e
268      * @return mixed
269      * @private
270      */
271     _handleDragDrop: function(e) {
273         var self = this,
274             host = this.get('host'),
275             template = Y.Handlebars.compile(IMAGETEMPLATE);
277         host.saveSelection();
278         e = e._event;
280         // Only handle the event if an image file was dropped in.
281         var handlesDataTransfer = (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length);
282         if (handlesDataTransfer && /^image\//.test(e.dataTransfer.files[0].type)) {
284             var options = host.get('filepickeroptions').image,
285                 savepath = (options.savepath === undefined) ? '/' : options.savepath,
286                 formData = new FormData(),
287                 timestamp = 0,
288                 uploadid = "",
289                 xhr = new XMLHttpRequest(),
290                 imagehtml = "",
291                 keys = Object.keys(options.repositories);
293             e.preventDefault();
294             e.stopPropagation();
295             formData.append('repo_upload_file', e.dataTransfer.files[0]);
296             formData.append('itemid', options.itemid);
298             // List of repositories is an object rather than an array.  This makes iteration more awkward.
299             for (var i = 0; i < keys.length; i++) {
300                 if (options.repositories[keys[i]].type === 'upload') {
301                     formData.append('repo_id', options.repositories[keys[i]].id);
302                     break;
303                 }
304             }
305             formData.append('env', options.env);
306             formData.append('sesskey', M.cfg.sesskey);
307             formData.append('client_id', options.client_id);
308             formData.append('savepath', savepath);
309             formData.append('ctx_id', options.context.id);
311             // Insert spinner as a placeholder.
312             timestamp = new Date().getTime();
313             uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
314             host.focus();
315             host.restoreSelection();
316             imagehtml = template({
317                 url: M.util.image_url("i/loading_small", 'moodle'),
318                 alt: M.util.get_string('uploading', COMPONENTNAME),
319                 id: uploadid
320             });
321             host.insertContentAtFocusPoint(imagehtml);
322             self.markUpdated();
324             // Kick off a XMLHttpRequest.
325             xhr.onreadystatechange = function() {
326                 var placeholder = self.editor.one('#' + uploadid),
327                     result,
328                     file,
329                     newhtml,
330                     newimage;
332                 if (xhr.readyState === 4) {
333                     if (xhr.status === 200) {
334                         result = JSON.parse(xhr.responseText);
335                         if (result) {
336                             if (result.error) {
337                                 if (placeholder) {
338                                     placeholder.remove(true);
339                                 }
340                                 return new M.core.ajaxException(result);
341                             }
343                             file = result;
344                             if (result.event && result.event === 'fileexists') {
345                                 // A file with this name is already in use here - rename to avoid conflict.
346                                 // Chances are, it's a different image (stored in a different folder on the user's computer).
347                                 // If the user wants to reuse an existing image, they can copy/paste it within the editor.
348                                 file = result.newfile;
349                             }
351                             // Replace placeholder with actual image.
352                             newhtml = template({
353                                 url: file.url,
354                                 presentation: true
355                             });
356                             newimage = Y.Node.create(newhtml);
357                             if (placeholder) {
358                                 placeholder.replace(newimage);
359                             } else {
360                                 self.editor.appendChild(newimage);
361                             }
362                             self.markUpdated();
363                         }
364                     } else {
365                         Y.use('moodle-core-notification-alert', function() {
366                             new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
367                         });
368                         if (placeholder) {
369                             placeholder.remove(true);
370                         }
371                     }
372                 }
373             };
374             xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
375             xhr.send(formData);
376             return false;
377         }
379 },
381     /**
382      * Handle a click on an image.
383      *
384      * @method _handleClick
385      * @param {EventFacade} e
386      * @private
387      */
388     _handleClick: function(e) {
389         var image = e.target;
391         var selection = this.get('host').getSelectionFromNode(image);
392         if (this.get('host').getSelection() !== selection) {
393             this.get('host').setSelection(selection);
394         }
395     },
397     /**
398      * Display the image editing tool.
399      *
400      * @method _displayDialogue
401      * @private
402      */
403     _displayDialogue: function() {
404         // Store the current selection.
405         this._currentSelection = this.get('host').getSelection();
406         if (this._currentSelection === false) {
407             return;
408         }
410         // Reset the image dimensions.
411         this._rawImageDimensions = null;
413         var dialogue = this.getDialogue({
414             headerContent: M.util.get_string('imageproperties', COMPONENTNAME),
415             width: 'auto',
416             focusAfterHide: true,
417             focusOnShowSelector: SELECTORS.INPUTURL
418         });
420         // Set the dialogue content, and then show the dialogue.
421         dialogue.set('bodyContent', this._getDialogueContent())
422                 .show();
423     },
425     /**
426      * Set the inputs for width and height if they are not set, and calculate
427      * if the constrain checkbox should be checked or not.
428      *
429      * @method _loadPreviewImage
430      * @param {String} url
431      * @private
432      */
433     _loadPreviewImage: function(url) {
434         var image = new Image();
435         var self = this;
437         image.onerror = function() {
438             var preview = self._form.one('.' + CSS.IMAGEPREVIEW);
439             preview.setStyles({
440                 'display': 'none'
441             });
443             // Centre the dialogue when clearing the image preview.
444             self.getDialogue().centerDialogue();
445         };
447         image.onload = function() {
448             var input, currentwidth, currentheight, widthRatio, heightRatio;
450             self._rawImageDimensions = {
451                 width: this.width,
452                 height: this.height
453             };
455             input = self._form.one('.' + CSS.INPUTWIDTH);
456             currentwidth = input.get('value');
457             if (currentwidth === '') {
458                 input.set('value', this.width);
459                 currentwidth = "" + this.width;
460             }
461             input = self._form.one('.' + CSS.INPUTHEIGHT);
462             currentheight = input.get('value');
463             if (currentheight === '') {
464                 input.set('value', this.height);
465                 currentheight = "" + this.height;
466             }
467             input = self._form.one('.' + CSS.IMAGEPREVIEW);
468             input.setAttribute('src', this.src);
469             input.setStyles({
470                 'display': 'inline'
471             });
473             input = self._form.one('.' + CSS.INPUTCONSTRAIN);
474             if (currentwidth.match(REGEX.ISPERCENT) && currentheight.match(REGEX.ISPERCENT)) {
475                 input.set('checked', currentwidth === currentheight);
476             } else {
477                 if (this.width === 0) {
478                     this.width = 1;
479                 }
480                 if (this.height === 0) {
481                     this.height = 1;
482                 }
483                 // This is the same as comparing to 3 decimal places.
484                 widthRatio = Math.round(1000 * parseInt(currentwidth, 10) / this.width);
485                 heightRatio = Math.round(1000 * parseInt(currentheight, 10) / this.height);
486                 input.set('checked', widthRatio === heightRatio);
487             }
489             // Apply the image sizing.
490             self._autoAdjustSize(self);
492             // Centre the dialogue once the preview image has loaded.
493             self.getDialogue().centerDialogue();
494         };
496         image.src = url;
497     },
499     /**
500      * Return the dialogue content for the tool, attaching any required
501      * events.
502      *
503      * @method _getDialogueContent
504      * @return {Node} The content to place in the dialogue.
505      * @private
506      */
507     _getDialogueContent: function() {
508         var template = Y.Handlebars.compile(TEMPLATE),
509             canShowFilepicker = this.get('host').canShowFilepicker('image'),
510             content = Y.Node.create(template({
511                 elementid: this.get('host').get('elementid'),
512                 CSS: CSS,
513                 component: COMPONENTNAME,
514                 showFilepicker: canShowFilepicker,
515                 alignments: ALIGNMENTS
516             }));
518         this._form = content;
520         // Configure the view of the current image.
521         this._applyImageProperties(this._form);
523         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
524         this._form.one('.' + CSS.IMAGEPRESENTATION).on('change', this._updateWarning, this);
525         this._form.one('.' + CSS.INPUTALT).on('change', this._updateWarning, this);
526         this._form.one('.' + CSS.INPUTWIDTH).on('blur', this._autoAdjustSize, this);
527         this._form.one('.' + CSS.INPUTHEIGHT).on('blur', this._autoAdjustSize, this, true);
528         this._form.one('.' + CSS.INPUTCONSTRAIN).on('change', function(event) {
529             if (event.target.get('checked')) {
530                 this._autoAdjustSize(event);
531             }
532         }, this);
533         this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
534         this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setImage, this);
536         if (canShowFilepicker) {
537             this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {
538                     this.get('host').showFilepicker('image', this._filepickerCallback, this);
539             }, this);
540         }
542         // Character count.
543         this._form.one('.' + CSS.INPUTALT).on('keyup', this._handleKeyup, this);
545         return content;
546     },
548     _autoAdjustSize: function(e, forceHeight) {
549         forceHeight = forceHeight || false;
551         var keyField = this._form.one('.' + CSS.INPUTWIDTH),
552             keyFieldType = 'width',
553             subField = this._form.one('.' + CSS.INPUTHEIGHT),
554             subFieldType = 'height',
555             constrainField = this._form.one('.' + CSS.INPUTCONSTRAIN),
556             keyFieldValue = keyField.get('value'),
557             subFieldValue = subField.get('value'),
558             imagePreview = this._form.one('.' + CSS.IMAGEPREVIEW),
559             rawPercentage,
560             rawSize;
562         // If we do not know the image size, do not do anything.
563         if (!this._rawImageDimensions) {
564             return;
565         }
567         // Set the width back to default if it is empty.
568         if (keyFieldValue === '') {
569             keyFieldValue = this._rawImageDimensions[keyFieldType];
570             keyField.set('value', keyFieldValue);
571             keyFieldValue = keyField.get('value');
572         }
574         // Clear the existing preview sizes.
575         imagePreview.setStyles({
576             width: null,
577             height: null
578         });
580         // Now update with the new values.
581         if (!constrainField.get('checked')) {
582             // We are not keeping the image proportion - update the preview accordingly.
584             // Width.
585             if (keyFieldValue.match(REGEX.ISPERCENT)) {
586                 rawPercentage = parseInt(keyFieldValue, 10);
587                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
588                 imagePreview.setStyle('width', rawSize + 'px');
589             } else {
590                 imagePreview.setStyle('width', keyFieldValue + 'px');
591             }
593             // Height.
594             if (subFieldValue.match(REGEX.ISPERCENT)) {
595                 rawPercentage = parseInt(subFieldValue, 10);
596                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
597                 imagePreview.setStyle('height', rawSize + 'px');
598             } else {
599                 imagePreview.setStyle('height', subFieldValue + 'px');
600             }
601         } else {
602             // We are keeping the image in proportion.
603             if (forceHeight) {
604                 // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.
605                 var _temporaryValue;
606                 _temporaryValue = keyField;
607                 keyField = subField;
608                 subField = _temporaryValue;
610                 _temporaryValue = keyFieldType;
611                 keyFieldType = subFieldType;
612                 subFieldType = _temporaryValue;
614                 _temporaryValue = keyFieldValue;
615                 keyFieldValue = subFieldValue;
616                 subFieldValue = _temporaryValue;
617             }
619             if (keyFieldValue.match(REGEX.ISPERCENT)) {
620                 // This is a percentage based change. Copy it verbatim.
621                 subFieldValue = keyFieldValue;
623                 // Set the width to the calculated pixel width.
624                 rawPercentage = parseInt(keyFieldValue, 10);
625                 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
627                 // And apply the width/height to the container.
628                 imagePreview.setStyle('width', rawSize);
629                 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
630                 imagePreview.setStyle('height', rawSize);
631             } else {
632                 // Calculate the scaled subFieldValue from the keyFieldValue.
633                 subFieldValue = Math.round((keyFieldValue / this._rawImageDimensions[keyFieldType]) *
634                         this._rawImageDimensions[subFieldType]);
636                 if (forceHeight) {
637                     imagePreview.setStyles({
638                         'width': subFieldValue,
639                         'height': keyFieldValue
640                     });
641                 } else {
642                     imagePreview.setStyles({
643                         'width': keyFieldValue,
644                         'height': subFieldValue
645                     });
646                 }
647             }
649             // Update the subField's value within the form to reflect the changes.
650             subField.set('value', subFieldValue);
651         }
652     },
654     /**
655      * Update the dialogue after an image was selected in the File Picker.
656      *
657      * @method _filepickerCallback
658      * @param {object} params The parameters provided by the filepicker
659      * containing information about the image.
660      * @private
661      */
662     _filepickerCallback: function(params) {
663         if (params.url !== '') {
664             var input = this._form.one('.' + CSS.INPUTURL);
665             input.set('value', params.url);
667             // Auto set the width and height.
668             this._form.one('.' + CSS.INPUTWIDTH).set('value', '');
669             this._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
671             // Load the preview image.
672             this._loadPreviewImage(params.url);
673         }
674     },
676     /**
677      * Applies properties of an existing image to the image dialogue for editing.
678      *
679      * @method _applyImageProperties
680      * @param {Node} form
681      * @private
682      */
683     _applyImageProperties: function(form) {
684         var properties = this._getSelectedImageProperties(),
685             img = form.one('.' + CSS.IMAGEPREVIEW);
687         if (properties === false) {
688             img.setStyle('display', 'none');
689             // Set the default alignment.
690             ALIGNMENTS.some(function(alignment) {
691                 if (alignment.isDefault) {
692                     form.one('.' + CSS.INPUTALIGNMENT).set('value', alignment.value);
693                     return true;
694                 }
696                 return false;
697             }, this);
699             return;
700         }
702         if (properties.align) {
703             form.one('.' + CSS.INPUTALIGNMENT).set('value', properties.align);
704         }
705         if (properties.customstyle) {
706             form.one('.' + CSS.INPUTCUSTOMSTYLE).set('value', properties.customstyle);
707         }
708         if (properties.width) {
709             form.one('.' + CSS.INPUTWIDTH).set('value', properties.width);
710         }
711         if (properties.height) {
712             form.one('.' + CSS.INPUTHEIGHT).set('value', properties.height);
713         }
714         if (properties.alt) {
715             form.one('.' + CSS.INPUTALT).set('value', properties.alt);
716         }
717         if (properties.src) {
718             form.one('.' + CSS.INPUTURL).set('value', properties.src);
719             this._loadPreviewImage(properties.src);
720         }
721         if (properties.presentation) {
722             form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');
723         }
725         // Update the image preview based on the form properties.
726         this._autoAdjustSize();
727     },
729     /**
730      * Gets the properties of the currently selected image.
731      *
732      * The first image only if multiple images are selected.
733      *
734      * @method _getSelectedImageProperties
735      * @return {object}
736      * @private
737      */
738     _getSelectedImageProperties: function() {
739         var properties = {
740                 src: null,
741                 alt: null,
742                 width: null,
743                 height: null,
744                 align: '',
745                 presentation: false
746             },
748             // Get the current selection.
749             images = this.get('host').getSelectedNodes(),
750             width,
751             height,
752             style,
753             image;
755         if (images) {
756             images = images.filter('img');
757         }
759         if (images && images.size()) {
760             image = this._removeLegacyAlignment(images.item(0));
761             this._selectedImage = image;
763             style = image.getAttribute('style');
764             properties.customstyle = style;
766             width = image.getAttribute('width');
767             if (!width.match(REGEX.ISPERCENT)) {
768                 width = parseInt(width, 10);
769             }
770             height = image.getAttribute('height');
771             if (!height.match(REGEX.ISPERCENT)) {
772                 height = parseInt(height, 10);
773             }
775             if (width !== 0) {
776                 properties.width = width;
777             }
778             if (height !== 0) {
779                 properties.height = height;
780             }
781             this._getAlignmentPropeties(image, properties);
782             properties.src = image.getAttribute('src');
783             properties.alt = image.getAttribute('alt') || '';
784             properties.presentation = (image.get('role') === 'presentation');
785             return properties;
786         }
788         // No image selected - clean up.
789         this._selectedImage = null;
790         return false;
791     },
793     /**
794      * Sets the alignment of a properties object.
795      *
796      * @method _getAlignmentPropeties
797      * @param {Node} image The image that the alignment properties should be found for
798      * @param {Object} properties The properties object that is created in _getSelectedImageProperties()
799      * @private
800      */
801     _getAlignmentPropeties: function(image, properties) {
802         var complete = false,
803             defaultAlignment;
805         // Check for an alignment value.
806         complete = ALIGNMENTS.some(function(alignment) {
807             var classname = this._getAlignmentClass(alignment.value);
808             if (image.hasClass(classname)) {
809                 properties.align = alignment.value;
811                 return true;
812             }
814             if (alignment.isDefault) {
815                 defaultAlignment = alignment.value;
816             }
818             return false;
819         }, this);
821         if (!complete && defaultAlignment) {
822             properties.align = defaultAlignment;
823         }
824     },
826     /**
827      * Update the form when the URL was changed. This includes updating the
828      * height, width, and image preview.
829      *
830      * @method _urlChanged
831      * @private
832      */
833     _urlChanged: function() {
834         var input = this._form.one('.' + CSS.INPUTURL);
836         if (input.get('value') !== '') {
837             // Load the preview image.
838             this._loadPreviewImage(input.get('value'));
839         }
840     },
842     /**
843      * Update the image in the contenteditable.
844      *
845      * @method _setImage
846      * @param {EventFacade} e
847      * @private
848      */
849     _setImage: function(e) {
850         var form = this._form,
851             url = form.one('.' + CSS.INPUTURL).get('value'),
852             alt = form.one('.' + CSS.INPUTALT).get('value'),
853             width = form.one('.' + CSS.INPUTWIDTH).get('value'),
854             height = form.one('.' + CSS.INPUTHEIGHT).get('value'),
855             alignment = this._getAlignmentClass(form.one('.' + CSS.INPUTALIGNMENT).get('value')),
856             presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked'),
857             constrain = form.one('.' + CSS.INPUTCONSTRAIN).get('checked'),
858             imagehtml,
859             customstyle = form.one('.' + CSS.INPUTCUSTOMSTYLE).get('value'),
860             classlist = [],
861             host = this.get('host');
863         e.preventDefault();
865         // Check if there are any accessibility issues.
866         if (this._updateWarning()) {
867             return;
868         }
870         // Focus on the editor in preparation for inserting the image.
871         host.focus();
872         if (url !== '') {
873             if (this._selectedImage) {
874                 host.setSelection(host.getSelectionFromNode(this._selectedImage));
875             } else {
876                 host.setSelection(this._currentSelection);
877             }
879             if (constrain) {
880                 classlist.push(CSS.RESPONSIVE);
881             }
883             // Add the alignment class for the image.
884             classlist.push(alignment);
886             if (!width.match(REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {
887                 form.one('.' + CSS.INPUTWIDTH).focus();
888                 return;
889             }
890             if (!height.match(REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {
891                 form.one('.' + CSS.INPUTHEIGHT).focus();
892                 return;
893             }
895             var template = Y.Handlebars.compile(IMAGETEMPLATE);
896             imagehtml = template({
897                 url: url,
898                 alt: alt,
899                 width: width,
900                 height: height,
901                 presentation: presentation,
902                 customstyle: customstyle,
903                 classlist: classlist.join(' ')
904             });
906             this.get('host').insertContentAtFocusPoint(imagehtml);
908             this.markUpdated();
909         }
911         this.getDialogue({
912             focusAfterHide: null
913         }).hide();
915     },
917     /**
918      * Removes any legacy styles added by previous versions of the atto image button.
919      *
920      * @method _removeLegacyAlignment
921      * @param {Y.Node} imageNode
922      * @return {Y.Node}
923      * @private
924      */
925     _removeLegacyAlignment: function(imageNode) {
926         if (!imageNode.getStyle('margin')) {
927             // There is no margin therefore this cannot match any known alignments.
928             return imageNode;
929         }
931         ALIGNMENTS.some(function(alignment) {
932             if (imageNode.getStyle(alignment.name) !== alignment.value) {
933                 // The name/value do not match. Skip.
934                 return false;
935             }
937             var normalisedNode = Y.Node.create('<div>');
938             normalisedNode.setStyle('margin', alignment.margin);
939             if (imageNode.getStyle('margin') !== normalisedNode.getStyle('margin')) {
940                 // The margin does not match.
941                 return false;
942             }
944             imageNode.addClass(this._getAlignmentClass(alignment.value));
945             imageNode.setStyle(alignment.name, null);
946             imageNode.setStyle('margin', null);
948             return true;
949         }, this);
951         return imageNode;
952     },
954     _getAlignmentClass: function(alignment) {
955         return CSS.ALIGNSETTINGS + '_' + alignment;
956     },
958     /**
959      * Update the alt text warning live.
960      *
961      * @method _updateWarning
962      * @return {boolean} whether a warning should be displayed.
963      * @private
964      */
965     _updateWarning: function() {
966         var form = this._form,
967             state = true,
968             alt = form.one('.' + CSS.INPUTALT).get('value'),
969             presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked');
970         if (alt === '' && !presentation) {
971             form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'block');
972             form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', true);
973             form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', true);
974             state = true;
975         } else {
976             form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'none');
977             form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', false);
978             form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', false);
979             state = false;
980         }
981         this.getDialogue().centerDialogue();
982         return state;
983     },
985     /**
986      * Handle the keyup to update the character count.
987      */
988     _handleKeyup: function() {
989         var form = this._form,
990             alt = form.one('.' + CSS.INPUTALT).get('value'),
991             characterCount = alt.length,
992             current = form.one('#currentcount');
993         current.setHTML(characterCount);
994     }
995 });
998 }, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});