1 YUI.add('moodle-atto_image-button', function (Y, NAME) {
3 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
20 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 * @module moodle-atto_image_alignment-button
29 * Atto image selection tool.
31 * @namespace M.atto_image
33 * @extends M.editor_atto.EditorPlugin
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'
55 INPUTURL: '.' + CSS.INPUTURL
58 // Vertical alignment.
60 name: 'verticalAlign',
65 name: 'verticalAlign',
66 str: 'alignment_middle',
70 name: 'verticalAlign',
71 str: 'alignment_bottom',
80 str: 'alignment_left',
85 str: 'alignment_right',
95 COMPONENTNAME = 'atto_image',
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>' +
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"/>' +
122 '<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.IMAGEALTWARNING}}">' +
123 '{{get_string "presentationoraltrequired" component}}' +
125 '<div class="mb-1">' +
126 '<label for="{{elementid}}_{{CSS.INPUTALT}}">{{get_string "enteralt" component}}</label>' +
127 '<input class="form-control fullwidth {{CSS.INPUTALT}}" type="text" value="" ' +
128 'id="{{elementid}}_{{CSS.INPUTALT}}" size="32"/>' +
130 // Add the presentation select box.
131 '<div class="form-check">' +
132 '<input type="checkbox" class="form-check-input {{CSS.IMAGEPRESENTATION}}" ' +
133 'id="{{elementid}}_{{CSS.IMAGEPRESENTATION}}"/>' +
134 '<label class="form-check-label" for="{{elementid}}_{{CSS.IMAGEPRESENTATION}}">' +
135 '{{get_string "presentation" component}}' +
140 // Add the size entry boxes.
141 '<div class="mb-1">' +
142 '<label class="" for="{{elementid}}_{{CSS.INPUTSIZE}}">{{get_string "size" component}}</label>' +
143 '<div id="{{elementid}}_{{CSS.INPUTSIZE}}" class="form-inline {{CSS.INPUTSIZE}}">' +
144 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTWIDTH}}">{{get_string "width" component}}</label>' +
145 '<input type="text" class="form-control mr-1 input-mini {{CSS.INPUTWIDTH}}" ' +
146 'id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="4"/> x' +
148 // Add the height entry box.
149 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTHEIGHT}}">{{get_string "height" component}}</label>' +
150 '<input type="text" class="form-control ml-1 input-mini {{CSS.INPUTHEIGHT}}" ' +
151 'id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +
153 // Add the constrain checkbox.
154 '<div class="form-check ml-2">' +
155 '<input type="checkbox" class="form-check-input {{CSS.INPUTCONSTRAIN}}" ' +
156 'id="{{elementid}}_{{CSS.INPUTCONSTRAIN}}"/>' +
157 '<label class="form-check-label" for="{{elementid}}_{{CSS.INPUTCONSTRAIN}}">' +
158 '{{get_string "constrain" component}}</label>' +
163 // Add the alignment selector.
164 '<div class="form-inline mb-1">' +
165 '<label class="for="{{elementid}}_{{CSS.INPUTALIGNMENT}}">{{get_string "alignment" component}}</label>' +
166 '<select class="custom-select {{CSS.INPUTALIGNMENT}}" id="{{elementid}}_{{CSS.INPUTALIGNMENT}}">' +
167 '{{#each alignments}}' +
168 '<option value="{{value}}">{{get_string str ../component}}</option>' +
172 // Hidden input to store custom styles.
173 '<input type="hidden" class="{{CSS.INPUTCUSTOMSTYLE}}"/>' +
176 // Add the image preview.
177 '<div class="mdl-align">' +
178 '<div class="{{CSS.IMAGEPREVIEWBOX}}">' +
179 '<img src="#" class="{{CSS.IMAGEPREVIEW}}" alt="" style="display: none;"/>' +
182 // Add the submit button and close the form.
183 '<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
184 '{{get_string "saveimage" component}}</button>' +
189 '<img src="{{url}}" alt="{{alt}}" ' +
190 '{{#if width}}width="{{width}}" {{/if}}' +
191 '{{#if height}}height="{{height}}" {{/if}}' +
192 '{{#if presentation}}role="presentation" {{/if}}' +
193 '{{#if customstyle}}style="{{customstyle}}" {{/if}}' +
194 '{{#if classlist}}class="{{classlist}}" {{/if}}' +
195 '{{#if id}}id="{{id}}" {{/if}}' +
198 Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
200 * A reference to the current selection at the time that the dialogue
203 * @property _currentSelection
207 _currentSelection: null,
210 * The most recently selected image.
212 * @param _selectedImage
216 _selectedImage: null,
219 * A reference to the currently open form.
228 * The dimensions of the raw image before we manipulate it.
230 * @param _rawImageDimensions
234 _rawImageDimensions: null,
236 initializer: function() {
239 icon: 'e/insert_edit_image',
240 callback: this._displayDialogue,
242 tagMatchRequiresAll: false
244 this.editor.delegate('dblclick', this._displayDialogue, 'img', this);
245 this.editor.delegate('click', this._handleClick, 'img', this);
246 this.editor.on('paste', this._handlePaste, this);
247 this.editor.on('drop', this._handleDragDrop, this);
249 // e.preventDefault needed to stop the default event from clobbering the desired behaviour in some browsers.
250 this.editor.on('dragover', function(e) {
253 this.editor.on('dragenter', function(e) {
259 * Handle a drag and drop event with an image.
261 * @method _handleDragDrop
262 * @param {EventFacade} e
263 * @return {boolean} false if we handled the event, else true.
266 _handleDragDrop: function(e) {
267 if (!e._event || !e._event.dataTransfer) {
268 // Drop not fully supported in this browser.
272 return this._handlePasteOrDropHelper(e, e._event.dataTransfer);
276 * Handles paste events where - if the thing being pasted is an image.
278 * @method _handlePaste
279 * @param {EventFacade} e
280 * @return {boolean} false if we handled the event, else true.
283 _handlePaste: function(e) {
284 if (!e._event || !e._event.clipboardData) {
285 // Paste not fully supported in this browser.
289 return this._handlePasteOrDropHelper(e, e._event.clipboardData);
293 * Handle a drag and drop event with an image.
295 * @method _handleDragDrop
296 * @param {EventFacade} e
297 * @param {DataTransfer} dataTransfer
298 * @return {boolean} false if we handled the event, else true.
301 _handlePasteOrDropHelper: function(e, dataTransfer) {
303 var items = dataTransfer.items,
305 for (var i = 0; i < items.length; i++) {
307 if (item.kind !== 'file') {
310 if (!this._isImage(item.type)) {
313 this._uploadImage(item.getAsFile());
323 // Let someone else try to handle it.
329 * Is this file an image?
332 * @param {string} mimeType the file's mime type.
333 * @return {boolean} true if the file has an image mimeType.
336 _isImage: function(mimeType) {
337 return mimeType.indexOf('image/') === 0;
341 * Used by _handleDragDrop and _handlePaste to upload an image and insert it.
343 * @method _uploadImage
344 * @param {File} fileToSave
347 _uploadImage: function(fileToSave) {
350 host = this.get('host'),
351 template = Y.Handlebars.compile(IMAGETEMPLATE);
353 host.saveSelection();
355 var options = host.get('filepickeroptions').image,
356 savepath = (options.savepath === undefined) ? '/' : options.savepath,
357 formData = new FormData(),
360 xhr = new XMLHttpRequest(),
362 keys = Object.keys(options.repositories);
364 formData.append('repo_upload_file', fileToSave);
365 formData.append('itemid', options.itemid);
367 // List of repositories is an object rather than an array. This makes iteration more awkward.
368 for (var i = 0; i < keys.length; i++) {
369 if (options.repositories[keys[i]].type === 'upload') {
370 formData.append('repo_id', options.repositories[keys[i]].id);
374 formData.append('env', options.env);
375 formData.append('sesskey', M.cfg.sesskey);
376 formData.append('client_id', options.client_id);
377 formData.append('savepath', savepath);
378 formData.append('ctx_id', options.context.id);
380 // Insert spinner as a placeholder.
381 timestamp = new Date().getTime();
382 uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
384 host.restoreSelection();
385 imagehtml = template({
386 url: M.util.image_url("i/loading_small", 'moodle'),
387 alt: M.util.get_string('uploading', COMPONENTNAME),
390 host.insertContentAtFocusPoint(imagehtml);
393 // Kick off a XMLHttpRequest.
394 xhr.onreadystatechange = function() {
395 var placeholder = self.editor.one('#' + uploadid),
401 if (xhr.readyState === 4) {
402 if (xhr.status === 200) {
403 result = JSON.parse(xhr.responseText);
407 placeholder.remove(true);
409 throw new M.core.ajaxException(result);
413 if (result.event && result.event === 'fileexists') {
414 // A file with this name is already in use here - rename to avoid conflict.
415 // Chances are, it's a different image (stored in a different folder on the user's computer).
416 // If the user wants to reuse an existing image, they can copy/paste it within the editor.
417 file = result.newfile;
420 // Replace placeholder with actual image.
425 newimage = Y.Node.create(newhtml);
427 placeholder.replace(newimage);
429 self.editor.appendChild(newimage);
434 Y.use('moodle-core-notification-alert', function() {
435 new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
438 placeholder.remove(true);
443 xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
448 * Handle a click on an image.
450 * @method _handleClick
451 * @param {EventFacade} e
454 _handleClick: function(e) {
455 var image = e.target;
457 var selection = this.get('host').getSelectionFromNode(image);
458 if (this.get('host').getSelection() !== selection) {
459 this.get('host').setSelection(selection);
464 * Display the image editing tool.
466 * @method _displayDialogue
469 _displayDialogue: function() {
470 // Store the current selection.
471 this._currentSelection = this.get('host').getSelection();
472 if (this._currentSelection === false) {
476 // Reset the image dimensions.
477 this._rawImageDimensions = null;
479 var dialogue = this.getDialogue({
480 headerContent: M.util.get_string('imageproperties', COMPONENTNAME),
482 focusAfterHide: true,
483 focusOnShowSelector: SELECTORS.INPUTURL
486 // Set the dialogue content, and then show the dialogue.
487 dialogue.set('bodyContent', this._getDialogueContent())
492 * Set the inputs for width and height if they are not set, and calculate
493 * if the constrain checkbox should be checked or not.
495 * @method _loadPreviewImage
496 * @param {String} url
499 _loadPreviewImage: function(url) {
500 var image = new Image();
503 image.onerror = function() {
504 var preview = self._form.one('.' + CSS.IMAGEPREVIEW);
509 // Centre the dialogue when clearing the image preview.
510 self.getDialogue().centerDialogue();
513 image.onload = function() {
514 var input, currentwidth, currentheight, widthRatio, heightRatio;
516 self._rawImageDimensions = {
521 input = self._form.one('.' + CSS.INPUTWIDTH);
522 currentwidth = input.get('value');
523 if (currentwidth === '') {
524 input.set('value', this.width);
525 currentwidth = "" + this.width;
527 input = self._form.one('.' + CSS.INPUTHEIGHT);
528 currentheight = input.get('value');
529 if (currentheight === '') {
530 input.set('value', this.height);
531 currentheight = "" + this.height;
533 input = self._form.one('.' + CSS.IMAGEPREVIEW);
534 input.setAttribute('src', this.src);
539 input = self._form.one('.' + CSS.INPUTCONSTRAIN);
540 if (currentwidth.match(REGEX.ISPERCENT) && currentheight.match(REGEX.ISPERCENT)) {
541 input.set('checked', currentwidth === currentheight);
543 if (this.width === 0) {
546 if (this.height === 0) {
549 // This is the same as comparing to 3 decimal places.
550 widthRatio = Math.round(1000 * parseInt(currentwidth, 10) / this.width);
551 heightRatio = Math.round(1000 * parseInt(currentheight, 10) / this.height);
552 input.set('checked', widthRatio === heightRatio);
555 // Apply the image sizing.
556 self._autoAdjustSize(self);
558 // Centre the dialogue once the preview image has loaded.
559 self.getDialogue().centerDialogue();
566 * Return the dialogue content for the tool, attaching any required
569 * @method _getDialogueContent
570 * @return {Node} The content to place in the dialogue.
573 _getDialogueContent: function() {
574 var template = Y.Handlebars.compile(TEMPLATE),
575 canShowFilepicker = this.get('host').canShowFilepicker('image'),
576 content = Y.Node.create(template({
577 elementid: this.get('host').get('elementid'),
579 component: COMPONENTNAME,
580 showFilepicker: canShowFilepicker,
581 alignments: ALIGNMENTS
584 this._form = content;
586 // Configure the view of the current image.
587 this._applyImageProperties(this._form);
589 this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
590 this._form.one('.' + CSS.IMAGEPRESENTATION).on('change', this._updateWarning, this);
591 this._form.one('.' + CSS.INPUTALT).on('change', this._updateWarning, this);
592 this._form.one('.' + CSS.INPUTWIDTH).on('blur', this._autoAdjustSize, this);
593 this._form.one('.' + CSS.INPUTHEIGHT).on('blur', this._autoAdjustSize, this, true);
594 this._form.one('.' + CSS.INPUTCONSTRAIN).on('change', function(event) {
595 if (event.target.get('checked')) {
596 this._autoAdjustSize(event);
599 this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
600 this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setImage, this);
602 if (canShowFilepicker) {
603 this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {
604 this.get('host').showFilepicker('image', this._filepickerCallback, this);
611 _autoAdjustSize: function(e, forceHeight) {
612 forceHeight = forceHeight || false;
614 var keyField = this._form.one('.' + CSS.INPUTWIDTH),
615 keyFieldType = 'width',
616 subField = this._form.one('.' + CSS.INPUTHEIGHT),
617 subFieldType = 'height',
618 constrainField = this._form.one('.' + CSS.INPUTCONSTRAIN),
619 keyFieldValue = keyField.get('value'),
620 subFieldValue = subField.get('value'),
621 imagePreview = this._form.one('.' + CSS.IMAGEPREVIEW),
625 // If we do not know the image size, do not do anything.
626 if (!this._rawImageDimensions) {
630 // Set the width back to default if it is empty.
631 if (keyFieldValue === '') {
632 keyFieldValue = this._rawImageDimensions[keyFieldType];
633 keyField.set('value', keyFieldValue);
634 keyFieldValue = keyField.get('value');
637 // Clear the existing preview sizes.
638 imagePreview.setStyles({
643 // Now update with the new values.
644 if (!constrainField.get('checked')) {
645 // We are not keeping the image proportion - update the preview accordingly.
648 if (keyFieldValue.match(REGEX.ISPERCENT)) {
649 rawPercentage = parseInt(keyFieldValue, 10);
650 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
651 imagePreview.setStyle('width', rawSize + 'px');
653 imagePreview.setStyle('width', keyFieldValue + 'px');
657 if (subFieldValue.match(REGEX.ISPERCENT)) {
658 rawPercentage = parseInt(subFieldValue, 10);
659 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
660 imagePreview.setStyle('height', rawSize + 'px');
662 imagePreview.setStyle('height', subFieldValue + 'px');
665 // We are keeping the image in proportion.
667 // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.
669 _temporaryValue = keyField;
671 subField = _temporaryValue;
673 _temporaryValue = keyFieldType;
674 keyFieldType = subFieldType;
675 subFieldType = _temporaryValue;
677 _temporaryValue = keyFieldValue;
678 keyFieldValue = subFieldValue;
679 subFieldValue = _temporaryValue;
682 if (keyFieldValue.match(REGEX.ISPERCENT)) {
683 // This is a percentage based change. Copy it verbatim.
684 subFieldValue = keyFieldValue;
686 // Set the width to the calculated pixel width.
687 rawPercentage = parseInt(keyFieldValue, 10);
688 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
690 // And apply the width/height to the container.
691 imagePreview.setStyle('width', rawSize);
692 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
693 imagePreview.setStyle('height', rawSize);
695 // Calculate the scaled subFieldValue from the keyFieldValue.
696 subFieldValue = Math.round((keyFieldValue / this._rawImageDimensions[keyFieldType]) *
697 this._rawImageDimensions[subFieldType]);
700 imagePreview.setStyles({
701 'width': subFieldValue,
702 'height': keyFieldValue
705 imagePreview.setStyles({
706 'width': keyFieldValue,
707 'height': subFieldValue
712 // Update the subField's value within the form to reflect the changes.
713 subField.set('value', subFieldValue);
718 * Update the dialogue after an image was selected in the File Picker.
720 * @method _filepickerCallback
721 * @param {object} params The parameters provided by the filepicker
722 * containing information about the image.
725 _filepickerCallback: function(params) {
726 if (params.url !== '') {
727 var input = this._form.one('.' + CSS.INPUTURL);
728 input.set('value', params.url);
730 // Auto set the width and height.
731 this._form.one('.' + CSS.INPUTWIDTH).set('value', '');
732 this._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
734 // Load the preview image.
735 this._loadPreviewImage(params.url);
740 * Applies properties of an existing image to the image dialogue for editing.
742 * @method _applyImageProperties
746 _applyImageProperties: function(form) {
747 var properties = this._getSelectedImageProperties(),
748 img = form.one('.' + CSS.IMAGEPREVIEW);
750 if (properties === false) {
751 img.setStyle('display', 'none');
752 // Set the default alignment.
753 ALIGNMENTS.some(function(alignment) {
754 if (alignment.isDefault) {
755 form.one('.' + CSS.INPUTALIGNMENT).set('value', alignment.value);
765 if (properties.align) {
766 form.one('.' + CSS.INPUTALIGNMENT).set('value', properties.align);
768 if (properties.customstyle) {
769 form.one('.' + CSS.INPUTCUSTOMSTYLE).set('value', properties.customstyle);
771 if (properties.width) {
772 form.one('.' + CSS.INPUTWIDTH).set('value', properties.width);
774 if (properties.height) {
775 form.one('.' + CSS.INPUTHEIGHT).set('value', properties.height);
777 if (properties.alt) {
778 form.one('.' + CSS.INPUTALT).set('value', properties.alt);
780 if (properties.src) {
781 form.one('.' + CSS.INPUTURL).set('value', properties.src);
782 this._loadPreviewImage(properties.src);
784 if (properties.presentation) {
785 form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');
788 // Update the image preview based on the form properties.
789 this._autoAdjustSize();
793 * Gets the properties of the currently selected image.
795 * The first image only if multiple images are selected.
797 * @method _getSelectedImageProperties
801 _getSelectedImageProperties: function() {
811 // Get the current selection.
812 images = this.get('host').getSelectedNodes(),
819 images = images.filter('img');
822 if (images && images.size()) {
823 image = this._removeLegacyAlignment(images.item(0));
824 this._selectedImage = image;
826 style = image.getAttribute('style');
827 properties.customstyle = style;
829 width = image.getAttribute('width');
830 if (!width.match(REGEX.ISPERCENT)) {
831 width = parseInt(width, 10);
833 height = image.getAttribute('height');
834 if (!height.match(REGEX.ISPERCENT)) {
835 height = parseInt(height, 10);
839 properties.width = width;
842 properties.height = height;
844 this._getAlignmentPropeties(image, properties);
845 properties.src = image.getAttribute('src');
846 properties.alt = image.getAttribute('alt') || '';
847 properties.presentation = (image.get('role') === 'presentation');
851 // No image selected - clean up.
852 this._selectedImage = null;
857 * Sets the alignment of a properties object.
859 * @method _getAlignmentPropeties
860 * @param {Node} image The image that the alignment properties should be found for
861 * @param {Object} properties The properties object that is created in _getSelectedImageProperties()
864 _getAlignmentPropeties: function(image, properties) {
865 var complete = false,
868 // Check for an alignment value.
869 complete = ALIGNMENTS.some(function(alignment) {
870 var classname = this._getAlignmentClass(alignment.value);
871 if (image.hasClass(classname)) {
872 properties.align = alignment.value;
877 if (alignment.isDefault) {
878 defaultAlignment = alignment.value;
884 if (!complete && defaultAlignment) {
885 properties.align = defaultAlignment;
890 * Update the form when the URL was changed. This includes updating the
891 * height, width, and image preview.
893 * @method _urlChanged
896 _urlChanged: function() {
897 var input = this._form.one('.' + CSS.INPUTURL);
899 if (input.get('value') !== '') {
900 // Load the preview image.
901 this._loadPreviewImage(input.get('value'));
906 * Update the image in the contenteditable.
909 * @param {EventFacade} e
912 _setImage: function(e) {
913 var form = this._form,
914 url = form.one('.' + CSS.INPUTURL).get('value'),
915 alt = form.one('.' + CSS.INPUTALT).get('value'),
916 width = form.one('.' + CSS.INPUTWIDTH).get('value'),
917 height = form.one('.' + CSS.INPUTHEIGHT).get('value'),
918 alignment = this._getAlignmentClass(form.one('.' + CSS.INPUTALIGNMENT).get('value')),
919 presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked'),
920 constrain = form.one('.' + CSS.INPUTCONSTRAIN).get('checked'),
922 customstyle = form.one('.' + CSS.INPUTCUSTOMSTYLE).get('value'),
924 host = this.get('host');
928 // Check if there are any accessibility issues.
929 if (this._updateWarning()) {
933 // Focus on the editor in preparation for inserting the image.
936 if (this._selectedImage) {
937 host.setSelection(host.getSelectionFromNode(this._selectedImage));
939 host.setSelection(this._currentSelection);
943 classlist.push(CSS.RESPONSIVE);
946 // Add the alignment class for the image.
947 classlist.push(alignment);
949 if (!width.match(REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {
950 form.one('.' + CSS.INPUTWIDTH).focus();
953 if (!height.match(REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {
954 form.one('.' + CSS.INPUTHEIGHT).focus();
958 var template = Y.Handlebars.compile(IMAGETEMPLATE);
959 imagehtml = template({
964 presentation: presentation,
965 customstyle: customstyle,
966 classlist: classlist.join(' ')
969 this.get('host').insertContentAtFocusPoint(imagehtml);
981 * Removes any legacy styles added by previous versions of the atto image button.
983 * @method _removeLegacyAlignment
984 * @param {Y.Node} imageNode
988 _removeLegacyAlignment: function(imageNode) {
989 if (!imageNode.getStyle('margin')) {
990 // There is no margin therefore this cannot match any known alignments.
994 ALIGNMENTS.some(function(alignment) {
995 if (imageNode.getStyle(alignment.name) !== alignment.value) {
996 // The name/value do not match. Skip.
1000 var normalisedNode = Y.Node.create('<div>');
1001 normalisedNode.setStyle('margin', alignment.margin);
1002 if (imageNode.getStyle('margin') !== normalisedNode.getStyle('margin')) {
1003 // The margin does not match.
1007 imageNode.addClass(this._getAlignmentClass(alignment.value));
1008 imageNode.setStyle(alignment.name, null);
1009 imageNode.setStyle('margin', null);
1017 _getAlignmentClass: function(alignment) {
1018 return CSS.ALIGNSETTINGS + '_' + alignment;
1022 * Update the alt text warning live.
1024 * @method _updateWarning
1025 * @return {boolean} whether a warning should be displayed.
1028 _updateWarning: function() {
1029 var form = this._form,
1031 alt = form.one('.' + CSS.INPUTALT).get('value'),
1032 presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked');
1033 if (alt === '' && !presentation) {
1034 form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'block');
1035 form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', true);
1036 form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', true);
1039 form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'none');
1040 form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', false);
1041 form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', false);
1044 this.getDialogue().centerDialogue();
1050 }, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});