1 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
19 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 * @module moodle-atto_image_alignment-button
27 * Atto image selection tool.
29 * @namespace M.atto_image
31 * @extends M.editor_atto.EditorPlugin
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'
53 INPUTURL: '.' + CSS.INPUTURL
56 // Vertical alignment.
58 name: 'verticalAlign',
63 name: 'verticalAlign',
64 str: 'alignment_middle',
68 name: 'verticalAlign',
69 str: 'alignment_bottom',
78 str: 'alignment_left',
83 str: 'alignment_right',
93 COMPONENTNAME = 'atto_image',
96 '<form class="atto_form">' +
98 // Add the repository browser button.
99 '{{#if showFilepicker}}' +
100 '<div class="mb-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-secondary {{CSS.IMAGEBROWSER}}" type="button">' +
107 '{{get_string "browserepositories" component}}</button>' +
112 '<div class="mb-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"/>' +
120 '<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.IMAGEALTWARNING}}">' +
121 '{{get_string "presentationoraltrequired" component}}' +
123 '<div class="mb-1">' +
124 '<label for="{{elementid}}_{{CSS.INPUTALT}}">{{get_string "enteralt" component}}</label>' +
125 '<textarea class="form-control fullwidth {{CSS.INPUTALT}}" ' +
126 'id="{{elementid}}_{{CSS.INPUTALT}}" maxlength="125"></textarea>' +
128 // Add the character count.
129 '<div id="the-count" class="d-flex justify-content-end small">' +
130 '<span id="currentcount">0</span>' +
131 '<span id="maximumcount"> / 125</span>' +
134 // Add the presentation select box.
135 '<div class="form-check">' +
136 '<input type="checkbox" class="form-check-input {{CSS.IMAGEPRESENTATION}}" ' +
137 'id="{{elementid}}_{{CSS.IMAGEPRESENTATION}}"/>' +
138 '<label class="form-check-label" for="{{elementid}}_{{CSS.IMAGEPRESENTATION}}">' +
139 '{{get_string "presentation" component}}' +
144 // Add the size entry boxes.
145 '<div class="mb-1">' +
146 '<label class="" for="{{elementid}}_{{CSS.INPUTSIZE}}">{{get_string "size" component}}</label>' +
147 '<div id="{{elementid}}_{{CSS.INPUTSIZE}}" class="form-inline {{CSS.INPUTSIZE}}">' +
148 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTWIDTH}}">{{get_string "width" component}}</label>' +
149 '<input type="text" class="form-control mr-1 input-mini {{CSS.INPUTWIDTH}}" ' +
150 'id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="4"/> x' +
152 // Add the height entry box.
153 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTHEIGHT}}">{{get_string "height" component}}</label>' +
154 '<input type="text" class="form-control ml-1 input-mini {{CSS.INPUTHEIGHT}}" ' +
155 'id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +
157 // Add the constrain checkbox.
158 '<div class="form-check ml-2">' +
159 '<input type="checkbox" class="form-check-input {{CSS.INPUTCONSTRAIN}}" ' +
160 'id="{{elementid}}_{{CSS.INPUTCONSTRAIN}}"/>' +
161 '<label class="form-check-label" for="{{elementid}}_{{CSS.INPUTCONSTRAIN}}">' +
162 '{{get_string "constrain" component}}</label>' +
167 // Add the alignment selector.
168 '<div class="form-inline mb-1">' +
169 '<label class="for="{{elementid}}_{{CSS.INPUTALIGNMENT}}">{{get_string "alignment" component}}</label>' +
170 '<select class="custom-select {{CSS.INPUTALIGNMENT}}" id="{{elementid}}_{{CSS.INPUTALIGNMENT}}">' +
171 '{{#each alignments}}' +
172 '<option value="{{value}}">{{get_string str ../component}}</option>' +
176 // Hidden input to store custom styles.
177 '<input type="hidden" class="{{CSS.INPUTCUSTOMSTYLE}}"/>' +
180 // Add the image preview.
181 '<div class="mdl-align">' +
182 '<div class="{{CSS.IMAGEPREVIEWBOX}}">' +
183 '<img src="#" class="{{CSS.IMAGEPREVIEW}}" alt="" style="display: none;"/>' +
186 // Add the submit button and close the form.
187 '<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
188 '{{get_string "saveimage" component}}</button>' +
193 '<img src="{{url}}" alt="{{alt}}" ' +
194 '{{#if width}}width="{{width}}" {{/if}}' +
195 '{{#if height}}height="{{height}}" {{/if}}' +
196 '{{#if presentation}}role="presentation" {{/if}}' +
197 '{{#if customstyle}}style="{{customstyle}}" {{/if}}' +
198 '{{#if classlist}}class="{{classlist}}" {{/if}}' +
199 '{{#if id}}id="{{id}}" {{/if}}' +
202 Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
204 * A reference to the current selection at the time that the dialogue
207 * @property _currentSelection
211 _currentSelection: null,
214 * The most recently selected image.
216 * @param _selectedImage
220 _selectedImage: null,
223 * A reference to the currently open form.
232 * The dimensions of the raw image before we manipulate it.
234 * @param _rawImageDimensions
238 _rawImageDimensions: null,
240 initializer: function() {
243 icon: 'e/insert_edit_image',
244 callback: this._displayDialogue,
246 tagMatchRequiresAll: false
248 this.editor.delegate('dblclick', this._displayDialogue, 'img', this);
249 this.editor.delegate('click', this._handleClick, 'img', this);
250 this.editor.on('paste', this._handlePaste, this);
251 this.editor.on('drop', this._handleDragDrop, this);
253 // e.preventDefault needed to stop the default event from clobbering the desired behaviour in some browsers.
254 this.editor.on('dragover', function(e) {
257 this.editor.on('dragenter', function(e) {
263 * Handle a drag and drop event with an image.
265 * @method _handleDragDrop
266 * @param {EventFacade} e
267 * @return {boolean} false if we handled the event, else true.
270 _handleDragDrop: function(e) {
271 if (!e._event || !e._event.dataTransfer) {
272 // Drop not fully supported in this browser.
276 return this._handlePasteOrDropHelper(e, e._event.dataTransfer);
280 * Handles paste events where - if the thing being pasted is an image.
282 * @method _handlePaste
283 * @param {EventFacade} e
284 * @return {boolean} false if we handled the event, else true.
287 _handlePaste: function(e) {
288 if (!e._event || !e._event.clipboardData) {
289 // Paste not fully supported in this browser.
293 return this._handlePasteOrDropHelper(e, e._event.clipboardData);
297 * Handle a drag and drop event with an image.
299 * @method _handleDragDrop
300 * @param {EventFacade} e
301 * @param {DataTransfer} dataTransfer
302 * @return {boolean} false if we handled the event, else true.
305 _handlePasteOrDropHelper: function(e, dataTransfer) {
307 var items = dataTransfer.items,
309 for (var i = 0; i < items.length; i++) {
311 if (item.kind !== 'file') {
314 if (!this._isImage(item.type)) {
317 this._uploadImage(item.getAsFile());
327 // Let someone else try to handle it.
333 * Is this file an image?
336 * @param {string} mimeType the file's mime type.
337 * @return {boolean} true if the file has an image mimeType.
340 _isImage: function(mimeType) {
341 return mimeType.indexOf('image/') === 0;
345 * Used by _handleDragDrop and _handlePaste to upload an image and insert it.
347 * @method _uploadImage
348 * @param {File} fileToSave
351 _uploadImage: function(fileToSave) {
354 host = this.get('host'),
355 template = Y.Handlebars.compile(IMAGETEMPLATE);
357 host.saveSelection();
359 var options = host.get('filepickeroptions').image,
360 savepath = (options.savepath === undefined) ? '/' : options.savepath,
361 formData = new FormData(),
364 xhr = new XMLHttpRequest(),
366 keys = Object.keys(options.repositories);
368 formData.append('repo_upload_file', fileToSave);
369 formData.append('itemid', options.itemid);
371 // List of repositories is an object rather than an array. This makes iteration more awkward.
372 for (var i = 0; i < keys.length; i++) {
373 if (options.repositories[keys[i]].type === 'upload') {
374 formData.append('repo_id', options.repositories[keys[i]].id);
378 formData.append('env', options.env);
379 formData.append('sesskey', M.cfg.sesskey);
380 formData.append('client_id', options.client_id);
381 formData.append('savepath', savepath);
382 formData.append('ctx_id', options.context.id);
384 // Insert spinner as a placeholder.
385 timestamp = new Date().getTime();
386 uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
388 host.restoreSelection();
389 imagehtml = template({
390 url: M.util.image_url("i/loading_small", 'moodle'),
391 alt: M.util.get_string('uploading', COMPONENTNAME),
394 host.insertContentAtFocusPoint(imagehtml);
397 // Kick off a XMLHttpRequest.
398 xhr.onreadystatechange = function() {
399 var placeholder = self.editor.one('#' + uploadid),
405 if (xhr.readyState === 4) {
406 if (xhr.status === 200) {
407 result = JSON.parse(xhr.responseText);
411 placeholder.remove(true);
413 throw new M.core.ajaxException(result);
417 if (result.event && result.event === 'fileexists') {
418 // A file with this name is already in use here - rename to avoid conflict.
419 // Chances are, it's a different image (stored in a different folder on the user's computer).
420 // If the user wants to reuse an existing image, they can copy/paste it within the editor.
421 file = result.newfile;
424 // Replace placeholder with actual image.
429 newimage = Y.Node.create(newhtml);
431 placeholder.replace(newimage);
433 self.editor.appendChild(newimage);
438 Y.use('moodle-core-notification-alert', function() {
439 new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
442 placeholder.remove(true);
447 xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
452 * Handle a click on an image.
454 * @method _handleClick
455 * @param {EventFacade} e
458 _handleClick: function(e) {
459 var image = e.target;
461 var selection = this.get('host').getSelectionFromNode(image);
462 if (this.get('host').getSelection() !== selection) {
463 this.get('host').setSelection(selection);
468 * Display the image editing tool.
470 * @method _displayDialogue
473 _displayDialogue: function() {
474 // Store the current selection.
475 this._currentSelection = this.get('host').getSelection();
476 if (this._currentSelection === false) {
480 // Reset the image dimensions.
481 this._rawImageDimensions = null;
483 var dialogue = this.getDialogue({
484 headerContent: M.util.get_string('imageproperties', COMPONENTNAME),
486 focusAfterHide: true,
487 focusOnShowSelector: SELECTORS.INPUTURL
490 // Set the dialogue content, and then show the dialogue.
491 dialogue.set('bodyContent', this._getDialogueContent())
496 * Set the inputs for width and height if they are not set, and calculate
497 * if the constrain checkbox should be checked or not.
499 * @method _loadPreviewImage
500 * @param {String} url
503 _loadPreviewImage: function(url) {
504 var image = new Image();
507 image.onerror = function() {
508 var preview = self._form.one('.' + CSS.IMAGEPREVIEW);
513 // Centre the dialogue when clearing the image preview.
514 self.getDialogue().centerDialogue();
517 image.onload = function() {
518 var input, currentwidth, currentheight, widthRatio, heightRatio;
520 self._rawImageDimensions = {
525 input = self._form.one('.' + CSS.INPUTWIDTH);
526 currentwidth = input.get('value');
527 if (currentwidth === '') {
528 input.set('value', this.width);
529 currentwidth = "" + this.width;
531 input = self._form.one('.' + CSS.INPUTHEIGHT);
532 currentheight = input.get('value');
533 if (currentheight === '') {
534 input.set('value', this.height);
535 currentheight = "" + this.height;
537 input = self._form.one('.' + CSS.IMAGEPREVIEW);
538 input.setAttribute('src', this.src);
543 input = self._form.one('.' + CSS.INPUTCONSTRAIN);
544 if (currentwidth.match(REGEX.ISPERCENT) && currentheight.match(REGEX.ISPERCENT)) {
545 input.set('checked', currentwidth === currentheight);
547 if (this.width === 0) {
550 if (this.height === 0) {
553 // This is the same as comparing to 3 decimal places.
554 widthRatio = Math.round(1000 * parseInt(currentwidth, 10) / this.width);
555 heightRatio = Math.round(1000 * parseInt(currentheight, 10) / this.height);
556 input.set('checked', widthRatio === heightRatio);
559 // Apply the image sizing.
560 self._autoAdjustSize(self);
562 // Centre the dialogue once the preview image has loaded.
563 self.getDialogue().centerDialogue();
570 * Return the dialogue content for the tool, attaching any required
573 * @method _getDialogueContent
574 * @return {Node} The content to place in the dialogue.
577 _getDialogueContent: function() {
578 var template = Y.Handlebars.compile(TEMPLATE),
579 canShowFilepicker = this.get('host').canShowFilepicker('image'),
580 content = Y.Node.create(template({
581 elementid: this.get('host').get('elementid'),
583 component: COMPONENTNAME,
584 showFilepicker: canShowFilepicker,
585 alignments: ALIGNMENTS
588 this._form = content;
590 // Configure the view of the current image.
591 this._applyImageProperties(this._form);
593 this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
594 this._form.one('.' + CSS.IMAGEPRESENTATION).on('change', this._updateWarning, this);
595 this._form.one('.' + CSS.INPUTALT).on('change', this._updateWarning, this);
596 this._form.one('.' + CSS.INPUTWIDTH).on('blur', this._autoAdjustSize, this);
597 this._form.one('.' + CSS.INPUTHEIGHT).on('blur', this._autoAdjustSize, this, true);
598 this._form.one('.' + CSS.INPUTCONSTRAIN).on('change', function(event) {
599 if (event.target.get('checked')) {
600 this._autoAdjustSize(event);
603 this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
604 this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setImage, this);
606 if (canShowFilepicker) {
607 this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {
608 this.get('host').showFilepicker('image', this._filepickerCallback, this);
613 this._form.one('.' + CSS.INPUTALT).on('keyup', this._handleKeyup, this);
618 _autoAdjustSize: function(e, forceHeight) {
619 forceHeight = forceHeight || false;
621 var keyField = this._form.one('.' + CSS.INPUTWIDTH),
622 keyFieldType = 'width',
623 subField = this._form.one('.' + CSS.INPUTHEIGHT),
624 subFieldType = 'height',
625 constrainField = this._form.one('.' + CSS.INPUTCONSTRAIN),
626 keyFieldValue = keyField.get('value'),
627 subFieldValue = subField.get('value'),
628 imagePreview = this._form.one('.' + CSS.IMAGEPREVIEW),
632 // If we do not know the image size, do not do anything.
633 if (!this._rawImageDimensions) {
637 // Set the width back to default if it is empty.
638 if (keyFieldValue === '') {
639 keyFieldValue = this._rawImageDimensions[keyFieldType];
640 keyField.set('value', keyFieldValue);
641 keyFieldValue = keyField.get('value');
644 // Clear the existing preview sizes.
645 imagePreview.setStyles({
650 // Now update with the new values.
651 if (!constrainField.get('checked')) {
652 // We are not keeping the image proportion - update the preview accordingly.
655 if (keyFieldValue.match(REGEX.ISPERCENT)) {
656 rawPercentage = parseInt(keyFieldValue, 10);
657 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
658 imagePreview.setStyle('width', rawSize + 'px');
660 imagePreview.setStyle('width', keyFieldValue + 'px');
664 if (subFieldValue.match(REGEX.ISPERCENT)) {
665 rawPercentage = parseInt(subFieldValue, 10);
666 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
667 imagePreview.setStyle('height', rawSize + 'px');
669 imagePreview.setStyle('height', subFieldValue + 'px');
672 // We are keeping the image in proportion.
674 // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.
676 _temporaryValue = keyField;
678 subField = _temporaryValue;
680 _temporaryValue = keyFieldType;
681 keyFieldType = subFieldType;
682 subFieldType = _temporaryValue;
684 _temporaryValue = keyFieldValue;
685 keyFieldValue = subFieldValue;
686 subFieldValue = _temporaryValue;
689 if (keyFieldValue.match(REGEX.ISPERCENT)) {
690 // This is a percentage based change. Copy it verbatim.
691 subFieldValue = keyFieldValue;
693 // Set the width to the calculated pixel width.
694 rawPercentage = parseInt(keyFieldValue, 10);
695 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
697 // And apply the width/height to the container.
698 imagePreview.setStyle('width', rawSize);
699 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
700 imagePreview.setStyle('height', rawSize);
702 // Calculate the scaled subFieldValue from the keyFieldValue.
703 subFieldValue = Math.round((keyFieldValue / this._rawImageDimensions[keyFieldType]) *
704 this._rawImageDimensions[subFieldType]);
707 imagePreview.setStyles({
708 'width': subFieldValue,
709 'height': keyFieldValue
712 imagePreview.setStyles({
713 'width': keyFieldValue,
714 'height': subFieldValue
719 // Update the subField's value within the form to reflect the changes.
720 subField.set('value', subFieldValue);
725 * Update the dialogue after an image was selected in the File Picker.
727 * @method _filepickerCallback
728 * @param {object} params The parameters provided by the filepicker
729 * containing information about the image.
732 _filepickerCallback: function(params) {
733 if (params.url !== '') {
734 var input = this._form.one('.' + CSS.INPUTURL);
735 input.set('value', params.url);
737 // Auto set the width and height.
738 this._form.one('.' + CSS.INPUTWIDTH).set('value', '');
739 this._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
741 // Load the preview image.
742 this._loadPreviewImage(params.url);
747 * Applies properties of an existing image to the image dialogue for editing.
749 * @method _applyImageProperties
753 _applyImageProperties: function(form) {
754 var properties = this._getSelectedImageProperties(),
755 img = form.one('.' + CSS.IMAGEPREVIEW);
757 if (properties === false) {
758 img.setStyle('display', 'none');
759 // Set the default alignment.
760 ALIGNMENTS.some(function(alignment) {
761 if (alignment.isDefault) {
762 form.one('.' + CSS.INPUTALIGNMENT).set('value', alignment.value);
772 if (properties.align) {
773 form.one('.' + CSS.INPUTALIGNMENT).set('value', properties.align);
775 if (properties.customstyle) {
776 form.one('.' + CSS.INPUTCUSTOMSTYLE).set('value', properties.customstyle);
778 if (properties.width) {
779 form.one('.' + CSS.INPUTWIDTH).set('value', properties.width);
781 if (properties.height) {
782 form.one('.' + CSS.INPUTHEIGHT).set('value', properties.height);
784 if (properties.alt) {
785 form.one('.' + CSS.INPUTALT).set('value', properties.alt);
787 if (properties.src) {
788 form.one('.' + CSS.INPUTURL).set('value', properties.src);
789 this._loadPreviewImage(properties.src);
791 if (properties.presentation) {
792 form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');
795 // Update the image preview based on the form properties.
796 this._autoAdjustSize();
800 * Gets the properties of the currently selected image.
802 * The first image only if multiple images are selected.
804 * @method _getSelectedImageProperties
808 _getSelectedImageProperties: function() {
818 // Get the current selection.
819 images = this.get('host').getSelectedNodes(),
826 images = images.filter('img');
829 if (images && images.size()) {
830 image = this._removeLegacyAlignment(images.item(0));
831 this._selectedImage = image;
833 style = image.getAttribute('style');
834 properties.customstyle = style;
836 width = image.getAttribute('width');
837 if (!width.match(REGEX.ISPERCENT)) {
838 width = parseInt(width, 10);
840 height = image.getAttribute('height');
841 if (!height.match(REGEX.ISPERCENT)) {
842 height = parseInt(height, 10);
846 properties.width = width;
849 properties.height = height;
851 this._getAlignmentPropeties(image, properties);
852 properties.src = image.getAttribute('src');
853 properties.alt = image.getAttribute('alt') || '';
854 properties.presentation = (image.get('role') === 'presentation');
858 // No image selected - clean up.
859 this._selectedImage = null;
864 * Sets the alignment of a properties object.
866 * @method _getAlignmentPropeties
867 * @param {Node} image The image that the alignment properties should be found for
868 * @param {Object} properties The properties object that is created in _getSelectedImageProperties()
871 _getAlignmentPropeties: function(image, properties) {
872 var complete = false,
875 // Check for an alignment value.
876 complete = ALIGNMENTS.some(function(alignment) {
877 var classname = this._getAlignmentClass(alignment.value);
878 if (image.hasClass(classname)) {
879 properties.align = alignment.value;
880 Y.log('Found alignment ' + alignment.value, 'debug', 'atto_image-button');
885 if (alignment.isDefault) {
886 defaultAlignment = alignment.value;
892 if (!complete && defaultAlignment) {
893 properties.align = defaultAlignment;
898 * Update the form when the URL was changed. This includes updating the
899 * height, width, and image preview.
901 * @method _urlChanged
904 _urlChanged: function() {
905 var input = this._form.one('.' + CSS.INPUTURL);
907 if (input.get('value') !== '') {
908 // Load the preview image.
909 this._loadPreviewImage(input.get('value'));
914 * Update the image in the contenteditable.
917 * @param {EventFacade} e
920 _setImage: function(e) {
921 var form = this._form,
922 url = form.one('.' + CSS.INPUTURL).get('value'),
923 alt = form.one('.' + CSS.INPUTALT).get('value'),
924 width = form.one('.' + CSS.INPUTWIDTH).get('value'),
925 height = form.one('.' + CSS.INPUTHEIGHT).get('value'),
926 alignment = this._getAlignmentClass(form.one('.' + CSS.INPUTALIGNMENT).get('value')),
927 presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked'),
928 constrain = form.one('.' + CSS.INPUTCONSTRAIN).get('checked'),
930 customstyle = form.one('.' + CSS.INPUTCUSTOMSTYLE).get('value'),
932 host = this.get('host');
936 // Check if there are any accessibility issues.
937 if (this._updateWarning()) {
941 // Focus on the editor in preparation for inserting the image.
944 if (this._selectedImage) {
945 host.setSelection(host.getSelectionFromNode(this._selectedImage));
947 host.setSelection(this._currentSelection);
951 classlist.push(CSS.RESPONSIVE);
954 // Add the alignment class for the image.
955 classlist.push(alignment);
957 if (!width.match(REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {
958 form.one('.' + CSS.INPUTWIDTH).focus();
961 if (!height.match(REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {
962 form.one('.' + CSS.INPUTHEIGHT).focus();
966 var template = Y.Handlebars.compile(IMAGETEMPLATE);
967 imagehtml = template({
972 presentation: presentation,
973 customstyle: customstyle,
974 classlist: classlist.join(' ')
977 this.get('host').insertContentAtFocusPoint(imagehtml);
989 * Removes any legacy styles added by previous versions of the atto image button.
991 * @method _removeLegacyAlignment
992 * @param {Y.Node} imageNode
996 _removeLegacyAlignment: function(imageNode) {
997 if (!imageNode.getStyle('margin')) {
998 // There is no margin therefore this cannot match any known alignments.
1002 ALIGNMENTS.some(function(alignment) {
1003 if (imageNode.getStyle(alignment.name) !== alignment.value) {
1004 // The name/value do not match. Skip.
1008 var normalisedNode = Y.Node.create('<div>');
1009 normalisedNode.setStyle('margin', alignment.margin);
1010 if (imageNode.getStyle('margin') !== normalisedNode.getStyle('margin')) {
1011 // The margin does not match.
1015 Y.log('Legacy alignment found and removed.', 'info', 'atto_image-button');
1016 imageNode.addClass(this._getAlignmentClass(alignment.value));
1017 imageNode.setStyle(alignment.name, null);
1018 imageNode.setStyle('margin', null);
1026 _getAlignmentClass: function(alignment) {
1027 return CSS.ALIGNSETTINGS + '_' + alignment;
1031 * Update the alt text warning live.
1033 * @method _updateWarning
1034 * @return {boolean} whether a warning should be displayed.
1037 _updateWarning: function() {
1038 var form = this._form,
1040 alt = form.one('.' + CSS.INPUTALT).get('value'),
1041 presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked');
1042 if (alt === '' && !presentation) {
1043 form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'block');
1044 form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', true);
1045 form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', true);
1048 form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'none');
1049 form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', false);
1050 form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', false);
1053 this.getDialogue().centerDialogue();
1058 * Handle the keyup to update the character count.
1060 _handleKeyup: function() {
1061 var form = this._form,
1062 alt = form.one('.' + CSS.INPUTALT).get('value'),
1063 characterCount = alt.length,
1064 current = form.one('#currentcount');
1065 current.setHTML(characterCount);