2
* Image Cropper (v. 1.2.0 - 2006-10-30 )
3
* Copyright (c) 2006 David Spurr (http://www.defusion.org.uk/)
5
* The image cropper provides a way to draw a crop area on an image and capture
6
* the coordinates of the drawn crop area.
9
* - Based on Prototype and Scriptaculous
10
* - Image editing package styling, the crop area functions and looks
11
* like those found in popular image editing software
12
* - Dynamic inclusion of required styles
13
* - Drag to draw areas
14
* - Shift drag to draw/resize areas as squares
15
* - Selection area can be moved
16
* - Seleciton area can be resized using resize handles
17
* - Allows dimension ratio limited crop areas
18
* - Allows minimum dimension crop areas
19
* - Allows maximum dimesion crop areas
20
* - If both min & max dimension options set to the same value for a single axis,then the cropper will not
21
* display the resize handles as appropriate (when min & max dimensions are passed for both axes this
22
* results in a 'fixed size' crop area)
23
* - Allows dynamic preview of resultant crop ( if minimum width & height are provided ), this is
24
* implemented as a subclass so can be excluded when not required
25
* - Movement of selection area by arrow keys ( shift + arrow key will move selection area by
27
* - All operations stay within bounds of image
28
* - All functionality & display compatible with most popular browsers supported by Prototype:
29
* PC: IE 7, 6 & 5.5, Firefox 1.5, Opera 8.5 (see known issues) & 9.0b
30
* MAC: Camino 1.0, Firefox 1.5, Safari 2.0
33
* - Prototype v. 1.5.0_rc0 > (as packaged with Scriptaculous 1.6.1)
34
* - Scriptaculous v. 1.6.1 > modules: builder, dragdrop
37
* - Safari animated gifs, only one of each will animate, this seems to be a known Safari issue
39
* - After drawing an area and then clicking to start a new drag in IE 5.5 the rendered height
40
* appears as the last height until the user drags, this appears to be the related to the error
41
* that the forceReRender() method fixes for IE 6, i.e. IE 5.5 is not redrawing the box properly.
43
* - Lack of CSS opacity support in Opera before version 9 mean we disable those style rules, these
44
* could be fixed by using PNGs with transparency if Opera 8.5 support is high priority for you
46
* - Marching ants keep reloading in IE <6 (not tested in IE7), it is a known issue in IE and I have
47
* found no viable workarounds that can be included in the release. If this really is an issue for you
48
* either try this post: http://mir.aculo.us/articles/2005/08/28/internet-explorer-and-ajax-image-caching-woes
49
* or uncomment the 'FIX MARCHING ANTS IN IE' rules in the CSS file
51
* - Styling & borders on image, any CSS styling applied directly to the image itself (floats, borders, padding, margin, etc.) will
52
* cause problems with the cropper. The use of a wrapper element to apply these styles to is recommended.
54
* - overflow: auto or overflow: scroll on parent will cause cropper to burst out of parent in IE and Opera (maybe Mac browsers too)
55
* I'm not sure why yet.
58
* See Cropper.Img & Cropper.ImgWithPreview for usage details
62
* + Added id to the preview image element using 'imgCrop_[originalImageID]'
63
* * #00001 - Fixed bug: Doesn't account for scroll offsets
64
* * #00009 - Fixed bug: Placing the cropper inside differently positioned elements causes incorrect co-ordinates and display
65
* * #00013 - Fixed bug: I-bar cursor appears on drag plane
66
* * #00014 - Fixed bug: If ID for image tag is not found in document script throws error
67
* * Fixed bug with drag start co-ordinates if wrapper element has moved in browser (e.g. dragged to a new position)
68
* * Fixed bug with drag start co-ordinates if image contained in a wrapper with scrolling - this may be buggy if image
69
* has other ancestors with scrolling applied (except the body)
70
* * #00015 - Fixed bug: When cropper removed and then reapplied onEndCrop callback gets called multiple times, solution suggestion from Bill Smith
71
* * Various speed increases & code cleanup which meant improved performance in Mac - which allowed removal of different overlay methods for
72
* IE and all other browsers, which led to a fix for:
73
* * #00010 - Fixed bug: Select area doesn't adhere to image size when image resized using img attributes
74
* - #00006 - Removed default behaviour of automatically setting a ratio when both min width & height passed, the ratioDimensions must be passed in
75
* + #00005 - Added ability to set maximum crop dimensions, if both min & max set as the same value then we'll get a fixed cropper size on the axes as appropriate
76
* and the resize handles will not be displayed as appropriate
77
* * Switched keydown for keypress for moving select area with cursor keys (makes for nicer action) - doesn't appear to work in Safari
80
* * Fixed wrong cursor on western handle in CSS
81
* + #00008 & #00003 - Added feature: Allow to set dimensions & position for cropper on load
82
* * #00002 - Fixed bug: Pressing 'remove cropper' twice removes image in IE
85
* * Fixed bugs with ratios when GCD is low (patch submitted by Andy Skelton)
88
* * Fixed bug with rendering issues fix in IE 5.5
89
* * Fixed bug with endCrop callback issues once cropper had been removed & reset in IE
92
* * Fixed bug with IE constantly trying to reload select area background image
93
* * Applied more robust fix to Safari & IE rendering issues
94
* + Added method to reset parameters - useful for when dynamically changing img cropper attached to
95
* + Added method to remove cropper from image
101
* Copyright (c) 2006, David Spurr (http://www.defusion.org.uk/)
102
* All rights reserved.
105
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
107
* * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
108
* * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
109
* * Neither the name of the David Spurr nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
111
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
113
* http://www.opensource.org/licenses/bsd-license.php
115
* See scriptaculous.js for full scriptaculous licence
119
* Extend the Draggable class to allow us to pass the rendering
120
* down to the Cropper object.
122
var CropDraggable = Class.create();
124
Object.extend( Object.extend( CropDraggable.prototype, Draggable.prototype), {
126
initialize: function(element) {
127
this.options = Object.extend(
130
* The draw method to defer drawing to
132
drawMethod: function() {}
137
this.element = $(element);
139
this.handle = this.element;
141
this.delta = this.currentDelta();
142
this.dragging = false;
144
this.eventMouseDown = this.initDrag.bindAsEventListener(this);
145
Event.observe(this.handle, "mousedown", this.eventMouseDown);
147
Draggables.register(this);
151
* Defers the drawing of the draggable to the supplied method
153
draw: function(point) {
154
var pos = Position.cumulativeOffset(this.element);
155
var d = this.currentDelta();
159
var p = [0,1].map(function(i) {
160
return (point[i]-pos[i]-this.offset[i])
163
this.options.drawMethod( p );
170
* The Cropper object, this will attach itself to the provided image by wrapping it with
171
* the generated xHTML structure required by the cropper.
174
* @param obj Image element to attach to
175
* @param obj Optional options:
177
* The pixel dimensions to apply as a restrictive ratio, with properties x & y
180
* The minimum width for the select area in pixels
183
* The mimimum height for the select area in pixels
186
* The maximum width for the select areas in pixels (if both minWidth & maxWidth set to same the width of the cropper will be fixed)
189
* The maximum height for the select areas in pixels (if both minHeight & maxHeight set to same the height of the cropper will be fixed)
191
* - displayOnInit int
192
* Whether to display the select area on initialisation, only used when providing minimum width & height or ratio
195
* The callback function to provide the crop details to on end of a crop (see below)
197
* - captureKeys boolean
198
* Whether to capture the keys for moving the select area, as these can cause some problems at the moment
201
* A coordinates object with properties x1, y1, x2 & y2; for the coordinates of the select area to display onload
203
*----------------------------------------------
205
* The callback function provided via the onEndCrop option should accept the following parameters:
207
* The coordinates object with properties x1, y1, x2 & y2; for the coordinates of the select area
210
* The dimensions object with properites width & height; for the dimensions of the select area
214
* function onEndCrop( coords, dimensions ) {
215
* $( 'x1' ).value = coords.x1;
216
* $( 'y1' ).value = coords.y1;
217
* $( 'x2' ).value = coords.x2;
218
* $( 'y2' ).value = coords.y2;
219
* $( 'width' ).value = dimensions.width;
220
* $( 'height' ).value = dimensions.height;
225
Cropper.Img = Class.create();
226
Cropper.Img.prototype = {
229
* Initialises the class
232
* @param obj Image element to attach to
236
initialize: function(element, options) {
237
this.options = Object.extend(
241
* The pixel dimensions to apply as a restrictive ratio
243
ratioDim: { x: 0, y: 0 },
246
* The minimum pixel width, also used as restrictive ratio if min height passed too
251
* The minimum pixel height, also used as restrictive ratio if min width passed too
256
* Whether to display the select area on initialisation, only used when providing minimum width & height or ratio
258
displayOnInit: false,
261
* The call back function to pass the final values to
263
onEndCrop: Prototype.emptyFunction,
266
* Whether to capture key presses or not
270
* @var obj Coordinate object x1, y1, x2, y2
271
* The coordinates to optionally display the select area at onload
276
* The maximum width for the select areas in pixels (if both minWidth & maxWidth set to same the width of the cropper will be fixed)
281
* The maximum height for the select areas in pixels (if both minHeight & maxHeight set to same the height of the cropper will be fixed)
289
* The img node to attach to
291
this.img = $( element );
294
* The x & y coordinates of the click point
296
this.clickCoords = { x: 0, y: 0 };
299
* Whether the user is dragging
301
this.dragging = false;
304
* Whether the user is resizing
306
this.resizing = false;
309
* Whether the user is on a webKit browser
311
this.isWebKit = /Konqueror|Safari|KHTML/.test( navigator.userAgent );
314
* Whether the user is on IE
316
this.isIE = /MSIE/.test( navigator.userAgent );
319
* Whether the user is on Opera below version 9
321
this.isOpera8 = /Opera\s[1-8]/.test( navigator.userAgent );
334
* Whether we've attached sucessfully
336
this.attached = false;
339
* Whether we've got a fixed width (if minWidth EQ or GT maxWidth then we have a fixed width
340
* in the case of minWidth > maxWidth maxWidth wins as the fixed width)
342
this.fixedWidth = ( this.options.maxWidth > 0 && ( this.options.minWidth >= this.options.maxWidth ) );
345
* Whether we've got a fixed height (if minHeight EQ or GT maxHeight then we have a fixed height
346
* in the case of minHeight > maxHeight maxHeight wins as the fixed height)
348
this.fixedHeight = ( this.options.maxHeight > 0 && ( this.options.minHeight >= this.options.maxHeight ) );
350
// quit if the image element doesn't exist
351
if( typeof this.img == 'undefined' ) return;
353
// include the stylesheet
354
$A( document.getElementsByTagName( 'script' ) ).each(
356
if( s.src.match( /cropper\.js/ ) ) {
357
var path = s.src.replace( /cropper\.js(.*)?/, '' );
358
// '<link rel="stylesheet" type="text/css" href="' + path + 'cropper.css" media="screen" />';
359
var style = document.createElement( 'link' );
360
style.rel = 'stylesheet';
361
style.type = 'text/css';
362
style.href = path + 'cropper.css';
363
style.media = 'screen';
364
document.getElementsByTagName( 'head' )[0].appendChild( style );
369
// calculate the ratio when neccessary
370
if( this.options.ratioDim.x > 0 && this.options.ratioDim.y > 0 ) {
371
var gcd = this.getGCD( this.options.ratioDim.x, this.options.ratioDim.y );
372
this.ratioX = this.options.ratioDim.x / gcd;
373
this.ratioY = this.options.ratioDim.y / gcd;
374
// dump( 'RATIO : ' + this.ratioX + ':' + this.ratioY + '\n' );
377
// initialise sub classes
378
this.subInitialize();
380
// only load the event observers etc. once the image is loaded
381
// this is done after the subInitialize() call just in case the sub class does anything
382
// that will affect the result of the call to onLoad()
383
if( this.img.complete || this.isWebKit ) this.onLoad(); // for some reason Safari seems to support img.complete but returns 'undefined' on the this.img object
384
else Event.observe( this.img, 'load', this.onLoad.bindAsEventListener( this) );
388
* The Euclidean algorithm used to find the greatest common divisor
395
getGCD : function( a , b ) {
396
if( b == 0 ) return a;
397
return this.getGCD(b, a % b );
401
* Attaches the cropper to the image once it has loaded
406
onLoad: function( ) {
408
* Build the container and all related elements, will result in the following
410
* <div class="imgCrop_wrap">
411
* <img ... this.img ... />
412
* <div class="imgCrop_dragArea">
413
* <!-- the inner spans are only required for IE to stop it making the divs 1px high/wide -->
414
* <div class="imgCrop_overlay imageCrop_north"><span></span></div>
415
* <div class="imgCrop_overlay imageCrop_east"><span></span></div>
416
* <div class="imgCrop_overlay imageCrop_south"><span></span></div>
417
* <div class="imgCrop_overlay imageCrop_west"><span></span></div>
418
* <div class="imgCrop_selArea">
420
* <!-- the inner spans are only required for IE to stop it making the divs 1px high/wide -->
421
* <div class="imgCrop_marqueeHoriz imgCrop_marqueeNorth"><span></span></div>
422
* <div class="imgCrop_marqueeVert imgCrop_marqueeEast"><span></span></div>
423
* <div class="imgCrop_marqueeHoriz imgCrop_marqueeSouth"><span></span></div>
424
* <div class="imgCrop_marqueeVert imgCrop_marqueeWest"><span></span></div>
426
* <div class="imgCrop_handle imgCrop_handleN"></div>
427
* <div class="imgCrop_handle imgCrop_handleNE"></div>
428
* <div class="imgCrop_handle imgCrop_handleE"></div>
429
* <div class="imgCrop_handle imgCrop_handleSE"></div>
430
* <div class="imgCrop_handle imgCrop_handleS"></div>
431
* <div class="imgCrop_handle imgCrop_handleSW"></div>
432
* <div class="imgCrop_handle imgCrop_handleW"></div>
433
* <div class="imgCrop_handle imgCrop_handleNW"></div>
434
* <div class="imgCrop_clickArea"></div>
436
* <div class="imgCrop_clickArea"></div>
440
var cNamePrefix = 'imgCrop_';
442
// get the point to insert the container
443
var insertPoint = this.img.parentNode;
445
// apply an extra class to the wrapper to fix Opera below version 9
446
var fixOperaClass = '';
447
if( this.isOpera8 ) fixOperaClass = ' opera8';
448
this.imgWrap = Builder.node( 'div', { 'class': cNamePrefix + 'wrap' + fixOperaClass } );
450
this.north = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'north' }, [Builder.node( 'span' )] );
451
this.east = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'east' } , [Builder.node( 'span' )] );
452
this.south = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'south' }, [Builder.node( 'span' )] );
453
this.west = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'west' } , [Builder.node( 'span' )] );
455
var overlays = [ this.north, this.east, this.south, this.west ];
457
this.dragArea = Builder.node( 'div', { 'class': cNamePrefix + 'dragArea' }, overlays );
459
this.handleN = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleN' } );
460
this.handleNE = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleNE' } );
461
this.handleE = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleE' } );
462
this.handleSE = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleSE' } );
463
this.handleS = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleS' } );
464
this.handleSW = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleSW' } );
465
this.handleW = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleW' } );
466
this.handleNW = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleNW' } );
468
this.selArea = Builder.node( 'div', { 'class': cNamePrefix + 'selArea' },
470
Builder.node( 'div', { 'class': cNamePrefix + 'marqueeHoriz ' + cNamePrefix + 'marqueeNorth' }, [Builder.node( 'span' )] ),
471
Builder.node( 'div', { 'class': cNamePrefix + 'marqueeVert ' + cNamePrefix + 'marqueeEast' } , [Builder.node( 'span' )] ),
472
Builder.node( 'div', { 'class': cNamePrefix + 'marqueeHoriz ' + cNamePrefix + 'marqueeSouth' }, [Builder.node( 'span' )] ),
473
Builder.node( 'div', { 'class': cNamePrefix + 'marqueeVert ' + cNamePrefix + 'marqueeWest' } , [Builder.node( 'span' )] ),
482
Builder.node( 'div', { 'class': cNamePrefix + 'clickArea' } )
486
this.imgWrap.appendChild( this.img );
487
this.imgWrap.appendChild( this.dragArea );
488
this.dragArea.appendChild( this.selArea );
489
this.dragArea.appendChild( Builder.node( 'div', { 'class': cNamePrefix + 'clickArea' } ) );
491
insertPoint.appendChild( this.imgWrap );
493
// add event observers
494
this.startDragBind = this.startDrag.bindAsEventListener( this );
495
Event.observe( this.dragArea, 'mousedown', this.startDragBind );
497
this.onDragBind = this.onDrag.bindAsEventListener( this );
498
Event.observe( document, 'mousemove', this.onDragBind );
500
this.endCropBind = this.endCrop.bindAsEventListener( this );
501
Event.observe( document, 'mouseup', this.endCropBind );
503
this.resizeBind = this.startResize.bindAsEventListener( this );
504
this.handles = [ this.handleN, this.handleNE, this.handleE, this.handleSE, this.handleS, this.handleSW, this.handleW, this.handleNW ];
505
this.registerHandles( true );
507
if( this.options.captureKeys ) {
508
this.keysBind = this.handleKeys.bindAsEventListener( this );
509
Event.observe( document, 'keypress', this.keysBind );
512
// attach the dragable to the select area
513
new CropDraggable( this.selArea, { drawMethod: this.moveArea.bindAsEventListener( this ) } );
519
* Manages adding or removing the handle event handler and hiding or displaying them as appropriate
522
* @param boolean registration true = add, false = remove
525
registerHandles: function( registration ) {
526
for( var i = 0; i < this.handles.length; i++ ) {
527
var handle = $( this.handles[i] );
530
var hideHandle = false; // whether to hide the handle
532
// disable handles asappropriate if we've got fixed dimensions
533
// if both dimensions are fixed we don't need to do much
534
if( this.fixedWidth && this.fixedHeight ) hideHandle = true;
535
else if( this.fixedWidth || this.fixedHeight ) {
536
// if one of the dimensions is fixed then just hide those handles
537
var isCornerHandle = handle.className.match( /([S|N][E|W])$/ )
538
var isWidthHandle = handle.className.match( /(E|W)$/ );
539
var isHeightHandle = handle.className.match( /(N|S)$/ );
540
if( isCornerHandle ) hideHandle = true;
541
else if( this.fixedWidth && isWidthHandle ) hideHandle = true;
542
else if( this.fixedHeight && isHeightHandle ) hideHandle = true;
544
if( hideHandle ) handle.hide();
545
else Event.observe( handle, 'mousedown', this.resizeBind );
548
Event.stopObserving( handle, 'mousedown', this.resizeBind );
554
* Sets up all the cropper parameters, this can be used to reset the cropper when dynamically
555
* changing the images
560
setParams: function() {
565
this.imgW = this.img.width;
570
this.imgH = this.img.height;
572
$( this.north ).setStyle( { height: 0 } );
573
$( this.east ).setStyle( { width: 0, height: 0 } );
574
$( this.south ).setStyle( { height: 0 } );
575
$( this.west ).setStyle( { width: 0, height: 0 } );
577
// resize the container to fit the image
578
$( this.imgWrap ).setStyle( { 'width': this.imgW + 'px', 'height': this.imgH + 'px' } );
580
// hide the select area
581
$( this.selArea ).hide();
583
// setup the starting position of the select area
584
var startCoords = { x1: 0, y1: 0, x2: 0, y2: 0 };
585
var validCoordsSet = false;
587
// display the select area
588
if( this.options.onloadCoords != null ) {
589
// if we've being given some coordinates to
590
startCoords = this.cloneCoords( this.options.onloadCoords );
591
validCoordsSet = true;
592
} else if( this.options.ratioDim.x > 0 && this.options.ratioDim.y > 0 ) {
593
// if there is a ratio limit applied and the then set it to initial ratio
594
startCoords.x1 = Math.ceil( ( this.imgW - this.options.ratioDim.x ) / 2 );
595
startCoords.y1 = Math.ceil( ( this.imgH - this.options.ratioDim.y ) / 2 );
596
startCoords.x2 = startCoords.x1 + this.options.ratioDim.x;
597
startCoords.y2 = startCoords.y1 + this.options.ratioDim.y;
598
validCoordsSet = true;
601
this.setAreaCoords( startCoords, false, false, 1 );
603
if( this.options.displayOnInit && validCoordsSet ) {
609
this.attached = true;
613
* Removes the cropper
619
if( this.attached ) {
620
this.attached = false;
622
// remove the elements we inserted
623
this.imgWrap.parentNode.insertBefore( this.img, this.imgWrap );
624
this.imgWrap.parentNode.removeChild( this.imgWrap );
626
// remove the event observers
627
Event.stopObserving( this.dragArea, 'mousedown', this.startDragBind );
628
Event.stopObserving( document, 'mousemove', this.onDragBind );
629
Event.stopObserving( document, 'mouseup', this.endCropBind );
630
this.registerHandles( false );
631
if( this.options.captureKeys ) Event.stopObserving( document, 'keypress', this.keysBind );
636
* Resets the cropper, can be used either after being removed or any time you wish
642
if( !this.attached ) this.onLoad();
643
else this.setParams();
648
* Handles the key functionality, currently just using arrow keys to move, if the user
649
* presses shift then the area will move by 10 pixels
651
handleKeys: function( e ) {
652
var dir = { x: 0, y: 0 }; // direction to move it in & the amount in pixels
653
if( !this.dragging ) {
655
// catch the arrow keys
656
switch( e.keyCode ) {
663
case( 39 ) : // right
671
if( dir.x != 0 || dir.y != 0 ) {
672
// if shift is pressed then move by 10 pixels
678
this.moveArea( [ this.areaCoords.x1 + dir.x, this.areaCoords.y1 + dir.y ] );
685
* Calculates the width from the areaCoords
691
return (this.areaCoords.x2 - this.areaCoords.x1)
695
* Calculates the height from the areaCoords
701
return (this.areaCoords.y2 - this.areaCoords.y1)
705
* Moves the select area to the supplied point (assumes the point is x1 & y1 of the select area)
708
* @param array Point for x1 & y1 to move select area to
711
moveArea: function( point ) {
712
// dump( 'moveArea : ' + point[0] + ',' + point[1] + ',' + ( point[0] + ( this.areaCoords.x2 - this.areaCoords.x1 ) ) + ',' + ( point[1] + ( this.areaCoords.y2 - this.areaCoords.y1 ) ) + '\n' );
717
x2: point[0] + this.calcW(),
718
y2: point[1] + this.calcH()
727
* Clones a co-ordinates object, stops problems with handling them by reference
730
* @param obj Coordinate object x1, y1, x2, y2
731
* @return obj Coordinate object x1, y1, x2, y2
733
cloneCoords: function( coords ) {
734
return { x1: coords.x1, y1: coords.y1, x2: coords.x2, y2: coords.y2 };
738
* Sets the select coords to those provided but ensures they don't go
739
* outside the bounding box
742
* @param obj Coordinates x1, y1, x2, y2
743
* @param boolean Whether this is a move
744
* @param boolean Whether to apply squaring
745
* @param obj Direction of mouse along both axis x, y ( -1 = negative, 1 = positive ) only required when moving etc.
746
* @param string The current resize handle || null
749
setAreaCoords: function( coords, moving, square, direction, resizeHandle ) {
750
// dump( 'setAreaCoords (in) : ' + coords.x1 + ',' + coords.y1 + ',' + coords.x2 + ',' + coords.y2 );
753
var targW = coords.x2 - coords.x1;
754
var targH = coords.y2 - coords.y1;
756
// ensure we're within the bounds
757
if( coords.x1 < 0 ) {
761
if( coords.y1 < 0 ) {
765
if( coords.x2 > this.imgW ) {
766
coords.x2 = this.imgW;
767
coords.x1 = this.imgW - targW;
769
if( coords.y2 > this.imgH ) {
770
coords.y2 = this.imgH;
771
coords.y1 = this.imgH - targH;
774
// ensure we're within the bounds
775
if( coords.x1 < 0 ) coords.x1 = 0;
776
if( coords.y1 < 0 ) coords.y1 = 0;
777
if( coords.x2 > this.imgW ) coords.x2 = this.imgW;
778
if( coords.y2 > this.imgH ) coords.y2 = this.imgH;
780
// This is passed as null in onload
781
if( direction != null ) {
783
// apply the ratio or squaring where appropriate
784
if( this.ratioX > 0 ) this.applyRatio( coords, { x: this.ratioX, y: this.ratioY }, direction, resizeHandle );
785
else if( square ) this.applyRatio( coords, { x: 1, y: 1 }, direction, resizeHandle );
787
var mins = [ this.options.minWidth, this.options.minHeight ]; // minimum dimensions [x,y]
788
var maxs = [ this.options.maxWidth, this.options.maxHeight ]; // maximum dimensions [x,y]
790
// apply dimensions where appropriate
791
if( mins[0] > 0 || mins[1] > 0 || maxs[0] > 0 || maxs[1] > 0) {
793
var coordsTransX = { a1: coords.x1, a2: coords.x2 };
794
var coordsTransY = { a1: coords.y1, a2: coords.y2 };
795
var boundsX = { min: 0, max: this.imgW };
796
var boundsY = { min: 0, max: this.imgH };
798
// handle squaring properly on single axis minimum dimensions
799
if( (mins[0] != 0 || mins[1] != 0) && square ) {
800
if( mins[0] > 0 ) mins[1] = mins[0];
801
else if( mins[1] > 0 ) mins[0] = mins[1];
804
if( (maxs[0] != 0 || maxs[0] != 0) && square ) {
805
// if we have a max x value & it is less than the max y value then we set the y max to the max x (so we don't go over the minimum maximum of one of the axes - if that makes sense)
806
if( maxs[0] > 0 && maxs[0] <= maxs[1] ) maxs[1] = maxs[0];
807
else if( maxs[1] > 0 && maxs[1] <= maxs[0] ) maxs[0] = maxs[1];
810
if( mins[0] > 0 ) this.applyDimRestriction( coordsTransX, mins[0], direction.x, boundsX, 'min' );
811
if( mins[1] > 1 ) this.applyDimRestriction( coordsTransY, mins[1], direction.y, boundsY, 'min' );
813
if( maxs[0] > 0 ) this.applyDimRestriction( coordsTransX, maxs[0], direction.x, boundsX, 'max' );
814
if( maxs[1] > 1 ) this.applyDimRestriction( coordsTransY, maxs[1], direction.y, boundsY, 'max' );
816
coords = { x1: coordsTransX.a1, y1: coordsTransY.a1, x2: coordsTransX.a2, y2: coordsTransY.a2 };
822
// dump( 'setAreaCoords (out) : ' + coords.x1 + ',' + coords.y1 + ',' + coords.x2 + ',' + coords.y2 + '\n' );
823
this.areaCoords = coords;
827
* Applies the supplied dimension restriction to the supplied coordinates along a single axis
830
* @param obj Single axis coordinates, a1, a2 (e.g. for the x axis a1 = x1 & a2 = x2)
831
* @param int The restriction value
832
* @param int The direction ( -1 = negative, 1 = positive )
833
* @param obj The bounds of the image ( for this axis )
834
* @param string The dimension restriction type ( 'min' | 'max' )
837
applyDimRestriction: function( coords, val, direction, bounds, type ) {
839
if( type == 'min' ) check = ( ( coords.a2 - coords.a1 ) < val );
840
else check = ( ( coords.a2 - coords.a1 ) > val );
842
if( direction == 1 ) coords.a2 = coords.a1 + val;
843
else coords.a1 = coords.a2 - val;
845
// make sure we're still in the bounds (not too pretty for the user, but needed)
846
if( coords.a1 < bounds.min ) {
847
coords.a1 = bounds.min;
849
} else if( coords.a2 > bounds.max ) {
850
coords.a1 = bounds.max - val;
851
coords.a2 = bounds.max;
857
* Applies the supplied ratio to the supplied coordinates
860
* @param obj Coordinates, x1, y1, x2, y2
861
* @param obj Ratio, x, y
862
* @param obj Direction of mouse, x & y : -1 == negative 1 == positive
863
* @param string The current resize handle || null
866
applyRatio : function( coords, ratio, direction, resizeHandle ) {
867
// dump( 'direction.y : ' + direction.y + '\n');
869
if( resizeHandle == 'N' || resizeHandle == 'S' ) {
870
// dump( 'north south \n');
871
// if moving on either the lone north & south handles apply the ratio on the y axis
872
newCoords = this.applyRatioToAxis(
873
{ a1: coords.y1, b1: coords.x1, a2: coords.y2, b2: coords.x2 },
874
{ a: ratio.y, b: ratio.x },
875
{ a: direction.y, b: direction.x },
876
{ min: 0, max: this.imgW }
878
coords.x1 = newCoords.b1;
879
coords.y1 = newCoords.a1;
880
coords.x2 = newCoords.b2;
881
coords.y2 = newCoords.a2;
883
// otherwise deal with it as if we're applying the ratio on the x axis
884
newCoords = this.applyRatioToAxis(
885
{ a1: coords.x1, b1: coords.y1, a2: coords.x2, b2: coords.y2 },
886
{ a: ratio.x, b: ratio.y },
887
{ a: direction.x, b: direction.y },
888
{ min: 0, max: this.imgH }
890
coords.x1 = newCoords.a1;
891
coords.y1 = newCoords.b1;
892
coords.x2 = newCoords.a2;
893
coords.y2 = newCoords.b2;
899
* Applies the provided ratio to the provided coordinates based on provided direction & bounds,
900
* use to encapsulate functionality to make it easy to apply to either axis. This is probably
901
* quite hard to visualise so see the x axis example within applyRatio()
903
* Example in parameter details & comments is for requesting applying ratio to x axis.
906
* @param obj Coords object (a1, b1, a2, b2) where a = x & b = y in example
907
* @param obj Ratio object (a, b) where a = x & b = y in example
908
* @param obj Direction object (a, b) where a = x & b = y in example
909
* @param obj Bounds (min, max)
910
* @return obj Coords object (a1, b1, a2, b2) where a = x & b = y in example
912
applyRatioToAxis: function( coords, ratio, direction, bounds ) {
913
var newCoords = Object.extend( coords, {} );
914
var calcDimA = newCoords.a2 - newCoords.a1; // calculate dimension a (e.g. width)
915
var targDimB = Math.floor( calcDimA * ratio.b / ratio.a ); // the target dimension b (e.g. height)
916
var targB; // to hold target b (e.g. y value)
917
var targDimA; // to hold target dimension a (e.g. width)
918
var calcDimB = null; // to hold calculated dimension b (e.g. height)
920
// dump( 'newCoords[0]: ' + newCoords.a1 + ',' + newCoords.b1 + ','+ newCoords.a2 + ',' + newCoords.b2 + '\n');
922
if( direction.b == 1 ) { // if travelling in a positive direction
923
// make sure we're not going out of bounds
924
targB = newCoords.b1 + targDimB;
925
if( targB > bounds.max ) {
927
calcDimB = targB - newCoords.b1; // calcuate dimension b (e.g. height)
930
newCoords.b2 = targB;
931
} else { // if travelling in a negative direction
932
// make sure we're not going out of bounds
933
targB = newCoords.b2 - targDimB;
934
if( targB < bounds.min ) {
936
calcDimB = targB + newCoords.b2; // calcuate dimension b (e.g. height)
938
newCoords.b1 = targB;
941
// dump( 'newCoords[1]: ' + newCoords.a1 + ',' + newCoords.b1 + ','+ newCoords.a2 + ',' + newCoords.b2 + '\n');
943
// apply the calculated dimensions
944
if( calcDimB != null ) {
945
targDimA = Math.floor( calcDimB * ratio.a / ratio.b );
947
if( direction.a == 1 ) newCoords.a2 = newCoords.a1 + targDimA;
948
else newCoords.a1 = newCoords.a1 = newCoords.a2 - targDimA;
951
// dump( 'newCoords[2]: ' + newCoords.a1 + ',' + newCoords.b1 + ','+ newCoords.a2 + ',' + newCoords.b2 + '\n');
957
* Draws the select area
962
drawArea: function( ) {
964
* NOTE: I'm not using the Element.setStyle() shortcut as they make it
965
* quite sluggish on Mac based browsers
967
// dump( 'drawArea : ' + this.areaCoords.x1 + ',' + this.areaCoords.y1 + ',' + this.areaCoords.x2 + ',' + this.areaCoords.y2 + '\n' );
968
var areaWidth = this.calcW();
969
var areaHeight = this.calcH();
972
* Calculate all the style strings before we use them, allows reuse & produces quicker
973
* rendering (especially noticable in Mac based browsers)
977
this.areaCoords.x1 + px, // the left of the selArea
978
this.areaCoords.y1 + px, // the top of the selArea
979
areaWidth + px, // width of the selArea
980
areaHeight + px, // height of the selArea
981
this.areaCoords.x2 + px, // bottom of the selArea
982
this.areaCoords.y2 + px, // right of the selArea
983
(this.img.width - this.areaCoords.x2) + px, // right edge of selArea
984
(this.img.height - this.areaCoords.y2) + px // bottom edge of selArea
987
// do the select area
988
var areaStyle = this.selArea.style;
989
areaStyle.left = params[0];
990
areaStyle.top = params[1];
991
areaStyle.width = params[2];
992
areaStyle.height = params[3];
994
// position the north, east, south & west handles
995
var horizHandlePos = Math.ceil( (areaWidth - 6) / 2 ) + px;
996
var vertHandlePos = Math.ceil( (areaHeight - 6) / 2 ) + px;
998
this.handleN.style.left = horizHandlePos;
999
this.handleE.style.top = vertHandlePos;
1000
this.handleS.style.left = horizHandlePos;
1001
this.handleW.style.top = vertHandlePos;
1003
// draw the four overlays
1004
this.north.style.height = params[1];
1006
var eastStyle = this.east.style;
1007
eastStyle.top = params[1];
1008
eastStyle.height = params[3];
1009
eastStyle.left = params[4];
1010
eastStyle.width = params[6];
1012
var southStyle = this.south.style;
1013
southStyle.top = params[5];
1014
southStyle.height = params[7];
1016
var westStyle = this.west.style;
1017
westStyle.top = params[1];
1018
westStyle.height = params[3];
1019
westStyle.width = params[0];
1021
// call the draw method on sub classes
1024
this.forceReRender();
1028
* Force the re-rendering of the selArea element which fixes rendering issues in Safari
1029
* & IE PC, especially evident when re-sizing perfectly vertical using any of the south handles
1034
forceReRender: function() {
1035
if( this.isIE || this.isWebKit) {
1036
var n = document.createTextNode(' ');
1039
if( this.isIE ) fixEl = this.selArea;
1040
else if( this.isWebKit ) {
1041
fixEl = document.getElementsByClassName( 'imgCrop_marqueeSouth', this.imgWrap )[0];
1042
/* we have to be a bit more forceful for Safari, otherwise the the marquee &
1043
* the south handles still don't move
1045
d = Builder.node( 'div', '' );
1046
d.style.visibility = 'hidden';
1048
var classList = ['SE','S','SW'];
1049
for( i = 0; i < classList.length; i++ ) {
1050
el = document.getElementsByClassName( 'imgCrop_handle' + classList[i], this.selArea )[0];
1051
if( el.childNodes.length ) el.removeChild( el.childNodes[0] );
1055
fixEl.appendChild(n);
1056
fixEl.removeChild(n);
1067
startResize: function( e ) {
1068
this.startCoords = this.cloneCoords( this.areaCoords );
1070
this.resizing = true;
1071
this.resizeHandle = Event.element( e ).classNames().toString().replace(/([^N|NE|E|SE|S|SW|W|NW])+/, '');
1072
// dump( 'this.resizeHandle : ' + this.resizeHandle + '\n' );
1083
startDrag: function( e ) {
1084
this.selArea.show();
1085
this.clickCoords = this.getCurPos( e );
1087
this.setAreaCoords( { x1: this.clickCoords.x, y1: this.clickCoords.y, x2: this.clickCoords.x, y2: this.clickCoords.y }, false, false, null );
1089
this.dragging = true;
1090
this.onDrag( e ); // incase the user just clicks once after already making a selection
1095
* Gets the current cursor position relative to the image
1099
* @return obj x,y pixels of the cursor
1101
getCurPos: function( e ) {
1102
// get the offsets for the wrapper within the document
1103
var el = this.imgWrap, wrapOffsets = Position.cumulativeOffset( el );
1104
// remove any scrolling that is applied to the wrapper (this may be buggy) - don't count the scroll on the body as that won't affect us
1105
while( el.nodeName != 'BODY' ) {
1106
wrapOffsets[1] -= el.scrollTop || 0;
1107
wrapOffsets[0] -= el.scrollLeft || 0;
1111
x: Event.pointerX(e) - wrapOffsets[0],
1112
y: Event.pointerY(e) - wrapOffsets[1]
1117
* Performs the drag for both resize & inital draw dragging
1123
onDrag: function( e ) {
1124
if( this.dragging || this.resizing ) {
1126
var resizeHandle = null;
1127
var curPos = this.getCurPos( e );
1128
var newCoords = this.cloneCoords( this.areaCoords );
1129
var direction = { x: 1, y: 1 };
1131
if( this.dragging ) {
1132
if( curPos.x < this.clickCoords.x ) direction.x = -1;
1133
if( curPos.y < this.clickCoords.y ) direction.y = -1;
1135
this.transformCoords( curPos.x, this.clickCoords.x, newCoords, 'x' );
1136
this.transformCoords( curPos.y, this.clickCoords.y, newCoords, 'y' );
1137
} else if( this.resizing ) {
1138
resizeHandle = this.resizeHandle;
1139
// do x movements first
1140
if( resizeHandle.match(/E/) ) {
1141
// if we're moving an east handle
1142
this.transformCoords( curPos.x, this.startCoords.x1, newCoords, 'x' );
1143
if( curPos.x < this.startCoords.x1 ) direction.x = -1;
1144
} else if( resizeHandle.match(/W/) ) {
1145
// if we're moving an west handle
1146
this.transformCoords( curPos.x, this.startCoords.x2, newCoords, 'x' );
1147
if( curPos.x < this.startCoords.x2 ) direction.x = -1;
1150
// do y movements second
1151
if( resizeHandle.match(/N/) ) {
1152
// if we're moving an north handle
1153
this.transformCoords( curPos.y, this.startCoords.y2, newCoords, 'y' );
1154
if( curPos.y < this.startCoords.y2 ) direction.y = -1;
1155
} else if( resizeHandle.match(/S/) ) {
1156
// if we're moving an south handle
1157
this.transformCoords( curPos.y, this.startCoords.y1, newCoords, 'y' );
1158
if( curPos.y < this.startCoords.y1 ) direction.y = -1;
1163
this.setAreaCoords( newCoords, false, e.shiftKey, direction, resizeHandle );
1165
Event.stop( e ); // stop the default event (selecting images & text) in Safari & IE PC
1170
* Applies the appropriate transform to supplied co-ordinates, on the
1171
* defined axis, depending on the relationship of the supplied values
1174
* @param int Current value of pointer
1175
* @param int Base value to compare current pointer val to
1176
* @param obj Coordinates to apply transformation on x1, x2, y1, y2
1177
* @param string Axis to apply transformation on 'x' || 'y'
1180
transformCoords : function( curVal, baseVal, coords, axis ) {
1181
var newVals = [ curVal, baseVal ];
1182
if( curVal > baseVal ) newVals.reverse();
1183
coords[ axis + '1' ] = newVals[0];
1184
coords[ axis + '2' ] = newVals[1];
1188
* Ends the crop & passes the values of the select area on to the appropriate
1189
* callback function on completion of a crop
1194
endCrop : function() {
1195
this.dragging = false;
1196
this.resizing = false;
1198
this.options.onEndCrop(
1201
width: this.calcW(),
1202
height: this.calcH()
1208
* Abstract method called on the end of initialization
1214
subInitialize: function() {},
1217
* Abstract method called on the end of drawArea()
1223
subDrawArea: function() {}
1230
* Extend the Cropper.Img class to allow for presentation of a preview image of the resulting crop,
1231
* the option for displayOnInit is always overridden to true when displaying a preview image
1234
* @param obj Image element to attach to
1235
* @param obj Optional options:
1236
* - see Cropper.Img for base options
1239
* HTML element that will be used as a container for the preview image
1241
Cropper.ImgWithPreview = Class.create();
1243
Object.extend( Object.extend( Cropper.ImgWithPreview.prototype, Cropper.Img.prototype ), {
1246
* Implements the abstract method from Cropper.Img to initialize preview image settings.
1247
* Will only attach a preview image is the previewWrap element is defined and the minWidth
1248
* & minHeight options are set.
1250
* @see Croper.Img.subInitialize
1252
subInitialize: function() {
1254
* Whether or not we've attached a preview image
1257
this.hasPreviewImg = false;
1258
if( typeof(this.options.previewWrap) != 'undefined'
1259
&& this.options.minWidth > 0
1260
&& this.options.minHeight > 0
1263
* The preview image wrapper element
1264
* @var obj HTML element
1266
this.previewWrap = $( this.options.previewWrap );
1268
* The preview image element
1269
* @var obj HTML IMG element
1271
this.previewImg = this.img.cloneNode( false );
1272
// set the ID of the preview image to be unique
1273
this.previewImg.id = 'imgCrop_' + this.previewImg.id;
1276
// set the displayOnInit option to true so we display the select area at the same time as the thumbnail
1277
this.options.displayOnInit = true;
1279
this.hasPreviewImg = true;
1281
this.previewWrap.addClassName( 'imgCrop_previewWrap' );
1283
this.previewWrap.setStyle(
1285
width: this.options.minWidth + 'px',
1286
height: this.options.minHeight + 'px'
1290
this.previewWrap.appendChild( this.previewImg );
1295
* Implements the abstract method from Cropper.Img to draw the preview image
1297
* @see Croper.Img.subDrawArea
1299
subDrawArea: function() {
1300
if( this.hasPreviewImg ) {
1301
// get the ratio of the select area to the src image
1302
var calcWidth = this.calcW();
1303
var calcHeight = this.calcH();
1304
// ratios for the dimensions of the preview image
1306
x: this.imgW / calcWidth,
1307
y: this.imgH / calcHeight
1309
//ratios for the positions within the preview
1311
x: calcWidth / this.options.minWidth,
1312
y: calcHeight / this.options.minHeight
1315
// setting the positions in an obj before apply styles for rendering speed increase
1317
w: Math.ceil( this.options.minWidth * dimRatio.x ) + 'px',
1318
h: Math.ceil( this.options.minHeight * dimRatio.y ) + 'px',
1319
x: '-' + Math.ceil( this.areaCoords.x1 / posRatio.x ) + 'px',
1320
y: '-' + Math.ceil( this.areaCoords.y1 / posRatio.y ) + 'px'
1323
var previewStyle = this.previewImg.style;
1324
previewStyle.width = calcPos.w;
1325
previewStyle.height = calcPos.h;
1326
previewStyle.left = calcPos.x;
1327
previewStyle.top = calcPos.y;