weekly release 4.0dev
[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 = {
4144e278 37 RESPONSIVE: 'img-fluid',
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}}' +
5cac5fa4 102 '<div class="mb-1">' +
1b217025
BB
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}}' +
5cac5fa4 114 '<div class="mb-1">' +
1b217025
BB
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.
5cac5fa4 122 '<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.IMAGEALTWARNING}}">' +
62467795
AN
123 '{{get_string "presentationoraltrequired" component}}' +
124 '</div>' +
5cac5fa4 125 '<div class="mb-1">' +
62467795 126 '<label for="{{elementid}}_{{CSS.INPUTALT}}">{{get_string "enteralt" component}}</label>' +
7e03b27d
MC
127 '<textarea class="form-control fullwidth {{CSS.INPUTALT}}" ' +
128 'id="{{elementid}}_{{CSS.INPUTALT}}" maxlength="125"></textarea>' +
129
130 // Add the character count.
131 '<div id="the-count" class="d-flex justify-content-end small">' +
132 '<span id="currentcount">0</span>' +
133 '<span id="maximumcount"> / 125</span>' +
134 '</div>' +
62467795
AN
135
136 // Add the presentation select box.
1b217025
BB
137 '<div class="form-check">' +
138 '<input type="checkbox" class="form-check-input {{CSS.IMAGEPRESENTATION}}" ' +
139 'id="{{elementid}}_{{CSS.IMAGEPRESENTATION}}"/>' +
140 '<label class="form-check-label" for="{{elementid}}_{{CSS.IMAGEPRESENTATION}}">' +
557f44d9
AN
141 '{{get_string "presentation" component}}' +
142 '</label>' +
1b217025
BB
143 '</div>' +
144 '</div>' +
62467795 145
ffb8aff6 146 // Add the size entry boxes.
5cac5fa4 147 '<div class="mb-1">' +
1b217025
BB
148 '<label class="" for="{{elementid}}_{{CSS.INPUTSIZE}}">{{get_string "size" component}}</label>' +
149 '<div id="{{elementid}}_{{CSS.INPUTSIZE}}" class="form-inline {{CSS.INPUTSIZE}}">' +
ffb8aff6 150 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTWIDTH}}">{{get_string "width" component}}</label>' +
5cac5fa4 151 '<input type="text" class="form-control mr-1 input-mini {{CSS.INPUTWIDTH}}" ' +
1b217025 152 'id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="4"/> x' +
62467795
AN
153
154 // Add the height entry box.
ffb8aff6 155 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTHEIGHT}}">{{get_string "height" component}}</label>' +
5cac5fa4 156 '<input type="text" class="form-control ml-1 input-mini {{CSS.INPUTHEIGHT}}" ' +
1b217025 157 'id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +
ffb8aff6
DW
158
159 // Add the constrain checkbox.
5cac5fa4 160 '<div class="form-check ml-2">' +
1b217025
BB
161 '<input type="checkbox" class="form-check-input {{CSS.INPUTCONSTRAIN}}" ' +
162 'id="{{elementid}}_{{CSS.INPUTCONSTRAIN}}"/>' +
163 '<label class="form-check-label" for="{{elementid}}_{{CSS.INPUTCONSTRAIN}}">' +
164 '{{get_string "constrain" component}}</label>' +
165 '</div>' +
166 '</div>' +
ffb8aff6 167 '</div>' +
62467795
AN
168
169 // Add the alignment selector.
5cac5fa4 170 '<div class="form-inline mb-1">' +
1b217025
BB
171 '<label class="for="{{elementid}}_{{CSS.INPUTALIGNMENT}}">{{get_string "alignment" component}}</label>' +
172 '<select class="custom-select {{CSS.INPUTALIGNMENT}}" id="{{elementid}}_{{CSS.INPUTALIGNMENT}}">' +
62467795 173 '{{#each alignments}}' +
c3e1c98f 174 '<option value="{{value}}">{{get_string str ../component}}</option>' +
62467795
AN
175 '{{/each}}' +
176 '</select>' +
1b217025 177 '</div>' +
d28af3d4
DW
178 // Hidden input to store custom styles.
179 '<input type="hidden" class="{{CSS.INPUTCUSTOMSTYLE}}"/>' +
62467795
AN
180 '<br/>' +
181
182 // Add the image preview.
62467795 183 '<div class="mdl-align">' +
d3931a7d 184 '<div class="{{CSS.IMAGEPREVIEWBOX}}">' +
0759da9e 185 '<img class="{{CSS.IMAGEPREVIEW}}" alt="" style="display: none;"/>' +
d3931a7d 186 '</div>' +
c9292b18 187
62467795 188 // Add the submit button and close the form.
29551c4b
MM
189 '<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
190 '{{get_string "saveimage" component}}</button>' +
62467795
AN
191 '</div>' +
192 '</form>',
193
194 IMAGETEMPLATE = '' +
195 '<img src="{{url}}" alt="{{alt}}" ' +
196 '{{#if width}}width="{{width}}" {{/if}}' +
197 '{{#if height}}height="{{height}}" {{/if}}' +
198 '{{#if presentation}}role="presentation" {{/if}}' +
c3e1c98f 199 '{{#if customstyle}}style="{{customstyle}}" {{/if}}' +
d3931a7d 200 '{{#if classlist}}class="{{classlist}}" {{/if}}' +
1461aee8 201 '{{#if id}}id="{{id}}" {{/if}}' +
62467795
AN
202 '/>';
203
204Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
c9292b18 205 /**
62467795
AN
206 * A reference to the current selection at the time that the dialogue
207 * was opened.
208 *
209 * @property _currentSelection
210 * @type Range
211 * @private
c9292b18 212 */
62467795 213 _currentSelection: null,
c9292b18
SH
214
215 /**
62467795
AN
216 * The most recently selected image.
217 *
218 * @param _selectedImage
219 * @type Node
220 * @private
c9292b18 221 */
62467795 222 _selectedImage: null,
c9292b18
SH
223
224 /**
62467795
AN
225 * A reference to the currently open form.
226 *
227 * @param _form
228 * @type Node
c9292b18 229 * @private
c9292b18 230 */
62467795
AN
231 _form: null,
232
ffb8aff6 233 /**
f3662213 234 * The dimensions of the raw image before we manipulate it.
ffb8aff6 235 *
f3662213
AN
236 * @param _rawImageDimensions
237 * @type Object
ffb8aff6
DW
238 * @private
239 */
f3662213 240 _rawImageDimensions: null,
ffb8aff6 241
62467795 242 initializer: function() {
1461aee8 243
62467795
AN
244 this.addButton({
245 icon: 'e/insert_edit_image',
246 callback: this._displayDialogue,
247 tags: 'img',
248 tagMatchRequiresAll: false
249 });
eb8b2425
DT
250 this.editor.delegate('dblclick', this._displayDialogue, 'img', this);
251 this.editor.delegate('click', this._handleClick, 'img', this);
33d4f4b0 252 this.editor.on('paste', this._handlePaste, this);
1461aee8 253 this.editor.on('drop', this._handleDragDrop, this);
c3c3c9f2
PN
254
255 // e.preventDefault needed to stop the default event from clobbering the desired behaviour in some browsers.
3a0bc0fd
DP
256 this.editor.on('dragover', function(e) {
257 e.preventDefault();
258 }, this);
259 this.editor.on('dragenter', function(e) {
260 e.preventDefault();
261 }, this);
1461aee8
PN
262 },
263
264 /**
265 * Handle a drag and drop event with an image.
266 *
267 * @method _handleDragDrop
268 * @param {EventFacade} e
33d4f4b0 269 * @return {boolean} false if we handled the event, else true.
1461aee8
PN
270 * @private
271 */
272 _handleDragDrop: function(e) {
33d4f4b0
TH
273 if (!e._event || !e._event.dataTransfer) {
274 // Drop not fully supported in this browser.
275 return true;
276 }
1461aee8 277
33d4f4b0
TH
278 return this._handlePasteOrDropHelper(e, e._event.dataTransfer);
279 },
1461aee8 280
33d4f4b0
TH
281 /**
282 * Handles paste events where - if the thing being pasted is an image.
283 *
284 * @method _handlePaste
285 * @param {EventFacade} e
286 * @return {boolean} false if we handled the event, else true.
287 * @private
288 */
289 _handlePaste: function(e) {
290 if (!e._event || !e._event.clipboardData) {
291 // Paste not fully supported in this browser.
292 return true;
293 }
1461aee8 294
33d4f4b0
TH
295 return this._handlePasteOrDropHelper(e, e._event.clipboardData);
296 },
1461aee8 297
33d4f4b0
TH
298 /**
299 * Handle a drag and drop event with an image.
300 *
301 * @method _handleDragDrop
302 * @param {EventFacade} e
303 * @param {DataTransfer} dataTransfer
304 * @return {boolean} false if we handled the event, else true.
305 * @private
306 */
307 _handlePasteOrDropHelper: function(e, dataTransfer) {
308
309 var items = dataTransfer.items,
310 didUpload = false;
311 for (var i = 0; i < items.length; i++) {
312 var item = items[i];
313 if (item.kind !== 'file') {
314 continue;
315 }
316 if (!this._isImage(item.type)) {
317 continue;
318 }
319 this._uploadImage(item.getAsFile());
320 didUpload = true;
321 }
1461aee8 322
33d4f4b0
TH
323 if (didUpload) {
324 // We handled this.
1461aee8
PN
325 e.preventDefault();
326 e.stopPropagation();
33d4f4b0
TH
327 return false;
328 } else {
329 // Let someone else try to handle it.
330 return true;
331 }
332 },
1461aee8 333
33d4f4b0
TH
334 /**
335 * Is this file an image?
336 *
337 * @method _isImage
338 * @param {string} mimeType the file's mime type.
339 * @return {boolean} true if the file has an image mimeType.
340 * @private
341 */
342 _isImage: function(mimeType) {
343 return mimeType.indexOf('image/') === 0;
344 },
345
346 /**
347 * Used by _handleDragDrop and _handlePaste to upload an image and insert it.
348 *
349 * @method _uploadImage
350 * @param {File} fileToSave
351 * @private
352 */
353 _uploadImage: function(fileToSave) {
354
355 var self = this,
356 host = this.get('host'),
357 template = Y.Handlebars.compile(IMAGETEMPLATE);
1461aee8 358
33d4f4b0
TH
359 host.saveSelection();
360
c6fb80b6
HN
361 // Trigger form upload start events.
362 require(['core_form/events'], function(FormEvent) {
a1ccefe2 363 FormEvent.notifyUploadStarted(self.editor.get('id'));
c6fb80b6
HN
364 });
365
33d4f4b0
TH
366 var options = host.get('filepickeroptions').image,
367 savepath = (options.savepath === undefined) ? '/' : options.savepath,
368 formData = new FormData(),
369 timestamp = 0,
370 uploadid = "",
371 xhr = new XMLHttpRequest(),
372 imagehtml = "",
373 keys = Object.keys(options.repositories);
374
375 formData.append('repo_upload_file', fileToSave);
376 formData.append('itemid', options.itemid);
377
378 // List of repositories is an object rather than an array. This makes iteration more awkward.
379 for (var i = 0; i < keys.length; i++) {
380 if (options.repositories[keys[i]].type === 'upload') {
381 formData.append('repo_id', options.repositories[keys[i]].id);
382 break;
383 }
384 }
385 formData.append('env', options.env);
386 formData.append('sesskey', M.cfg.sesskey);
387 formData.append('client_id', options.client_id);
388 formData.append('savepath', savepath);
389 formData.append('ctx_id', options.context.id);
390
391 // Insert spinner as a placeholder.
392 timestamp = new Date().getTime();
393 uploadid = 'moodleimage_' + Math.round(Math.random() * 100000) + '-' + timestamp;
394 host.focus();
395 host.restoreSelection();
396 imagehtml = template({
397 url: M.util.image_url("i/loading_small", 'moodle'),
398 alt: M.util.get_string('uploading', COMPONENTNAME),
399 id: uploadid
400 });
401 host.insertContentAtFocusPoint(imagehtml);
402 self.markUpdated();
403
404 // Kick off a XMLHttpRequest.
405 xhr.onreadystatechange = function() {
406 var placeholder = self.editor.one('#' + uploadid),
407 result,
408 file,
409 newhtml,
410 newimage;
411
412 if (xhr.readyState === 4) {
413 if (xhr.status === 200) {
414 result = JSON.parse(xhr.responseText);
415 if (result) {
416 if (result.error) {
1461aee8 417 if (placeholder) {
33d4f4b0 418 placeholder.remove(true);
1461aee8 419 }
c6fb80b6
HN
420 // Trigger form upload complete events.
421 require(['core_form/events'], function(FormEvent) {
a1ccefe2 422 FormEvent.notifyUploadCompleted(self.editor.get('id'));
c6fb80b6 423 });
33d4f4b0
TH
424 throw new M.core.ajaxException(result);
425 }
426
427 file = result;
428 if (result.event && result.event === 'fileexists') {
429 // A file with this name is already in use here - rename to avoid conflict.
430 // Chances are, it's a different image (stored in a different folder on the user's computer).
431 // If the user wants to reuse an existing image, they can copy/paste it within the editor.
432 file = result.newfile;
1461aee8 433 }
33d4f4b0
TH
434
435 // Replace placeholder with actual image.
436 newhtml = template({
437 url: file.url,
4144e278
C
438 presentation: true,
439 classlist: CSS.RESPONSIVE
557f44d9 440 });
33d4f4b0 441 newimage = Y.Node.create(newhtml);
1461aee8 442 if (placeholder) {
33d4f4b0
TH
443 placeholder.replace(newimage);
444 } else {
445 self.editor.appendChild(newimage);
1461aee8 446 }
33d4f4b0
TH
447 self.markUpdated();
448 }
449 } else {
450 Y.use('moodle-core-notification-alert', function() {
c6fb80b6
HN
451 // Trigger form upload complete events.
452 require(['core_form/events'], function(FormEvent) {
a1ccefe2 453 FormEvent.notifyUploadCompleted(self.editor.get('id'));
c6fb80b6 454 });
33d4f4b0
TH
455 new M.core.alert({message: M.util.get_string('servererror', 'moodle')});
456 });
457 if (placeholder) {
458 placeholder.remove(true);
1461aee8
PN
459 }
460 }
c6fb80b6
HN
461 // Trigger form upload complete events.
462 require(['core_form/events'], function(FormEvent) {
a1ccefe2 463 FormEvent.notifyUploadCompleted(self.editor.get('id'));
c6fb80b6 464 });
33d4f4b0
TH
465 }
466 };
467 xhr.open("POST", M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload', true);
468 xhr.send(formData);
469 },
43a737d1
DW
470
471 /**
eb8b2425 472 * Handle a click on an image.
43a737d1 473 *
eb8b2425 474 * @method _handleClick
43a737d1
DW
475 * @param {EventFacade} e
476 * @private
477 */
eb8b2425 478 _handleClick: function(e) {
43a737d1
DW
479 var image = e.target;
480
481 var selection = this.get('host').getSelectionFromNode(image);
eb8b2425
DT
482 if (this.get('host').getSelection() !== selection) {
483 this.get('host').setSelection(selection);
484 }
c9292b18
SH
485 },
486
487 /**
62467795
AN
488 * Display the image editing tool.
489 *
490 * @method _displayDialogue
491 * @private
c9292b18 492 */
62467795
AN
493 _displayDialogue: function() {
494 // Store the current selection.
495 this._currentSelection = this.get('host').getSelection();
496 if (this._currentSelection === false) {
497 return;
c9292b18 498 }
62467795 499
91bc9570
FM
500 // Reset the image dimensions.
501 this._rawImageDimensions = null;
502
62467795 503 var dialogue = this.getDialogue({
d3931a7d 504 headerContent: M.util.get_string('imageproperties', COMPONENTNAME),
104cc0e3 505 width: 'auto',
e5ddec38 506 focusAfterHide: true,
c1660772 507 focusOnShowSelector: SELECTORS.INPUTURL
62467795 508 });
ae671213
MG
509 // Set a maximum width for the dialog. This will prevent the dialog width to extend beyond the screen width
510 // in cases when the uploaded image has larger width.
511 dialogue.get('boundingBox').setStyle('maxWidth', '90%');
62467795
AN
512 // Set the dialogue content, and then show the dialogue.
513 dialogue.set('bodyContent', this._getDialogueContent())
514 .show();
c9292b18
SH
515 },
516
ffb8aff6
DW
517 /**
518 * Set the inputs for width and height if they are not set, and calculate
519 * if the constrain checkbox should be checked or not.
520 *
521 * @method _loadPreviewImage
522 * @param {String} url
523 * @private
524 */
525 _loadPreviewImage: function(url) {
3a0bc0fd
DP
526 var image = new Image();
527 var self = this;
ffb8aff6 528
f3662213
AN
529 image.onerror = function() {
530 var preview = self._form.one('.' + CSS.IMAGEPREVIEW);
531 preview.setStyles({
532 'display': 'none'
533 });
534
535 // Centre the dialogue when clearing the image preview.
536 self.getDialogue().centerDialogue();
537 };
538
ffb8aff6
DW
539 image.onload = function() {
540 var input, currentwidth, currentheight, widthRatio, heightRatio;
541
f3662213
AN
542 self._rawImageDimensions = {
543 width: this.width,
544 height: this.height
545 };
ffb8aff6
DW
546
547 input = self._form.one('.' + CSS.INPUTWIDTH);
548 currentwidth = input.get('value');
549 if (currentwidth === '') {
550 input.set('value', this.width);
d3931a7d 551 currentwidth = "" + this.width;
ffb8aff6
DW
552 }
553 input = self._form.one('.' + CSS.INPUTHEIGHT);
554 currentheight = input.get('value');
555 if (currentheight === '') {
556 input.set('value', this.height);
d3931a7d 557 currentheight = "" + this.height;
ffb8aff6
DW
558 }
559 input = self._form.one('.' + CSS.IMAGEPREVIEW);
f3662213
AN
560 input.setAttribute('src', this.src);
561 input.setStyles({
562 'display': 'inline'
563 });
ffb8aff6 564
ffb8aff6 565 input = self._form.one('.' + CSS.INPUTCONSTRAIN);
d3931a7d
DW
566 if (currentwidth.match(REGEX.ISPERCENT) && currentheight.match(REGEX.ISPERCENT)) {
567 input.set('checked', currentwidth === currentheight);
568 } else {
569 if (this.width === 0) {
570 this.width = 1;
571 }
572 if (this.height === 0) {
573 this.height = 1;
574 }
575 // This is the same as comparing to 3 decimal places.
3a0bc0fd
DP
576 widthRatio = Math.round(1000 * parseInt(currentwidth, 10) / this.width);
577 heightRatio = Math.round(1000 * parseInt(currentheight, 10) / this.height);
d3931a7d
DW
578 input.set('checked', widthRatio === heightRatio);
579 }
ffb8aff6 580
f3662213
AN
581 // Apply the image sizing.
582 self._autoAdjustSize(self);
583
ffb8aff6
DW
584 // Centre the dialogue once the preview image has loaded.
585 self.getDialogue().centerDialogue();
586 };
587
588 image.src = url;
589 },
590
c9292b18 591 /**
62467795
AN
592 * Return the dialogue content for the tool, attaching any required
593 * events.
594 *
595 * @method _getDialogueContent
596 * @return {Node} The content to place in the dialogue.
597 * @private
c9292b18 598 */
62467795
AN
599 _getDialogueContent: function() {
600 var template = Y.Handlebars.compile(TEMPLATE),
6cb48e04 601 canShowFilepicker = this.get('host').canShowFilepicker('image'),
62467795
AN
602 content = Y.Node.create(template({
603 elementid: this.get('host').get('elementid'),
604 CSS: CSS,
605 component: COMPONENTNAME,
6cb48e04 606 showFilepicker: canShowFilepicker,
62467795
AN
607 alignments: ALIGNMENTS
608 }));
609
610 this._form = content;
611
612 // Configure the view of the current image.
613 this._applyImageProperties(this._form);
614
ffb8aff6 615 this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
9754ab92
JF
616 this._form.one('.' + CSS.IMAGEPRESENTATION).on('change', this._updateWarning, this);
617 this._form.one('.' + CSS.INPUTALT).on('change', this._updateWarning, this);
f3662213
AN
618 this._form.one('.' + CSS.INPUTWIDTH).on('blur', this._autoAdjustSize, this);
619 this._form.one('.' + CSS.INPUTHEIGHT).on('blur', this._autoAdjustSize, this, true);
d3931a7d
DW
620 this._form.one('.' + CSS.INPUTCONSTRAIN).on('change', function(event) {
621 if (event.target.get('checked')) {
f3662213 622 this._autoAdjustSize(event);
d3931a7d
DW
623 }
624 }, this);
62467795
AN
625 this._form.one('.' + CSS.INPUTURL).on('blur', this._urlChanged, this);
626 this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setImage, this);
6cb48e04
FM
627
628 if (canShowFilepicker) {
629 this._form.one('.' + CSS.IMAGEBROWSER).on('click', function() {
630 this.get('host').showFilepicker('image', this._filepickerCallback, this);
631 }, this);
632 }
adca7326 633
7e03b27d
MC
634 // Character count.
635 this._form.one('.' + CSS.INPUTALT).on('keyup', this._handleKeyup, this);
636
62467795 637 return content;
adca7326 638 },
adca7326 639
f3662213
AN
640 _autoAdjustSize: function(e, forceHeight) {
641 forceHeight = forceHeight || false;
642
643 var keyField = this._form.one('.' + CSS.INPUTWIDTH),
644 keyFieldType = 'width',
645 subField = this._form.one('.' + CSS.INPUTHEIGHT),
646 subFieldType = 'height',
647 constrainField = this._form.one('.' + CSS.INPUTCONSTRAIN),
648 keyFieldValue = keyField.get('value'),
649 subFieldValue = subField.get('value'),
650 imagePreview = this._form.one('.' + CSS.IMAGEPREVIEW),
651 rawPercentage,
652 rawSize;
ffb8aff6 653
91bc9570
FM
654 // If we do not know the image size, do not do anything.
655 if (!this._rawImageDimensions) {
656 return;
657 }
658
d3931a7d 659 // Set the width back to default if it is empty.
f3662213
AN
660 if (keyFieldValue === '') {
661 keyFieldValue = this._rawImageDimensions[keyFieldType];
662 keyField.set('value', keyFieldValue);
663 keyFieldValue = keyField.get('value');
d3931a7d
DW
664 }
665
f3662213
AN
666 // Clear the existing preview sizes.
667 imagePreview.setStyles({
668 width: null,
669 height: null
670 });
ffb8aff6 671
f3662213
AN
672 // Now update with the new values.
673 if (!constrainField.get('checked')) {
674 // We are not keeping the image proportion - update the preview accordingly.
d3931a7d 675
f3662213
AN
676 // Width.
677 if (keyFieldValue.match(REGEX.ISPERCENT)) {
678 rawPercentage = parseInt(keyFieldValue, 10);
679 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
680 imagePreview.setStyle('width', rawSize + 'px');
681 } else {
682 imagePreview.setStyle('width', keyFieldValue + 'px');
d3931a7d 683 }
ffb8aff6 684
f3662213
AN
685 // Height.
686 if (subFieldValue.match(REGEX.ISPERCENT)) {
687 rawPercentage = parseInt(subFieldValue, 10);
688 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
689 imagePreview.setStyle('height', rawSize + 'px');
690 } else {
691 imagePreview.setStyle('height', subFieldValue + 'px');
692 }
693 } else {
694 // We are keeping the image in proportion.
695 if (forceHeight) {
696 // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.
697 var _temporaryValue;
698 _temporaryValue = keyField;
699 keyField = subField;
700 subField = _temporaryValue;
701
702 _temporaryValue = keyFieldType;
703 keyFieldType = subFieldType;
704 subFieldType = _temporaryValue;
705
706 _temporaryValue = keyFieldValue;
707 keyFieldValue = subFieldValue;
708 subFieldValue = _temporaryValue;
709 }
ffb8aff6 710
f3662213
AN
711 if (keyFieldValue.match(REGEX.ISPERCENT)) {
712 // This is a percentage based change. Copy it verbatim.
713 subFieldValue = keyFieldValue;
d3931a7d 714
f3662213
AN
715 // Set the width to the calculated pixel width.
716 rawPercentage = parseInt(keyFieldValue, 10);
717 rawSize = this._rawImageDimensions.width / 100 * rawPercentage;
ffb8aff6 718
f3662213
AN
719 // And apply the width/height to the container.
720 imagePreview.setStyle('width', rawSize);
721 rawSize = this._rawImageDimensions.height / 100 * rawPercentage;
722 imagePreview.setStyle('height', rawSize);
723 } else {
724 // Calculate the scaled subFieldValue from the keyFieldValue.
725 subFieldValue = Math.round((keyFieldValue / this._rawImageDimensions[keyFieldType]) *
726 this._rawImageDimensions[subFieldType]);
727
728 if (forceHeight) {
729 imagePreview.setStyles({
730 'width': subFieldValue,
731 'height': keyFieldValue
732 });
733 } else {
734 imagePreview.setStyles({
735 'width': keyFieldValue,
736 'height': subFieldValue
737 });
738 }
d3931a7d 739 }
f3662213
AN
740
741 // Update the subField's value within the form to reflect the changes.
742 subField.set('value', subFieldValue);
d3931a7d 743 }
ffb8aff6
DW
744 },
745
62467795
AN
746 /**
747 * Update the dialogue after an image was selected in the File Picker.
748 *
749 * @method _filepickerCallback
750 * @param {object} params The parameters provided by the filepicker
751 * containing information about the image.
752 * @private
753 */
754 _filepickerCallback: function(params) {
adca7326 755 if (params.url !== '') {
ffb8aff6 756 var input = this._form.one('.' + CSS.INPUTURL);
adca7326
DW
757 input.set('value', params.url);
758
759 // Auto set the width and height.
8d1c0179
DW
760 this._form.one('.' + CSS.INPUTWIDTH).set('value', '');
761 this._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
ffb8aff6
DW
762
763 // Load the preview image.
764 this._loadPreviewImage(params.url);
adca7326
DW
765 }
766 },
adca7326 767
62467795
AN
768 /**
769 * Applies properties of an existing image to the image dialogue for editing.
770 *
771 * @method _applyImageProperties
772 * @param {Node} form
773 * @private
774 */
775 _applyImageProperties: function(form) {
776 var properties = this._getSelectedImageProperties(),
c3e1c98f 777 img = form.one('.' + CSS.IMAGEPREVIEW);
adca7326 778
62467795
AN
779 if (properties === false) {
780 img.setStyle('display', 'none');
7bbc64b8 781 // Set the default alignment.
c3e1c98f
NM
782 ALIGNMENTS.some(function(alignment) {
783 if (alignment.isDefault) {
784 form.one('.' + CSS.INPUTALIGNMENT).set('value', alignment.value);
785 return true;
7bbc64b8 786 }
c3e1c98f
NM
787
788 return false;
789 }, this);
790
adca7326 791 return;
adca7326
DW
792 }
793
62467795 794 if (properties.align) {
da00661d 795 form.one('.' + CSS.INPUTALIGNMENT).set('value', properties.align);
d28af3d4
DW
796 }
797 if (properties.customstyle) {
798 form.one('.' + CSS.INPUTCUSTOMSTYLE).set('value', properties.customstyle);
62467795 799 }
62467795
AN
800 if (properties.width) {
801 form.one('.' + CSS.INPUTWIDTH).set('value', properties.width);
802 }
803 if (properties.height) {
804 form.one('.' + CSS.INPUTHEIGHT).set('value', properties.height);
805 }
806 if (properties.alt) {
807 form.one('.' + CSS.INPUTALT).set('value', properties.alt);
808 }
809 if (properties.src) {
810 form.one('.' + CSS.INPUTURL).set('value', properties.src);
ffb8aff6 811 this._loadPreviewImage(properties.src);
62467795
AN
812 }
813 if (properties.presentation) {
814 form.one('.' + CSS.IMAGEPRESENTATION).set('checked', 'checked');
adca7326 815 }
f3662213
AN
816
817 // Update the image preview based on the form properties.
818 this._autoAdjustSize();
adca7326 819 },
62467795 820
c9292b18
SH
821 /**
822 * Gets the properties of the currently selected image.
823 *
824 * The first image only if multiple images are selected.
825 *
62467795
AN
826 * @method _getSelectedImageProperties
827 * @return {object}
c9292b18 828 * @private
c9292b18 829 */
62467795 830 _getSelectedImageProperties: function() {
c9292b18
SH
831 var properties = {
832 src: null,
3a0bc0fd 833 alt: null,
c9292b18
SH
834 width: null,
835 height: null,
d28af3d4 836 align: '',
c9292b18
SH
837 presentation: false
838 },
62467795
AN
839
840 // Get the current selection.
841 images = this.get('host').getSelectedNodes(),
557f44d9
AN
842 width,
843 height,
844 style,
c3e1c98f 845 image;
c9292b18 846
d321f68b
DW
847 if (images) {
848 images = images.filter('img');
849 }
850
851 if (images && images.size()) {
c3e1c98f 852 image = this._removeLegacyAlignment(images.item(0));
62467795 853 this._selectedImage = image;
d321f68b 854
c9292b18 855 style = image.getAttribute('style');
d28af3d4 856 properties.customstyle = style;
c9292b18 857
d3931a7d
DW
858 width = image.getAttribute('width');
859 if (!width.match(REGEX.ISPERCENT)) {
860 width = parseInt(width, 10);
861 }
862 height = image.getAttribute('height');
863 if (!height.match(REGEX.ISPERCENT)) {
864 height = parseInt(height, 10);
865 }
866
867 if (width !== 0) {
c9292b18
SH
868 properties.width = width;
869 }
d3931a7d 870 if (height !== 0) {
c9292b18
SH
871 properties.height = height;
872 }
c3e1c98f 873 this._getAlignmentPropeties(image, properties);
c9292b18
SH
874 properties.src = image.getAttribute('src');
875 properties.alt = image.getAttribute('alt') || '';
876 properties.presentation = (image.get('role') === 'presentation');
877 return properties;
878 }
62467795
AN
879
880 // No image selected - clean up.
881 this._selectedImage = null;
c9292b18
SH
882 return false;
883 },
c9292b18 884
c3e1c98f
NM
885 /**
886 * Sets the alignment of a properties object.
887 *
888 * @method _getAlignmentPropeties
889 * @param {Node} image The image that the alignment properties should be found for
890 * @param {Object} properties The properties object that is created in _getSelectedImageProperties()
891 * @private
892 */
893 _getAlignmentPropeties: function(image, properties) {
894 var complete = false,
895 defaultAlignment;
896
897 // Check for an alignment value.
898 complete = ALIGNMENTS.some(function(alignment) {
899 var classname = this._getAlignmentClass(alignment.value);
900 if (image.hasClass(classname)) {
901 properties.align = alignment.value;
902 Y.log('Found alignment ' + alignment.value, 'debug', 'atto_image-button');
903
904 return true;
905 }
906
907 if (alignment.isDefault) {
908 defaultAlignment = alignment.value;
909 }
910
911 return false;
912 }, this);
913
914 if (!complete && defaultAlignment) {
915 properties.align = defaultAlignment;
916 }
917 },
918
62467795
AN
919 /**
920 * Update the form when the URL was changed. This includes updating the
921 * height, width, and image preview.
922 *
923 * @method _urlChanged
924 * @private
925 */
926 _urlChanged: function() {
ffb8aff6 927 var input = this._form.one('.' + CSS.INPUTURL);
b269f635 928
62467795 929 if (input.get('value') !== '') {
ffb8aff6
DW
930 // Load the preview image.
931 this._loadPreviewImage(input.get('value'));
b269f635 932 }
c9292b18 933 },
62467795 934
c9292b18 935 /**
62467795 936 * Update the image in the contenteditable.
c9292b18 937 *
62467795
AN
938 * @method _setImage
939 * @param {EventFacade} e
c9292b18 940 * @private
c9292b18 941 */
62467795
AN
942 _setImage: function(e) {
943 var form = this._form,
944 url = form.one('.' + CSS.INPUTURL).get('value'),
945 alt = form.one('.' + CSS.INPUTALT).get('value'),
946 width = form.one('.' + CSS.INPUTWIDTH).get('value'),
947 height = form.one('.' + CSS.INPUTHEIGHT).get('value'),
c3e1c98f 948 alignment = this._getAlignmentClass(form.one('.' + CSS.INPUTALIGNMENT).get('value')),
62467795 949 presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked'),
d3931a7d 950 constrain = form.one('.' + CSS.INPUTCONSTRAIN).get('checked'),
62467795 951 imagehtml,
c3e1c98f 952 customstyle = form.one('.' + CSS.INPUTCUSTOMSTYLE).get('value'),
d3931a7d 953 classlist = [],
62467795 954 host = this.get('host');
c9292b18 955
62467795
AN
956 e.preventDefault();
957
9754ab92
JF
958 // Check if there are any accessibility issues.
959 if (this._updateWarning()) {
c9292b18
SH
960 return;
961 }
962
62467795
AN
963 // Focus on the editor in preparation for inserting the image.
964 host.focus();
965 if (url !== '') {
966 if (this._selectedImage) {
967 host.setSelection(host.getSelectionFromNode(this._selectedImage));
968 } else {
969 host.setSelection(this._currentSelection);
970 }
d28af3d4 971
d3931a7d
DW
972 if (constrain) {
973 classlist.push(CSS.RESPONSIVE);
974 }
975
c3e1c98f
NM
976 // Add the alignment class for the image.
977 classlist.push(alignment);
978
d3931a7d
DW
979 if (!width.match(REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {
980 form.one('.' + CSS.INPUTWIDTH).focus();
981 return;
982 }
983 if (!height.match(REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {
984 form.one('.' + CSS.INPUTHEIGHT).focus();
985 return;
986 }
987
557f44d9 988 var template = Y.Handlebars.compile(IMAGETEMPLATE);
62467795
AN
989 imagehtml = template({
990 url: url,
991 alt: alt,
992 width: width,
993 height: height,
994 presentation: presentation,
d3931a7d
DW
995 customstyle: customstyle,
996 classlist: classlist.join(' ')
62467795
AN
997 });
998
999 this.get('host').insertContentAtFocusPoint(imagehtml);
1000
1001 this.markUpdated();
c9292b18 1002 }
d3931a7d
DW
1003
1004 this.getDialogue({
1005 focusAfterHide: null
1006 }).hide();
1007
9754ab92
JF
1008 },
1009
c3e1c98f
NM
1010 /**
1011 * Removes any legacy styles added by previous versions of the atto image button.
1012 *
1013 * @method _removeLegacyAlignment
1014 * @param {Y.Node} imageNode
1015 * @return {Y.Node}
1016 * @private
1017 */
1018 _removeLegacyAlignment: function(imageNode) {
1019 if (!imageNode.getStyle('margin')) {
1020 // There is no margin therefore this cannot match any known alignments.
1021 return imageNode;
1022 }
1023
1024 ALIGNMENTS.some(function(alignment) {
1025 if (imageNode.getStyle(alignment.name) !== alignment.value) {
1026 // The name/value do not match. Skip.
1027 return false;
1028 }
1029
1030 var normalisedNode = Y.Node.create('<div>');
1031 normalisedNode.setStyle('margin', alignment.margin);
1032 if (imageNode.getStyle('margin') !== normalisedNode.getStyle('margin')) {
1033 // The margin does not match.
1034 return false;
1035 }
1036
1037 Y.log('Legacy alignment found and removed.', 'info', 'atto_image-button');
1038 imageNode.addClass(this._getAlignmentClass(alignment.value));
1039 imageNode.setStyle(alignment.name, null);
1040 imageNode.setStyle('margin', null);
1041
1042 return true;
1043 }, this);
1044
1045 return imageNode;
1046 },
1047
1048 _getAlignmentClass: function(alignment) {
1049 return CSS.ALIGNSETTINGS + '_' + alignment;
1050 },
1051
9754ab92
JF
1052 /**
1053 * Update the alt text warning live.
1054 *
1055 * @method _updateWarning
1056 * @return {boolean} whether a warning should be displayed.
1057 * @private
1058 */
1059 _updateWarning: function() {
1060 var form = this._form,
1061 state = true,
1062 alt = form.one('.' + CSS.INPUTALT).get('value'),
1063 presentation = form.one('.' + CSS.IMAGEPRESENTATION).get('checked');
1064 if (alt === '' && !presentation) {
1065 form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'block');
1066 form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', true);
1067 form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', true);
1068 state = true;
1069 } else {
1070 form.one('.' + CSS.IMAGEALTWARNING).setStyle('display', 'none');
1071 form.one('.' + CSS.INPUTALT).setAttribute('aria-invalid', false);
1072 form.one('.' + CSS.IMAGEPRESENTATION).setAttribute('aria-invalid', false);
b983a5d8 1073 state = false;
9754ab92
JF
1074 }
1075 this.getDialogue().centerDialogue();
1076 return state;
7e03b27d
MC
1077 },
1078
1079 /**
1080 * Handle the keyup to update the character count.
1081 */
1082 _handleKeyup: function() {
1083 var form = this._form,
1084 alt = form.one('.' + CSS.INPUTALT).get('value'),
1085 characterCount = alt.length,
1086 current = form.one('#currentcount');
1087 current.setHTML(characterCount);
adca7326 1088 }
62467795 1089});
adca7326
DW
1090
1091
62467795 1092}, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});