Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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