Projet

Général

Profil

0001-wip-add-client-side-cropper-to-profile-image-field-2.patch

Benjamin Dauvergne, 28 mars 2019 12:22

Télécharger (70,7 ko)

Voir les différences:

Subject: [PATCH] wip: add client side cropper to profile image field (#26251)

 src/authentic2/forms/fields.py                |    5 +
 src/authentic2/forms/widgets.py               |    9 +
 src/authentic2/static/css/croppie.css         |  250 +++
 src/authentic2/static/js/croppie.js           | 1625 +++++++++++++++++
 .../authentic2/profile_image_input.html       |   97 +-
 5 files changed, 1981 insertions(+), 5 deletions(-)
 create mode 100644 src/authentic2/static/css/croppie.css
 create mode 100644 src/authentic2/static/js/croppie.js
src/authentic2/forms/fields.py
50 50
    def image_size(self):
51 51
        return app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_SIZE
52 52

  
53
    def widget_attrs(self, widget):
54
        return {
55
            'data-image-size': self.image_size,
56
        }
57

  
53 58
    def clean(self, data, initial=None):
54 59
        if data is FILE_INPUT_CONTRADICTION or data is False or data is None:
55 60
            return super(ProfileImageField, self).clean(data, initial=initial)
src/authentic2/forms/widgets.py
263 263
        attrs = kwargs.pop('attrs', {})
264 264
        attrs['accept'] = 'image/*'
265 265
        super(ProfileImageInput, self).__init__(*args, attrs=attrs, **kwargs)
266

  
267
    class Media:
268
        css = {
269
            'all': ('css/croppie.css',),
270
        }
271
        js = (
272
            xstatic('jquery', 'jquery.min.js'),
273
            'js/croppie.js',
274
        )
src/authentic2/static/css/croppie.css
1
.croppie-container {
2
    width: 100%;
3
    height: 100%;
4
}
5

  
6
.croppie-container .cr-image {
7
    z-index: -1;
8
    position: absolute;
9
    top: 0;
10
    left: 0;
11
    transform-origin: 0 0;
12
    max-height: none;
13
    max-width: none;
14
}
15

  
16
.croppie-container .cr-boundary {
17
    position: relative;
18
    overflow: hidden;
19
/*    margin: 0 auto; */
20
    z-index: 1;
21
    width: 100%;
22
    height: 100%;
23
}
24

  
25
.croppie-container .cr-viewport,
26
.croppie-container .cr-resizer {
27
    position: absolute;
28
    border: 2px solid #fff;
29
    margin: auto;
30
    top: 0;
31
    bottom: 0;
32
    right: 0;
33
    left: 0;
34
    box-shadow: 0 0 2000px 2000px rgba(0, 0, 0, 0.5);
35
    z-index: 0;
36
}
37

  
38
.croppie-container .cr-resizer {
39
  z-index: 2;
40
  box-shadow: none;
41
  pointer-events: none;
42
}
43

  
44
.croppie-container .cr-resizer-vertical,
45
.croppie-container .cr-resizer-horisontal {
46
  position: absolute;
47
  pointer-events: all;
48
}
49

  
50
.croppie-container .cr-resizer-vertical::after,
51
.croppie-container .cr-resizer-horisontal::after {
52
    display: block;
53
    position: absolute;
54
    box-sizing: border-box;
55
    border: 1px solid black;
56
    background: #fff;
57
    width: 10px;
58
    height: 10px;
59
    content: '';
60
}
61

  
62
.croppie-container .cr-resizer-vertical {
63
  bottom: -5px;
64
  cursor: row-resize;
65
  width: 100%;
66
  height: 10px;
67
}
68

  
69
.croppie-container .cr-resizer-vertical::after {
70
    left: 50%;
71
    margin-left: -5px;
72
}
73

  
74
.croppie-container .cr-resizer-horisontal {
75
  right: -5px;
76
  cursor: col-resize;
77
  width: 10px;
78
  height: 100%;
79
}
80

  
81
.croppie-container .cr-resizer-horisontal::after {
82
    top: 50%;
83
    margin-top: -5px;
84
}
85

  
86
.croppie-container .cr-original-image {
87
    display: none;
88
}
89

  
90
.croppie-container .cr-vp-circle {
91
    border-radius: 50%;
92
}
93

  
94
.croppie-container .cr-overlay {
95
    z-index: 1;
96
    position: absolute;
97
    cursor: move;
98
    touch-action: none;
99
}
100

  
101
.croppie-container .cr-slider-wrap {
102
    width: 75%;
103
    margin: 15px 0px;
104
/*    text-align: center; */
105
}
106

  
107
.croppie-result {
108
    position: relative;
109
    overflow: hidden;
110
}
111

  
112
.croppie-result img {
113
    position: absolute;
114
}
115

  
116
.croppie-container .cr-image,
117
.croppie-container .cr-overlay,
118
.croppie-container .cr-viewport {
119
    -webkit-transform: translateZ(0);
120
    -moz-transform: translateZ(0);
121
    -ms-transform: translateZ(0);
122
    transform: translateZ(0);
123
}
124

  
125
/*************************************/
126
/***** STYLING RANGE INPUT ***********/
127
/*************************************/
128
/*http://brennaobrien.com/blog/2014/05/style-input-type-range-in-every-browser.html */
129
/*************************************/
130

  
131
.cr-slider {
132
    -webkit-appearance: none;
133
/*removes default webkit styles*/
134
	/*border: 1px solid white; *//*fix for FF unable to apply focus style bug */
135
    width: 300px;
136
/*required for proper track sizing in FF*/
137
    max-width: 100%;
138
    padding-top: 8px;
139
    padding-bottom: 8px;
140
    background-color: transparent;
141
}
142

  
143
.cr-slider::-webkit-slider-runnable-track {
144
    width: 100%;
145
    height: 3px;
146
    background: rgba(0, 0, 0, 0.5);
147
    border: 0;
148
    border-radius: 3px;
149
}
150

  
151
.cr-slider::-webkit-slider-thumb {
152
    -webkit-appearance: none;
153
    border: none;
154
    height: 16px;
155
    width: 16px;
156
    border-radius: 50%;
157
    background: #ddd;
158
    margin-top: -6px;
159
}
160

  
161
.cr-slider:focus {
162
    outline: none;
163
}
164
/*
165
.cr-slider:focus::-webkit-slider-runnable-track {
166
background: #ccc;
167
}
168
*/
169

  
170
.cr-slider::-moz-range-track {
171
    width: 100%;
172
    height: 3px;
173
    background: rgba(0, 0, 0, 0.5);
174
    border: 0;
175
    border-radius: 3px;
176
}
177

  
178
.cr-slider::-moz-range-thumb {
179
    border: none;
180
    height: 16px;
181
    width: 16px;
182
    border-radius: 50%;
183
    background: #ddd;
184
    margin-top: -6px;
185
}
186

  
187
/*hide the outline behind the border*/
188
.cr-slider:-moz-focusring {
189
    outline: 1px solid white;
190
    outline-offset: -1px;
191
}
192

  
193
.cr-slider::-ms-track {
194
    width: 100%;
195
    height: 5px;
196
    background: transparent;
197
/*remove bg colour from the track, we'll use ms-fill-lower and ms-fill-upper instead */
198
	border-color: transparent;/*leave room for the larger thumb to overflow with a transparent border */
199
	border-width: 6px 0;
200
	color: transparent;/*remove default tick marks*/
201
}
202
.cr-slider::-ms-fill-lower {
203
	background: rgba(0, 0, 0, 0.5);
204
	border-radius: 10px;
205
}
206
.cr-slider::-ms-fill-upper {
207
	background: rgba(0, 0, 0, 0.5);
208
	border-radius: 10px;
209
}
210
.cr-slider::-ms-thumb {
211
	border: none;
212
	height: 16px;
213
	width: 16px;
214
	border-radius: 50%;
215
	background: #ddd;
216
	margin-top:1px;
217
}
218
.cr-slider:focus::-ms-fill-lower {
219
	background: rgba(0, 0, 0, 0.5);
220
}
221
.cr-slider:focus::-ms-fill-upper {
222
	background: rgba(0, 0, 0, 0.5);
223
}
224
/*******************************************/
225

  
226
/***********************************/
227
/* Rotation Tools */
228
/***********************************/
229
.cr-rotate-controls {
230
	position: absolute;
231
	bottom: 5px;
232
	left: 5px;
233
	z-index: 1;
234
}
235
.cr-rotate-controls button {
236
	border: 0;
237
	background: none;
238
}
239
.cr-rotate-controls i:before {
240
	display: inline-block;
241
	font-style: normal;
242
	font-weight: 900;
243
	font-size: 22px;
244
}
245
.cr-rotate-l i:before {
246
	content: '↺';
247
}
248
.cr-rotate-r i:before {
249
	content: '↻';
250
}
src/authentic2/static/js/croppie.js
1
/*************************
2
 * Croppie
3
 * Copyright 2018
4
 * Foliotek
5
 * Version: 2.6.3
6
 *************************/
7
(function (root, factory) {
8
    if (typeof define === 'function' && define.amd) {
9
        // AMD. Register as an anonymous module.
10
        define(factory);
11
    } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
12
        // CommonJS
13
        module.exports = factory();
14
    } else {
15
        // Browser globals
16
        root.Croppie = factory();
17
    }
18
}(typeof self !== 'undefined' ? self : this, function () {
19

  
20
    /* Polyfills */
21
    if (typeof Promise !== 'function') {
22
        /*! promise-polyfill 3.1.0 */
23
        !function(a){function b(a,b){return function(){a.apply(b,arguments)}}function c(a){if("object"!==typeof this)throw new TypeError("Promises must be constructed via new");if("function"!==typeof a)throw new TypeError("not a function");this._state=null,this._value=null,this._deferreds=[],i(a,b(e,this),b(f,this))}function d(a){var b=this;return null===this._state?void this._deferreds.push(a):void k(function(){var c=b._state?a.onFulfilled:a.onRejected;if(null===c)return void(b._state?a.resolve:a.reject)(b._value);var d;try{d=c(b._value)}catch(e){return void a.reject(e)}a.resolve(d)})}function e(a){try{if(a===this)throw new TypeError("A promise cannot be resolved with itself.");if(a&&("object"===typeof a||"function"===typeof a)){var c=a.then;if("function"===typeof c)return void i(b(c,a),b(e,this),b(f,this))}this._state=!0,this._value=a,g.call(this)}catch(d){f.call(this,d)}}function f(a){this._state=!1,this._value=a,g.call(this)}function g(){for(var a=0,b=this._deferreds.length;b>a;a++)d.call(this,this._deferreds[a]);this._deferreds=null}function h(a,b,c,d){this.onFulfilled="function"===typeof a?a:null,this.onRejected="function"===typeof b?b:null,this.resolve=c,this.reject=d}function i(a,b,c){var d=!1;try{a(function(a){d||(d=!0,b(a))},function(a){d||(d=!0,c(a))})}catch(e){if(d)return;d=!0,c(e)}}var j=setTimeout,k="function"===typeof setImmediate&&setImmediate||function(a){j(a,1)},l=Array.isArray||function(a){return"[object Array]"===Object.prototype.toString.call(a)};c.prototype["catch"]=function(a){return this.then(null,a)},c.prototype.then=function(a,b){var e=this;return new c(function(c,f){d.call(e,new h(a,b,c,f))})},c.all=function(){var a=Array.prototype.slice.call(1===arguments.length&&l(arguments[0])?arguments[0]:arguments);return new c(function(b,c){function d(f,g){try{if(g&&("object"===typeof g||"function"===typeof g)){var h=g.then;if("function"===typeof h)return void h.call(g,function(a){d(f,a)},c)}a[f]=g,0===--e&&b(a)}catch(i){c(i)}}if(0===a.length)return b([]);for(var e=a.length,f=0;f<a.length;f++)d(f,a[f])})},c.resolve=function(a){return a&&"object"===typeof a&&a.constructor===c?a:new c(function(b){b(a)})},c.reject=function(a){return new c(function(b,c){c(a)})},c.race=function(a){return new c(function(b,c){for(var d=0,e=a.length;e>d;d++)a[d].then(b,c)})},c._setImmediateFn=function(a){k=a},"undefined"!==typeof module&&module.exports?module.exports=c:a.Promise||(a.Promise=c)}(this);
24
    }
25

  
26
    if ( typeof window.CustomEvent !== "function" ) {
27
        (function(){
28
            function CustomEvent ( event, params ) {
29
                params = params || { bubbles: false, cancelable: false, detail: undefined };
30
                var evt = document.createEvent( 'CustomEvent' );
31
                evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
32
                return evt;
33
            }
34
            CustomEvent.prototype = window.Event.prototype;
35
            window.CustomEvent = CustomEvent;
36
        }());
37
    }
38

  
39
    if (!HTMLCanvasElement.prototype.toBlob) {
40
        Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
41
            value: function (callback, type, quality) {
42
                var binStr = atob( this.toDataURL(type, quality).split(',')[1] ),
43
                len = binStr.length,
44
                arr = new Uint8Array(len);
45

  
46
                for (var i=0; i<len; i++ ) {
47
                    arr[i] = binStr.charCodeAt(i);
48
                }
49

  
50
                callback( new Blob( [arr], {type: type || 'image/png'} ) );
51
            }
52
        });
53
    }
54
    /* End Polyfills */
55

  
56
    var cssPrefixes = ['Webkit', 'Moz', 'ms'],
57
        emptyStyles = document.createElement('div').style,
58
        EXIF_NORM = [1,8,3,6],
59
        EXIF_FLIP = [2,7,4,5],
60
        CSS_TRANS_ORG,
61
        CSS_TRANSFORM,
62
        CSS_USERSELECT;
63

  
64
    function vendorPrefix(prop) {
65
        if (prop in emptyStyles) {
66
            return prop;
67
        }
68

  
69
        var capProp = prop[0].toUpperCase() + prop.slice(1),
70
            i = cssPrefixes.length;
71

  
72
        while (i--) {
73
            prop = cssPrefixes[i] + capProp;
74
            if (prop in emptyStyles) {
75
                return prop;
76
            }
77
        }
78
    }
79

  
80
    CSS_TRANSFORM = vendorPrefix('transform');
81
    CSS_TRANS_ORG = vendorPrefix('transformOrigin');
82
    CSS_USERSELECT = vendorPrefix('userSelect');
83

  
84
    function getExifOffset(ornt, rotate) {
85
        var arr = EXIF_NORM.indexOf(ornt) > -1 ? EXIF_NORM : EXIF_FLIP,
86
            index = arr.indexOf(ornt),
87
            offset = (rotate / 90) % arr.length;// 180 = 2%4 = 2 shift exif by 2 indexes
88

  
89
        return arr[(arr.length + index + (offset % arr.length)) % arr.length];
90
    }
91

  
92
    // Credits to : Andrew Dupont - http://andrewdupont.net/2009/08/28/deep-extending-objects-in-javascript/
93
    function deepExtend(destination, source) {
94
        destination = destination || {};
95
        for (var property in source) {
96
            if (source[property] && source[property].constructor && source[property].constructor === Object) {
97
                destination[property] = destination[property] || {};
98
                deepExtend(destination[property], source[property]);
99
            } else {
100
                destination[property] = source[property];
101
            }
102
        }
103
        return destination;
104
    }
105

  
106
    function clone(object) {
107
        return deepExtend({}, object);
108
    }
109

  
110
    function debounce(func, wait, immediate) {
111
        var timeout;
112
        return function () {
113
            var context = this, args = arguments;
114
            var later = function () {
115
                timeout = null;
116
                if (!immediate) func.apply(context, args);
117
            };
118
            var callNow = immediate && !timeout;
119
            clearTimeout(timeout);
120
            timeout = setTimeout(later, wait);
121
            if (callNow) func.apply(context, args);
122
        };
123
    }
124

  
125
    function dispatchChange(element) {
126
        if ("createEvent" in document) {
127
            var evt = document.createEvent("HTMLEvents");
128
            evt.initEvent("change", false, true);
129
            element.dispatchEvent(evt);
130
        }
131
        else {
132
            element.fireEvent("onchange");
133
        }
134
    }
135

  
136
    //http://jsperf.com/vanilla-css
137
    function css(el, styles, val) {
138
        if (typeof (styles) === 'string') {
139
            var tmp = styles;
140
            styles = {};
141
            styles[tmp] = val;
142
        }
143

  
144
        for (var prop in styles) {
145
            el.style[prop] = styles[prop];
146
        }
147
    }
148

  
149
    function addClass(el, c) {
150
        if (el.classList) {
151
            el.classList.add(c);
152
        }
153
        else {
154
            el.className += ' ' + c;
155
        }
156
    }
157

  
158
    function removeClass(el, c) {
159
        if (el.classList) {
160
            el.classList.remove(c);
161
        }
162
        else {
163
            el.className = el.className.replace(c, '');
164
        }
165
    }
166

  
167
    function setAttributes(el, attrs) {
168
        for (var key in attrs) {
169
            el.setAttribute(key, attrs[key]);
170
        }
171
    }
172

  
173
    function num(v) {
174
        return parseInt(v, 10);
175
    }
176

  
177
    /* Utilities */
178
    function loadImage(src, doExif) {
179
        var img = new Image();
180
        img.style.opacity = '0';
181
        return new Promise(function (resolve, reject) {
182
            function _resolve() {
183
                img.style.opacity = '1';
184
                setTimeout(function () {
185
                    resolve(img);
186
                }, 1);
187
            }
188

  
189
            img.removeAttribute('crossOrigin');
190
            if (src.match(/^https?:\/\/|^\/\//)) {
191
                img.setAttribute('crossOrigin', 'anonymous');
192
            }
193

  
194
            img.onload = function () {
195
                if (doExif) {
196
                    EXIF.getData(img, function () {
197
                        _resolve();
198
                    });
199
                }
200
                else {
201
                    _resolve();
202
                }
203
            };
204
            img.onerror = function (ev) {
205
                img.style.opacity = 1;
206
                setTimeout(function () {
207
                    reject(ev);
208
                }, 1);
209
            };
210
            img.src = src;
211
        });
212
    }
213

  
214
    function naturalImageDimensions(img, ornt) {
215
        var w = img.naturalWidth;
216
        var h = img.naturalHeight;
217
        var orient = ornt || getExifOrientation(img);
218
        if (orient && orient >= 5) {
219
            var x= w;
220
            w = h;
221
            h = x;
222
        }
223
        return { width: w, height: h };
224
    }
225

  
226
    /* CSS Transform Prototype */
227
    var TRANSLATE_OPTS = {
228
        'translate3d': {
229
            suffix: ', 0px'
230
        },
231
        'translate': {
232
            suffix: ''
233
        }
234
    };
235
    var Transform = function (x, y, scale) {
236
        this.x = parseFloat(x);
237
        this.y = parseFloat(y);
238
        this.scale = parseFloat(scale);
239
    };
240

  
241
    Transform.parse = function (v) {
242
        if (v.style) {
243
            return Transform.parse(v.style[CSS_TRANSFORM]);
244
        }
245
        else if (v.indexOf('matrix') > -1 || v.indexOf('none') > -1) {
246
            return Transform.fromMatrix(v);
247
        }
248
        else {
249
            return Transform.fromString(v);
250
        }
251
    };
252

  
253
    Transform.fromMatrix = function (v) {
254
        var vals = v.substring(7).split(',');
255
        if (!vals.length || v === 'none') {
256
            vals = [1, 0, 0, 1, 0, 0];
257
        }
258

  
259
        return new Transform(num(vals[4]), num(vals[5]), parseFloat(vals[0]));
260
    };
261

  
262
    Transform.fromString = function (v) {
263
        var values = v.split(') '),
264
            translate = values[0].substring(Croppie.globals.translate.length + 1).split(','),
265
            scale = values.length > 1 ? values[1].substring(6) : 1,
266
            x = translate.length > 1 ? translate[0] : 0,
267
            y = translate.length > 1 ? translate[1] : 0;
268

  
269
        return new Transform(x, y, scale);
270
    };
271

  
272
    Transform.prototype.toString = function () {
273
        var suffix = TRANSLATE_OPTS[Croppie.globals.translate].suffix || '';
274
        return Croppie.globals.translate + '(' + this.x + 'px, ' + this.y + 'px' + suffix + ') scale(' + this.scale + ')';
275
    };
276

  
277
    var TransformOrigin = function (el) {
278
        if (!el || !el.style[CSS_TRANS_ORG]) {
279
            this.x = 0;
280
            this.y = 0;
281
            return;
282
        }
283
        var css = el.style[CSS_TRANS_ORG].split(' ');
284
        this.x = parseFloat(css[0]);
285
        this.y = parseFloat(css[1]);
286
    };
287

  
288
    TransformOrigin.prototype.toString = function () {
289
        return this.x + 'px ' + this.y + 'px';
290
    };
291

  
292
    function getExifOrientation (img) {
293
        return img.exifdata && img.exifdata.Orientation ? num(img.exifdata.Orientation) : 1;
294
    }
295

  
296
    function drawCanvas(canvas, img, orientation) {
297
        var width = img.width,
298
            height = img.height,
299
            ctx = canvas.getContext('2d');
300

  
301
        canvas.width = img.width;
302
        canvas.height = img.height;
303

  
304
        ctx.save();
305
        switch (orientation) {
306
          case 2:
307
             ctx.translate(width, 0);
308
             ctx.scale(-1, 1);
309
             break;
310

  
311
          case 3:
312
              ctx.translate(width, height);
313
              ctx.rotate(180*Math.PI/180);
314
              break;
315

  
316
          case 4:
317
              ctx.translate(0, height);
318
              ctx.scale(1, -1);
319
              break;
320

  
321
          case 5:
322
              canvas.width = height;
323
              canvas.height = width;
324
              ctx.rotate(90*Math.PI/180);
325
              ctx.scale(1, -1);
326
              break;
327

  
328
          case 6:
329
              canvas.width = height;
330
              canvas.height = width;
331
              ctx.rotate(90*Math.PI/180);
332
              ctx.translate(0, -height);
333
              break;
334

  
335
          case 7:
336
              canvas.width = height;
337
              canvas.height = width;
338
              ctx.rotate(-90*Math.PI/180);
339
              ctx.translate(-width, height);
340
              ctx.scale(1, -1);
341
              break;
342

  
343
          case 8:
344
              canvas.width = height;
345
              canvas.height = width;
346
              ctx.translate(0, width);
347
              ctx.rotate(-90*Math.PI/180);
348
              break;
349
        }
350
        ctx.drawImage(img, 0,0, width, height);
351
        ctx.restore();
352
    }
353

  
354
    /* Private Methods */
355
    function _create() {
356
        var self = this,
357
            contClass = 'croppie-container',
358
            customViewportClass = self.options.viewport.type ? 'cr-vp-' + self.options.viewport.type : null,
359
            boundary, img, viewport, overlay, bw, bh;
360

  
361
        self.options.useCanvas = self.options.enableOrientation || _hasExif.call(self);
362
        // Properties on class
363
        self.data = {};
364
        self.elements = {};
365

  
366
        boundary = self.elements.boundary = document.createElement('div');
367
        viewport = self.elements.viewport = document.createElement('div');
368
        img = self.elements.img = document.createElement('img');
369
        overlay = self.elements.overlay = document.createElement('div');
370

  
371
        if (self.options.useCanvas) {
372
            self.elements.canvas = document.createElement('canvas');
373
            self.elements.preview = self.elements.canvas;
374
        }
375
        else {
376
            self.elements.preview = img;
377
        }
378

  
379
        addClass(boundary, 'cr-boundary');
380
        boundary.setAttribute('aria-dropeffect', 'none');
381
        bw = self.options.boundary.width;
382
        bh = self.options.boundary.height;
383
        css(boundary, {
384
            width: (bw + (isNaN(bw) ? '' : 'px')),
385
            height: (bh + (isNaN(bh) ? '' : 'px'))
386
        });
387

  
388
        addClass(viewport, 'cr-viewport');
389
        if (customViewportClass) {
390
            addClass(viewport, customViewportClass);
391
        }
392
        css(viewport, {
393
            width: self.options.viewport.width + 'px',
394
            height: self.options.viewport.height + 'px'
395
        });
396
        viewport.setAttribute('tabindex', 0);
397

  
398
        addClass(self.elements.preview, 'cr-image');
399
        setAttributes(self.elements.preview, { 'alt': 'preview', 'aria-grabbed': 'false' });
400
        addClass(overlay, 'cr-overlay');
401

  
402
        self.element.appendChild(boundary);
403
        boundary.appendChild(self.elements.preview);
404
        boundary.appendChild(viewport);
405
        boundary.appendChild(overlay);
406

  
407
        addClass(self.element, contClass);
408
        if (self.options.customClass) {
409
            addClass(self.element, self.options.customClass);
410
        }
411

  
412
        _initDraggable.call(this);
413

  
414
        if (self.options.enableZoom) {
415
            _initializeZoom.call(self);
416
        }
417

  
418
        // if (self.options.enableOrientation) {
419
        //     _initRotationControls.call(self);
420
        // }
421

  
422
        if (self.options.enableResize) {
423
            _initializeResize.call(self);
424
        }
425
    }
426

  
427
    // function _initRotationControls () {
428
    //     var self = this,
429
    //         wrap, btnLeft, btnRight, iLeft, iRight;
430

  
431
    //     wrap = document.createElement('div');
432
    //     self.elements.orientationBtnLeft = btnLeft = document.createElement('button');
433
    //     self.elements.orientationBtnRight = btnRight = document.createElement('button');
434

  
435
    //     wrap.appendChild(btnLeft);
436
    //     wrap.appendChild(btnRight);
437

  
438
    //     iLeft = document.createElement('i');
439
    //     iRight = document.createElement('i');
440
    //     btnLeft.appendChild(iLeft);
441
    //     btnRight.appendChild(iRight);
442

  
443
    //     addClass(wrap, 'cr-rotate-controls');
444
    //     addClass(btnLeft, 'cr-rotate-l');
445
    //     addClass(btnRight, 'cr-rotate-r');
446

  
447
    //     self.elements.boundary.appendChild(wrap);
448

  
449
    //     btnLeft.addEventListener('click', function () {
450
    //         self.rotate(-90);
451
    //     });
452
    //     btnRight.addEventListener('click', function () {
453
    //         self.rotate(90);
454
    //     });
455
    // }
456

  
457
    function _hasExif() {
458
        return this.options.enableExif && window.EXIF;
459
    }
460

  
461
    function _initializeResize () {
462
        var self = this;
463
        var wrap = document.createElement('div');
464
        var isDragging = false;
465
        var direction;
466
        var originalX;
467
        var originalY;
468
        var minSize = 50;
469
        var maxWidth;
470
        var maxHeight;
471
        var vr;
472
        var hr;
473

  
474
        addClass(wrap, 'cr-resizer');
475
        css(wrap, {
476
            width: this.options.viewport.width + 'px',
477
            height: this.options.viewport.height + 'px'
478
        });
479

  
480
        if (this.options.resizeControls.height) {
481
            vr = document.createElement('div');
482
            addClass(vr, 'cr-resizer-vertical');
483
            wrap.appendChild(vr);
484
        }
485

  
486
        if (this.options.resizeControls.width) {
487
            hr = document.createElement('div');
488
            addClass(hr, 'cr-resizer-horisontal');
489
            wrap.appendChild(hr);
490
        }
491

  
492
        function mouseDown(ev) {
493
            if (ev.button !== undefined && ev.button !== 0) return;
494

  
495
            ev.preventDefault();
496
            if (isDragging) {
497
                return;
498
            }
499

  
500
            var overlayRect = self.elements.overlay.getBoundingClientRect();
501

  
502
            isDragging = true;
503
            originalX = ev.pageX;
504
            originalY = ev.pageY;
505
            direction = ev.currentTarget.className.indexOf('vertical') !== -1 ? 'v' : 'h';
506
            maxWidth = overlayRect.width;
507
            maxHeight = overlayRect.height;
508

  
509
            if (ev.touches) {
510
                var touches = ev.touches[0];
511
                originalX = touches.pageX;
512
                originalY = touches.pageY;
513
            }
514

  
515
            window.addEventListener('mousemove', mouseMove);
516
            window.addEventListener('touchmove', mouseMove);
517
            window.addEventListener('mouseup', mouseUp);
518
            window.addEventListener('touchend', mouseUp);
519
            document.body.style[CSS_USERSELECT] = 'none';
520
        }
521

  
522
        function mouseMove(ev) {
523
            var pageX = ev.pageX;
524
            var pageY = ev.pageY;
525

  
526
            ev.preventDefault();
527

  
528
            if (ev.touches) {
529
                var touches = ev.touches[0];
530
                pageX = touches.pageX;
531
                pageY = touches.pageY;
532
            }
533

  
534
            var deltaX = pageX - originalX;
535
            var deltaY = pageY - originalY;
536
            var newHeight = self.options.viewport.height + deltaY;
537
            var newWidth = self.options.viewport.width + deltaX;
538

  
539
            if (direction === 'v' && newHeight >= minSize && newHeight <= maxHeight) {
540
                css(wrap, {
541
                    height: newHeight + 'px'
542
                });
543

  
544
                self.options.boundary.height += deltaY;
545
                css(self.elements.boundary, {
546
                    height: self.options.boundary.height + 'px'
547
                });
548

  
549
                self.options.viewport.height += deltaY;
550
                css(self.elements.viewport, {
551
                    height: self.options.viewport.height + 'px'
552
                });
553
            }
554
            else if (direction === 'h' && newWidth >= minSize && newWidth <= maxWidth) {
555
                css(wrap, {
556
                    width: newWidth + 'px'
557
                });
558

  
559
                self.options.boundary.width += deltaX;
560
                css(self.elements.boundary, {
561
                    width: self.options.boundary.width + 'px'
562
                });
563

  
564
                self.options.viewport.width += deltaX;
565
                css(self.elements.viewport, {
566
                    width: self.options.viewport.width + 'px'
567
                });
568
            }
569

  
570
            _updateOverlay.call(self);
571
            _updateZoomLimits.call(self);
572
            _updateCenterPoint.call(self);
573
            _triggerUpdate.call(self);
574
            originalY = pageY;
575
            originalX = pageX;
576
        }
577

  
578
        function mouseUp() {
579
            isDragging = false;
580
            window.removeEventListener('mousemove', mouseMove);
581
            window.removeEventListener('touchmove', mouseMove);
582
            window.removeEventListener('mouseup', mouseUp);
583
            window.removeEventListener('touchend', mouseUp);
584
            document.body.style[CSS_USERSELECT] = '';
585
        }
586

  
587
        if (vr) {
588
            vr.addEventListener('mousedown', mouseDown);
589
            vr.addEventListener('touchstart', mouseDown);
590
        }
591

  
592
        if (hr) {
593
            hr.addEventListener('mousedown', mouseDown);
594
            hr.addEventListener('touchstart', mouseDown);
595
        }
596

  
597
        this.elements.boundary.appendChild(wrap);
598
    }
599

  
600
    function _setZoomerVal(v) {
601
        if (this.options.enableZoom) {
602
            var z = this.elements.zoomer,
603
                val = fix(v, 4);
604

  
605
            z.value = Math.max(parseFloat(z.min), Math.min(parseFloat(z.max), val)).toString();
606
        }
607
    }
608

  
609
    function _initializeZoom() {
610
        var self = this,
611
            wrap = self.elements.zoomerWrap = document.createElement('div'),
612
            zoomer = self.elements.zoomer = document.createElement('input');
613

  
614
        addClass(wrap, 'cr-slider-wrap');
615
        addClass(zoomer, 'cr-slider');
616
        zoomer.type = 'range';
617
        zoomer.step = '0.0001';
618
        zoomer.value = '1';
619
        zoomer.style.display = self.options.showZoomer ? '' : 'none';
620
        zoomer.setAttribute('aria-label', 'zoom');
621

  
622
        self.element.appendChild(wrap);
623
        wrap.appendChild(zoomer);
624

  
625
        self._currentZoom = 1;
626

  
627
        function change() {
628
            _onZoom.call(self, {
629
                value: parseFloat(zoomer.value),
630
                origin: new TransformOrigin(self.elements.preview),
631
                viewportRect: self.elements.viewport.getBoundingClientRect(),
632
                transform: Transform.parse(self.elements.preview)
633
            });
634
        }
635

  
636
        function scroll(ev) {
637
            var delta, targetZoom;
638

  
639
            if(self.options.mouseWheelZoom === 'ctrl' && ev.ctrlKey !== true){
640
              return 0; 
641
            } else if (ev.wheelDelta) {
642
                delta = ev.wheelDelta / 1200; //wheelDelta min: -120 max: 120 // max x 10 x 2
643
            } else if (ev.deltaY) {
644
                delta = ev.deltaY / 1060; //deltaY min: -53 max: 53 // max x 10 x 2
645
            } else if (ev.detail) {
646
                delta = ev.detail / -60; //delta min: -3 max: 3 // max x 10 x 2
647
            } else {
648
                delta = 0;
649
            }
650

  
651
            targetZoom = self._currentZoom + (delta * self._currentZoom);
652

  
653
            ev.preventDefault();
654
            _setZoomerVal.call(self, targetZoom);
655
            change.call(self);
656
        }
657

  
658
        self.elements.zoomer.addEventListener('input', change);// this is being fired twice on keypress
659
        self.elements.zoomer.addEventListener('change', change);
660

  
661
        if (self.options.mouseWheelZoom) {
662
            self.elements.boundary.addEventListener('mousewheel', scroll);
663
            self.elements.boundary.addEventListener('DOMMouseScroll', scroll);
664
        }
665
    }
666

  
667
    function _onZoom(ui) {
668
        var self = this,
669
            transform = ui ? ui.transform : Transform.parse(self.elements.preview),
670
            vpRect = ui ? ui.viewportRect : self.elements.viewport.getBoundingClientRect(),
671
            origin = ui ? ui.origin : new TransformOrigin(self.elements.preview);
672

  
673
        function applyCss() {
674
            var transCss = {};
675
            transCss[CSS_TRANSFORM] = transform.toString();
676
            transCss[CSS_TRANS_ORG] = origin.toString();
677
            css(self.elements.preview, transCss);
678
        }
679

  
680
        self._currentZoom = ui ? ui.value : self._currentZoom;
681
        transform.scale = self._currentZoom;
682
        self.elements.zoomer.setAttribute('aria-valuenow', self._currentZoom);
683
        applyCss();
684

  
685
        if (self.options.enforceBoundary) {
686
            var boundaries = _getVirtualBoundaries.call(self, vpRect),
687
                transBoundaries = boundaries.translate,
688
                oBoundaries = boundaries.origin;
689

  
690
            if (transform.x >= transBoundaries.maxX) {
691
                origin.x = oBoundaries.minX;
692
                transform.x = transBoundaries.maxX;
693
            }
694

  
695
            if (transform.x <= transBoundaries.minX) {
696
                origin.x = oBoundaries.maxX;
697
                transform.x = transBoundaries.minX;
698
            }
699

  
700
            if (transform.y >= transBoundaries.maxY) {
701
                origin.y = oBoundaries.minY;
702
                transform.y = transBoundaries.maxY;
703
            }
704

  
705
            if (transform.y <= transBoundaries.minY) {
706
                origin.y = oBoundaries.maxY;
707
                transform.y = transBoundaries.minY;
708
            }
709
        }
710
        applyCss();
711
        _debouncedOverlay.call(self);
712
        _triggerUpdate.call(self);
713
    }
714

  
715
    function _getVirtualBoundaries(viewport) {
716
        var self = this,
717
            scale = self._currentZoom,
718
            vpWidth = viewport.width,
719
            vpHeight = viewport.height,
720
            centerFromBoundaryX = self.elements.boundary.clientWidth / 2,
721
            centerFromBoundaryY = self.elements.boundary.clientHeight / 2,
722
            imgRect = self.elements.preview.getBoundingClientRect(),
723
            curImgWidth = imgRect.width,
724
            curImgHeight = imgRect.height,
725
            halfWidth = vpWidth / 2,
726
            halfHeight = vpHeight / 2;
727

  
728
        var maxX = ((halfWidth / scale) - centerFromBoundaryX) * -1;
729
        var minX = maxX - ((curImgWidth * (1 / scale)) - (vpWidth * (1 / scale)));
730

  
731
        var maxY = ((halfHeight / scale) - centerFromBoundaryY) * -1;
732
        var minY = maxY - ((curImgHeight * (1 / scale)) - (vpHeight * (1 / scale)));
733

  
734
        var originMinX = (1 / scale) * halfWidth;
735
        var originMaxX = (curImgWidth * (1 / scale)) - originMinX;
736

  
737
        var originMinY = (1 / scale) * halfHeight;
738
        var originMaxY = (curImgHeight * (1 / scale)) - originMinY;
739

  
740
        return {
741
            translate: {
742
                maxX: maxX,
743
                minX: minX,
744
                maxY: maxY,
745
                minY: minY
746
            },
747
            origin: {
748
                maxX: originMaxX,
749
                minX: originMinX,
750
                maxY: originMaxY,
751
                minY: originMinY
752
            }
753
        };
754
    }
755

  
756
    function _updateCenterPoint(rotate) {
757
        var self = this,
758
            scale = self._currentZoom,
759
            data = self.elements.preview.getBoundingClientRect(),
760
            vpData = self.elements.viewport.getBoundingClientRect(),
761
            transform = Transform.parse(self.elements.preview.style[CSS_TRANSFORM]),
762
            pc = new TransformOrigin(self.elements.preview),
763
            top = (vpData.top - data.top) + (vpData.height / 2),
764
            left = (vpData.left - data.left) + (vpData.width / 2),
765
            center = {},
766
            adj = {};
767

  
768
        if (rotate) {
769
            var cx = pc.x;
770
            var cy = pc.y;
771
            var tx = transform.x;
772
            var ty = transform.y;
773

  
774
            center.y = cx;
775
            center.x = cy;
776
            transform.y = tx;
777
            transform.x = ty;
778
        }
779
        else {
780
            center.y = top / scale;
781
            center.x = left / scale;
782

  
783
            adj.y = (center.y - pc.y) * (1 - scale);
784
            adj.x = (center.x - pc.x) * (1 - scale);
785

  
786
            transform.x -= adj.x;
787
            transform.y -= adj.y;
788
        }
789

  
790
        var newCss = {};
791
        newCss[CSS_TRANS_ORG] = center.x + 'px ' + center.y + 'px';
792
        newCss[CSS_TRANSFORM] = transform.toString();
793
        css(self.elements.preview, newCss);
794
    }
795

  
796
    function _initDraggable() {
797
        var self = this,
798
            isDragging = false,
799
            originalX,
800
            originalY,
801
            originalDistance,
802
            vpRect,
803
            transform;
804

  
805
        function assignTransformCoordinates(deltaX, deltaY) {
806
            var imgRect = self.elements.preview.getBoundingClientRect(),
807
                top = transform.y + deltaY,
808
                left = transform.x + deltaX;
809

  
810
            if (self.options.enforceBoundary) {
811
                if (vpRect.top > imgRect.top + deltaY && vpRect.bottom < imgRect.bottom + deltaY) {
812
                    transform.y = top;
813
                }
814

  
815
                if (vpRect.left > imgRect.left + deltaX && vpRect.right < imgRect.right + deltaX) {
816
                    transform.x = left;
817
                }
818
            }
819
            else {
820
                transform.y = top;
821
                transform.x = left;
822
            }
823
        }
824

  
825
        function toggleGrabState(isDragging) {
826
          self.elements.preview.setAttribute('aria-grabbed', isDragging);
827
          self.elements.boundary.setAttribute('aria-dropeffect', isDragging? 'move': 'none');
828
        }
829

  
830
        function keyDown(ev) {
831
            var LEFT_ARROW  = 37,
832
                UP_ARROW    = 38,
833
                RIGHT_ARROW = 39,
834
                DOWN_ARROW  = 40;
835

  
836
            if (ev.shiftKey && (ev.keyCode === UP_ARROW || ev.keyCode === DOWN_ARROW)) {
837
                var zoom;
838
                if (ev.keyCode === UP_ARROW) {
839
                    zoom = parseFloat(self.elements.zoomer.value) + parseFloat(self.elements.zoomer.step)
840
                }
841
                else {
842
                    zoom = parseFloat(self.elements.zoomer.value) - parseFloat(self.elements.zoomer.step)
843
                }
844
                self.setZoom(zoom);
845
            }
846
            else if (self.options.enableKeyMovement && (ev.keyCode >= 37 && ev.keyCode <= 40)) {
847
                ev.preventDefault();
848
                var movement = parseKeyDown(ev.keyCode);
849

  
850
                transform = Transform.parse(self.elements.preview);
851
                document.body.style[CSS_USERSELECT] = 'none';
852
                vpRect = self.elements.viewport.getBoundingClientRect();
853
                keyMove(movement);
854
            }
855

  
856
            function parseKeyDown(key) {
857
                switch (key) {
858
                    case LEFT_ARROW:
859
                        return [1, 0];
860
                    case UP_ARROW:
861
                        return [0, 1];
862
                    case RIGHT_ARROW:
863
                        return [-1, 0];
864
                    case DOWN_ARROW:
865
                        return [0, -1];
866
                }
867
            }
868
        }
869

  
870
        function keyMove(movement) {
871
            var deltaX = movement[0],
872
                deltaY = movement[1],
873
                newCss = {};
874

  
875
            assignTransformCoordinates(deltaX, deltaY);
876

  
877
            newCss[CSS_TRANSFORM] = transform.toString();
878
            css(self.elements.preview, newCss);
879
            _updateOverlay.call(self);
880
            document.body.style[CSS_USERSELECT] = '';
881
            _updateCenterPoint.call(self);
882
            _triggerUpdate.call(self);
883
            originalDistance = 0;
884
        }
885

  
886
        function mouseDown(ev) {
887
            if (ev.button !== undefined && ev.button !== 0) return;
888

  
889
            ev.preventDefault();
890
            if (isDragging) return;
891
            isDragging = true;
892
            originalX = ev.pageX;
893
            originalY = ev.pageY;
894

  
895
            if (ev.touches) {
896
                var touches = ev.touches[0];
897
                originalX = touches.pageX;
898
                originalY = touches.pageY;
899
            }
900
            toggleGrabState(isDragging);
901
            transform = Transform.parse(self.elements.preview);
902
            window.addEventListener('mousemove', mouseMove);
903
            window.addEventListener('touchmove', mouseMove);
904
            window.addEventListener('mouseup', mouseUp);
905
            window.addEventListener('touchend', mouseUp);
906
            document.body.style[CSS_USERSELECT] = 'none';
907
            vpRect = self.elements.viewport.getBoundingClientRect();
908
        }
909

  
910
        function mouseMove(ev) {
911
            ev.preventDefault();
912
            var pageX = ev.pageX,
913
                pageY = ev.pageY;
914

  
915
            if (ev.touches) {
916
                var touches = ev.touches[0];
917
                pageX = touches.pageX;
918
                pageY = touches.pageY;
919
            }
920

  
921
            var deltaX = pageX - originalX,
922
                deltaY = pageY - originalY,
923
                newCss = {};
924

  
925
            if (ev.type === 'touchmove') {
926
                if (ev.touches.length > 1) {
927
                    var touch1 = ev.touches[0];
928
                    var touch2 = ev.touches[1];
929
                    var dist = Math.sqrt((touch1.pageX - touch2.pageX) * (touch1.pageX - touch2.pageX) + (touch1.pageY - touch2.pageY) * (touch1.pageY - touch2.pageY));
930

  
931
                    if (!originalDistance) {
932
                        originalDistance = dist / self._currentZoom;
933
                    }
934

  
935
                    var scale = dist / originalDistance;
936

  
937
                    _setZoomerVal.call(self, scale);
938
                    dispatchChange(self.elements.zoomer);
939
                    return;
940
                }
941
            }
942

  
943
            assignTransformCoordinates(deltaX, deltaY);
944

  
945
            newCss[CSS_TRANSFORM] = transform.toString();
946
            css(self.elements.preview, newCss);
947
            _updateOverlay.call(self);
948
            originalY = pageY;
949
            originalX = pageX;
950
        }
951

  
952
        function mouseUp() {
953
            isDragging = false;
954
            toggleGrabState(isDragging);
955
            window.removeEventListener('mousemove', mouseMove);
956
            window.removeEventListener('touchmove', mouseMove);
957
            window.removeEventListener('mouseup', mouseUp);
958
            window.removeEventListener('touchend', mouseUp);
959
            document.body.style[CSS_USERSELECT] = '';
960
            _updateCenterPoint.call(self);
961
            _triggerUpdate.call(self);
962
            originalDistance = 0;
963
        }
964

  
965
        self.elements.overlay.addEventListener('mousedown', mouseDown);
966
        self.elements.viewport.addEventListener('keydown', keyDown);
967
        self.elements.overlay.addEventListener('touchstart', mouseDown);
968
    }
969

  
970
    function _updateOverlay() {
971
        if (!this.elements) return; // since this is debounced, it can be fired after destroy
972
        var self = this,
973
            boundRect = self.elements.boundary.getBoundingClientRect(),
974
            imgData = self.elements.preview.getBoundingClientRect();
975

  
976
        css(self.elements.overlay, {
977
            width: imgData.width + 'px',
978
            height: imgData.height + 'px',
979
            top: (imgData.top - boundRect.top) + 'px',
980
            left: (imgData.left - boundRect.left) + 'px'
981
        });
982
    }
983
    var _debouncedOverlay = debounce(_updateOverlay, 500);
984

  
985
    function _triggerUpdate() {
986
        var self = this,
987
            data = self.get();
988

  
989
        if (!_isVisible.call(self)) {
990
            return;
991
        }
992

  
993
        self.options.update.call(self, data);
994
        if (self.$ && typeof Prototype === 'undefined') {
995
            self.$(self.element).trigger('update.croppie', data);
996
        }
997
        else {
998
            var ev;
999
            if (window.CustomEvent) {
1000
                ev = new CustomEvent('update', { detail: data });
1001
            } else {
1002
                ev = document.createEvent('CustomEvent');
1003
                ev.initCustomEvent('update', true, true, data);
1004
            }
1005

  
1006
            self.element.dispatchEvent(ev);
1007
        }
1008
    }
1009

  
1010
    function _isVisible() {
1011
        return this.elements.preview.offsetHeight > 0 && this.elements.preview.offsetWidth > 0;
1012
    }
1013

  
1014
    function _updatePropertiesFromImage() {
1015
        var self = this,
1016
            initialZoom = 1,
1017
            cssReset = {},
1018
            img = self.elements.preview,
1019
            imgData,
1020
            transformReset = new Transform(0, 0, initialZoom),
1021
            originReset = new TransformOrigin(),
1022
            isVisible = _isVisible.call(self);
1023

  
1024
        if (!isVisible || self.data.bound) {// if the croppie isn't visible or it doesn't need binding
1025
            return;
1026
        }
1027

  
1028
        self.data.bound = true;
1029
        cssReset[CSS_TRANSFORM] = transformReset.toString();
1030
        cssReset[CSS_TRANS_ORG] = originReset.toString();
1031
        cssReset['opacity'] = 1;
1032
        css(img, cssReset);
1033

  
1034
        imgData = self.elements.preview.getBoundingClientRect();
1035

  
1036
        self._originalImageWidth = imgData.width;
1037
        self._originalImageHeight = imgData.height;
1038
        self.data.orientation = getExifOrientation(self.elements.img);
1039

  
1040
        if (self.options.enableZoom) {
1041
            _updateZoomLimits.call(self, true);
1042
        }
1043
        else {
1044
            self._currentZoom = initialZoom;
1045
        }
1046

  
1047
        transformReset.scale = self._currentZoom;
1048
        cssReset[CSS_TRANSFORM] = transformReset.toString();
1049
        css(img, cssReset);
1050

  
1051
        if (self.data.points.length) {
1052
            _bindPoints.call(self, self.data.points);
1053
        }
1054
        else {
1055
            _centerImage.call(self);
1056
        }
1057

  
1058
        _updateCenterPoint.call(self);
1059
        _updateOverlay.call(self);
1060
    }
1061

  
1062
    function _updateZoomLimits (initial) {
1063
        var self = this,
1064
            minZoom = Math.max(self.options.minZoom, 0) || 0,
1065
            maxZoom = self.options.maxZoom || 1.5,
1066
            initialZoom,
1067
            defaultInitialZoom,
1068
            zoomer = self.elements.zoomer,
1069
            scale = parseFloat(zoomer.value),
1070
            boundaryData = self.elements.boundary.getBoundingClientRect(),
1071
            imgData = naturalImageDimensions(self.elements.img, self.data.orientation),
1072
            vpData = self.elements.viewport.getBoundingClientRect(),
1073
            minW,
1074
            minH;
1075
        if (self.options.enforceBoundary) {
1076
            minW = vpData.width / imgData.width;
1077
            minH = vpData.height / imgData.height;
1078
            minZoom = Math.max(minW, minH);
1079
        }
1080

  
1081
        if (minZoom >= maxZoom) {
1082
            maxZoom = minZoom + 1;
1083
        }
1084

  
1085
        zoomer.min = fix(minZoom, 4);
1086
        zoomer.max = fix(maxZoom, 4);
1087
        
1088
        if (!initial && (scale < zoomer.min || scale > zoomer.max)) {
1089
            _setZoomerVal.call(self, scale < zoomer.min ? zoomer.min : zoomer.max);
1090
        }
1091
        else if (initial) {
1092
            defaultInitialZoom = Math.max((boundaryData.width / imgData.width), (boundaryData.height / imgData.height));
1093
            initialZoom = self.data.boundZoom !== null ? self.data.boundZoom : defaultInitialZoom;
1094
            _setZoomerVal.call(self, initialZoom);
1095
        }
1096

  
1097
        dispatchChange(zoomer);
1098
    }
1099

  
1100
    function _bindPoints(points) {
1101
        if (points.length !== 4) {
1102
            throw "Croppie - Invalid number of points supplied: " + points;
1103
        }
1104
        var self = this,
1105
            pointsWidth = points[2] - points[0],
1106
            // pointsHeight = points[3] - points[1],
1107
            vpData = self.elements.viewport.getBoundingClientRect(),
1108
            boundRect = self.elements.boundary.getBoundingClientRect(),
1109
            vpOffset = {
1110
                left: vpData.left - boundRect.left,
1111
                top: vpData.top - boundRect.top
1112
            },
1113
            scale = vpData.width / pointsWidth,
1114
            originTop = points[1],
1115
            originLeft = points[0],
1116
            transformTop = (-1 * points[1]) + vpOffset.top,
1117
            transformLeft = (-1 * points[0]) + vpOffset.left,
1118
            newCss = {};
1119

  
1120
        newCss[CSS_TRANS_ORG] = originLeft + 'px ' + originTop + 'px';
1121
        newCss[CSS_TRANSFORM] = new Transform(transformLeft, transformTop, scale).toString();
1122
        css(self.elements.preview, newCss);
1123

  
1124
        _setZoomerVal.call(self, scale);
1125
        self._currentZoom = scale;
1126
    }
1127

  
1128
    function _centerImage() {
1129
        var self = this,
1130
            imgDim = self.elements.preview.getBoundingClientRect(),
1131
            vpDim = self.elements.viewport.getBoundingClientRect(),
1132
            boundDim = self.elements.boundary.getBoundingClientRect(),
1133
            vpLeft = vpDim.left - boundDim.left,
1134
            vpTop = vpDim.top - boundDim.top,
1135
            w = vpLeft - ((imgDim.width - vpDim.width) / 2),
1136
            h = vpTop - ((imgDim.height - vpDim.height) / 2),
1137
            transform = new Transform(w, h, self._currentZoom);
1138

  
1139
        css(self.elements.preview, CSS_TRANSFORM, transform.toString());
1140
    }
1141

  
1142
    function _transferImageToCanvas(customOrientation) {
1143
        var self = this,
1144
            canvas = self.elements.canvas,
1145
            img = self.elements.img,
1146
            ctx = canvas.getContext('2d');
1147

  
1148
        ctx.clearRect(0, 0, canvas.width, canvas.height);
1149
        canvas.width = img.width;
1150
        canvas.height = img.height;
1151

  
1152
        var orientation = self.options.enableOrientation && customOrientation || getExifOrientation(img);
1153
        drawCanvas(canvas, img, orientation);
1154
    }
1155

  
1156
    function _getCanvas(data) {
1157
        var self = this,
1158
            points = data.points,
1159
            left = num(points[0]),
1160
            top = num(points[1]),
1161
            right = num(points[2]),
1162
            bottom = num(points[3]),
1163
            width = right-left,
1164
            height = bottom-top,
1165
            circle = data.circle,
1166
            canvas = document.createElement('canvas'),
1167
            ctx = canvas.getContext('2d'),
1168
            startX = 0,
1169
            startY = 0,
1170
            canvasWidth = data.outputWidth || width,
1171
            canvasHeight = data.outputHeight || height;
1172

  
1173
        canvas.width = canvasWidth;
1174
        canvas.height = canvasHeight;
1175

  
1176
        if (data.backgroundColor) {
1177
            ctx.fillStyle = data.backgroundColor;
1178
            ctx.fillRect(0, 0, canvasWidth, canvasHeight);
1179
        }
1180

  
1181
        // By default assume we're going to draw the entire
1182
        // source image onto the destination canvas.
1183
        sx = left;
1184
        sy = top;
1185
        sWidth = width;
1186
        sHeight = height;
1187
        dx = 0;
1188
        dy = 0;
1189
        dWidth = canvasWidth;
1190
        dHeight = canvasHeight;
1191

  
1192
        //
1193
        // Do not go outside of the original image's bounds along the x-axis.
1194
        // Handle translations when projecting onto the destination canvas.
1195
        //
1196

  
1197
        // The smallest possible source x-position is 0.
1198
        if (left < 0) {
1199
            sx = 0;
1200
            dx = (Math.abs(left) / width) * canvasWidth;
1201
        }
1202

  
1203
        // The largest possible source width is the original image's width.
1204
        if (sWidth + sx > self._originalImageWidth) {
1205
            sWidth = self._originalImageWidth - sx;
1206
            dWidth =  (sWidth / width) * canvasWidth;
1207
        }
1208

  
1209
        //
1210
        // Do not go outside of the original image's bounds along the y-axis.
1211
        //
1212

  
1213
        // The smallest possible source y-position is 0.
1214
        if (top < 0) {
1215
            sy = 0;
1216
            dy = (Math.abs(top) / height) * canvasHeight;
1217
        }
1218

  
1219
        // The largest possible source height is the original image's height.
1220
        if (sHeight + sy > self._originalImageHeight) {
1221
            sHeight = self._originalImageHeight - sy;
1222
            dHeight = (sHeight / height) * canvasHeight;
1223
        }
1224

  
1225
        // console.table({ left, right, top, bottom, canvasWidth, canvasHeight, width, height, startX, startY, circle, sx, sy, dx, dy, sWidth, sHeight, dWidth, dHeight });
1226

  
1227
        ctx.drawImage(this.elements.preview, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
1228
        if (circle) {
1229
            ctx.fillStyle = '#fff';
1230
            ctx.globalCompositeOperation = 'destination-in';
1231
            ctx.beginPath();
1232
            ctx.arc(canvas.width / 2, canvas.height / 2, canvas.width / 2, 0, Math.PI * 2, true);
1233
            ctx.closePath();
1234
            ctx.fill();
1235
        }
1236
        return canvas;
1237
    }
1238

  
1239
    function _getHtmlResult(data) {
1240
        var points = data.points,
1241
            div = document.createElement('div'),
1242
            img = document.createElement('img'),
1243
            width = points[2] - points[0],
1244
            height = points[3] - points[1];
1245

  
1246
        addClass(div, 'croppie-result');
1247
        div.appendChild(img);
1248
        css(img, {
1249
            left: (-1 * points[0]) + 'px',
1250
            top: (-1 * points[1]) + 'px'
1251
        });
1252
        img.src = data.url;
1253
        css(div, {
1254
            width: width + 'px',
1255
            height: height + 'px'
1256
        });
1257

  
1258
        return div;
1259
    }
1260

  
1261
    function _getBase64Result(data) {
1262
        return _getCanvas.call(this, data).toDataURL(data.format, data.quality);
1263
    }
1264

  
1265
    function _getBlobResult(data) {
1266
        var self = this;
1267
        return new Promise(function (resolve) {
1268
            _getCanvas.call(self, data).toBlob(function (blob) {
1269
                resolve(blob);
1270
            }, data.format, data.quality);
1271
        });
1272
    }
1273

  
1274
    function _replaceImage(img) {
1275
        if (this.elements.img.parentNode) {
1276
            Array.prototype.forEach.call(this.elements.img.classList, function(c) { img.classList.add(c); });
1277
            this.elements.img.parentNode.replaceChild(img, this.elements.img);
1278
            this.elements.preview = img; // if the img is attached to the DOM, they're not using the canvas
1279
        }
1280
        this.elements.img = img;
1281
    }
1282

  
1283
    function _bind(options, cb) {
1284
        var self = this,
1285
            url,
1286
            points = [],
1287
            zoom = null,
1288
            hasExif = _hasExif.call(self);
1289

  
1290
        if (typeof (options) === 'string') {
1291
            url = options;
1292
            options = {};
1293
        }
1294
        else if (Array.isArray(options)) {
1295
            points = options.slice();
1296
        }
1297
        else if (typeof (options) === 'undefined' && self.data.url) { //refreshing
1298
            _updatePropertiesFromImage.call(self);
1299
            _triggerUpdate.call(self);
1300
            return null;
1301
        }
1302
        else {
1303
            url = options.url;
1304
            points = options.points || [];
1305
            zoom = typeof(options.zoom) === 'undefined' ? null : options.zoom;
1306
        }
1307

  
1308
        self.data.bound = false;
1309
        self.data.url = url || self.data.url;
1310
        self.data.boundZoom = zoom;
1311

  
1312
        return loadImage(url, hasExif).then(function (img) {
1313
            _replaceImage.call(self, img);
1314
            if (!points.length) {
1315
                var natDim = naturalImageDimensions(img);
1316
                var rect = self.elements.viewport.getBoundingClientRect();
1317
                var aspectRatio = rect.width / rect.height;
1318
                var imgAspectRatio = natDim.width / natDim.height;
1319
                var width, height;
1320

  
1321
                if (imgAspectRatio > aspectRatio) {
1322
                    height = natDim.height;
1323
                    width = height * aspectRatio;
1324
                }
1325
                else {
1326
                    width = natDim.width;
1327
                    height = natDim.height / aspectRatio;
1328
                }
1329

  
1330
                var x0 = (natDim.width - width) / 2;
1331
                var y0 = (natDim.height - height) / 2;
1332
                var x1 = x0 + width;
1333
                var y1 = y0 + height;
1334
                self.data.points = [x0, y0, x1, y1];
1335
            }
1336
            else if (self.options.relative) {
1337
                points = [
1338
                    points[0] * img.naturalWidth / 100,
1339
                    points[1] * img.naturalHeight / 100,
1340
                    points[2] * img.naturalWidth / 100,
1341
                    points[3] * img.naturalHeight / 100
1342
                ];
1343
            }
1344

  
1345
            self.data.points = points.map(function (p) {
1346
                return parseFloat(p);
1347
            });
1348
            if (self.options.useCanvas) {
1349
                _transferImageToCanvas.call(self, options.orientation);
1350
            }
1351
            _updatePropertiesFromImage.call(self);
1352
            _triggerUpdate.call(self);
1353
            cb && cb();
1354
        });
1355
    }
1356

  
1357
    function fix(v, decimalPoints) {
1358
        return parseFloat(v).toFixed(decimalPoints || 0);
1359
    }
1360

  
1361
    function _get() {
1362
        var self = this,
1363
            imgData = self.elements.preview.getBoundingClientRect(),
1364
            vpData = self.elements.viewport.getBoundingClientRect(),
1365
            x1 = vpData.left - imgData.left,
1366
            y1 = vpData.top - imgData.top,
1367
            widthDiff = (vpData.width - self.elements.viewport.offsetWidth) / 2, //border
1368
            heightDiff = (vpData.height - self.elements.viewport.offsetHeight) / 2,
1369
            x2 = x1 + self.elements.viewport.offsetWidth + widthDiff,
1370
            y2 = y1 + self.elements.viewport.offsetHeight + heightDiff,
1371
            scale = self._currentZoom;
1372

  
1373
        if (scale === Infinity || isNaN(scale)) {
1374
            scale = 1;
1375
        }
1376

  
1377
        var max = self.options.enforceBoundary ? 0 : Number.NEGATIVE_INFINITY;
1378
        x1 = Math.max(max, x1 / scale);
1379
        y1 = Math.max(max, y1 / scale);
1380
        x2 = Math.max(max, x2 / scale);
1381
        y2 = Math.max(max, y2 / scale);
1382

  
1383
        return {
1384
            points: [fix(x1), fix(y1), fix(x2), fix(y2)],
1385
            zoom: scale,
1386
            orientation: self.data.orientation
1387
        };
1388
    }
1389

  
1390
    var RESULT_DEFAULTS = {
1391
            type: 'canvas',
1392
            format: 'png',
1393
            quality: 1
1394
        },
1395
        RESULT_FORMATS = ['jpeg', 'webp', 'png'];
1396

  
1397
    function _result(options) {
1398
        var self = this,
1399
            data = _get.call(self),
1400
            opts = deepExtend(clone(RESULT_DEFAULTS), clone(options)),
1401
            resultType = (typeof (options) === 'string' ? options : (opts.type || 'base64')),
1402
            size = opts.size || 'viewport',
1403
            format = opts.format,
1404
            quality = opts.quality,
1405
            backgroundColor = opts.backgroundColor,
1406
            circle = typeof opts.circle === 'boolean' ? opts.circle : (self.options.viewport.type === 'circle'),
1407
            vpRect = self.elements.viewport.getBoundingClientRect(),
1408
            ratio = vpRect.width / vpRect.height,
1409
            prom;
1410

  
1411
        if (size === 'viewport') {
1412
            data.outputWidth = vpRect.width;
1413
            data.outputHeight = vpRect.height;
1414
        } else if (typeof size === 'object') {
1415
            if (size.width && size.height) {
1416
                data.outputWidth = size.width;
1417
                data.outputHeight = size.height;
1418
            } else if (size.width) {
1419
                data.outputWidth = size.width;
1420
                data.outputHeight = size.width / ratio;
1421
            } else if (size.height) {
1422
                data.outputWidth = size.height * ratio;
1423
                data.outputHeight = size.height;
1424
            }
1425
        }
1426

  
1427
        if (RESULT_FORMATS.indexOf(format) > -1) {
1428
            data.format = 'image/' + format;
1429
            data.quality = quality;
1430
        }
1431

  
1432
        data.circle = circle;
1433
        data.url = self.data.url;
1434
        data.backgroundColor = backgroundColor;
1435

  
1436
        prom = new Promise(function (resolve) {
1437
            switch(resultType.toLowerCase())
1438
            {
1439
                case 'rawcanvas':
1440
                    resolve(_getCanvas.call(self, data));
1441
                    break;
1442
                case 'canvas':
1443
                case 'base64':
1444
                    resolve(_getBase64Result.call(self, data));
1445
                    break;
1446
                case 'blob':
1447
                    _getBlobResult.call(self, data).then(resolve);
1448
                    break;
1449
                default:
1450
                    resolve(_getHtmlResult.call(self, data));
1451
                    break;
1452
            }
1453
        });
1454
        return prom;
1455
    }
1456

  
1457
    function _refresh() {
1458
        _updatePropertiesFromImage.call(this);
1459
    }
1460

  
1461
    function _rotate(deg) {
1462
        if (!this.options.useCanvas || !this.options.enableOrientation) {
1463
            throw 'Croppie: Cannot rotate without enableOrientation && EXIF.js included';
1464
        }
1465

  
1466
        var self = this,
1467
            canvas = self.elements.canvas;
1468

  
1469
        self.data.orientation = getExifOffset(self.data.orientation, deg);
1470
        drawCanvas(canvas, self.elements.img, self.data.orientation);
1471
        _updateCenterPoint.call(self, true);
1472
        _updateZoomLimits.call(self);
1473
    }
1474

  
1475
    function _destroy() {
1476
        var self = this;
1477
        self.element.removeChild(self.elements.boundary);
1478
        removeClass(self.element, 'croppie-container');
1479
        if (self.options.enableZoom) {
1480
            self.element.removeChild(self.elements.zoomerWrap);
1481
        }
1482
        delete self.elements;
1483
    }
1484

  
1485
    if (window.jQuery) {
1486
        var $ = window.jQuery;
1487
        $.fn.croppie = function (opts) {
1488
            var ot = typeof opts;
1489

  
1490
            if (ot === 'string') {
1491
                var args = Array.prototype.slice.call(arguments, 1);
1492
                var singleInst = $(this).data('croppie');
1493

  
1494
                if (opts === 'get') {
1495
                    return singleInst.get();
1496
                }
1497
                else if (opts === 'result') {
1498
                    return singleInst.result.apply(singleInst, args);
1499
                }
1500
                else if (opts === 'bind') {
1501
                    return singleInst.bind.apply(singleInst, args);
1502
                }
1503

  
1504
                return this.each(function () {
1505
                    var i = $(this).data('croppie');
1506
                    if (!i) return;
1507

  
1508
                    var method = i[opts];
1509
                    if ($.isFunction(method)) {
1510
                        method.apply(i, args);
1511
                        if (opts === 'destroy') {
1512
                            $(this).removeData('croppie');
1513
                        }
1514
                    }
1515
                    else {
1516
                        throw 'Croppie ' + opts + ' method not found';
1517
                    }
1518
                });
1519
            }
1520
            else {
1521
                return this.each(function () {
1522
                    var i = new Croppie(this, opts);
1523
                    i.$ = $;
1524
                    $(this).data('croppie', i);
1525
                });
1526
            }
1527
        };
1528
    }
1529

  
1530
    function Croppie(element, opts) {
1531
        if (element.className.indexOf('croppie-container') > -1) {
1532
            throw new Error("Croppie: Can't initialize croppie more than once");
1533
        }
1534
        this.element = element;
1535
        this.options = deepExtend(clone(Croppie.defaults), opts);
1536

  
1537
        if (this.element.tagName.toLowerCase() === 'img') {
1538
            var origImage = this.element;
1539
            addClass(origImage, 'cr-original-image');
1540
            setAttributes(origImage, {'aria-hidden' : 'true', 'alt' : '' });
1541
            var replacementDiv = document.createElement('div');
1542
            this.element.parentNode.appendChild(replacementDiv);
1543
            replacementDiv.appendChild(origImage);
1544
            this.element = replacementDiv;
1545
            this.options.url = this.options.url || origImage.src;
1546
        }
1547

  
1548
        _create.call(this);
1549
        if (this.options.url) {
1550
            var bindOpts = {
1551
                url: this.options.url,
1552
                points: this.options.points
1553
            };
1554
            delete this.options['url'];
1555
            delete this.options['points'];
1556
            _bind.call(this, bindOpts);
1557
        }
1558
    }
1559

  
1560
    Croppie.defaults = {
1561
        viewport: {
1562
            width: 100,
1563
            height: 100,
1564
            type: 'square'
1565
        },
1566
        boundary: { },
1567
        orientationControls: {
1568
            enabled: true,
1569
            leftClass: '',
1570
            rightClass: ''
1571
        },
1572
        resizeControls: {
1573
            width: true,
1574
            height: true
1575
        },
1576
        customClass: '',
1577
        showZoomer: true,
1578
        enableZoom: true,
1579
        enableResize: false,
1580
        mouseWheelZoom: true,
1581
        enableExif: false,
1582
        enforceBoundary: true,
1583
        enableOrientation: false,
1584
        enableKeyMovement: true,
1585
        update: function () { }
1586
    };
1587

  
1588
    Croppie.globals = {
1589
        translate: 'translate3d'
1590
    };
1591

  
1592
    deepExtend(Croppie.prototype, {
1593
        bind: function (options, cb) {
1594
            return _bind.call(this, options, cb);
1595
        },
1596
        get: function () {
1597
            var data = _get.call(this);
1598
            var points = data.points;
1599
            if (this.options.relative) {
1600
                points[0] /= this.elements.img.naturalWidth / 100;
1601
                points[1] /= this.elements.img.naturalHeight / 100;
1602
                points[2] /= this.elements.img.naturalWidth / 100;
1603
                points[3] /= this.elements.img.naturalHeight / 100;
1604
            }
1605
            return data;
1606
        },
1607
        result: function (type) {
1608
            return _result.call(this, type);
1609
        },
1610
        refresh: function () {
1611
            return _refresh.call(this);
1612
        },
1613
        setZoom: function (v) {
1614
            _setZoomerVal.call(this, v);
1615
            dispatchChange(this.elements.zoomer);
1616
        },
1617
        rotate: function (deg) {
1618
            _rotate.call(this, deg);
1619
        },
1620
        destroy: function () {
1621
            return _destroy.call(this);
1622
        }
1623
    });
1624
    return Croppie;
1625
}));
src/authentic2/templates/authentic2/profile_image_input.html
1
{% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}"><img src="{{ widget.value.url }}"/></a>{% if not widget.required %}
2
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}" />
3
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}<br />
4
{{ widget.input_text }}:{% endif %}
5
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} />
1
{% load i18n %}
2
<div>
3
    <div id="{{ widget.attrs.id }}_preview"></div>
4
    <input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} />
5

  
6
    <button type="button" id="{{ widget.attrs.id}}_load_image_btn">{% trans "Load image" %}</button>
7
{% if widget.is_initial and not widget.required%}
8
        <label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}
9
            <input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}" />
10
        </label>
11
{% endif %}
12
    <script type="text/javascript">
13
        $(function () {
14
            var default_name = "{{ widget.value.name }}";
15
            var base_id = "{{ widget.attrs.id }}";
16
            var initial_url = "{{ widget.value.url }}";
17
            //return a promise that resolves with a File instance
18
            function urltoFile(url, filename, mimeType){
19
                mimeType = mimeType || (url.match(/^data:([^;]+);/)||'')[1];
20
                return (fetch(url)
21
                    .then(function(res){return res.arrayBuffer();})
22
                    .then(function(buf){return new File([buf], filename, {type:mimeType});})
23
                );
24
            }
25
            // Hack to create FileList objects, https://gist.github.com/guest271314/7eac2c21911f5e40f48933ac78e518bd#file-setfilelist-html
26
            class _DataTransfer {
27
                constructor() {
28
                    return new ClipboardEvent("").clipboardData || new DataTransfer();
29
                }
30
            }
31
            var $fileinput = $('#' + base_id);
32
            var $preview = $('#' + base_id + '_preview');
33
            var $checkbox = $('#{{ widget.checkbox_id }}');
34
            var image_size = $fileinput.data('image-size') || 180;
35
            $preview.croppie({
36
                viewport: {
37
                    width: image_size,
38
                    height: image_size,
39
                },
40
                boundary: {
41
                    width: 300,
42
                    height: 300,
43
                },
44
                zoom: 1.0,
45
            });
46
            if (! initial_url) {
47
                $preview.hide();
48
            } else {
49
                $preview.croppie('bind', {
50
                    url: initial_url,
51
                    zoom: 1.0,
52
                });
53
            }
54
            $fileinput.hide();
55
            $('#' + base_id + '_load_image_btn').on('click', function () {
56
                $fileinput.click();
57
            });
58
            $fileinput.on('change', function () {
59
                $checkbox.prop('checked', false);
60
                var file = $fileinput[0].files[0];
61
                var reader = new FileReader();
62
                reader.addEventListener("load", function () {
63
                    $preview.croppie('bind', {
64
                        url: reader.result,
65
                    });
66
                    $preview.show();
67
                });
68
                reader.readAsDataURL(file);
69
            });
70
            $preview.on('update.croppie', function () {
71
                if ($checkbox.prop('checked')) {
72
                    return;
73
                }
74
                $preview.croppie('result', {type: "blob", format: "jpeg"}).then(function (blob) {
75
                    var data_transfer = new _DataTransfer();
76
                    var file = new File([blob], default_name || 'avatar.jpg', {type: 'image/jpeg'});
77
                    data_transfer.items.add(file);
78

  
79
                    $fileinput[0].files = data_transfer.files;
80
                    console.log('update fileinput with file', data_transfer.files[0], 'of length', data_transfer.files[0].size);
81
                });
82
            });
83
            $checkbox.on('change', function (ev) {
84
                if ($checkbox.prop('checked')) {
85
                    $fileinput.val('');
86
                } else {
87
                    $preview.trigger('update.croppie');
88
                }
89
            });
90
        });
91
    </script>
92
</div>
6
-