Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
/* global ns Cropper */
2
H5PEditor.ImageEditingPopup = (function ($, EventDispatcher) {
3
  var instanceCounter = 0;
4
  var scriptsLoaded = false;
5
 
6
  /**
7
   * Popup for editing images
8
   *
9
   * @param {number} [ratio] Ratio that cropping must keep
10
   * @constructor
11
   */
12
  function ImageEditingPopup(ratio) {
13
    var self = this;
14
    EventDispatcher.call(this);
15
    var uniqueId = instanceCounter;
16
    var isShowing = false;
17
    var isReset = false;
18
    var topOffset = 0;
19
    var maxWidth;
20
    var maxHeight;
21
 
22
    // Create elements
23
    var background = document.createElement('div');
24
    background.className = 'h5p-editing-image-popup-background hidden';
25
 
26
    var popup = document.createElement('div');
27
    popup.className = 'h5p-editing-image-popup';
28
    background.appendChild(popup);
29
 
30
    var header = document.createElement('div');
31
    header.className = 'h5p-editing-image-header';
32
    popup.appendChild(header);
33
 
34
    var headerTitle = document.createElement('div');
35
    headerTitle.className = 'h5p-editing-image-header-title';
36
    headerTitle.textContent = H5PEditor.t('core', 'editImage');
37
    header.appendChild(headerTitle);
38
 
39
    var headerButtons = document.createElement('div');
40
    headerButtons.className = 'h5p-editing-image-header-buttons';
41
    header.appendChild(headerButtons);
42
 
43
    var editingContainer = document.createElement('div');
44
    editingContainer.className = 'h5p-editing-image-editing-container';
45
    popup.appendChild(editingContainer);
46
 
47
    var imageLoading = document.createElement('div');
48
    imageLoading.className = 'h5p-editing-image-loading';
49
    imageLoading.textContent = ns.t('core', 'loadingImageEditor');
50
    popup.appendChild(imageLoading);
51
 
52
    // Create editing image
53
    var editingImage = new Image();
54
    editingImage.className = 'h5p-editing-image hidden';
55
    editingImage.id = 'h5p-editing-image-' + uniqueId;
56
    editingContainer.appendChild(editingImage);
57
 
58
    // Close popup on background click
59
    background.addEventListener('click', function () {
60
      this.hide();
61
    }.bind(this));
62
 
63
    // Prevent closing popup
64
    popup.addEventListener('click', function (e) {
65
      e.stopPropagation();
66
    });
67
 
68
    // Make sure each ImageEditingPopup instance has a unique ID
69
    instanceCounter += 1;
70
 
71
    /**
72
     * Create header button
73
     *
74
     * @param {string} coreString Must be specified in core translations
75
     * @param {string} className Unique button identifier that will be added to classname
76
     * @param {function} clickEvent OnClick function
77
     */
78
    var createButton = function (coreString, className, clickEvent) {
79
      var button = document.createElement('button');
80
      button.textContent = ns.t('core', coreString);
81
      button.className = className;
82
      button.addEventListener('click', clickEvent);
83
      headerButtons.appendChild(button);
84
    };
85
 
86
    /**
87
     * Set max width and height for image editing tool
88
     */
89
    var setCropperDimensions = function () {
90
      // Set max dimensions
91
      var dims = ImageEditingPopup.staticDimensions;
92
      maxWidth = background.offsetWidth - dims.backgroundPaddingWidth;
93
 
94
      // Only use 65% of window height
95
      var maxScreenHeight = window.innerHeight * dims.maxScreenHeightPercentage;
96
 
97
      // Calculate editor max height
98
      var editorHeight = background.offsetHeight - dims.backgroundPaddingHeight - dims.popupHeaderHeight;
99
 
100
      // Use smallest of screen height and editor height,
101
      // we don't want to overflow editor or screen
102
      maxHeight = maxScreenHeight < editorHeight ? maxScreenHeight : editorHeight;
103
      maxHeight = Math.min(maxHeight, maxWidth); // prevent maxHeight from getting too big in long editors like h5p column
104
    };
105
 
106
    /**
107
     * Load a script dynamically
108
     *
109
     * @param {string} path Path to script
110
     * @param {function} [callback]
111
     */
112
    var loadScript = function (path, callback) {
113
      $.ajax({
114
        url: path,
115
        dataType: 'script',
116
        success: function () {
117
          if (callback) {
118
            callback();
119
          }
120
        },
121
        async: true
122
      });
123
    };
124
 
125
    /**
126
     * Load scripts dynamically
127
     */
128
    var loadScripts = function (callback) {
129
      loadScript(H5PEditor.basePath + 'libs/cropper.js', function () {
130
        scriptsLoaded = true;
131
        if (callback) {
132
          callback();
133
        }
134
      });
135
    };
136
 
137
    /**
138
     * Grab canvas data and pass data to listeners.
139
     */
140
    var saveImage = () => {
141
      var convertData = function () {
142
        const finished = function (blob) {
143
          self.trigger('savedImage', blob);
144
        };
145
        if (self.cropper.mirror.toBlob) {
146
          // Export canvas as blob to save processing time and bandwidth
147
          self.cropper.mirror.toBlob(finished, self.mime);
148
        }
149
        else {
150
          // Blob export not supported by canvas, export as dataURL and export
151
          // to blob before uploading (saves processing resources on server)
152
          finished(dataURLtoBlob(this.cropper.mirror.toDataURL({
153
            format: self.mime.split('/')[1]
154
          })));
155
        }
156
      };
157
      convertData();
158
      isReset = false;
159
    };
160
 
161
    /**
162
     * Adjust popup offset.
163
     * Make sure it is centered on top of offset.
164
     *
165
     * @param {Object} [offset] Offset that popup should center on.
166
     * @param {number} [offset.top] Offset to top.
167
     */
168
    this.adjustPopupOffset = function (offset) {
169
      if (offset) {
170
        topOffset = offset.top;
171
      }
172
 
173
      // Only use 65% of window height
174
      var maxScreenHeight = window.innerHeight * 0.65;
175
 
176
      // Calculate editor max height
177
      var dims = ImageEditingPopup.staticDimensions;
178
      var backgroundHeight = H5P.$body.get(0).offsetHeight - dims.backgroundPaddingHeight;
179
      var popupHeightNoImage = dims.darkroomToolbarHeight + dims.popupHeaderHeight;
180
      var editorHeight =  backgroundHeight - popupHeightNoImage;
181
 
182
      // Available editor height
183
      var availableHeight = maxScreenHeight < editorHeight ? maxScreenHeight : editorHeight;
184
 
185
      // Check if image is smaller than available height
186
      var actualImageHeight;
187
      if (editingImage.naturalHeight < availableHeight) {
188
        actualImageHeight = editingImage.naturalHeight;
189
      }
190
      else {
191
        actualImageHeight = availableHeight;
192
 
193
        // We must check ratio as well
194
        var imageRatio = editingImage.naturalHeight / editingImage.naturalWidth;
195
        var maxActualImageHeight = maxWidth * imageRatio;
196
        if (maxActualImageHeight < actualImageHeight) {
197
          actualImageHeight = maxActualImageHeight;
198
        }
199
      }
200
 
201
      var popupHeightWImage = actualImageHeight + popupHeightNoImage;
202
      var offsetCentered = topOffset - (popupHeightWImage / 2) -
203
        (dims.backgroundPaddingHeight / 2);
204
 
205
      // Min offset is 0
206
      offsetCentered = offsetCentered > 0 ? offsetCentered : 0;
207
 
208
      // Check that popup does not overflow editor
209
      if (popupHeightWImage + offsetCentered > backgroundHeight) {
210
        var newOffset = backgroundHeight - popupHeightWImage;
211
        offsetCentered = newOffset < 0 ? 0 : newOffset;
212
      }
213
 
214
      popup.style.top = offsetCentered + 'px';
215
    };
216
 
217
    /**
218
     * Resize cropper canvas, selector and mask.
219
     */
220
    this.resizeCropper = () => {
221
      setCropperDimensions();
222
      this.cropper.canvas.width = maxWidth - 2; // leave out 2px for container css border
223
      this.cropper.canvas.height = maxHeight;
224
      this.cropper.loadImage();
225
      this.cropper.loadMirror();
226
      this.cropper.toggleSection('tools');
227
      this.cropper.toggleSelector(false);
228
    }
229
 
230
    /**
231
     * Create image editing tool from image.
232
     */
233
    const createCropper = (image) => {
234
      if (this.cropper) {
235
        this.cropper.options.canvas.image = image;
236
        this.cropper.reset();
237
        return;
238
      }
239
      this.cropper = new Cropper({
240
        uniqueId,
241
        container: editingContainer,
242
        canvas: {
243
          width: maxWidth,
244
          height: maxHeight,
245
          background: '#2f323a',
246
          image
247
        },
248
        selector: {
249
          min: {
250
            width: 50,
251
            height: 50
252
          },
253
          mask: true
254
        },
255
        labels: {
256
          rotateLeft: H5P.t('rotateLeft'),
257
          rotateRight: H5P.t('rotateRight'),
258
          cropImage: H5P.t('cropImage'),
259
          confirmCrop: H5P.t('confirmCrop'),
260
          cancelCrop: H5P.t('cancelCrop')
261
        }
262
      });
263
      const classes = ['cropper-h5p-tooltip'];
264
      H5P.Tooltip(this.cropper.buttons.rotateLeft, { text: H5P.t('rotateLeft'), classes });
265
      H5P.Tooltip(this.cropper.buttons.rotateRight, { text: H5P.t('rotateRight'), classes });
266
      H5P.Tooltip(this.cropper.buttons.crop, { text: H5P.t('cropImage'), classes });
267
 
268
      // set before & after rotation events
269
      const beforeRotation = () => {
270
        this.cropper.sections.tools.classList.add('hidden');
271
        this.rotationTimer = setTimeout(() => {
272
          this.cropper.sections.tools.classList.add('wait');
273
          this.cropper.container.style.cursor = 'wait';
274
          this.cropper.masks.left.style.display = 'block';
275
          this.cropper.masks.left.style.width = '100%';
276
          this.cropper.masks.left.style.height = '100%';
277
        }, 1000);
278
      }
279
      const afterRotation = () => {
280
        clearTimeout(this.rotationTimer);
281
        this.cropper.container.style.cursor = 'auto';
282
        this.cropper.sections.tools.classList.remove('hidden', 'wait');
283
        this.cropper.masks.left.style.display = 'none';
284
      }
285
      const oldRotate = this.cropper.rotate;
286
      this.cropper.rotate = (rotation) => {
287
        beforeRotation();
288
        oldRotate(rotation, afterRotation);
289
      }
290
    };
291
 
292
    /**
293
     * Set new image in editing tool
294
     *
295
     * @param {string} imgSrc Source of new image
296
     */
297
    this.setImage = function (imgSrc, callback) {
298
      H5P.setSource(editingImage, imgSrc, H5PEditor.contentId);
299
      editingImage.onload = () => {
300
        createCropper(editingImage);
301
        editingImage.onload = null;
302
        imageLoading.classList.add('hidden');
303
        if (callback) {
304
          callback();
305
        }
306
      };
307
      imageLoading.classList.remove('hidden');
308
      editingImage.classList.add('hidden');
309
      editingContainer.appendChild(editingImage);
310
    };
311
 
312
    /**
313
     * Show popup
314
     *
315
     * @param {Object} [offset] Offset that popup should center on.
316
     * @param {string} [imageSrc] Source of image that will be edited
317
     * @param {Event} [event] Event object (button) for positioning the popup
318
     */
319
    this.show = function (offset, imageSrc, event) {
320
      const openImageEditor = () => {
321
        H5P.$body.get(0).classList.add('h5p-editor-image-popup');
322
        background.classList.remove('hidden');
323
        self.trigger('initialized');
324
      }
325
      const alignPopup = () => {
326
        if (event) {
327
          let top = event.target.getBoundingClientRect().top + window.scrollY;
328
          if (window.innerHeight - top < popup.offsetHeight) {
329
            top = window.innerHeight - popup.offsetHeight - 58; // 48px background padding + 10px so that the popup does not touch the bottom
330
          }
331
          popup.style.top = top + 'px';
332
        }
333
      }
334
      const imageLoaded = () => {
335
        if (offset) {
336
          self.adjustPopupOffset(offset);
337
          openImageEditor();
338
          self.resizeCropper();
339
          window.addEventListener('resize', this.resizeCropper);
340
        }
341
        alignPopup();
342
      }
343
      H5P.$body.get(0).appendChild(background);
344
      background.classList.remove('hidden');
345
      setCropperDimensions();
346
      background.classList.add('hidden');
347
      if (imageSrc) {
348
        // Load image editing scripts dynamically
349
        if (!scriptsLoaded) {
350
          loadScripts(() => self.setImage(imageSrc, imageLoaded));
351
        }
352
        else {
353
          self.setImage(imageSrc, imageLoaded);
354
        }
355
      }
356
      else {
357
        openImageEditor();
358
        alignPopup();
359
      }
360
      isShowing = true;
361
    };
362
 
363
    /**
364
     * Hide popup
365
     */
366
    this.hide = () => {
367
      isShowing = false;
368
      H5P.$body.get(0).classList.remove('h5p-editor-image-popup');
369
      background.classList.add('hidden');
370
      H5P.$body.get(0).removeChild(background);
371
      window.removeEventListener('resize', this.resizeCropper);
372
    };
373
 
374
    /**
375
     * Toggle popup visibility
376
     */
377
    this.toggle = function () {
378
      if (isShowing) {
379
        this.hide();
380
      }
381
      else {
382
        this.show();
383
      }
384
    };
385
 
386
    // Create header buttons
387
    createButton('resetToOriginalLabel', 'h5p-editing-image-reset-button h5p-remove', function () {
388
      self.trigger('resetImage');
389
      isReset = true;
390
    });
391
    createButton('cancelLabel', 'h5p-editing-image-cancel-button', function () {
392
      self.trigger('canceled');
393
      self.hide();
394
      self.cropper.toggleSelector(false);
395
    });
396
    createButton('saveLabel', 'h5p-editing-image-save-button h5p-done', function () {
397
      if (self.cropper.selector.style.display !== 'none') {
398
        self.cropper.crop(() => {
399
          self.cropper.toggleSelector(false);
400
          saveImage();
401
          self.hide();
402
        });
403
      }
404
      else {
405
        saveImage();
406
        self.hide();
407
      }
408
    });
409
  }
410
 
411
  ImageEditingPopup.prototype = Object.create(EventDispatcher.prototype);
412
  ImageEditingPopup.prototype.constructor = ImageEditingPopup;
413
 
414
  ImageEditingPopup.staticDimensions = {
415
    backgroundPaddingWidth: 32,
416
    backgroundPaddingHeight: 96,
417
    maxScreenHeightPercentage: 0.65,
418
    popupHeaderHeight: 60
419
  };
420
 
421
  /**
422
   * Convert a data URL(base64) into blob.
423
   *
424
   * @param {string} dataURL
425
   * @return {Blob}
426
   */
427
  const dataURLtoBlob = function (dataURL) {
428
    const split = dataURL.split(',');
429
 
430
    // First part is the mime type
431
    const mime = split[0].match(/data:(.*);base64/i)[1];
432
 
433
    // Second part is the base64 data
434
    const bytes = atob(split[1]);
435
 
436
    // Convert string into char code array
437
    const bits = new Uint8Array(bytes.length);
438
    for (let i = 0; i < bytes.length; i++) {
439
      bits[i] = bytes.charCodeAt(i);
440
    }
441
 
442
    // Make the codes into a Blob, and we're done!
443
    return new Blob([bits], {
444
      type: mime
445
    });
446
  }
447
 
448
  return ImageEditingPopup;
449
 
450
}(H5P.jQuery, H5P.EventDispatcher));
451