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 '<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>' +
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}}' +
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>' +
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>' +
178 // Hidden input to store custom styles.
179 '<input type="hidden" class="{{CSS.INPUTCUSTOMSTYLE}}"/>' +
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;"/>' +
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>' +
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}}' +
204 Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
206 * A reference to the current selection at the time that the dialogue
209 * @property _currentSelection
213 _currentSelection: null,
216 * The most recently selected image.
218 * @param _selectedImage
222 _selectedImage: null,
225 * A reference to the currently open form.
234 * The dimensions of the raw image before we manipulate it.
236 * @param _rawImageDimensions
240 _rawImageDimensions: null,
242 initializer: function() {
245 icon: 'e/insert_edit_image',
246 callback: this._displayDialogue,
248 tagMatchRequiresAll: false
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) {
258 this.editor.on('dragenter', function(e) {
264 * Handle a drag and drop event with an image.
266 * @method _handleDragDrop
267 * @param {EventFacade} e
271 _handleDragDrop: function(e) {
274 host = this.get('host'),
275 template = Y.Handlebars.compile(IMAGETEMPLATE);
277 host.saveSelection();
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(),
289 xhr = new XMLHttpRequest(),
291 keys = Object.keys(options.repositories);
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);
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;
315 host.restoreSelection();
316 imagehtml = template({
317 url: M.util.image_url("i/loading_small", 'moodle'),
318 alt: M.util.get_string('uploading', COMPONENTNAME),
321 host.insertContentAtFocusPoint(imagehtml);
324 // Kick off a XMLHttpRequest.
325 xhr.onreadystatechange = function() {
326 var placeholder = self.editor.one('#' + uploadid),
332 if (xhr.readyState === 4) {
333 if (xhr.status === 200) {
334 result = JSON.parse(xhr.responseText);
338 placeholder.remove(true);
340 return new M.core.ajaxException(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;
351 // Replace placeholder with actual image.
356 newimage = Y.Node.create(newhtml);
358 placeholder.replace(newimage);
360 self.editor.appendChild(newimage);
365 Y.use('moodle-core-notification-alert', function() {
366 new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
369 placeholder.remove(true);
374 xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
382 * Handle a click on an image.
384 * @method _handleClick
385 * @param {EventFacade} e
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);
398 * Display the image editing tool.
400 * @method _displayDialogue
403 _displayDialogue: function() {
404 // Store the current selection.
405 this._currentSelection = this.get('host').getSelection();
406 if (this._currentSelection === false) {
410 // Reset the image dimensions.
411 this._rawImageDimensions = null;
413 var dialogue = this.getDialogue({
414 headerContent: M.util.get_string('imageproperties', COMPONENTNAME),
416 focusAfterHide: true,
417 focusOnShowSelector: SELECTORS.INPUTURL
420 // Set the dialogue content, and then show the dialogue.
421 dialogue.set('bodyContent', this._getDialogueContent())
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.
429 * @method _loadPreviewImage
430 * @param {String} url
433 _loadPreviewImage: function(url) {
434 var image = new Image();
437 image.onerror = function() {
438 var preview = self._form.one('.' + CSS.IMAGEPREVIEW);
443 // Centre the dialogue when clearing the image preview.
444 self.getDialogue().centerDialogue();
447 image.onload = function() {
448 var input, currentwidth, currentheight, widthRatio, heightRatio;
450 self._rawImageDimensions = {
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;
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;
467 input = self._form.one('.' + CSS.IMAGEPREVIEW);
468 input.setAttribute('src', this.src);
473 input = self._form.one('.' + CSS.INPUTCONSTRAIN);
474 if (currentwidth.match(REGEX.ISPERCENT) && currentheight.match(REGEX.ISPERCENT)) {
475 input.set('checked', currentwidth === currentheight);
477 if (this.width === 0) {
480 if (this.height === 0) {
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);
489 // Apply the image sizing.
490 self._autoAdjustSize(self);
492 // Centre the dialogue once the preview image has loaded.
493 self.getDialogue().centerDialogue();
500 * Return the dialogue content for the tool, attaching any required
503 * @method _getDialogueContent
504 * @return {Node} The content to place in the dialogue.
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'),
513 component: COMPONENTNAME,
514 showFilepicker: canShowFilepicker,
515 alignments: ALIGNMENTS
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);
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);
543 this._form.one('.' + CSS.INPUTALT).on('keyup', this._handleKeyup, this);
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),
562 // If we do not know the image size, do not do anything.
563 if (!this._rawImageDimensions) {
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');
574 // Clear the existing preview sizes.
575 imagePreview.setStyles({
580 // Now update with the new values.
581 if (!constrainField.get('checked')) {
582 // We are not keeping the image proportion - update the preview accordingly.
585 if (keyFieldValue.match(REGEX.ISPERCENT)) {
586 rawPercentage = parseInt(keyFieldValue, 10);
587 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
588 imagePreview.setStyle('width', rawSize + 'px');
590 imagePreview.setStyle('width', keyFieldValue + 'px');
594 if (subFieldValue.match(REGEX.ISPERCENT)) {
595 rawPercentage = parseInt(subFieldValue, 10);
596 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
597 imagePreview.setStyle('height', rawSize + 'px');
599 imagePreview.setStyle('height', subFieldValue + 'px');
602 // We are keeping the image in proportion.
604 // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.
606 _temporaryValue = keyField;
608 subField = _temporaryValue;
610 _temporaryValue = keyFieldType;
611 keyFieldType = subFieldType;
612 subFieldType = _temporaryValue;
614 _temporaryValue = keyFieldValue;
615 keyFieldValue = subFieldValue;
616 subFieldValue = _temporaryValue;
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);
632 // Calculate the scaled subFieldValue from the keyFieldValue.
633 subFieldValue = Math.round((keyFieldValue / this._rawImageDimensions[keyFieldType]) *
634 this._rawImageDimensions[subFieldType]);
637 imagePreview.setStyles({
638 'width': subFieldValue,
639 'height': keyFieldValue
642 imagePreview.setStyles({
643 'width': keyFieldValue,
644 'height': subFieldValue
649 // Update the subField's value within the form to reflect the changes.
650 subField.set('value', subFieldValue);
655 * Update the dialogue after an image was selected in the File Picker.
657 * @method _filepickerCallback
658 * @param {object} params The parameters provided by the filepicker
659 * containing information about the image.
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);
677 * Applies properties of an existing image to the image dialogue for editing.
679 * @method _applyImageProperties
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);
702 if (properties.align) {
703 form.one('.' + CSS.INPUTALIGNMENT).set('value', properties.align);
705 if (properties.customstyle) {
706 form.one('.' + CSS.INPUTCUSTOMSTYLE).set('value', properties.customstyle);
708 if (properties.width) {
709 form.one('.' + CSS.INPUTWIDTH).set('value', properties.width);
711 if (properties.height) {
712 form.one('.' + CSS.INPUTHEIGHT).set('value', properties.height);
714 if (properties.alt) {
715 form.one('.' + CSS.INPUTALT).set('value', properties.alt);
717 if (properties.src) {
718 form.one('.' + CSS.INPUTURL).set('value', properties.src);
719 this._loadPreviewImage(properties.src);
721 if (properties.presentation) {
722 form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');
725 // Update the image preview based on the form properties.
726 this._autoAdjustSize();
730 * Gets the properties of the currently selected image.
732 * The first image only if multiple images are selected.
734 * @method _getSelectedImageProperties
738 _getSelectedImageProperties: function() {
748 // Get the current selection.
749 images = this.get('host').getSelectedNodes(),
756 images = images.filter('img');
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);
770 height = image.getAttribute('height');
771 if (!height.match(REGEX.ISPERCENT)) {
772 height = parseInt(height, 10);
776 properties.width = width;
779 properties.height = height;
781 this._getAlignmentPropeties(image, properties);
782 properties.src = image.getAttribute('src');
783 properties.alt = image.getAttribute('alt') || '';
784 properties.presentation = (image.get('role') === 'presentation');
788 // No image selected - clean up.
789 this._selectedImage = null;
794 * Sets the alignment of a properties object.
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()
801 _getAlignmentPropeties: function(image, properties) {
802 var complete = false,
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;
810 Y.log('Found alignment ' + alignment.value, 'debug', 'atto_image-button');
815 if (alignment.isDefault) {
816 defaultAlignment = alignment.value;
822 if (!complete && defaultAlignment) {
823 properties.align = defaultAlignment;
828 * Update the form when the URL was changed. This includes updating the
829 * height, width, and image preview.
831 * @method _urlChanged
834 _urlChanged: function() {
835 var input = this._form.one('.' + CSS.INPUTURL);
837 if (input.get('value') !== '') {
838 // Load the preview image.
839 this._loadPreviewImage(input.get('value'));
844 * Update the image in the contenteditable.
847 * @param {EventFacade} e
850 _setImage: function(e) {
851 var form = this._form,
852 url = form.one('.' + CSS.INPUTURL).get('value'),
853 alt = form.one('.' + CSS.INPUTALT).get('value'),
854 width = form.one('.' + CSS.INPUTWIDTH).get('value'),
855 height = form.one('.' + CSS.INPUTHEIGHT).get('value'),
856 alignment = this._getAlignmentClass(form.one('.' + CSS.INPUTALIGNMENT).get('value')),
857 presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked'),
858 constrain = form.one('.' + CSS.INPUTCONSTRAIN).get('checked'),
860 customstyle = form.one('.' + CSS.INPUTCUSTOMSTYLE).get('value'),
862 host = this.get('host');
866 // Check if there are any accessibility issues.
867 if (this._updateWarning()) {
871 // Focus on the editor in preparation for inserting the image.
874 if (this._selectedImage) {
875 host.setSelection(host.getSelectionFromNode(this._selectedImage));
877 host.setSelection(this._currentSelection);
881 classlist.push(CSS.RESPONSIVE);
884 // Add the alignment class for the image.
885 classlist.push(alignment);
887 if (!width.match(REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {
888 form.one('.' + CSS.INPUTWIDTH).focus();
891 if (!height.match(REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {
892 form.one('.' + CSS.INPUTHEIGHT).focus();
896 var template = Y.Handlebars.compile(IMAGETEMPLATE);
897 imagehtml = template({
902 presentation: presentation,
903 customstyle: customstyle,
904 classlist: classlist.join(' ')
907 this.get('host').insertContentAtFocusPoint(imagehtml);
919 * Removes any legacy styles added by previous versions of the atto image button.
921 * @method _removeLegacyAlignment
922 * @param {Y.Node} imageNode
926 _removeLegacyAlignment: function(imageNode) {
927 if (!imageNode.getStyle('margin')) {
928 // There is no margin therefore this cannot match any known alignments.
932 ALIGNMENTS.some(function(alignment) {
933 if (imageNode.getStyle(alignment.name) !== alignment.value) {
934 // The name/value do not match. Skip.
938 var normalisedNode = Y.Node.create('<div>');
939 normalisedNode.setStyle('margin', alignment.margin);
940 if (imageNode.getStyle('margin') !== normalisedNode.getStyle('margin')) {
941 // The margin does not match.
945 Y.log('Legacy alignment found and removed.', 'info', 'atto_image-button');
946 imageNode.addClass(this._getAlignmentClass(alignment.value));
947 imageNode.setStyle(alignment.name, null);
948 imageNode.setStyle('margin', null);
956 _getAlignmentClass: function(alignment) {
957 return CSS.ALIGNSETTINGS + '_' + alignment;
961 * Update the alt text warning live.
963 * @method _updateWarning
964 * @return {boolean} whether a warning should be displayed.
967 _updateWarning: function() {
968 var form = this._form,
970 alt = form.one('.' + CSS.INPUTALT).get('value'),
971 presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked');
972 if (alt === '' && !presentation) {
973 form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'block');
974 form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', true);
975 form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', true);
978 form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'none');
979 form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', false);
980 form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', false);
983 this.getDialogue().centerDialogue();
988 * Handle the keyup to update the character count.
990 _handleKeyup: function() {
991 var form = this._form,
992 alt = form.one('.' + CSS.INPUTALT).get('value'),
993 characterCount = alt.length,
994 current = form.one('#currentcount');
995 current.setHTML(characterCount);
1000 }, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});