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