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('paste', this._handlePaste, this);
253 this.editor.on('drop', this._handleDragDrop, this);
255 // e.preventDefault needed to stop the default event from clobbering the desired behaviour in some browsers.
256 this.editor.on('dragover', function(e) {
259 this.editor.on('dragenter', function(e) {
265 * Handle a drag and drop event with an image.
267 * @method _handleDragDrop
268 * @param {EventFacade} e
269 * @return {boolean} false if we handled the event, else true.
272 _handleDragDrop: function(e) {
273 if (!e._event || !e._event.dataTransfer) {
274 // Drop not fully supported in this browser.
278 return this._handlePasteOrDropHelper(e, e._event.dataTransfer);
282 * Handles paste events where - if the thing being pasted is an image.
284 * @method _handlePaste
285 * @param {EventFacade} e
286 * @return {boolean} false if we handled the event, else true.
289 _handlePaste: function(e) {
290 if (!e._event || !e._event.clipboardData) {
291 // Paste not fully supported in this browser.
295 return this._handlePasteOrDropHelper(e, e._event.clipboardData);
299 * Handle a drag and drop event with an image.
301 * @method _handleDragDrop
302 * @param {EventFacade} e
303 * @param {DataTransfer} dataTransfer
304 * @return {boolean} false if we handled the event, else true.
307 _handlePasteOrDropHelper: function(e, dataTransfer) {
309 var items = dataTransfer.items,
311 for (var i = 0; i < items.length; i++) {
313 if (item.kind !== 'file') {
316 if (!this._isImage(item.type)) {
319 this._uploadImage(item.getAsFile());
329 // Let someone else try to handle it.
335 * Is this file an image?
338 * @param {string} mimeType the file's mime type.
339 * @return {boolean} true if the file has an image mimeType.
342 _isImage: function(mimeType) {
343 return mimeType.indexOf('image/') === 0;
347 * Used by _handleDragDrop and _handlePaste to upload an image and insert it.
349 * @method _uploadImage
350 * @param {File} fileToSave
353 _uploadImage: function(fileToSave) {
356 host = this.get('host'),
357 template = Y.Handlebars.compile(IMAGETEMPLATE);
359 host.saveSelection();
361 var options = host.get('filepickeroptions').image,
362 savepath = (options.savepath === undefined) ? '/' : options.savepath,
363 formData = new FormData(),
366 xhr = new XMLHttpRequest(),
368 keys = Object.keys(options.repositories);
370 formData.append('repo_upload_file', fileToSave);
371 formData.append('itemid', options.itemid);
373 // List of repositories is an object rather than an array. This makes iteration more awkward.
374 for (var i = 0; i < keys.length; i++) {
375 if (options.repositories[keys[i]].type === 'upload') {
376 formData.append('repo_id', options.repositories[keys[i]].id);
380 formData.append('env', options.env);
381 formData.append('sesskey', M.cfg.sesskey);
382 formData.append('client_id', options.client_id);
383 formData.append('savepath', savepath);
384 formData.append('ctx_id', options.context.id);
386 // Insert spinner as a placeholder.
387 timestamp = new Date().getTime();
388 uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
390 host.restoreSelection();
391 imagehtml = template({
392 url: M.util.image_url("i/loading_small", 'moodle'),
393 alt: M.util.get_string('uploading', COMPONENTNAME),
396 host.insertContentAtFocusPoint(imagehtml);
399 // Kick off a XMLHttpRequest.
400 xhr.onreadystatechange = function() {
401 var placeholder = self.editor.one('#' + uploadid),
407 if (xhr.readyState === 4) {
408 if (xhr.status === 200) {
409 result = JSON.parse(xhr.responseText);
413 placeholder.remove(true);
415 throw new M.core.ajaxException(result);
419 if (result.event && result.event === 'fileexists') {
420 // A file with this name is already in use here - rename to avoid conflict.
421 // Chances are, it's a different image (stored in a different folder on the user's computer).
422 // If the user wants to reuse an existing image, they can copy/paste it within the editor.
423 file = result.newfile;
426 // Replace placeholder with actual image.
431 newimage = Y.Node.create(newhtml);
433 placeholder.replace(newimage);
435 self.editor.appendChild(newimage);
440 Y.use('moodle-core-notification-alert', function() {
441 new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
444 placeholder.remove(true);
449 xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
454 * Handle a click on an image.
456 * @method _handleClick
457 * @param {EventFacade} e
460 _handleClick: function(e) {
461 var image = e.target;
463 var selection = this.get('host').getSelectionFromNode(image);
464 if (this.get('host').getSelection() !== selection) {
465 this.get('host').setSelection(selection);
470 * Display the image editing tool.
472 * @method _displayDialogue
475 _displayDialogue: function() {
476 // Store the current selection.
477 this._currentSelection = this.get('host').getSelection();
478 if (this._currentSelection === false) {
482 // Reset the image dimensions.
483 this._rawImageDimensions = null;
485 var dialogue = this.getDialogue({
486 headerContent: M.util.get_string('imageproperties', COMPONENTNAME),
488 focusAfterHide: true,
489 focusOnShowSelector: SELECTORS.INPUTURL
492 // Set the dialogue content, and then show the dialogue.
493 dialogue.set('bodyContent', this._getDialogueContent())
498 * Set the inputs for width and height if they are not set, and calculate
499 * if the constrain checkbox should be checked or not.
501 * @method _loadPreviewImage
502 * @param {String} url
505 _loadPreviewImage: function(url) {
506 var image = new Image();
509 image.onerror = function() {
510 var preview = self._form.one('.' + CSS.IMAGEPREVIEW);
515 // Centre the dialogue when clearing the image preview.
516 self.getDialogue().centerDialogue();
519 image.onload = function() {
520 var input, currentwidth, currentheight, widthRatio, heightRatio;
522 self._rawImageDimensions = {
527 input = self._form.one('.' + CSS.INPUTWIDTH);
528 currentwidth = input.get('value');
529 if (currentwidth === '') {
530 input.set('value', this.width);
531 currentwidth = "" + this.width;
533 input = self._form.one('.' + CSS.INPUTHEIGHT);
534 currentheight = input.get('value');
535 if (currentheight === '') {
536 input.set('value', this.height);
537 currentheight = "" + this.height;
539 input = self._form.one('.' + CSS.IMAGEPREVIEW);
540 input.setAttribute('src', this.src);
545 input = self._form.one('.' + CSS.INPUTCONSTRAIN);
546 if (currentwidth.match(REGEX.ISPERCENT) && currentheight.match(REGEX.ISPERCENT)) {
547 input.set('checked', currentwidth === currentheight);
549 if (this.width === 0) {
552 if (this.height === 0) {
555 // This is the same as comparing to 3 decimal places.
556 widthRatio = Math.round(1000 * parseInt(currentwidth, 10) / this.width);
557 heightRatio = Math.round(1000 * parseInt(currentheight, 10) / this.height);
558 input.set('checked', widthRatio === heightRatio);
561 // Apply the image sizing.
562 self._autoAdjustSize(self);
564 // Centre the dialogue once the preview image has loaded.
565 self.getDialogue().centerDialogue();
572 * Return the dialogue content for the tool, attaching any required
575 * @method _getDialogueContent
576 * @return {Node} The content to place in the dialogue.
579 _getDialogueContent: function() {
580 var template = Y.Handlebars.compile(TEMPLATE),
581 canShowFilepicker = this.get('host').canShowFilepicker('image'),
582 content = Y.Node.create(template({
583 elementid: this.get('host').get('elementid'),
585 component: COMPONENTNAME,
586 showFilepicker: canShowFilepicker,
587 alignments: ALIGNMENTS
590 this._form = content;
592 // Configure the view of the current image.
593 this._applyImageProperties(this._form);
595 this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
596 this._form.one('.' + CSS.IMAGEPRESENTATION).on('change', this._updateWarning, this);
597 this._form.one('.' + CSS.INPUTALT).on('change', this._updateWarning, this);
598 this._form.one('.' + CSS.INPUTWIDTH).on('blur', this._autoAdjustSize, this);
599 this._form.one('.' + CSS.INPUTHEIGHT).on('blur', this._autoAdjustSize, this, true);
600 this._form.one('.' + CSS.INPUTCONSTRAIN).on('change', function(event) {
601 if (event.target.get('checked')) {
602 this._autoAdjustSize(event);
605 this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
606 this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setImage, this);
608 if (canShowFilepicker) {
609 this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {
610 this.get('host').showFilepicker('image', this._filepickerCallback, this);
615 this._form.one('.' + CSS.INPUTALT).on('keyup', this._handleKeyup, this);
620 _autoAdjustSize: function(e, forceHeight) {
621 forceHeight = forceHeight || false;
623 var keyField = this._form.one('.' + CSS.INPUTWIDTH),
624 keyFieldType = 'width',
625 subField = this._form.one('.' + CSS.INPUTHEIGHT),
626 subFieldType = 'height',
627 constrainField = this._form.one('.' + CSS.INPUTCONSTRAIN),
628 keyFieldValue = keyField.get('value'),
629 subFieldValue = subField.get('value'),
630 imagePreview = this._form.one('.' + CSS.IMAGEPREVIEW),
634 // If we do not know the image size, do not do anything.
635 if (!this._rawImageDimensions) {
639 // Set the width back to default if it is empty.
640 if (keyFieldValue === '') {
641 keyFieldValue = this._rawImageDimensions[keyFieldType];
642 keyField.set('value', keyFieldValue);
643 keyFieldValue = keyField.get('value');
646 // Clear the existing preview sizes.
647 imagePreview.setStyles({
652 // Now update with the new values.
653 if (!constrainField.get('checked')) {
654 // We are not keeping the image proportion - update the preview accordingly.
657 if (keyFieldValue.match(REGEX.ISPERCENT)) {
658 rawPercentage = parseInt(keyFieldValue, 10);
659 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
660 imagePreview.setStyle('width', rawSize + 'px');
662 imagePreview.setStyle('width', keyFieldValue + 'px');
666 if (subFieldValue.match(REGEX.ISPERCENT)) {
667 rawPercentage = parseInt(subFieldValue, 10);
668 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
669 imagePreview.setStyle('height', rawSize + 'px');
671 imagePreview.setStyle('height', subFieldValue + 'px');
674 // We are keeping the image in proportion.
676 // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.
678 _temporaryValue = keyField;
680 subField = _temporaryValue;
682 _temporaryValue = keyFieldType;
683 keyFieldType = subFieldType;
684 subFieldType = _temporaryValue;
686 _temporaryValue = keyFieldValue;
687 keyFieldValue = subFieldValue;
688 subFieldValue = _temporaryValue;
691 if (keyFieldValue.match(REGEX.ISPERCENT)) {
692 // This is a percentage based change. Copy it verbatim.
693 subFieldValue = keyFieldValue;
695 // Set the width to the calculated pixel width.
696 rawPercentage = parseInt(keyFieldValue, 10);
697 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
699 // And apply the width/height to the container.
700 imagePreview.setStyle('width', rawSize);
701 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
702 imagePreview.setStyle('height', rawSize);
704 // Calculate the scaled subFieldValue from the keyFieldValue.
705 subFieldValue = Math.round((keyFieldValue / this._rawImageDimensions[keyFieldType]) *
706 this._rawImageDimensions[subFieldType]);
709 imagePreview.setStyles({
710 'width': subFieldValue,
711 'height': keyFieldValue
714 imagePreview.setStyles({
715 'width': keyFieldValue,
716 'height': subFieldValue
721 // Update the subField's value within the form to reflect the changes.
722 subField.set('value', subFieldValue);
727 * Update the dialogue after an image was selected in the File Picker.
729 * @method _filepickerCallback
730 * @param {object} params The parameters provided by the filepicker
731 * containing information about the image.
734 _filepickerCallback: function(params) {
735 if (params.url !== '') {
736 var input = this._form.one('.' + CSS.INPUTURL);
737 input.set('value', params.url);
739 // Auto set the width and height.
740 this._form.one('.' + CSS.INPUTWIDTH).set('value', '');
741 this._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
743 // Load the preview image.
744 this._loadPreviewImage(params.url);
749 * Applies properties of an existing image to the image dialogue for editing.
751 * @method _applyImageProperties
755 _applyImageProperties: function(form) {
756 var properties = this._getSelectedImageProperties(),
757 img = form.one('.' + CSS.IMAGEPREVIEW);
759 if (properties === false) {
760 img.setStyle('display', 'none');
761 // Set the default alignment.
762 ALIGNMENTS.some(function(alignment) {
763 if (alignment.isDefault) {
764 form.one('.' + CSS.INPUTALIGNMENT).set('value', alignment.value);
774 if (properties.align) {
775 form.one('.' + CSS.INPUTALIGNMENT).set('value', properties.align);
777 if (properties.customstyle) {
778 form.one('.' + CSS.INPUTCUSTOMSTYLE).set('value', properties.customstyle);
780 if (properties.width) {
781 form.one('.' + CSS.INPUTWIDTH).set('value', properties.width);
783 if (properties.height) {
784 form.one('.' + CSS.INPUTHEIGHT).set('value', properties.height);
786 if (properties.alt) {
787 form.one('.' + CSS.INPUTALT).set('value', properties.alt);
789 if (properties.src) {
790 form.one('.' + CSS.INPUTURL).set('value', properties.src);
791 this._loadPreviewImage(properties.src);
793 if (properties.presentation) {
794 form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');
797 // Update the image preview based on the form properties.
798 this._autoAdjustSize();
802 * Gets the properties of the currently selected image.
804 * The first image only if multiple images are selected.
806 * @method _getSelectedImageProperties
810 _getSelectedImageProperties: function() {
820 // Get the current selection.
821 images = this.get('host').getSelectedNodes(),
828 images = images.filter('img');
831 if (images && images.size()) {
832 image = this._removeLegacyAlignment(images.item(0));
833 this._selectedImage = image;
835 style = image.getAttribute('style');
836 properties.customstyle = style;
838 width = image.getAttribute('width');
839 if (!width.match(REGEX.ISPERCENT)) {
840 width = parseInt(width, 10);
842 height = image.getAttribute('height');
843 if (!height.match(REGEX.ISPERCENT)) {
844 height = parseInt(height, 10);
848 properties.width = width;
851 properties.height = height;
853 this._getAlignmentPropeties(image, properties);
854 properties.src = image.getAttribute('src');
855 properties.alt = image.getAttribute('alt') || '';
856 properties.presentation = (image.get('role') === 'presentation');
860 // No image selected - clean up.
861 this._selectedImage = null;
866 * Sets the alignment of a properties object.
868 * @method _getAlignmentPropeties
869 * @param {Node} image The image that the alignment properties should be found for
870 * @param {Object} properties The properties object that is created in _getSelectedImageProperties()
873 _getAlignmentPropeties: function(image, properties) {
874 var complete = false,
877 // Check for an alignment value.
878 complete = ALIGNMENTS.some(function(alignment) {
879 var classname = this._getAlignmentClass(alignment.value);
880 if (image.hasClass(classname)) {
881 properties.align = alignment.value;
882 Y.log('Found alignment ' + alignment.value, 'debug', 'atto_image-button');
887 if (alignment.isDefault) {
888 defaultAlignment = alignment.value;
894 if (!complete && defaultAlignment) {
895 properties.align = defaultAlignment;
900 * Update the form when the URL was changed. This includes updating the
901 * height, width, and image preview.
903 * @method _urlChanged
906 _urlChanged: function() {
907 var input = this._form.one('.' + CSS.INPUTURL);
909 if (input.get('value') !== '') {
910 // Load the preview image.
911 this._loadPreviewImage(input.get('value'));
916 * Update the image in the contenteditable.
919 * @param {EventFacade} e
922 _setImage: function(e) {
923 var form = this._form,
924 url = form.one('.' + CSS.INPUTURL).get('value'),
925 alt = form.one('.' + CSS.INPUTALT).get('value'),
926 width = form.one('.' + CSS.INPUTWIDTH).get('value'),
927 height = form.one('.' + CSS.INPUTHEIGHT).get('value'),
928 alignment = this._getAlignmentClass(form.one('.' + CSS.INPUTALIGNMENT).get('value')),
929 presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked'),
930 constrain = form.one('.' + CSS.INPUTCONSTRAIN).get('checked'),
932 customstyle = form.one('.' + CSS.INPUTCUSTOMSTYLE).get('value'),
934 host = this.get('host');
938 // Check if there are any accessibility issues.
939 if (this._updateWarning()) {
943 // Focus on the editor in preparation for inserting the image.
946 if (this._selectedImage) {
947 host.setSelection(host.getSelectionFromNode(this._selectedImage));
949 host.setSelection(this._currentSelection);
953 classlist.push(CSS.RESPONSIVE);
956 // Add the alignment class for the image.
957 classlist.push(alignment);
959 if (!width.match(REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {
960 form.one('.' + CSS.INPUTWIDTH).focus();
963 if (!height.match(REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {
964 form.one('.' + CSS.INPUTHEIGHT).focus();
968 var template = Y.Handlebars.compile(IMAGETEMPLATE);
969 imagehtml = template({
974 presentation: presentation,
975 customstyle: customstyle,
976 classlist: classlist.join(' ')
979 this.get('host').insertContentAtFocusPoint(imagehtml);
991 * Removes any legacy styles added by previous versions of the atto image button.
993 * @method _removeLegacyAlignment
994 * @param {Y.Node} imageNode
998 _removeLegacyAlignment: function(imageNode) {
999 if (!imageNode.getStyle('margin')) {
1000 // There is no margin therefore this cannot match any known alignments.
1004 ALIGNMENTS.some(function(alignment) {
1005 if (imageNode.getStyle(alignment.name) !== alignment.value) {
1006 // The name/value do not match. Skip.
1010 var normalisedNode = Y.Node.create('<div>');
1011 normalisedNode.setStyle('margin', alignment.margin);
1012 if (imageNode.getStyle('margin') !== normalisedNode.getStyle('margin')) {
1013 // The margin does not match.
1017 Y.log('Legacy alignment found and removed.', 'info', 'atto_image-button');
1018 imageNode.addClass(this._getAlignmentClass(alignment.value));
1019 imageNode.setStyle(alignment.name, null);
1020 imageNode.setStyle('margin', null);
1028 _getAlignmentClass: function(alignment) {
1029 return CSS.ALIGNSETTINGS + '_' + alignment;
1033 * Update the alt text warning live.
1035 * @method _updateWarning
1036 * @return {boolean} whether a warning should be displayed.
1039 _updateWarning: function() {
1040 var form = this._form,
1042 alt = form.one('.' + CSS.INPUTALT).get('value'),
1043 presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked');
1044 if (alt === '' && !presentation) {
1045 form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'block');
1046 form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', true);
1047 form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', true);
1050 form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'none');
1051 form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', false);
1052 form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', false);
1055 this.getDialogue().centerDialogue();
1060 * Handle the keyup to update the character count.
1062 _handleKeyup: function() {
1063 var form = this._form,
1064 alt = form.one('.' + CSS.INPUTALT).get('value'),
1065 characterCount = alt.length,
1066 current = form.one('#currentcount');
1067 current.setHTML(characterCount);
1072 }, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});