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