AutorÃa | Ultima modificación | Ver Log |
// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./** @package atto_image* @copyright 2013 Damyon Wiese <damyon@moodle.com>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*//*** @module moodle-atto_image_alignment-button*//*** Atto image selection tool.** @namespace M.atto_image* @class Button* @extends M.editor_atto.EditorPlugin*/var CSS = {RESPONSIVE: 'img-fluid',INPUTALIGNMENT: 'atto_image_alignment',INPUTALT: 'atto_image_altentry',INPUTHEIGHT: 'atto_image_heightentry',INPUTSUBMIT: 'atto_image_urlentrysubmit',INPUTURL: 'atto_image_urlentry',INPUTSIZE: 'atto_image_size',INPUTWIDTH: 'atto_image_widthentry',IMAGEALTWARNING: 'atto_image_altwarning',IMAGEURLWARNING: 'atto_image_urlwarning',IMAGEBROWSER: 'openimagebrowser',IMAGEPRESENTATION: 'atto_image_presentation',INPUTCONSTRAIN: 'atto_image_constrain',INPUTCUSTOMSTYLE: 'atto_image_customstyle',IMAGEPREVIEW: 'atto_image_preview',IMAGEPREVIEWBOX: 'atto_image_preview_box',ALIGNSETTINGS: 'atto_image_button'},FORMNAMES = {URL: 'urlentry',ALT: 'altentry'},SELECTORS = {INPUTURL: '.' + CSS.INPUTURL},ALIGNMENTS = [// Vertical alignment.{name: 'verticalAlign',str: 'alignment_top',value: 'text-top',margin: '0 0.5em'}, {name: 'verticalAlign',str: 'alignment_middle',value: 'middle',margin: '0 0.5em'}, {name: 'verticalAlign',str: 'alignment_bottom',value: 'text-bottom',margin: '0 0.5em',isDefault: true},// Floats.{name: 'float',str: 'alignment_left',value: 'left',margin: '0 0.5em 0 0'}, {name: 'float',str: 'alignment_right',value: 'right',margin: '0 0 0 0.5em'}],DEFAULTS = {WIDTH: 160,HEIGHT: 160,},REGEX = {ISPERCENT: /\d+%/},COMPONENTNAME = 'atto_image',TEMPLATE = '' +'<form class="atto_form">' +// Add the repository browser button.'<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.IMAGEURLWARNING}}">' +'<label for="{{elementid}}_{{CSS.INPUTURL}}">' +'{{get_string "imageurlrequired" component}}' +'</label>' +'</div>' +'{{#if showFilepicker}}' +'<div class="mb-1">' +'<label for="{{elementid}}_{{CSS.INPUTURL}}">{{get_string "enterurl" component}}</label>' +'<div class="input-group input-append w-100">' +'<input name="{{FORMNAMES.URL}}" class="form-control {{CSS.INPUTURL}}" type="url" ' +'id="{{elementid}}_{{CSS.INPUTURL}}" size="32"/>' +'<span class="input-group-append">' +'<button class="btn btn-secondary {{CSS.IMAGEBROWSER}}" type="button">' +'{{get_string "browserepositories" component}}</button>' +'</span>' +'</div>' +'</div>' +'{{else}}' +'<div class="mb-1">' +'<label for="{{elementid}}_{{CSS.INPUTURL}}">{{get_string "enterurl" component}}</label>' +'<input name="{{FORMNAMES.URL}}" class="form-control fullwidth {{CSS.INPUTURL}}" type="url" ' +'id="{{elementid}}_{{CSS.INPUTURL}}" size="32"/>' +'</div>' +'{{/if}}' +'<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.IMAGEALTWARNING}}">' +'<label for="{{elementid}}_{{CSS.INPUTALT}}">' +'{{get_string "presentationoraltrequired" component}}' +'</label>' +'</div>' +// Add the Alt box.'<div class="mb-1">' +'<label for="{{elementid}}_{{CSS.INPUTALT}}">{{get_string "enteralt" component}}</label>' +'<textarea class="form-control fullwidth {{CSS.INPUTALT}}" ' +'id="{{elementid}}_{{CSS.INPUTALT}}" name="{{FORMNAMES.ALT}}" maxlength="125"></textarea>' +// Add the character count.'<div id="the-count" class="d-flex justify-content-end small">' +'<span id="currentcount">0</span>' +'<span id="maximumcount"> / 125</span>' +'</div>' +// Add the presentation select box.'<div class="form-check">' +'<input type="checkbox" class="form-check-input {{CSS.IMAGEPRESENTATION}}" ' +'id="{{elementid}}_{{CSS.IMAGEPRESENTATION}}"/>' +'<label class="form-check-label" for="{{elementid}}_{{CSS.IMAGEPRESENTATION}}">' +'{{get_string "presentation" component}}' +'</label>' +'</div>' +'</div>' +// Add the size entry boxes.'<div class="mb-1">' +'<label class="" for="{{elementid}}_{{CSS.INPUTSIZE}}">{{get_string "size" component}}</label>' +'<div id="{{elementid}}_{{CSS.INPUTSIZE}}" class="d-flex flex-wrap align-items-center {{CSS.INPUTSIZE}}">' +'<label class="accesshide" for="{{elementid}}_{{CSS.INPUTWIDTH}}">{{get_string "width" component}}</label>' +'<input type="text" class="form-control w-auto mr-1 input-mini {{CSS.INPUTWIDTH}}" ' +'id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="4"/> x' +// Add the height entry box.'<label class="accesshide" for="{{elementid}}_{{CSS.INPUTHEIGHT}}">{{get_string "height" component}}</label>' +'<input type="text" class="form-control w-auto ml-1 input-mini {{CSS.INPUTHEIGHT}}" ' +'id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +// Add the constrain checkbox.'<div class="form-check ml-2">' +'<input type="checkbox" class="form-check-input {{CSS.INPUTCONSTRAIN}}" ' +'id="{{elementid}}_{{CSS.INPUTCONSTRAIN}}"/>' +'<label class="form-check-label" for="{{elementid}}_{{CSS.INPUTCONSTRAIN}}">' +'{{get_string "constrain" component}}</label>' +'</div>' +'</div>' +'</div>' +// Add the alignment selector.'<div class="d-flex flex-wrap align-items-center mb-1">' +'<label class="mb-0" for="{{elementid}}_{{CSS.INPUTALIGNMENT}}">{{get_string "alignment" component}}</label>' +'<select class="custom-select ml-2 {{CSS.INPUTALIGNMENT}}" id="{{elementid}}_{{CSS.INPUTALIGNMENT}}">' +'{{#each alignments}}' +'<option value="{{value}}">{{get_string str ../component}}</option>' +'{{/each}}' +'</select>' +'</div>' +// Hidden input to store custom styles.'<input type="hidden" class="{{CSS.INPUTCUSTOMSTYLE}}"/>' +'<br/>' +// Add the image preview.'<div class="mdl-align">' +'<div class="{{CSS.IMAGEPREVIEWBOX}}">' +'<img class="{{CSS.IMAGEPREVIEW}}" alt="" style="display: none;"/>' +'</div>' +// Add the submit button and close the form.'<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +'{{get_string "saveimage" component}}</button>' +'</div>' +'</form>',IMAGETEMPLATE = '' +'<img src="{{url}}" alt="{{alt}}" ' +'{{#if width}}width="{{width}}" {{/if}}' +'{{#if height}}height="{{height}}" {{/if}}' +'{{#if presentation}}role="presentation" {{/if}}' +'{{#if customstyle}}style="{{customstyle}}" {{/if}}' +'{{#if classlist}}class="{{classlist}}" {{/if}}' +'{{#if id}}id="{{id}}" {{/if}}' +'/>';Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {/*** A reference to the current selection at the time that the dialogue* was opened.** @property _currentSelection* @type Range* @private*/_currentSelection: null,/*** The most recently selected image.** @param _selectedImage* @type Node* @private*/_selectedImage: null,/*** A reference to the currently open form.** @param _form* @type Node* @private*/_form: null,/*** The dimensions of the raw image before we manipulate it.** @param _rawImageDimensions* @type Object* @private*/_rawImageDimensions: null,initializer: function() {this.addButton({icon: 'e/insert_edit_image',callback: this._displayDialogue,tags: 'img',tagMatchRequiresAll: false});this.editor.delegate('dblclick', this._displayDialogue, 'img', this);this.editor.delegate('click', this._handleClick, 'img', this);this.editor.on('paste', this._handlePaste, this);this.editor.on('drop', this._handleDragDrop, this);// ...e.preventDefault needed to stop the default event from clobbering the desired behaviour in some browsers.this.editor.on('dragover', function(e) {e.preventDefault();}, this);this.editor.on('dragenter', function(e) {e.preventDefault();}, this);},/*** Handle a drag and drop event with an image.** @method _handleDragDrop* @param {EventFacade} e* @private*/_handleDragDrop: function(e) {if (!e._event || !e._event.dataTransfer) {// Drop not fully supported in this browser.return;}this._handlePasteOrDropHelper(e, e._event.dataTransfer);},/*** Handles paste events where - if the thing being pasted is an image.** @method _handlePaste* @param {EventFacade} e* @return {boolean} false if we handled the event, else true.* @private*/_handlePaste: function(e) {if (!e._event || !e._event.clipboardData) {// Paste not fully supported in this browser.return true;}return this._handlePasteOrDropHelper(e, e._event.clipboardData);},/*** Handle a drag and drop event with an image.** @method _handleDragDrop* @param {EventFacade} e* @param {DataTransfer} dataTransfer* @return {boolean} false if we handled the event, else true.* @private*/_handlePasteOrDropHelper: function(e, dataTransfer) {var items = dataTransfer.items,didUpload = false;for (var i = 0; i < items.length; i++) {var item = items[i];if (item.kind !== 'file') {continue;}if (!this._isImage(item.type)) {continue;}this._uploadImage(item.getAsFile());didUpload = true;}if (didUpload) {e.preventDefault();}},/*** Is this file an image?** @method _isImage* @param {string} mimeType the file's mime type.* @return {boolean} true if the file has an image mimeType.* @private*/_isImage: function(mimeType) {return mimeType.indexOf('image/') === 0;},/*** Used by _handleDragDrop and _handlePaste to upload an image and insert it.** @method _uploadImage* @param {File} fileToSave* @private*/_uploadImage: function(fileToSave) {var self = this,host = this.get('host'),template = Y.Handlebars.compile(IMAGETEMPLATE);host.saveSelection();// Trigger form upload start events.require(['core_form/events'], function(FormEvent) {FormEvent.notifyUploadStarted(self.editor.get('id'));});var options = host.get('filepickeroptions').image,savepath = (options.savepath === undefined) ? '/' : options.savepath,formData = new FormData(),timestamp = 0,uploadid = "",xhr = new XMLHttpRequest(),imagehtml = "",keys = Object.keys(options.repositories);formData.append('repo_upload_file', fileToSave);formData.append('itemid', options.itemid);// List of repositories is an object rather than an array. This makes iteration more awkward.for (var i = 0; i < keys.length; i++) {if (options.repositories[keys[i]].type === 'upload') {formData.append('repo_id', options.repositories[keys[i]].id);break;}}formData.append('env', options.env);formData.append('sesskey', M.cfg.sesskey);formData.append('client_id', options.client_id);formData.append('savepath', savepath);formData.append('ctx_id', options.context.id);// Insert spinner as a placeholder.timestamp = new Date().getTime();uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;host.focus();host.restoreSelection();imagehtml = template({url: M.util.image_url("i/loading_small", 'moodle'),alt: M.util.get_string('uploading', COMPONENTNAME),id: uploadid});host.insertContentAtFocusPoint(imagehtml);self.markUpdated();// Kick off a XMLHttpRequest.xhr.onreadystatechange = function() {var placeholder = self.editor.one('#' + uploadid),result,file,newhtml,newimage;if (xhr.readyState === 4) {if (xhr.status === 200) {result = JSON.parse(xhr.responseText);if (result) {if (result.error) {if (placeholder) {placeholder.remove(true);}// Trigger form upload complete events.require(['core_form/events'], function(FormEvent) {FormEvent.notifyUploadCompleted(self.editor.get('id'));});throw new M.core.ajaxException(result);}file = result;if (result.event && result.event === 'fileexists') {// A file with this name is already in use here - rename to avoid conflict.// Chances are, it's a different image (stored in a different folder on the user's computer).// If the user wants to reuse an existing image, they can copy/paste it within the editor.file = result.newfile;}// Replace placeholder with actual image.newhtml = template({url: file.url,presentation: true,classlist: CSS.RESPONSIVE});newimage = Y.Node.create(newhtml);if (placeholder) {placeholder.replace(newimage);} else {self.editor.appendChild(newimage);}self.markUpdated();}} else {Y.use('moodle-core-notification-alert', function() {// Trigger form upload complete events.require(['core_form/events'], function(FormEvent) {FormEvent.notifyUploadCompleted(self.editor.get('id'));});new M.core.alert({message: M.util.get_string('servererror', 'moodle')});});if (placeholder) {placeholder.remove(true);}}// Trigger form upload complete events.require(['core_form/events'], function(FormEvent) {FormEvent.notifyUploadCompleted(self.editor.get('id'));});}};xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);xhr.send(formData);},/*** Handle a click on an image.** @method _handleClick* @param {EventFacade} e* @private*/_handleClick: function(e) {var image = e.target;var selection = this.get('host').getSelectionFromNode(image);if (this.get('host').getSelection() !== selection) {this.get('host').setSelection(selection);}},/*** Display the image editing tool.** @method _displayDialogue* @private*/_displayDialogue: function() {// Store the current selection.this._currentSelection = this.get('host').getSelection();if (this._currentSelection === false) {return;}// Reset the image dimensions.this._rawImageDimensions = null;var dialogue = this.getDialogue({headerContent: M.util.get_string('imageproperties', COMPONENTNAME),width: 'auto',focusAfterHide: true,focusOnShowSelector: SELECTORS.INPUTURL});// Set a maximum width for the dialog. This will prevent the dialog width to extend beyond the screen width// in cases when the uploaded image has larger width.dialogue.get('boundingBox').setStyle('maxWidth', '90%');// Set the dialogue content, and then show the dialogue.dialogue.set('bodyContent', this._getDialogueContent()).show();},/*** Set the inputs for width and height if they are not set, and calculate* if the constrain checkbox should be checked or not.** @method _loadPreviewImage* @param {String} url* @private*/_loadPreviewImage: function(url) {var image = new Image();var self = this;image.onerror = function() {var preview = self._form.one('.' + CSS.IMAGEPREVIEW);preview.setStyles({'display': 'none'});// Centre the dialogue when clearing the image preview.self.getDialogue().centerDialogue();};image.onload = function() {var input, currentwidth, currentheight, widthRatio, heightRatio;// Store dimensions of the raw image, falling back to defaults for images without dimensions (e.g. SVG).self._rawImageDimensions = {width: this.width || DEFAULTS.WIDTH,height: this.height || DEFAULTS.HEIGHT,};input = self._form.one('.' + CSS.INPUTWIDTH);currentwidth = input.get('value');if (currentwidth === '') {input.set('value', self._rawImageDimensions.width);currentwidth = "" + self._rawImageDimensions.width;}input = self._form.one('.' + CSS.INPUTHEIGHT);currentheight = input.get('value');if (currentheight === '') {input.set('value', self._rawImageDimensions.height);currentheight = "" + self._rawImageDimensions.height;}input = self._form.one('.' + CSS.IMAGEPREVIEW);input.setAttribute('src', this.src);input.setStyles({'display': 'inline'});input = self._form.one('.' + CSS.INPUTCONSTRAIN);if (currentwidth.match(REGEX.ISPERCENT) && currentheight.match(REGEX.ISPERCENT)) {input.set('checked', currentwidth === currentheight);} else if (this.width === 0 || this.height === 0) {// If we don't have both dimensions of the image, we can't auto-size it, so disable control.input.set('disabled', 'disabled');} else {// This is the same as comparing to 3 decimal places.widthRatio = Math.round(1000 * parseInt(currentwidth, 10) / this.width);heightRatio = Math.round(1000 * parseInt(currentheight, 10) / this.height);input.set('checked', widthRatio === heightRatio);}// Apply the image sizing.self._autoAdjustSize(self);// Centre the dialogue once the preview image has loaded.self.getDialogue().centerDialogue();};image.src = url;},/*** Return the dialogue content for the tool, attaching any required* events.** @method _getDialogueContent* @return {Node} The content to place in the dialogue.* @private*/_getDialogueContent: function() {var template = Y.Handlebars.compile(TEMPLATE),canShowFilepicker = this.get('host').canShowFilepicker('image'),content = Y.Node.create(template({elementid: this.get('host').get('elementid'),CSS: CSS,FORMNAMES: FORMNAMES,component: COMPONENTNAME,showFilepicker: canShowFilepicker,alignments: ALIGNMENTS}));this._form = content;// Configure the view of the current image.this._applyImageProperties(this._form);this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);this._form.one('.' + CSS.INPUTURL).on('change', this._hasErrorUrlField, this);this._form.one('.' + CSS.IMAGEPRESENTATION).on('change', this._hasErrorAltField, this);this._form.one('.' + CSS.INPUTALT).on('blur', this._hasErrorAltField, this);this._form.one('.' + CSS.INPUTWIDTH).on('blur', this._autoAdjustSize, this);this._form.one('.' + CSS.INPUTHEIGHT).on('blur', this._autoAdjustSize, this, true);this._form.one('.' + CSS.INPUTCONSTRAIN).on('change', function(event) {if (event.target.get('checked')) {this._autoAdjustSize(event);}}, this);this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setImage, this);if (canShowFilepicker) {this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {this.get('host').showFilepicker('image', this._filepickerCallback, this);}, this);}// Character count.this._form.one('.' + CSS.INPUTALT).on('keyup', this._handleKeyup, this);return content;},_autoAdjustSize: function(e, forceHeight) {forceHeight = forceHeight || false;var keyField = this._form.one('.' + CSS.INPUTWIDTH),keyFieldType = 'width',subField = this._form.one('.' + CSS.INPUTHEIGHT),subFieldType = 'height',constrainField = this._form.one('.' + CSS.INPUTCONSTRAIN),keyFieldValue = keyField.get('value'),subFieldValue = subField.get('value'),imagePreview = this._form.one('.' + CSS.IMAGEPREVIEW),rawPercentage,rawSize;// If we do not know the image size, do not do anything.if (!this._rawImageDimensions) {return;}// Set the width back to default if it is empty.if (keyFieldValue === '') {keyFieldValue = this._rawImageDimensions[keyFieldType];keyField.set('value', keyFieldValue);keyFieldValue = keyField.get('value');}// Clear the existing preview sizes.imagePreview.setStyles({width: null,height: null});// Now update with the new values.if (!constrainField.get('checked')) {// We are not keeping the image proportion - update the preview accordingly.// Width.if (keyFieldValue.match(REGEX.ISPERCENT)) {rawPercentage = parseInt(keyFieldValue, 10);rawSize = this._rawImageDimensions.width / 100 * rawPercentage;imagePreview.setStyle('width', rawSize + 'px');} else {imagePreview.setStyle('width', keyFieldValue + 'px');}// Height.if (subFieldValue.match(REGEX.ISPERCENT)) {rawPercentage = parseInt(subFieldValue, 10);rawSize = this._rawImageDimensions.height / 100 * rawPercentage;imagePreview.setStyle('height', rawSize + 'px');} else {imagePreview.setStyle('height', subFieldValue + 'px');}} else {// We are keeping the image in proportion.if (forceHeight) {// By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.var _temporaryValue;_temporaryValue = keyField;keyField = subField;subField = _temporaryValue;_temporaryValue = keyFieldType;keyFieldType = subFieldType;subFieldType = _temporaryValue;_temporaryValue = keyFieldValue;keyFieldValue = subFieldValue;subFieldValue = _temporaryValue;}if (keyFieldValue.match(REGEX.ISPERCENT)) {// This is a percentage based change. Copy it verbatim.subFieldValue = keyFieldValue;// Set the width to the calculated pixel width.rawPercentage = parseInt(keyFieldValue, 10);rawSize = this._rawImageDimensions.width / 100 * rawPercentage;// And apply the width/height to the container.imagePreview.setStyle('width', rawSize);rawSize = this._rawImageDimensions.height / 100 * rawPercentage;imagePreview.setStyle('height', rawSize);} else {// Calculate the scaled subFieldValue from the keyFieldValue.subFieldValue = Math.round((keyFieldValue / this._rawImageDimensions[keyFieldType]) *this._rawImageDimensions[subFieldType]);if (forceHeight) {imagePreview.setStyles({'width': subFieldValue,'height': keyFieldValue});} else {imagePreview.setStyles({'width': keyFieldValue,'height': subFieldValue});}}// Update the subField's value within the form to reflect the changes.subField.set('value', subFieldValue);}},/*** Update the dialogue after an image was selected in the File Picker.** @method _filepickerCallback* @param {object} params The parameters provided by the filepicker* containing information about the image.* @private*/_filepickerCallback: function(params) {if (params.url !== '') {var input = this._form.one('.' + CSS.INPUTURL);input.set('value', params.url);// Auto set the width and height.this._form.one('.' + CSS.INPUTWIDTH).set('value', '');this._form.one('.' + CSS.INPUTHEIGHT).set('value', '');// Load the preview image.this._loadPreviewImage(params.url);}},/*** Applies properties of an existing image to the image dialogue for editing.** @method _applyImageProperties* @param {Node} form* @private*/_applyImageProperties: function(form) {var properties = this._getSelectedImageProperties(),img = form.one('.' + CSS.IMAGEPREVIEW);if (properties === false) {img.setStyle('display', 'none');// Set the default alignment.ALIGNMENTS.some(function(alignment) {if (alignment.isDefault) {form.one('.' + CSS.INPUTALIGNMENT).set('value', alignment.value);return true;}return false;}, this);return;}if (properties.align) {form.one('.' + CSS.INPUTALIGNMENT).set('value', properties.align);}if (properties.customstyle) {form.one('.' + CSS.INPUTCUSTOMSTYLE).set('value', properties.customstyle);}if (properties.width) {form.one('.' + CSS.INPUTWIDTH).set('value', properties.width);}if (properties.height) {form.one('.' + CSS.INPUTHEIGHT).set('value', properties.height);}if (properties.alt) {form.one('.' + CSS.INPUTALT).set('value', properties.alt);}if (properties.src) {form.one('.' + CSS.INPUTURL).set('value', properties.src);this._loadPreviewImage(properties.src);}if (properties.presentation) {form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');}// Update the image preview based on the form properties.this._autoAdjustSize();},/*** Gets the properties of the currently selected image.** The first image only if multiple images are selected.** @method _getSelectedImageProperties* @return {object}* @private*/_getSelectedImageProperties: function() {var properties = {src: null,alt: null,width: null,height: null,align: '',presentation: false},// Get the current selection.images = this.get('host').getSelectedNodes(),width,height,style,image;if (images) {images = images.filter('img');}if (images && images.size()) {image = this._removeLegacyAlignment(images.item(0));this._selectedImage = image;style = image.getAttribute('style');properties.customstyle = style;width = image.getAttribute('width');if (!width.match(REGEX.ISPERCENT)) {width = parseInt(width, 10);}height = image.getAttribute('height');if (!height.match(REGEX.ISPERCENT)) {height = parseInt(height, 10);}if (width !== 0) {properties.width = width;}if (height !== 0) {properties.height = height;}this._getAlignmentPropeties(image, properties);properties.src = image.getAttribute('src');properties.alt = image.getAttribute('alt') || '';properties.presentation = (image.get('role') === 'presentation');return properties;}// No image selected - clean up.this._selectedImage = null;return false;},/*** Sets the alignment of a properties object.** @method _getAlignmentPropeties* @param {Node} image The image that the alignment properties should be found for* @param {Object} properties The properties object that is created in _getSelectedImageProperties()* @private*/_getAlignmentPropeties: function(image, properties) {var complete = false,defaultAlignment;// Check for an alignment value.complete = ALIGNMENTS.some(function(alignment) {var classname = this._getAlignmentClass(alignment.value);if (image.hasClass(classname)) {properties.align = alignment.value;Y.log('Found alignment ' + alignment.value, 'debug', 'atto_image-button');return true;}if (alignment.isDefault) {defaultAlignment = alignment.value;}return false;}, this);if (!complete && defaultAlignment) {properties.align = defaultAlignment;}},/*** Update the form when the URL was changed. This includes updating the* height, width, and image preview.** @method _urlChanged* @private*/_urlChanged: function() {var input = this._form.one('.' + CSS.INPUTURL);if (input.get('value') !== '') {// Load the preview image.this._loadPreviewImage(input.get('value'));}},/*** Update the image in the contenteditable.** @method _setImage* @param {EventFacade} e* @private*/_setImage: function(e) {var form = this._form,url = form.one('.' + CSS.INPUTURL).get('value'),alt = form.one('.' + CSS.INPUTALT).get('value'),width = form.one('.' + CSS.INPUTWIDTH).get('value'),height = form.one('.' + CSS.INPUTHEIGHT).get('value'),alignment = this._getAlignmentClass(form.one('.' + CSS.INPUTALIGNMENT).get('value')),presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked'),constrain = form.one('.' + CSS.INPUTCONSTRAIN).get('checked'),imagehtml,customstyle = form.one('.' + CSS.INPUTCUSTOMSTYLE).get('value'),classlist = [],host = this.get('host');e.preventDefault();// Check if there are any accessibility issues.if (this._updateWarning()) {return;}// Focus on the editor in preparation for inserting the image.host.focus();if (url !== '') {if (this._selectedImage) {host.setSelection(host.getSelectionFromNode(this._selectedImage));} else {host.setSelection(this._currentSelection);}if (constrain) {classlist.push(CSS.RESPONSIVE);}// Add the alignment class for the image.classlist.push(alignment);if (!width.match(REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {form.one('.' + CSS.INPUTWIDTH).focus();return;}if (!height.match(REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {form.one('.' + CSS.INPUTHEIGHT).focus();return;}var template = Y.Handlebars.compile(IMAGETEMPLATE);imagehtml = template({url: url,alt: alt,width: width,height: height,presentation: presentation,customstyle: customstyle,classlist: classlist.join(' ')});this.get('host').insertContentAtFocusPoint(imagehtml);this.markUpdated();}this.getDialogue({focusAfterHide: null}).hide();},/*** Removes any legacy styles added by previous versions of the atto image button.** @method _removeLegacyAlignment* @param {Y.Node} imageNode* @return {Y.Node}* @private*/_removeLegacyAlignment: function(imageNode) {if (!imageNode.getStyle('margin')) {// There is no margin therefore this cannot match any known alignments.return imageNode;}ALIGNMENTS.some(function(alignment) {if (imageNode.getStyle(alignment.name) !== alignment.value) {// The name/value do not match. Skip.return false;}var normalisedNode = Y.Node.create('<div>');normalisedNode.setStyle('margin', alignment.margin);if (imageNode.getStyle('margin') !== normalisedNode.getStyle('margin')) {// The margin does not match.return false;}Y.log('Legacy alignment found and removed.', 'info', 'atto_image-button');imageNode.addClass(this._getAlignmentClass(alignment.value));imageNode.setStyle(alignment.name, null);imageNode.setStyle('margin', null);return true;}, this);return imageNode;},_getAlignmentClass: function(alignment) {return CSS.ALIGNSETTINGS + '_' + alignment;},_toggleVisibility: function(selector, predicate) {var form = this._form;var element = form.all(selector);element.setStyle('display', predicate ? 'block' : 'none');},_toggleAriaInvalid: function(selectors, predicate) {var form = this._form;selectors.forEach(function(selector) {var element = form.all(selector);element.setAttribute('aria-invalid', predicate);});},_hasErrorUrlField: function() {var form = this._form;var url = form.one('.' + CSS.INPUTURL).get('value');var urlerror = url === '';this._toggleVisibility('.' + CSS.IMAGEURLWARNING, urlerror);this._toggleAriaInvalid(['.' + CSS.INPUTURL], urlerror);return urlerror;},_hasErrorAltField: function() {var form = this._form;var alt = form.one('.' + CSS.INPUTALT).get('value');var presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked');var imagealterror = alt === '' && !presentation;this._toggleVisibility('.' + CSS.IMAGEALTWARNING, imagealterror);this._toggleAriaInvalid(['.' + CSS.INPUTALT, '.' + CSS.IMAGEPRESENTATION], imagealterror);return imagealterror;},/*** Update the alt text warning live.** @method _updateWarning* @return {boolean} whether a warning should be displayed.* @private*/_updateWarning: function() {var urlerror = this._hasErrorUrlField();var imagealterror = this._hasErrorAltField();var haserrors = urlerror || imagealterror;this.getDialogue().centerDialogue();return haserrors;},/*** Handle the keyup to update the character count.*/_handleKeyup: function() {var form = this._form,alt = form.one('.' + CSS.INPUTALT).get('value'),characterCount = alt.length,current = form.one('#currentcount');current.setHTML(characterCount);}});