MDL-64506 templates: Move BS2 btns' to BS4 btns'
[moodle.git] / lib / editor / atto / plugins / image / yui / src / button / js / button.js
CommitLineData
adca7326
DW
1// This file is part of Moodle - http://moodle.org/
2//
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.
7//
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.
12//
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/>.
15
62467795
AN
16/*
17 * @package atto_image
18 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
19 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
20 */
21
22/**
23 * @module moodle-atto_image_alignment-button
24 */
25
26/**
27 * Atto image selection tool.
28 *
29 * @namespace M.atto_image
30 * @class Button
31 * @extends M.editor_atto.EditorPlugin
32 */
33
34var CSS = {
d3931a7d 35 RESPONSIVE: 'img-responsive',
c9292b18
SH
36 INPUTALIGNMENT: 'atto_image_alignment',
37 INPUTALT: 'atto_image_altentry',
38 INPUTHEIGHT: 'atto_image_heightentry',
62467795 39 INPUTSUBMIT: 'atto_image_urlentrysubmit',
c9292b18 40 INPUTURL: 'atto_image_urlentry',
ffb8aff6 41 INPUTSIZE: 'atto_image_size',
c9292b18
SH
42 INPUTWIDTH: 'atto_image_widthentry',
43 IMAGEALTWARNING: 'atto_image_altwarning',
44 IMAGEBROWSER: 'openimagebrowser',
45 IMAGEPRESENTATION: 'atto_image_presentation',
ffb8aff6 46 INPUTCONSTRAIN: 'atto_image_constrain',
d28af3d4 47 INPUTCUSTOMSTYLE: 'atto_image_customstyle',
d3931a7d 48 IMAGEPREVIEW: 'atto_image_preview',
c3e1c98f
NM
49 IMAGEPREVIEWBOX: 'atto_image_preview_box',
50 ALIGNSETTINGS: 'atto_image_button'
c9292b18 51 },
e5ddec38
DW
52 SELECTORS = {
53 INPUTURL: '.' + CSS.INPUTURL
54 },
62467795
AN
55 ALIGNMENTS = [
56 // Vertical alignment.
57 {
c3e1c98f 58 name: 'verticalAlign',
2e8cbbb3 59 str: 'alignment_top',
c3e1c98f
NM
60 value: 'text-top',
61 margin: '0 0.5em'
62467795 62 }, {
c3e1c98f 63 name: 'verticalAlign',
62467795 64 str: 'alignment_middle',
c3e1c98f
NM
65 value: 'middle',
66 margin: '0 0.5em'
62467795 67 }, {
c3e1c98f 68 name: 'verticalAlign',
2e8cbbb3 69 str: 'alignment_bottom',
c3e1c98f
NM
70 value: 'text-bottom',
71 margin: '0 0.5em',
7bbc64b8 72 isDefault: true
62467795
AN
73 },
74
75 // Floats.
76 {
c3e1c98f 77 name: 'float',
62467795 78 str: 'alignment_left',
c3e1c98f
NM
79 value: 'left',
80 margin: '0 0.5em 0 0'
62467795 81 }, {
c3e1c98f 82 name: 'float',
62467795 83 str: 'alignment_right',
c3e1c98f
NM
84 value: 'right',
85 margin: '0 0 0 0.5em'
62467795 86 }
d3931a7d 87 ],
c9292b18 88
d3931a7d
DW
89 REGEX = {
90 ISPERCENT: /\d+%/
91 },
92
93 COMPONENTNAME = 'atto_image',
c9292b18 94
62467795
AN
95 TEMPLATE = '' +
96 '<form class="atto_form">' +
c9292b18 97
62467795
AN
98 // Add the repository browser button.
99 '{{#if showFilepicker}}' +
1b217025
BB
100 '<div class="m-b-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">' +
29551c4b 106 '<button class="btn btn-secondary {{CSS.IMAGEBROWSER}}" type="button">' +
1b217025
BB
107 '{{get_string "browserepositories" component}}</button>' +
108 '</span>' +
109 '</div>' +
110 '</div>' +
111 '{{else}}' +
112 '<div class="m-b-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"/>' +
116 '</div>' +
62467795
AN
117 '{{/if}}' +
118
119 // Add the Alt box.
1b217025 120 '<div style="display:none" role="alert" class="alert alert-warning m-b-1 {{CSS.IMAGEALTWARNING}}">' +
62467795
AN
121 '{{get_string "presentationoraltrequired" component}}' +
122 '</div>' +
1b217025 123 '<div class="m-b-1">' +
62467795 124 '<label for="{{elementid}}_{{CSS.INPUTALT}}">{{get_string "enteralt" component}}</label>' +
1b217025
BB
125 '<input class="form-control fullwidth {{CSS.INPUTALT}}" type="text" value="" ' +
126 'id="{{elementid}}_{{CSS.INPUTALT}}" size="32"/>' +
62467795
AN
127
128 // Add the presentation select box.
1b217025
BB
129 '<div class="form-check">' +
130 '<input type="checkbox" class="form-check-input {{CSS.IMAGEPRESENTATION}}" ' +
131 'id="{{elementid}}_{{CSS.IMAGEPRESENTATION}}"/>' +
132 '<label class="form-check-label" for="{{elementid}}_{{CSS.IMAGEPRESENTATION}}">' +
557f44d9
AN
133 '{{get_string "presentation" component}}' +
134 '</label>' +
1b217025
BB
135 '</div>' +
136 '</div>' +
62467795 137
ffb8aff6 138 // Add the size entry boxes.
1b217025
BB
139 '<div class="m-b-1">' +
140 '<label class="" for="{{elementid}}_{{CSS.INPUTSIZE}}">{{get_string "size" component}}</label>' +
141 '<div id="{{elementid}}_{{CSS.INPUTSIZE}}" class="form-inline {{CSS.INPUTSIZE}}">' +
ffb8aff6 142 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTWIDTH}}">{{get_string "width" component}}</label>' +
1b217025
BB
143 '<input type="text" class="form-control m-r-1 input-mini {{CSS.INPUTWIDTH}}" ' +
144 'id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="4"/> x' +
62467795
AN
145
146 // Add the height entry box.
ffb8aff6 147 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTHEIGHT}}">{{get_string "height" component}}</label>' +
1b217025
BB
148 '<input type="text" class="form-control m-l-1 input-mini {{CSS.INPUTHEIGHT}}" ' +
149 'id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +
ffb8aff6
DW
150
151 // Add the constrain checkbox.
1b217025
BB
152 '<div class="form-check m-l-2">' +
153 '<input type="checkbox" class="form-check-input {{CSS.INPUTCONSTRAIN}}" ' +
154 'id="{{elementid}}_{{CSS.INPUTCONSTRAIN}}"/>' +
155 '<label class="form-check-label" for="{{elementid}}_{{CSS.INPUTCONSTRAIN}}">' +
156 '{{get_string "constrain" component}}</label>' +
157 '</div>' +
158 '</div>' +
ffb8aff6 159 '</div>' +
62467795
AN
160
161 // Add the alignment selector.
1b217025
BB
162 '<div class="form-inline m-b-1">' +
163 '<label class="for="{{elementid}}_{{CSS.INPUTALIGNMENT}}">{{get_string "alignment" component}}</label>' +
164 '<select class="custom-select {{CSS.INPUTALIGNMENT}}" id="{{elementid}}_{{CSS.INPUTALIGNMENT}}">' +
62467795 165 '{{#each alignments}}' +
c3e1c98f 166 '<option value="{{value}}">{{get_string str ../component}}</option>' +
62467795
AN
167 '{{/each}}' +
168 '</select>' +
1b217025 169 '</div>' +
d28af3d4
DW
170 // Hidden input to store custom styles.
171 '<input type="hidden" class="{{CSS.INPUTCUSTOMSTYLE}}"/>' +
62467795
AN
172 '<br/>' +
173
174 // Add the image preview.
62467795 175 '<div class="mdl-align">' +
d3931a7d 176 '<div class="{{CSS.IMAGEPREVIEWBOX}}">' +
f3662213 177 '<img src="#" class="{{CSS.IMAGEPREVIEW}}" alt="" style="display: none;"/>' +
d3931a7d 178 '</div>' +
c9292b18 179
62467795 180 // Add the submit button and close the form.
29551c4b
MM
181 '<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
182 '{{get_string "saveimage" component}}</button>' +
62467795
AN
183 '</div>' +
184 '</form>',
185
186 IMAGETEMPLATE = '' +
187 '<img src="{{url}}" alt="{{alt}}" ' +
188 '{{#if width}}width="{{width}}" {{/if}}' +
189 '{{#if height}}height="{{height}}" {{/if}}' +
190 '{{#if presentation}}role="presentation" {{/if}}' +
c3e1c98f 191 '{{#if customstyle}}style="{{customstyle}}" {{/if}}' +
d3931a7d 192 '{{#if classlist}}class="{{classlist}}" {{/if}}' +
1461aee8 193 '{{#if id}}id="{{id}}" {{/if}}' +
62467795
AN
194 '/>';
195
196Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
c9292b18 197 /**
62467795
AN
198 * A reference to the current selection at the time that the dialogue
199 * was opened.
200 *
201 * @property _currentSelection
202 * @type Range
203 * @private
c9292b18 204 */
62467795 205 _currentSelection: null,
c9292b18
SH
206
207 /**
62467795
AN
208 * The most recently selected image.
209 *
210 * @param _selectedImage
211 * @type Node
212 * @private
c9292b18 213 */
62467795 214 _selectedImage: null,
c9292b18
SH
215
216 /**
62467795
AN
217 * A reference to the currently open form.
218 *
219 * @param _form
220 * @type Node
c9292b18 221 * @private
c9292b18 222 */
62467795
AN
223 _form: null,
224
ffb8aff6 225 /**
f3662213 226 * The dimensions of the raw image before we manipulate it.
ffb8aff6 227 *
f3662213
AN
228 * @param _rawImageDimensions
229 * @type Object
ffb8aff6
DW
230 * @private
231 */
f3662213 232 _rawImageDimensions: null,
ffb8aff6 233
62467795 234 initializer: function() {
1461aee8 235
62467795
AN
236 this.addButton({
237 icon: 'e/insert_edit_image',
238 callback: this._displayDialogue,
239 tags: 'img',
240 tagMatchRequiresAll: false
241 });
eb8b2425
DT
242 this.editor.delegate('dblclick', this._displayDialogue, 'img', this);
243 this.editor.delegate('click', this._handleClick, 'img', this);
1461aee8 244 this.editor.on('drop', this._handleDragDrop, this);
c3c3c9f2
PN
245
246 // e.preventDefault needed to stop the default event from clobbering the desired behaviour in some browsers.
bc8b6dc6
DP
247 this.editor.on('dragover', function(e) {
248 e.preventDefault();
249 }, this);
250 this.editor.on('dragenter', function(e) {
251 e.preventDefault();
252 }, this);
1461aee8
PN
253 },
254
255 /**
256 * Handle a drag and drop event with an image.
257 *
258 * @method _handleDragDrop
259 * @param {EventFacade} e
c3e1c98f 260 * @return mixed
1461aee8
PN
261 * @private
262 */
263 _handleDragDrop: function(e) {
264
265 var self = this,
266 host = this.get('host'),
267 template = Y.Handlebars.compile(IMAGETEMPLATE);
268
269 host.saveSelection();
270 e = e._event;
271
272 // Only handle the event if an image file was dropped in.
557f44d9
AN
273 var handlesDataTransfer = (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length);
274 if (handlesDataTransfer && /^image\//.test(e.dataTransfer.files[0].type)) {
1461aee8
PN
275
276 var options = host.get('filepickeroptions').image,
277 savepath = (options.savepath === undefined) ? '/' : options.savepath,
278 formData = new FormData(),
279 timestamp = 0,
280 uploadid = "",
281 xhr = new XMLHttpRequest(),
282 imagehtml = "",
283 keys = Object.keys(options.repositories);
284
285 e.preventDefault();
286 e.stopPropagation();
287 formData.append('repo_upload_file', e.dataTransfer.files[0]);
288 formData.append('itemid', options.itemid);
289
290 // List of repositories is an object rather than an array. This makes iteration more awkward.
291 for (var i = 0; i < keys.length; i++) {
292 if (options.repositories[keys[i]].type === 'upload') {
293 formData.append('repo_id', options.repositories[keys[i]].id);
294 break;
295 }
296 }
297 formData.append('env', options.env);
298 formData.append('sesskey', M.cfg.sesskey);
299 formData.append('client_id', options.client_id);
300 formData.append('savepath', savepath);
301 formData.append('ctx_id', options.context.id);
302
303 // Insert spinner as a placeholder.
304 timestamp = new Date().getTime();
305 uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
306 host.focus();
307 host.restoreSelection();
308 imagehtml = template({
309 url: M.util.image_url("i/loading_small", 'moodle'),
310 alt: M.util.get_string('uploading', COMPONENTNAME),
311 id: uploadid
312 });
313 host.insertContentAtFocusPoint(imagehtml);
314 self.markUpdated();
315
316 // Kick off a XMLHttpRequest.
317 xhr.onreadystatechange = function() {
318 var placeholder = self.editor.one('#' + uploadid),
319 result,
320 file,
321 newhtml,
322 newimage;
323
324 if (xhr.readyState === 4) {
325 if (xhr.status === 200) {
326 result = JSON.parse(xhr.responseText);
327 if (result) {
328 if (result.error) {
329 if (placeholder) {
330 placeholder.remove(true);
331 }
332 return new M.core.ajaxException(result);
333 }
334
335 file = result;
336 if (result.event && result.event === 'fileexists') {
337 // A file with this name is already in use here - rename to avoid conflict.
338 // Chances are, it's a different image (stored in a different folder on the user's computer).
339 // If the user wants to reuse an existing image, they can copy/paste it within the editor.
340 file = result.newfile;
341 }
342
343 // Replace placeholder with actual image.
344 newhtml = template({
345 url: file.url,
346 presentation: true
347 });
348 newimage = Y.Node.create(newhtml);
349 if (placeholder) {
350 placeholder.replace(newimage);
351 } else {
352 self.editor.appendChild(newimage);
353 }
354 self.markUpdated();
355 }
356 } else {
557f44d9
AN
357 Y.use('moodle-core-notification-alert', function() {
358 new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
359 });
1461aee8
PN
360 if (placeholder) {
361 placeholder.remove(true);
362 }
363 }
364 }
365 };
366 xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
367 xhr.send(formData);
ca50279d 368 return false;
1461aee8 369 }
1461aee8 370
eb8b2425 371},
43a737d1
DW
372
373 /**
eb8b2425 374 * Handle a click on an image.
43a737d1 375 *
eb8b2425 376 * @method _handleClick
43a737d1
DW
377 * @param {EventFacade} e
378 * @private
379 */
eb8b2425 380 _handleClick: function(e) {
43a737d1
DW
381 var image = e.target;
382
383 var selection = this.get('host').getSelectionFromNode(image);
eb8b2425
DT
384 if (this.get('host').getSelection() !== selection) {
385 this.get('host').setSelection(selection);
386 }
c9292b18
SH
387 },
388
389 /**
62467795
AN
390 * Display the image editing tool.
391 *
392 * @method _displayDialogue
393 * @private
c9292b18 394 */
62467795
AN
395 _displayDialogue: function() {
396 // Store the current selection.
397 this._currentSelection = this.get('host').getSelection();
398 if (this._currentSelection === false) {
399 return;
c9292b18 400 }
62467795 401
91bc9570
FM
402 // Reset the image dimensions.
403 this._rawImageDimensions = null;
404
62467795 405 var dialogue = this.getDialogue({
d3931a7d 406 headerContent: M.util.get_string('imageproperties', COMPONENTNAME),
104cc0e3 407 width: 'auto',
e5ddec38 408 focusAfterHide: true,
c1660772 409 focusOnShowSelector: SELECTORS.INPUTURL
62467795
AN
410 });
411
412 // Set the dialogue content, and then show the dialogue.
413 dialogue.set('bodyContent', this._getDialogueContent())
414 .show();
c9292b18
SH
415 },
416
ffb8aff6
DW
417 /**
418 * Set the inputs for width and height if they are not set, and calculate
419 * if the constrain checkbox should be checked or not.
420 *
421 * @method _loadPreviewImage
422 * @param {String} url
423 * @private
424 */
425 _loadPreviewImage: function(url) {
bc8b6dc6
DP
426 var image = new Image();
427 var self = this;
ffb8aff6 428
f3662213
AN
429 image.onerror = function() {
430 var preview = self._form.one('.' + CSS.IMAGEPREVIEW);
431 preview.setStyles({
432 'display': 'none'
433 });
434
435 // Centre the dialogue when clearing the image preview.
436 self.getDialogue().centerDialogue();
437 };
438
ffb8aff6
DW
439 image.onload = function() {
440 var input, currentwidth, currentheight, widthRatio, heightRatio;
441
f3662213
AN
442 self._rawImageDimensions = {
443 width: this.width,
444 height: this.height
445 };
ffb8aff6
DW
446
447 input = self._form.one('.' + CSS.INPUTWIDTH);
448 currentwidth = input.get('value');
449 if (currentwidth === '') {
450 input.set('value', this.width);
d3931a7d 451 currentwidth = "" + this.width;
ffb8aff6
DW
452 }
453 input = self._form.one('.' + CSS.INPUTHEIGHT);
454 currentheight = input.get('value');
455 if (currentheight === '') {
456 input.set('value', this.height);
d3931a7d 457 currentheight = "" + this.height;
ffb8aff6
DW
458 }
459 input = self._form.one('.' + CSS.IMAGEPREVIEW);
f3662213
AN
460 input.setAttribute('src', this.src);
461 input.setStyles({
462 'display': 'inline'
463 });
ffb8aff6 464
ffb8aff6 465 input = self._form.one('.' + CSS.INPUTCONSTRAIN);
d3931a7d
DW
466 if (currentwidth.match(REGEX.ISPERCENT) && currentheight.match(REGEX.ISPERCENT)) {
467 input.set('checked', currentwidth === currentheight);
468 } else {
469 if (this.width === 0) {
470 this.width = 1;
471 }
472 if (this.height === 0) {
473 this.height = 1;
474 }
475 // This is the same as comparing to 3 decimal places.
5bb4f444
DP
476 widthRatio = Math.round(1000 * parseInt(currentwidth, 10) / this.width);
477 heightRatio = Math.round(1000 * parseInt(currentheight, 10) / this.height);
d3931a7d
DW
478 input.set('checked', widthRatio === heightRatio);
479 }
ffb8aff6 480
f3662213
AN
481 // Apply the image sizing.
482 self._autoAdjustSize(self);
483
ffb8aff6
DW
484 // Centre the dialogue once the preview image has loaded.
485 self.getDialogue().centerDialogue();
486 };
487
488 image.src = url;
489 },
490
c9292b18 491 /**
62467795
AN
492 * Return the dialogue content for the tool, attaching any required
493 * events.
494 *
495 * @method _getDialogueContent
496 * @return {Node} The content to place in the dialogue.
497 * @private
c9292b18 498 */
62467795
AN
499 _getDialogueContent: function() {
500 var template = Y.Handlebars.compile(TEMPLATE),
6cb48e04 501 canShowFilepicker = this.get('host').canShowFilepicker('image'),
62467795
AN
502 content = Y.Node.create(template({
503 elementid: this.get('host').get('elementid'),
504 CSS: CSS,
505 component: COMPONENTNAME,
6cb48e04 506 showFilepicker: canShowFilepicker,
62467795
AN
507 alignments: ALIGNMENTS
508 }));
509
510 this._form = content;
511
512 // Configure the view of the current image.
513 this._applyImageProperties(this._form);
514
ffb8aff6 515 this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
9754ab92
JF
516 this._form.one('.' + CSS.IMAGEPRESENTATION).on('change', this._updateWarning, this);
517 this._form.one('.' + CSS.INPUTALT).on('change', this._updateWarning, this);
f3662213
AN
518 this._form.one('.' + CSS.INPUTWIDTH).on('blur', this._autoAdjustSize, this);
519 this._form.one('.' + CSS.INPUTHEIGHT).on('blur', this._autoAdjustSize, this, true);
d3931a7d
DW
520 this._form.one('.' + CSS.INPUTCONSTRAIN).on('change', function(event) {
521 if (event.target.get('checked')) {
f3662213 522 this._autoAdjustSize(event);
d3931a7d
DW
523 }
524 }, this);
62467795
AN
525 this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
526 this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setImage, this);
6cb48e04
FM
527
528 if (canShowFilepicker) {
529 this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {
530 this.get('host').showFilepicker('image', this._filepickerCallback, this);
531 }, this);
532 }
adca7326 533
62467795 534 return content;
adca7326 535 },
adca7326 536
f3662213
AN
537 _autoAdjustSize: function(e, forceHeight) {
538 forceHeight = forceHeight || false;
539
540 var keyField = this._form.one('.' + CSS.INPUTWIDTH),
541 keyFieldType = 'width',
542 subField = this._form.one('.' + CSS.INPUTHEIGHT),
543 subFieldType = 'height',
544 constrainField = this._form.one('.' + CSS.INPUTCONSTRAIN),
545 keyFieldValue = keyField.get('value'),
546 subFieldValue = subField.get('value'),
547 imagePreview = this._form.one('.' + CSS.IMAGEPREVIEW),
548 rawPercentage,
549 rawSize;
ffb8aff6 550
91bc9570
FM
551 // If we do not know the image size, do not do anything.
552 if (!this._rawImageDimensions) {
553 return;
554 }
555
d3931a7d 556 // Set the width back to default if it is empty.
f3662213
AN
557 if (keyFieldValue === '') {
558 keyFieldValue = this._rawImageDimensions[keyFieldType];
559 keyField.set('value', keyFieldValue);
560 keyFieldValue = keyField.get('value');
d3931a7d
DW
561 }
562
f3662213
AN
563 // Clear the existing preview sizes.
564 imagePreview.setStyles({
565 width: null,
566 height: null
567 });
ffb8aff6 568
f3662213
AN
569 // Now update with the new values.
570 if (!constrainField.get('checked')) {
571 // We are not keeping the image proportion - update the preview accordingly.
d3931a7d 572
f3662213
AN
573 // Width.
574 if (keyFieldValue.match(REGEX.ISPERCENT)) {
575 rawPercentage = parseInt(keyFieldValue, 10);
576 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
577 imagePreview.setStyle('width', rawSize + 'px');
578 } else {
579 imagePreview.setStyle('width', keyFieldValue + 'px');
d3931a7d 580 }
ffb8aff6 581
f3662213
AN
582 // Height.
583 if (subFieldValue.match(REGEX.ISPERCENT)) {
584 rawPercentage = parseInt(subFieldValue, 10);
585 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
586 imagePreview.setStyle('height', rawSize + 'px');
587 } else {
588 imagePreview.setStyle('height', subFieldValue + 'px');
589 }
590 } else {
591 // We are keeping the image in proportion.
592 if (forceHeight) {
593 // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.
594 var _temporaryValue;
595 _temporaryValue = keyField;
596 keyField = subField;
597 subField = _temporaryValue;
598
599 _temporaryValue = keyFieldType;
600 keyFieldType = subFieldType;
601 subFieldType = _temporaryValue;
602
603 _temporaryValue = keyFieldValue;
604 keyFieldValue = subFieldValue;
605 subFieldValue = _temporaryValue;
606 }
ffb8aff6 607
f3662213
AN
608 if (keyFieldValue.match(REGEX.ISPERCENT)) {
609 // This is a percentage based change. Copy it verbatim.
610 subFieldValue = keyFieldValue;
d3931a7d 611
f3662213
AN
612 // Set the width to the calculated pixel width.
613 rawPercentage = parseInt(keyFieldValue, 10);
614 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
ffb8aff6 615
f3662213
AN
616 // And apply the width/height to the container.
617 imagePreview.setStyle('width', rawSize);
618 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
619 imagePreview.setStyle('height', rawSize);
620 } else {
621 // Calculate the scaled subFieldValue from the keyFieldValue.
622 subFieldValue = Math.round((keyFieldValue / this._rawImageDimensions[keyFieldType]) *
623 this._rawImageDimensions[subFieldType]);
624
625 if (forceHeight) {
626 imagePreview.setStyles({
627 'width': subFieldValue,
628 'height': keyFieldValue
629 });
630 } else {
631 imagePreview.setStyles({
632 'width': keyFieldValue,
633 'height': subFieldValue
634 });
635 }
d3931a7d 636 }
f3662213
AN
637
638 // Update the subField's value within the form to reflect the changes.
639 subField.set('value', subFieldValue);
d3931a7d 640 }
ffb8aff6
DW
641 },
642
62467795
AN
643 /**
644 * Update the dialogue after an image was selected in the File Picker.
645 *
646 * @method _filepickerCallback
647 * @param {object} params The parameters provided by the filepicker
648 * containing information about the image.
649 * @private
650 */
651 _filepickerCallback: function(params) {
adca7326 652 if (params.url !== '') {
ffb8aff6 653 var input = this._form.one('.' + CSS.INPUTURL);
adca7326
DW
654 input.set('value', params.url);
655
656 // Auto set the width and height.
8d1c0179
DW
657 this._form.one('.' + CSS.INPUTWIDTH).set('value', '');
658 this._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
ffb8aff6
DW
659
660 // Load the preview image.
661 this._loadPreviewImage(params.url);
adca7326
DW
662 }
663 },
adca7326 664
62467795
AN
665 /**
666 * Applies properties of an existing image to the image dialogue for editing.
667 *
668 * @method _applyImageProperties
669 * @param {Node} form
670 * @private
671 */
672 _applyImageProperties: function(form) {
673 var properties = this._getSelectedImageProperties(),
c3e1c98f 674 img = form.one('.' + CSS.IMAGEPREVIEW);
adca7326 675
62467795
AN
676 if (properties === false) {
677 img.setStyle('display', 'none');
7bbc64b8 678 // Set the default alignment.
c3e1c98f
NM
679 ALIGNMENTS.some(function(alignment) {
680 if (alignment.isDefault) {
681 form.one('.' + CSS.INPUTALIGNMENT).set('value', alignment.value);
682 return true;
7bbc64b8 683 }
c3e1c98f
NM
684
685 return false;
686 }, this);
687
adca7326 688 return;
adca7326
DW
689 }
690
62467795 691 if (properties.align) {
da00661d 692 form.one('.' + CSS.INPUTALIGNMENT).set('value', properties.align);
d28af3d4
DW
693 }
694 if (properties.customstyle) {
695 form.one('.' + CSS.INPUTCUSTOMSTYLE).set('value', properties.customstyle);
62467795 696 }
62467795
AN
697 if (properties.width) {
698 form.one('.' + CSS.INPUTWIDTH).set('value', properties.width);
699 }
700 if (properties.height) {
701 form.one('.' + CSS.INPUTHEIGHT).set('value', properties.height);
702 }
703 if (properties.alt) {
704 form.one('.' + CSS.INPUTALT).set('value', properties.alt);
705 }
706 if (properties.src) {
707 form.one('.' + CSS.INPUTURL).set('value', properties.src);
ffb8aff6 708 this._loadPreviewImage(properties.src);
62467795
AN
709 }
710 if (properties.presentation) {
711 form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');
adca7326 712 }
f3662213
AN
713
714 // Update the image preview based on the form properties.
715 this._autoAdjustSize();
adca7326 716 },
62467795 717
c9292b18
SH
718 /**
719 * Gets the properties of the currently selected image.
720 *
721 * The first image only if multiple images are selected.
722 *
62467795
AN
723 * @method _getSelectedImageProperties
724 * @return {object}
c9292b18 725 * @private
c9292b18 726 */
62467795 727 _getSelectedImageProperties: function() {
c9292b18
SH
728 var properties = {
729 src: null,
5bb4f444 730 alt: null,
c9292b18
SH
731 width: null,
732 height: null,
d28af3d4 733 align: '',
c9292b18
SH
734 presentation: false
735 },
62467795
AN
736
737 // Get the current selection.
738 images = this.get('host').getSelectedNodes(),
557f44d9
AN
739 width,
740 height,
741 style,
c3e1c98f 742 image;
c9292b18 743
d321f68b
DW
744 if (images) {
745 images = images.filter('img');
746 }
747
748 if (images && images.size()) {
c3e1c98f 749 image = this._removeLegacyAlignment(images.item(0));
62467795 750 this._selectedImage = image;
d321f68b 751
c9292b18 752 style = image.getAttribute('style');
d28af3d4 753 properties.customstyle = style;
c9292b18 754
d3931a7d
DW
755 width = image.getAttribute('width');
756 if (!width.match(REGEX.ISPERCENT)) {
757 width = parseInt(width, 10);
758 }
759 height = image.getAttribute('height');
760 if (!height.match(REGEX.ISPERCENT)) {
761 height = parseInt(height, 10);
762 }
763
764 if (width !== 0) {
c9292b18
SH
765 properties.width = width;
766 }
d3931a7d 767 if (height !== 0) {
c9292b18
SH
768 properties.height = height;
769 }
c3e1c98f 770 this._getAlignmentPropeties(image, properties);
c9292b18
SH
771 properties.src = image.getAttribute('src');
772 properties.alt = image.getAttribute('alt') || '';
773 properties.presentation = (image.get('role') === 'presentation');
774 return properties;
775 }
62467795
AN
776
777 // No image selected - clean up.
778 this._selectedImage = null;
c9292b18
SH
779 return false;
780 },
c9292b18 781
c3e1c98f
NM
782 /**
783 * Sets the alignment of a properties object.
784 *
785 * @method _getAlignmentPropeties
786 * @param {Node} image The image that the alignment properties should be found for
787 * @param {Object} properties The properties object that is created in _getSelectedImageProperties()
788 * @private
789 */
790 _getAlignmentPropeties: function(image, properties) {
791 var complete = false,
792 defaultAlignment;
793
794 // Check for an alignment value.
795 complete = ALIGNMENTS.some(function(alignment) {
796 var classname = this._getAlignmentClass(alignment.value);
797 if (image.hasClass(classname)) {
798 properties.align = alignment.value;
799 Y.log('Found alignment ' + alignment.value, 'debug', 'atto_image-button');
800
801 return true;
802 }
803
804 if (alignment.isDefault) {
805 defaultAlignment = alignment.value;
806 }
807
808 return false;
809 }, this);
810
811 if (!complete && defaultAlignment) {
812 properties.align = defaultAlignment;
813 }
814 },
815
62467795
AN
816 /**
817 * Update the form when the URL was changed. This includes updating the
818 * height, width, and image preview.
819 *
820 * @method _urlChanged
821 * @private
822 */
823 _urlChanged: function() {
ffb8aff6 824 var input = this._form.one('.' + CSS.INPUTURL);
b269f635 825
62467795 826 if (input.get('value') !== '') {
ffb8aff6
DW
827 // Load the preview image.
828 this._loadPreviewImage(input.get('value'));
b269f635 829 }
c9292b18 830 },
62467795 831
c9292b18 832 /**
62467795 833 * Update the image in the contenteditable.
c9292b18 834 *
62467795
AN
835 * @method _setImage
836 * @param {EventFacade} e
c9292b18 837 * @private
c9292b18 838 */
62467795
AN
839 _setImage: function(e) {
840 var form = this._form,
841 url = form.one('.' + CSS.INPUTURL).get('value'),
842 alt = form.one('.' + CSS.INPUTALT).get('value'),
843 width = form.one('.' + CSS.INPUTWIDTH).get('value'),
844 height = form.one('.' + CSS.INPUTHEIGHT).get('value'),
c3e1c98f 845 alignment = this._getAlignmentClass(form.one('.' + CSS.INPUTALIGNMENT).get('value')),
62467795 846 presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked'),
d3931a7d 847 constrain = form.one('.' + CSS.INPUTCONSTRAIN).get('checked'),
62467795 848 imagehtml,
c3e1c98f 849 customstyle = form.one('.' + CSS.INPUTCUSTOMSTYLE).get('value'),
d3931a7d 850 classlist = [],
62467795 851 host = this.get('host');
c9292b18 852
62467795
AN
853 e.preventDefault();
854
9754ab92
JF
855 // Check if there are any accessibility issues.
856 if (this._updateWarning()) {
c9292b18
SH
857 return;
858 }
859
62467795
AN
860 // Focus on the editor in preparation for inserting the image.
861 host.focus();
862 if (url !== '') {
863 if (this._selectedImage) {
864 host.setSelection(host.getSelectionFromNode(this._selectedImage));
865 } else {
866 host.setSelection(this._currentSelection);
867 }
d28af3d4 868
d3931a7d
DW
869 if (constrain) {
870 classlist.push(CSS.RESPONSIVE);
871 }
872
c3e1c98f
NM
873 // Add the alignment class for the image.
874 classlist.push(alignment);
875
d3931a7d
DW
876 if (!width.match(REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {
877 form.one('.' + CSS.INPUTWIDTH).focus();
878 return;
879 }
880 if (!height.match(REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {
881 form.one('.' + CSS.INPUTHEIGHT).focus();
882 return;
883 }
884
557f44d9 885 var template = Y.Handlebars.compile(IMAGETEMPLATE);
62467795
AN
886 imagehtml = template({
887 url: url,
888 alt: alt,
889 width: width,
890 height: height,
891 presentation: presentation,
d3931a7d
DW
892 customstyle: customstyle,
893 classlist: classlist.join(' ')
62467795
AN
894 });
895
896 this.get('host').insertContentAtFocusPoint(imagehtml);
897
898 this.markUpdated();
c9292b18 899 }
d3931a7d
DW
900
901 this.getDialogue({
902 focusAfterHide: null
903 }).hide();
904
9754ab92
JF
905 },
906
c3e1c98f
NM
907 /**
908 * Removes any legacy styles added by previous versions of the atto image button.
909 *
910 * @method _removeLegacyAlignment
911 * @param {Y.Node} imageNode
912 * @return {Y.Node}
913 * @private
914 */
915 _removeLegacyAlignment: function(imageNode) {
916 if (!imageNode.getStyle('margin')) {
917 // There is no margin therefore this cannot match any known alignments.
918 return imageNode;
919 }
920
921 ALIGNMENTS.some(function(alignment) {
922 if (imageNode.getStyle(alignment.name) !== alignment.value) {
923 // The name/value do not match. Skip.
924 return false;
925 }
926
927 var normalisedNode = Y.Node.create('<div>');
928 normalisedNode.setStyle('margin', alignment.margin);
929 if (imageNode.getStyle('margin') !== normalisedNode.getStyle('margin')) {
930 // The margin does not match.
931 return false;
932 }
933
934 Y.log('Legacy alignment found and removed.', 'info', 'atto_image-button');
935 imageNode.addClass(this._getAlignmentClass(alignment.value));
936 imageNode.setStyle(alignment.name, null);
937 imageNode.setStyle('margin', null);
938
939 return true;
940 }, this);
941
942 return imageNode;
943 },
944
945 _getAlignmentClass: function(alignment) {
946 return CSS.ALIGNSETTINGS + '_' + alignment;
947 },
948
9754ab92
JF
949 /**
950 * Update the alt text warning live.
951 *
952 * @method _updateWarning
953 * @return {boolean} whether a warning should be displayed.
954 * @private
955 */
956 _updateWarning: function() {
957 var form = this._form,
958 state = true,
959 alt = form.one('.' + CSS.INPUTALT).get('value'),
960 presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked');
961 if (alt === '' && !presentation) {
962 form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'block');
963 form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', true);
964 form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', true);
965 state = true;
966 } else {
967 form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'none');
968 form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', false);
969 form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', false);
970 state = false;
971 }
972 this.getDialogue().centerDialogue();
973 return state;
adca7326 974 }
62467795 975});