noted the new URL for git repository
[editimage_extension] / cropper.uncompressed.js
1 /**\r
2  * Image Cropper (v. 1.2.0 - 2006-10-30 )\r
3  * Copyright (c) 2006 David Spurr (http://www.defusion.org.uk/)\r
4  * \r
5  * The image cropper provides a way to draw a crop area on an image and capture\r
6  * the coordinates of the drawn crop area.\r
7  * \r
8  * Features include:\r
9  *              - Based on Prototype and Scriptaculous\r
10  *              - Image editing package styling, the crop area functions and looks \r
11  *                like those found in popular image editing software\r
12  *              - Dynamic inclusion of required styles\r
13  *              - Drag to draw areas\r
14  *              - Shift drag to draw/resize areas as squares\r
15  *              - Selection area can be moved \r
16  *              - Seleciton area can be resized using resize handles\r
17  *              - Allows dimension ratio limited crop areas\r
18  *              - Allows minimum dimension crop areas\r
19  *              - Allows maximum dimesion crop areas\r
20  *              - If both min & max dimension options set to the same value for a single axis,then the cropper will not \r
21  *                display the resize handles as appropriate (when min & max dimensions are passed for both axes this\r
22  *                results in a 'fixed size' crop area)\r
23  *              - Allows dynamic preview of resultant crop ( if minimum width & height are provided ), this is\r
24  *                implemented as a subclass so can be excluded when not required\r
25  *              - Movement of selection area by arrow keys ( shift + arrow key will move selection area by\r
26  *                10 pixels )\r
27  *              - All operations stay within bounds of image\r
28  *              - All functionality & display compatible with most popular browsers supported by Prototype:\r
29  *                      PC:     IE 7, 6 & 5.5, Firefox 1.5, Opera 8.5 (see known issues) & 9.0b\r
30  *                      MAC: Camino 1.0, Firefox 1.5, Safari 2.0\r
31  * \r
32  * Requires:\r
33  *              - Prototype v. 1.5.0_rc0 > (as packaged with Scriptaculous 1.6.1)\r
34  *              - Scriptaculous v. 1.6.1 > modules: builder, dragdrop \r
35  *              \r
36  * Known issues:\r
37  *              - Safari animated gifs, only one of each will animate, this seems to be a known Safari issue\r
38  * \r
39  *              - After drawing an area and then clicking to start a new drag in IE 5.5 the rendered height \r
40  *        appears as the last height until the user drags, this appears to be the related to the error \r
41  *        that the forceReRender() method fixes for IE 6, i.e. IE 5.5 is not redrawing the box properly.\r
42  * \r
43  *              - Lack of CSS opacity support in Opera before version 9 mean we disable those style rules, these \r
44  *                could be fixed by using PNGs with transparency if Opera 8.5 support is high priority for you\r
45  * \r
46  *              - Marching ants keep reloading in IE <6 (not tested in IE7), it is a known issue in IE and I have \r
47  *        found no viable workarounds that can be included in the release. If this really is an issue for you\r
48  *        either try this post: http://mir.aculo.us/articles/2005/08/28/internet-explorer-and-ajax-image-caching-woes\r
49  *        or uncomment the 'FIX MARCHING ANTS IN IE' rules in the CSS file\r
50  *              \r
51  *              - Styling & borders on image, any CSS styling applied directly to the image itself (floats, borders, padding, margin, etc.) will \r
52  *                cause problems with the cropper. The use of a wrapper element to apply these styles to is recommended.\r
53  * \r
54  *              - overflow: auto or overflow: scroll on parent will cause cropper to burst out of parent in IE and Opera (maybe Mac browsers too)\r
55  *                I'm not sure why yet.\r
56  * \r
57  * Usage:\r
58  *              See Cropper.Img & Cropper.ImgWithPreview for usage details\r
59  * \r
60  * Changelog:\r
61  * v1.2.0 - 2006-10-30\r
62  *              + Added id to the preview image element using 'imgCrop_[originalImageID]'\r
63  *      * #00001 - Fixed bug: Doesn't account for scroll offsets\r
64  *      * #00009 - Fixed bug: Placing the cropper inside differently positioned elements causes incorrect co-ordinates and display\r
65  *      * #00013 - Fixed bug: I-bar cursor appears on drag plane\r
66  *      * #00014 - Fixed bug: If ID for image tag is not found in document script throws error\r
67  *      * Fixed bug with drag start co-ordinates if wrapper element has moved in browser (e.g. dragged to a new position)\r
68  *      * Fixed bug with drag start co-ordinates if image contained in a wrapper with scrolling - this may be buggy if image \r
69  *                has other ancestors with scrolling applied (except the body)\r
70  *      * #00015 - Fixed bug: When cropper removed and then reapplied onEndCrop callback gets called multiple times, solution suggestion from Bill Smith\r
71  *      * Various speed increases & code cleanup which meant improved performance in Mac - which allowed removal of different overlay methods for\r
72  *        IE and all other browsers, which led to a fix for:\r
73  *              * #00010 - Fixed bug: Select area doesn't adhere to image size when image resized using img attributes\r
74  *      - #00006 - Removed default behaviour of automatically setting a ratio when both min width & height passed, the ratioDimensions must be passed in\r
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\r
76  *        and the resize handles will not be displayed as appropriate\r
77  *              * Switched keydown for keypress for moving select area with cursor keys (makes for nicer action) - doesn't appear to work in Safari\r
78  * \r
79  * v1.1.3 - 2006-08-21\r
80  *              * Fixed wrong cursor on western handle in CSS\r
81  *              + #00008 & #00003 - Added feature: Allow to set dimensions & position for cropper on load\r
82  *      * #00002 - Fixed bug: Pressing 'remove cropper' twice removes image in IE\r
83  * \r
84  * v1.1.2 - 2006-06-09\r
85  *              * Fixed bugs with ratios when GCD is low (patch submitted by Andy Skelton)\r
86  * \r
87  * v1.1.1 - 2006-06-03\r
88  *              * Fixed bug with rendering issues fix in IE 5.5\r
89  *              * Fixed bug with endCrop callback issues once cropper had been removed & reset in IE\r
90  * \r
91  * v1.1.0 - 2006-06-02\r
92  *              * Fixed bug with IE constantly trying to reload select area background image\r
93  *              * Applied more robust fix to Safari & IE rendering issues\r
94  *              + Added method to reset parameters - useful for when dynamically changing img cropper attached to\r
95  *              + Added method to remove cropper from image\r
96  * \r
97  * v1.0.0 - 2006-05-18 \r
98  *              + Initial verison\r
99  * \r
100  * \r
101  * Copyright (c) 2006, David Spurr (http://www.defusion.org.uk/)\r
102  * All rights reserved.\r
103  * \r
104  * \r
105  * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\r
106  * \r
107  *     * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\r
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.\r
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.\r
110  * \r
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.\r
112  * \r
113  * http://www.opensource.org/licenses/bsd-license.php\r
114  * \r
115  * See scriptaculous.js for full scriptaculous licence\r
116  */\r
117  \r
118 /**\r
119  * Extend the Draggable class to allow us to pass the rendering\r
120  * down to the Cropper object.\r
121  */\r
122 var CropDraggable = Class.create();\r
123 \r
124 Object.extend( Object.extend( CropDraggable.prototype, Draggable.prototype), {\r
125         \r
126         initialize: function(element) {\r
127                 this.options = Object.extend(\r
128                         {\r
129                                 /**\r
130                                  * The draw method to defer drawing to\r
131                                  */\r
132                                 drawMethod: function() {}\r
133                         }, \r
134                         arguments[1] || {}\r
135                 );\r
136 \r
137                 this.element = $(element);\r
138 \r
139                 this.handle = this.element;\r
140 \r
141                 this.delta    = this.currentDelta();\r
142                 this.dragging = false;   \r
143 \r
144                 this.eventMouseDown = this.initDrag.bindAsEventListener(this);\r
145                 Event.observe(this.handle, "mousedown", this.eventMouseDown);\r
146 \r
147                 Draggables.register(this);\r
148         },\r
149         \r
150         /**\r
151          * Defers the drawing of the draggable to the supplied method\r
152          */\r
153         draw: function(point) {\r
154                 var pos = Position.cumulativeOffset(this.element);\r
155                 var d = this.currentDelta();\r
156                 pos[0] -= d[0]; \r
157                 pos[1] -= d[1];\r
158                                 \r
159                 var p = [0,1].map(function(i) { \r
160                         return (point[i]-pos[i]-this.offset[i]) \r
161                 }.bind(this));\r
162                                 \r
163                 this.options.drawMethod( p );\r
164         }\r
165         \r
166 });\r
167 \r
168 \r
169 /**\r
170  * The Cropper object, this will attach itself to the provided image by wrapping it with \r
171  * the generated xHTML structure required by the cropper.\r
172  * \r
173  * Usage:\r
174  *      @param obj Image element to attach to\r
175  *      @param obj Optional options:\r
176  *              - ratioDim obj \r
177  *                      The pixel dimensions to apply as a restrictive ratio, with properties x & y\r
178  * \r
179  *              - minWidth int \r
180  *                      The minimum width for the select area in pixels\r
181  * \r
182  *              - minHeight     int \r
183  *                      The mimimum height for the select area in pixels\r
184  * \r
185  *              - maxWidth int\r
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)\r
187  * \r
188  *              - maxHeight int\r
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)\r
190  * \r
191  *              - displayOnInit int \r
192  *                      Whether to display the select area on initialisation, only used when providing minimum width & height or ratio\r
193  * \r
194  *              - onEndCrop func\r
195  *                      The callback function to provide the crop details to on end of a crop (see below)\r
196  * \r
197  *              - captureKeys boolean\r
198  *                      Whether to capture the keys for moving the select area, as these can cause some problems at the moment\r
199  * \r
200  *              - onloadCoords obj\r
201  *                      A coordinates object with properties x1, y1, x2 & y2; for the coordinates of the select area to display onload\r
202  *      \r
203  *----------------------------------------------\r
204  * \r
205  * The callback function provided via the onEndCrop option should accept the following parameters:\r
206  *              - coords obj\r
207  *                      The coordinates object with properties x1, y1, x2 & y2; for the coordinates of the select area\r
208  * \r
209  *              - dimensions obj\r
210  *                      The dimensions object with properites width & height; for the dimensions of the select area\r
211  *              \r
212  *\r
213  *              Example:\r
214  *                      function onEndCrop( coords, dimensions ) {\r
215  *                              $( 'x1' ).value         = coords.x1;\r
216  *                              $( 'y1' ).value         = coords.y1;\r
217  *                              $( 'x2' ).value         = coords.x2;\r
218  *                              $( 'y2' ).value         = coords.y2;\r
219  *                              $( 'width' ).value      = dimensions.width;\r
220  *                              $( 'height' ).value     = dimensions.height;\r
221  *                      }\r
222  * \r
223  */\r
224 var Cropper = {};\r
225 Cropper.Img = Class.create();\r
226 Cropper.Img.prototype = {\r
227         \r
228         /**\r
229          * Initialises the class\r
230          * \r
231          * @access public\r
232          * @param obj Image element to attach to\r
233          * @param obj Options\r
234          * @return void\r
235          */\r
236         initialize: function(element, options) {\r
237                 this.options = Object.extend(\r
238                         {\r
239                                 /**\r
240                                  * @var obj\r
241                                  * The pixel dimensions to apply as a restrictive ratio\r
242                                  */\r
243                                 ratioDim: { x: 0, y: 0 },\r
244                                 /**\r
245                                  * @var int\r
246                                  * The minimum pixel width, also used as restrictive ratio if min height passed too\r
247                                  */\r
248                                 minWidth:               0,\r
249                                 /**\r
250                                  * @var int\r
251                                  * The minimum pixel height, also used as restrictive ratio if min width passed too\r
252                                  */\r
253                                 minHeight:              0,\r
254                                 /**\r
255                                  * @var boolean\r
256                                  * Whether to display the select area on initialisation, only used when providing minimum width & height or ratio\r
257                                  */\r
258                                 displayOnInit:  false,\r
259                                 /**\r
260                                  * @var function\r
261                                  * The call back function to pass the final values to\r
262                                  */\r
263                                 onEndCrop: Prototype.emptyFunction,\r
264                                 /**\r
265                                  * @var boolean\r
266                                  * Whether to capture key presses or not\r
267                                  */\r
268                                 captureKeys: true,\r
269                                 /**\r
270                                  * @var obj Coordinate object x1, y1, x2, y2\r
271                                  * The coordinates to optionally display the select area at onload\r
272                                  */\r
273                                 onloadCoords: null,\r
274                                 /**\r
275                                  * @var int\r
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)\r
277                                  */\r
278                                 maxWidth: 0,\r
279                                 /**\r
280                                  * @var int\r
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)\r
282                                  */\r
283                                 maxHeight: 0\r
284                         }, \r
285                         options || {}\r
286                 );                              \r
287                 /**\r
288                  * @var obj\r
289                  * The img node to attach to\r
290                  */\r
291                 this.img                        = $( element );\r
292                 /**\r
293                  * @var obj\r
294                  * The x & y coordinates of the click point\r
295                  */\r
296                 this.clickCoords        = { x: 0, y: 0 };\r
297                 /**\r
298                  * @var boolean\r
299                  * Whether the user is dragging\r
300                  */\r
301                 this.dragging           = false;\r
302                 /**\r
303                  * @var boolean\r
304                  * Whether the user is resizing\r
305                  */\r
306                 this.resizing           = false;\r
307                 /**\r
308                  * @var boolean\r
309                  * Whether the user is on a webKit browser\r
310                  */\r
311                 this.isWebKit           = /Konqueror|Safari|KHTML/.test( navigator.userAgent );\r
312                 /**\r
313                  * @var boolean\r
314                  * Whether the user is on IE\r
315                  */\r
316                 this.isIE                       = /MSIE/.test( navigator.userAgent );\r
317                 /**\r
318                  * @var boolean\r
319                  * Whether the user is on Opera below version 9\r
320                  */\r
321                 this.isOpera8           = /Opera\s[1-8]/.test( navigator.userAgent );\r
322                 /**\r
323                  * @var int\r
324                  * The x ratio \r
325                  */\r
326                 this.ratioX                     = 0;\r
327                 /**\r
328                  * @var int\r
329                  * The y ratio\r
330                  */\r
331                 this.ratioY                     = 0;\r
332                 /**\r
333                  * @var boolean\r
334                  * Whether we've attached sucessfully\r
335                  */\r
336                 this.attached           = false;\r
337                 /**\r
338                  * @var boolean\r
339                  * Whether we've got a fixed width (if minWidth EQ or GT maxWidth then we have a fixed width\r
340                  * in the case of minWidth > maxWidth maxWidth wins as the fixed width)\r
341                  */\r
342                 this.fixedWidth         = ( this.options.maxWidth > 0 && ( this.options.minWidth >= this.options.maxWidth ) );\r
343                 /**\r
344                  * @var boolean\r
345                  * Whether we've got a fixed height (if minHeight EQ or GT maxHeight then we have a fixed height\r
346                  * in the case of minHeight > maxHeight maxHeight wins as the fixed height)\r
347                  */\r
348                 this.fixedHeight        = ( this.options.maxHeight > 0 && ( this.options.minHeight >= this.options.maxHeight ) );\r
349                 \r
350                 // quit if the image element doesn't exist\r
351                 if( typeof this.img == 'undefined' ) return;\r
352                                 \r
353                 // include the stylesheet               \r
354                 $A( document.getElementsByTagName( 'script' ) ).each( \r
355                         function(s) {\r
356                                 if( s.src.match( /cropper\.js/ ) ) {\r
357                                         var path        = s.src.replace( /cropper\.js(.*)?/, '' );\r
358                                         // '<link rel="stylesheet" type="text/css" href="' + path + 'cropper.css" media="screen" />';\r
359                                         var style               = document.createElement( 'link' );\r
360                                         style.rel               = 'stylesheet';\r
361                                         style.type              = 'text/css';\r
362                                         style.href              = path + 'cropper.css';\r
363                                         style.media     = 'screen';\r
364                                         document.getElementsByTagName( 'head' )[0].appendChild( style );\r
365                                 }\r
366                 }\r
367             );   \r
368         \r
369                 // calculate the ratio when neccessary\r
370                 if( this.options.ratioDim.x > 0 && this.options.ratioDim.y > 0 ) {\r
371                         var gcd = this.getGCD( this.options.ratioDim.x, this.options.ratioDim.y );\r
372                         this.ratioX = this.options.ratioDim.x / gcd;\r
373                         this.ratioY = this.options.ratioDim.y / gcd;\r
374                         // dump( 'RATIO : ' + this.ratioX + ':' + this.ratioY + '\n' );\r
375                 }\r
376                                                         \r
377                 // initialise sub classes\r
378                 this.subInitialize();\r
379 \r
380                 // only load the event observers etc. once the image is loaded\r
381                 // this is done after the subInitialize() call just in case the sub class does anything\r
382                 // that will affect the result of the call to onLoad()\r
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\r
384                 else Event.observe( this.img, 'load', this.onLoad.bindAsEventListener( this) );         \r
385         },\r
386         \r
387         /**\r
388          * The Euclidean algorithm used to find the greatest common divisor\r
389          * \r
390          * @acces private\r
391          * @param int Value 1\r
392          * @param int Value 2\r
393          * @return int\r
394          */\r
395         getGCD : function( a , b ) {\r
396                 if( b == 0 ) return a;\r
397                 return this.getGCD(b, a % b );\r
398         },\r
399         \r
400         /**\r
401          * Attaches the cropper to the image once it has loaded\r
402          * \r
403          * @access private\r
404          * @return void\r
405          */\r
406         onLoad: function( ) {\r
407                 /*\r
408                  * Build the container and all related elements, will result in the following\r
409                  *\r
410                  * <div class="imgCrop_wrap">\r
411                  *              <img ... this.img ... />\r
412                  *              <div class="imgCrop_dragArea">\r
413                  *                      <!-- the inner spans are only required for IE to stop it making the divs 1px high/wide -->\r
414                  *                      <div class="imgCrop_overlay imageCrop_north"><span></span></div>\r
415                  *                      <div class="imgCrop_overlay imageCrop_east"><span></span></div>\r
416                  *                      <div class="imgCrop_overlay imageCrop_south"><span></span></div>\r
417                  *                      <div class="imgCrop_overlay imageCrop_west"><span></span></div>\r
418                  *                      <div class="imgCrop_selArea">\r
419                  *                              <!-- marquees -->\r
420                  *                              <!-- the inner spans are only required for IE to stop it making the divs 1px high/wide -->\r
421                  *                              <div class="imgCrop_marqueeHoriz imgCrop_marqueeNorth"><span></span></div>\r
422                  *                              <div class="imgCrop_marqueeVert imgCrop_marqueeEast"><span></span></div>\r
423                  *                              <div class="imgCrop_marqueeHoriz imgCrop_marqueeSouth"><span></span></div>\r
424                  *                              <div class="imgCrop_marqueeVert imgCrop_marqueeWest"><span></span></div>                        \r
425                  *                              <!-- handles -->\r
426                  *                              <div class="imgCrop_handle imgCrop_handleN"></div>\r
427                  *                              <div class="imgCrop_handle imgCrop_handleNE"></div>\r
428                  *                              <div class="imgCrop_handle imgCrop_handleE"></div>\r
429                  *                              <div class="imgCrop_handle imgCrop_handleSE"></div>\r
430                  *                              <div class="imgCrop_handle imgCrop_handleS"></div>\r
431                  *                              <div class="imgCrop_handle imgCrop_handleSW"></div>\r
432                  *                              <div class="imgCrop_handle imgCrop_handleW"></div>\r
433                  *                              <div class="imgCrop_handle imgCrop_handleNW"></div>\r
434                  *                              <div class="imgCrop_clickArea"></div>\r
435                  *                      </div>  \r
436                  *                      <div class="imgCrop_clickArea"></div>\r
437                  *              </div>  \r
438                  * </div>\r
439                  */\r
440                 var cNamePrefix = 'imgCrop_';\r
441                 \r
442                 // get the point to insert the container\r
443                 var insertPoint = this.img.parentNode;\r
444                 \r
445                 // apply an extra class to the wrapper to fix Opera below version 9\r
446                 var fixOperaClass = '';\r
447                 if( this.isOpera8 ) fixOperaClass = ' opera8';\r
448                 this.imgWrap = Builder.node( 'div', { 'class': cNamePrefix + 'wrap' + fixOperaClass } );\r
449                 \r
450                 this.north              = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'north' }, [Builder.node( 'span' )] );\r
451                 this.east               = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'east' } , [Builder.node( 'span' )] );\r
452                 this.south              = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'south' }, [Builder.node( 'span' )] );\r
453                 this.west               = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'west' } , [Builder.node( 'span' )] );\r
454                 \r
455                 var overlays    = [ this.north, this.east, this.south, this.west ];\r
456 \r
457                 this.dragArea   = Builder.node( 'div', { 'class': cNamePrefix + 'dragArea' }, overlays );\r
458                                                 \r
459                 this.handleN    = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleN' } );\r
460                 this.handleNE   = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleNE' } );\r
461                 this.handleE    = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleE' } );\r
462                 this.handleSE   = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleSE' } );\r
463                 this.handleS    = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleS' } );\r
464                 this.handleSW   = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleSW' } );\r
465                 this.handleW    = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleW' } );\r
466                 this.handleNW   = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleNW' } );\r
467                                 \r
468                 this.selArea    = Builder.node( 'div', { 'class': cNamePrefix + 'selArea' },\r
469                         [\r
470                                 Builder.node( 'div', { 'class': cNamePrefix + 'marqueeHoriz ' + cNamePrefix + 'marqueeNorth' }, [Builder.node( 'span' )] ),\r
471                                 Builder.node( 'div', { 'class': cNamePrefix + 'marqueeVert ' + cNamePrefix + 'marqueeEast' }  , [Builder.node( 'span' )] ),\r
472                                 Builder.node( 'div', { 'class': cNamePrefix + 'marqueeHoriz ' + cNamePrefix + 'marqueeSouth' }, [Builder.node( 'span' )] ),\r
473                                 Builder.node( 'div', { 'class': cNamePrefix + 'marqueeVert ' + cNamePrefix + 'marqueeWest' }  , [Builder.node( 'span' )] ),\r
474                                 this.handleN,\r
475                                 this.handleNE,\r
476                                 this.handleE,\r
477                                 this.handleSE,\r
478                                 this.handleS,\r
479                                 this.handleSW,\r
480                                 this.handleW,\r
481                                 this.handleNW,\r
482                                 Builder.node( 'div', { 'class': cNamePrefix + 'clickArea' } )\r
483                         ]\r
484                 );\r
485                                 \r
486                 this.imgWrap.appendChild( this.img );\r
487                 this.imgWrap.appendChild( this.dragArea );\r
488                 this.dragArea.appendChild( this.selArea );\r
489                 this.dragArea.appendChild( Builder.node( 'div', { 'class': cNamePrefix + 'clickArea' } ) );\r
490 \r
491                 insertPoint.appendChild( this.imgWrap );\r
492 \r
493                 // add event observers\r
494                 this.startDragBind      = this.startDrag.bindAsEventListener( this );\r
495                 Event.observe( this.dragArea, 'mousedown', this.startDragBind );\r
496                 \r
497                 this.onDragBind         = this.onDrag.bindAsEventListener( this );\r
498                 Event.observe( document, 'mousemove', this.onDragBind );\r
499                 \r
500                 this.endCropBind        = this.endCrop.bindAsEventListener( this );\r
501                 Event.observe( document, 'mouseup', this.endCropBind );\r
502                 \r
503                 this.resizeBind         = this.startResize.bindAsEventListener( this );\r
504                 this.handles = [ this.handleN, this.handleNE, this.handleE, this.handleSE, this.handleS, this.handleSW, this.handleW, this.handleNW ];\r
505                 this.registerHandles( true );\r
506                 \r
507                 if( this.options.captureKeys ) {\r
508                         this.keysBind = this.handleKeys.bindAsEventListener( this );\r
509                         Event.observe( document, 'keypress', this.keysBind );\r
510                 }\r
511 \r
512                 // attach the dragable to the select area\r
513                 new CropDraggable( this.selArea, { drawMethod: this.moveArea.bindAsEventListener( this ) } );\r
514                 \r
515                 this.setParams();\r
516         },\r
517         \r
518         /**\r
519          * Manages adding or removing the handle event handler and hiding or displaying them as appropriate\r
520          * \r
521          * @access private\r
522          * @param boolean registration true = add, false = remove\r
523          * @return void\r
524          */\r
525         registerHandles: function( registration ) {     \r
526                 for( var i = 0; i < this.handles.length; i++ ) {\r
527                         var handle = $( this.handles[i] );\r
528                         \r
529                         if( registration ) {\r
530                                 var hideHandle  = false;        // whether to hide the handle\r
531                                 \r
532                                 // disable handles asappropriate if we've got fixed dimensions\r
533                                 // if both dimensions are fixed we don't need to do much\r
534                                 if( this.fixedWidth && this.fixedHeight ) hideHandle = true;\r
535                                 else if( this.fixedWidth || this.fixedHeight ) {\r
536                                         // if one of the dimensions is fixed then just hide those handles\r
537                                         var isCornerHandle      = handle.className.match( /([S|N][E|W])$/ )\r
538                                         var isWidthHandle       = handle.className.match( /(E|W)$/ );\r
539                                         var isHeightHandle      = handle.className.match( /(N|S)$/ );\r
540                                         if( isCornerHandle ) hideHandle = true;\r
541                                         else if( this.fixedWidth && isWidthHandle ) hideHandle = true;\r
542                                         else if( this.fixedHeight && isHeightHandle ) hideHandle = true;\r
543                                 }\r
544                                 if( hideHandle ) handle.hide();\r
545                                 else Event.observe( handle, 'mousedown', this.resizeBind );\r
546                         } else {\r
547                                 handle.show();\r
548                                 Event.stopObserving( handle, 'mousedown', this.resizeBind );\r
549                         }\r
550                 }\r
551         },\r
552                 \r
553         /**\r
554          * Sets up all the cropper parameters, this can be used to reset the cropper when dynamically\r
555          * changing the images\r
556          * \r
557          * @access private\r
558          * @return void\r
559          */\r
560         setParams: function() {\r
561                 /**\r
562                  * @var int\r
563                  * The image width\r
564                  */\r
565                 this.imgW = this.img.width;\r
566                 /**\r
567                  * @var int\r
568                  * The image height\r
569                  */\r
570                 this.imgH = this.img.height;                    \r
571 \r
572                 $( this.north ).setStyle( { height: 0 } );\r
573                 $( this.east ).setStyle( { width: 0, height: 0 } );\r
574                 $( this.south ).setStyle( { height: 0 } );\r
575                 $( this.west ).setStyle( { width: 0, height: 0 } );\r
576                 \r
577                 // resize the container to fit the image\r
578                 $( this.imgWrap ).setStyle( { 'width': this.imgW + 'px', 'height': this.imgH + 'px' } );\r
579                 \r
580                 // hide the select area\r
581                 $( this.selArea ).hide();\r
582                                                 \r
583                 // setup the starting position of the select area\r
584                 var startCoords = { x1: 0, y1: 0, x2: 0, y2: 0 };\r
585                 var validCoordsSet = false;\r
586                 \r
587                 // display the select area \r
588                 if( this.options.onloadCoords != null ) {\r
589                         // if we've being given some coordinates to \r
590                         startCoords = this.cloneCoords( this.options.onloadCoords );\r
591                         validCoordsSet = true;\r
592                 } else if( this.options.ratioDim.x > 0 && this.options.ratioDim.y > 0 ) {\r
593                         // if there is a ratio limit applied and the then set it to initial ratio\r
594                         startCoords.x1 = Math.ceil( ( this.imgW - this.options.ratioDim.x ) / 2 );\r
595                         startCoords.y1 = Math.ceil( ( this.imgH - this.options.ratioDim.y ) / 2 );\r
596                         startCoords.x2 = startCoords.x1 + this.options.ratioDim.x;\r
597                         startCoords.y2 = startCoords.y1 + this.options.ratioDim.y;\r
598                         validCoordsSet = true;\r
599                 }\r
600                 \r
601                 this.setAreaCoords( startCoords, false, false, 1 );\r
602                 \r
603                 if( this.options.displayOnInit && validCoordsSet ) {\r
604                         this.selArea.show();\r
605                         this.drawArea();\r
606                         this.endCrop();\r
607                 }\r
608                 \r
609                 this.attached = true;\r
610         },\r
611         \r
612         /**\r
613          * Removes the cropper\r
614          * \r
615          * @access public\r
616          * @return void\r
617          */\r
618         remove: function() {\r
619                 if( this.attached ) {\r
620                         this.attached = false;\r
621                         \r
622                         // remove the elements we inserted\r
623                         this.imgWrap.parentNode.insertBefore( this.img, this.imgWrap );\r
624                         this.imgWrap.parentNode.removeChild( this.imgWrap );\r
625                         \r
626                         // remove the event observers\r
627                         Event.stopObserving( this.dragArea, 'mousedown', this.startDragBind );\r
628                         Event.stopObserving( document, 'mousemove', this.onDragBind );          \r
629                         Event.stopObserving( document, 'mouseup', this.endCropBind );\r
630                         this.registerHandles( false );\r
631                         if( this.options.captureKeys ) Event.stopObserving( document, 'keypress', this.keysBind );\r
632                 }\r
633         },\r
634         \r
635         /**\r
636          * Resets the cropper, can be used either after being removed or any time you wish\r
637          * \r
638          * @access public\r
639          * @return void\r
640          */\r
641         reset: function() {\r
642                 if( !this.attached ) this.onLoad();\r
643                 else this.setParams();\r
644                 this.endCrop();\r
645         },\r
646         \r
647         /**\r
648          * Handles the key functionality, currently just using arrow keys to move, if the user\r
649          * presses shift then the area will move by 10 pixels\r
650          */\r
651         handleKeys: function( e ) {\r
652                 var dir = { x: 0, y: 0 }; // direction to move it in & the amount in pixels\r
653                 if( !this.dragging ) {\r
654                         \r
655                         // catch the arrow keys\r
656                         switch( e.keyCode ) {\r
657                                 case( 37 ) : // left\r
658                                         dir.x = -1;\r
659                                         break;\r
660                                 case( 38 ) : // up\r
661                                         dir.y = -1;\r
662                                         break;\r
663                                 case( 39 ) : // right\r
664                                         dir.x = 1;\r
665                                         break\r
666                                 case( 40 ) : // down\r
667                                         dir.y = 1;\r
668                                         break;\r
669                         }\r
670                         \r
671                         if( dir.x != 0 || dir.y != 0 ) {\r
672                                 // if shift is pressed then move by 10 pixels\r
673                                 if( e.shiftKey ) {\r
674                                         dir.x *= 10;\r
675                                         dir.y *= 10;\r
676                                 }\r
677                                 \r
678                                 this.moveArea( [ this.areaCoords.x1 + dir.x, this.areaCoords.y1 + dir.y ] );\r
679                                 Event.stop( e ); \r
680                         }\r
681                 }\r
682         },\r
683         \r
684         /**\r
685          * Calculates the width from the areaCoords\r
686          * \r
687          * @access private\r
688          * @return int\r
689          */\r
690         calcW: function() {\r
691                 return (this.areaCoords.x2 - this.areaCoords.x1)\r
692         },\r
693         \r
694         /**\r
695          * Calculates the height from the areaCoords\r
696          * \r
697          * @access private\r
698          * @return int\r
699          */\r
700         calcH: function() {\r
701                 return (this.areaCoords.y2 - this.areaCoords.y1)\r
702         },\r
703         \r
704         /**\r
705          * Moves the select area to the supplied point (assumes the point is x1 & y1 of the select area)\r
706          * \r
707          * @access public\r
708          * @param array Point for x1 & y1 to move select area to\r
709          * @return void\r
710          */\r
711         moveArea: function( point ) {\r
712                 // dump( 'moveArea        : ' + point[0] + ',' + point[1] + ',' + ( point[0] + ( this.areaCoords.x2 - this.areaCoords.x1 ) ) + ',' + ( point[1] + ( this.areaCoords.y2 - this.areaCoords.y1 ) ) + '\n' );\r
713                 this.setAreaCoords( \r
714                         {\r
715                                 x1: point[0], \r
716                                 y1: point[1],\r
717                                 x2: point[0] + this.calcW(),\r
718                                 y2: point[1] + this.calcH()\r
719                         },\r
720                         true,\r
721                         false\r
722                 );\r
723                 this.drawArea();\r
724         },\r
725 \r
726         /**\r
727          * Clones a co-ordinates object, stops problems with handling them by reference\r
728          * \r
729          * @access private\r
730          * @param obj Coordinate object x1, y1, x2, y2\r
731          * @return obj Coordinate object x1, y1, x2, y2\r
732          */\r
733         cloneCoords: function( coords ) {\r
734                 return { x1: coords.x1, y1: coords.y1, x2: coords.x2, y2: coords.y2 };\r
735         },\r
736 \r
737         /**\r
738          * Sets the select coords to those provided but ensures they don't go\r
739          * outside the bounding box\r
740          * \r
741          * @access private\r
742          * @param obj Coordinates x1, y1, x2, y2\r
743          * @param boolean Whether this is a move\r
744          * @param boolean Whether to apply squaring\r
745          * @param obj Direction of mouse along both axis x, y ( -1 = negative, 1 = positive ) only required when moving etc.\r
746          * @param string The current resize handle || null\r
747          * @return void\r
748          */\r
749         setAreaCoords: function( coords, moving, square, direction, resizeHandle ) {\r
750                 // dump( 'setAreaCoords (in) : ' + coords.x1 + ',' + coords.y1 + ',' + coords.x2 + ',' + coords.y2 );\r
751                 if( moving ) {\r
752                         // if moving\r
753                         var targW = coords.x2 - coords.x1;\r
754                         var targH = coords.y2 - coords.y1;\r
755                         \r
756                         // ensure we're within the bounds\r
757                         if( coords.x1 < 0 ) {\r
758                                 coords.x1 = 0;\r
759                                 coords.x2 = targW;\r
760                         }\r
761                         if( coords.y1 < 0 ) {\r
762                                 coords.y1 = 0;\r
763                                 coords.y2 = targH;\r
764                         }\r
765                         if( coords.x2 > this.imgW ) {\r
766                                 coords.x2 = this.imgW;\r
767                                 coords.x1 = this.imgW - targW;\r
768                         }\r
769                         if( coords.y2 > this.imgH ) {\r
770                                 coords.y2 = this.imgH;\r
771                                 coords.y1 = this.imgH - targH;\r
772                         }                       \r
773                 } else {\r
774                         // ensure we're within the bounds\r
775                         if( coords.x1 < 0 ) coords.x1 = 0;\r
776                         if( coords.y1 < 0 ) coords.y1 = 0;\r
777                         if( coords.x2 > this.imgW ) coords.x2 = this.imgW;\r
778                         if( coords.y2 > this.imgH ) coords.y2 = this.imgH;\r
779                         \r
780                         // This is passed as null in onload\r
781                         if( direction != null ) {\r
782                                                                 \r
783                                 // apply the ratio or squaring where appropriate\r
784                                 if( this.ratioX > 0 ) this.applyRatio( coords, { x: this.ratioX, y: this.ratioY }, direction, resizeHandle );\r
785                                 else if( square ) this.applyRatio( coords, { x: 1, y: 1 }, direction, resizeHandle );\r
786                                                                                 \r
787                                 var mins = [ this.options.minWidth, this.options.minHeight ]; // minimum dimensions [x,y]                       \r
788                                 var maxs = [ this.options.maxWidth, this.options.maxHeight ]; // maximum dimensions [x,y]\r
789                 \r
790                                 // apply dimensions where appropriate\r
791                                 if( mins[0] > 0 || mins[1] > 0 || maxs[0] > 0 || maxs[1] > 0) {\r
792                                 \r
793                                         var coordsTransX        = { a1: coords.x1, a2: coords.x2 };\r
794                                         var coordsTransY        = { a1: coords.y1, a2: coords.y2 };\r
795                                         var boundsX                     = { min: 0, max: this.imgW };\r
796                                         var boundsY                     = { min: 0, max: this.imgH };\r
797                                         \r
798                                         // handle squaring properly on single axis minimum dimensions\r
799                                         if( (mins[0] != 0 || mins[1] != 0) && square ) {\r
800                                                 if( mins[0] > 0 ) mins[1] = mins[0];\r
801                                                 else if( mins[1] > 0 ) mins[0] = mins[1];\r
802                                         }\r
803                                         \r
804                                         if( (maxs[0] != 0 || maxs[0] != 0) && square ) {\r
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)\r
806                                                 if( maxs[0] > 0 && maxs[0] <= maxs[1] ) maxs[1] = maxs[0];\r
807                                                 else if( maxs[1] > 0 && maxs[1] <= maxs[0] ) maxs[0] = maxs[1];\r
808                                         }\r
809                                         \r
810                                         if( mins[0] > 0 ) this.applyDimRestriction( coordsTransX, mins[0], direction.x, boundsX, 'min' );\r
811                                         if( mins[1] > 1 ) this.applyDimRestriction( coordsTransY, mins[1], direction.y, boundsY, 'min' );\r
812                                         \r
813                                         if( maxs[0] > 0 ) this.applyDimRestriction( coordsTransX, maxs[0], direction.x, boundsX, 'max' );\r
814                                         if( maxs[1] > 1 ) this.applyDimRestriction( coordsTransY, maxs[1], direction.y, boundsY, 'max' );\r
815                                         \r
816                                         coords = { x1: coordsTransX.a1, y1: coordsTransY.a1, x2: coordsTransX.a2, y2: coordsTransY.a2 };\r
817                                 }\r
818                                 \r
819                         }\r
820                 }\r
821                 \r
822                 // dump( 'setAreaCoords (out) : ' + coords.x1 + ',' + coords.y1 + ',' + coords.x2 + ',' + coords.y2 + '\n' );\r
823                 this.areaCoords = coords;\r
824         },\r
825         \r
826         /**\r
827          * Applies the supplied dimension restriction to the supplied coordinates along a single axis\r
828          * \r
829          * @access private\r
830          * @param obj Single axis coordinates, a1, a2 (e.g. for the x axis a1 = x1 & a2 = x2)\r
831          * @param int The restriction value\r
832          * @param int The direction ( -1 = negative, 1 = positive )\r
833          * @param obj The bounds of the image ( for this axis )\r
834          * @param string The dimension restriction type ( 'min' | 'max' )\r
835          * @return void\r
836          */\r
837         applyDimRestriction: function( coords, val, direction, bounds, type ) {\r
838                 var check;\r
839                 if( type == 'min' ) check = ( ( coords.a2 - coords.a1 ) < val );\r
840                 else check = ( ( coords.a2 - coords.a1 ) > val );\r
841                 if( check ) {\r
842                         if( direction == 1 ) coords.a2 = coords.a1 + val;\r
843                         else coords.a1 = coords.a2 - val;\r
844                         \r
845                         // make sure we're still in the bounds (not too pretty for the user, but needed)\r
846                         if( coords.a1 < bounds.min ) {\r
847                                 coords.a1 = bounds.min;\r
848                                 coords.a2 = val;\r
849                         } else if( coords.a2 > bounds.max ) {\r
850                                 coords.a1 = bounds.max - val;\r
851                                 coords.a2 = bounds.max;\r
852                         }\r
853                 }\r
854         },\r
855                 \r
856         /**\r
857          * Applies the supplied ratio to the supplied coordinates\r
858          * \r
859          * @access private\r
860          * @param obj Coordinates, x1, y1, x2, y2\r
861          * @param obj Ratio, x, y\r
862          * @param obj Direction of mouse, x & y : -1 == negative 1 == positive\r
863          * @param string The current resize handle || null\r
864          * @return void\r
865          */\r
866         applyRatio : function( coords, ratio, direction, resizeHandle ) {\r
867                 // dump( 'direction.y : ' + direction.y + '\n');\r
868                 var newCoords;\r
869                 if( resizeHandle == 'N' || resizeHandle == 'S' ) {\r
870                         // dump( 'north south \n');\r
871                         // if moving on either the lone north & south handles apply the ratio on the y axis\r
872                         newCoords = this.applyRatioToAxis( \r
873                                 { a1: coords.y1, b1: coords.x1, a2: coords.y2, b2: coords.x2 },\r
874                                 { a: ratio.y, b: ratio.x },\r
875                                 { a: direction.y, b: direction.x },\r
876                                 { min: 0, max: this.imgW }\r
877                         );\r
878                         coords.x1 = newCoords.b1;\r
879                         coords.y1 = newCoords.a1;\r
880                         coords.x2 = newCoords.b2;\r
881                         coords.y2 = newCoords.a2;\r
882                 } else {\r
883                         // otherwise deal with it as if we're applying the ratio on the x axis\r
884                         newCoords = this.applyRatioToAxis( \r
885                                 { a1: coords.x1, b1: coords.y1, a2: coords.x2, b2: coords.y2 },\r
886                                 { a: ratio.x, b: ratio.y },\r
887                                 { a: direction.x, b: direction.y },\r
888                                 { min: 0, max: this.imgH }\r
889                         );\r
890                         coords.x1 = newCoords.a1;\r
891                         coords.y1 = newCoords.b1;\r
892                         coords.x2 = newCoords.a2;\r
893                         coords.y2 = newCoords.b2;\r
894                 }\r
895                 \r
896         },\r
897         \r
898         /**\r
899          * Applies the provided ratio to the provided coordinates based on provided direction & bounds,\r
900          * use to encapsulate functionality to make it easy to apply to either axis. This is probably\r
901          * quite hard to visualise so see the x axis example within applyRatio()\r
902          * \r
903          * Example in parameter details & comments is for requesting applying ratio to x axis.\r
904          * \r
905          * @access private\r
906          * @param obj Coords object (a1, b1, a2, b2) where a = x & b = y in example\r
907          * @param obj Ratio object (a, b) where a = x & b = y in example\r
908          * @param obj Direction object (a, b) where a = x & b = y in example\r
909          * @param obj Bounds (min, max)\r
910          * @return obj Coords object (a1, b1, a2, b2) where a = x & b = y in example\r
911          */\r
912         applyRatioToAxis: function( coords, ratio, direction, bounds ) {\r
913                 var newCoords = Object.extend( coords, {} );\r
914                 var calcDimA = newCoords.a2 - newCoords.a1;                     // calculate dimension a (e.g. width)\r
915                 var targDimB = Math.floor( calcDimA * ratio.b / ratio.a );      // the target dimension b (e.g. height)\r
916                 var targB;                                                                                      // to hold target b (e.g. y value)\r
917                 var targDimA;                                           // to hold target dimension a (e.g. width)\r
918                 var calcDimB = null;                                                            // to hold calculated dimension b (e.g. height)\r
919                 \r
920                 // dump( 'newCoords[0]: ' + newCoords.a1 + ',' + newCoords.b1 + ','+ newCoords.a2 + ',' + newCoords.b2 + '\n');\r
921                                 \r
922                 if( direction.b == 1 ) {                                                        // if travelling in a positive direction\r
923                         // make sure we're not going out of bounds\r
924                         targB = newCoords.b1 + targDimB;\r
925                         if( targB > bounds.max ) {\r
926                                 targB = bounds.max;\r
927                                 calcDimB = targB - newCoords.b1;                        // calcuate dimension b (e.g. height)\r
928                         }\r
929                         \r
930                         newCoords.b2 = targB;\r
931                 } else {                                                                                        // if travelling in a negative direction\r
932                         // make sure we're not going out of bounds\r
933                         targB = newCoords.b2 - targDimB;\r
934                         if( targB < bounds.min ) {\r
935                                 targB = bounds.min;\r
936                                 calcDimB = targB + newCoords.b2;                        // calcuate dimension b (e.g. height)\r
937                         }\r
938                         newCoords.b1 = targB;\r
939                 }\r
940                 \r
941                 // dump( 'newCoords[1]: ' + newCoords.a1 + ',' + newCoords.b1 + ','+ newCoords.a2 + ',' + newCoords.b2 + '\n');\r
942                         \r
943                 // apply the calculated dimensions\r
944                 if( calcDimB != null ) {\r
945                         targDimA = Math.floor( calcDimB * ratio.a / ratio.b );\r
946                         \r
947                         if( direction.a == 1 ) newCoords.a2 = newCoords.a1 + targDimA;\r
948                         else newCoords.a1 = newCoords.a1 = newCoords.a2 - targDimA;\r
949                 }\r
950                 \r
951                 // dump( 'newCoords[2]: ' + newCoords.a1 + ',' + newCoords.b1 + ','+ newCoords.a2 + ',' + newCoords.b2 + '\n');\r
952                         \r
953                 return newCoords;\r
954         },\r
955         \r
956         /**\r
957          * Draws the select area\r
958          * \r
959          * @access private\r
960          * @return void\r
961          */\r
962         drawArea: function( ) { \r
963                 /*\r
964                  * NOTE: I'm not using the Element.setStyle() shortcut as they make it \r
965                  * quite sluggish on Mac based browsers\r
966                  */\r
967                 // dump( 'drawArea        : ' + this.areaCoords.x1 + ',' + this.areaCoords.y1 + ',' + this.areaCoords.x2 + ',' + this.areaCoords.y2 + '\n' );\r
968                 var areaWidth     = this.calcW();\r
969                 var areaHeight    = this.calcH();\r
970                 \r
971                 /*\r
972                  * Calculate all the style strings before we use them, allows reuse & produces quicker\r
973                  * rendering (especially noticable in Mac based browsers)\r
974                  */\r
975                 var px = 'px';\r
976                 var params = [\r
977                         this.areaCoords.x1 + px,        // the left of the selArea\r
978                         this.areaCoords.y1 + px,                // the top of the selArea\r
979                         areaWidth + px,                                 // width of the selArea\r
980                         areaHeight + px,                                        // height of the selArea\r
981                         this.areaCoords.x2 + px,                // bottom of the selArea\r
982                         this.areaCoords.y2 + px,                // right of the selArea\r
983                         (this.img.width - this.areaCoords.x2) + px,     // right edge of selArea\r
984                         (this.img.height - this.areaCoords.y2) + px     // bottom edge of selArea\r
985                 ];\r
986                                 \r
987                 // do the select area\r
988                 var areaStyle                           = this.selArea.style;\r
989                 areaStyle.left                          = params[0];\r
990                 areaStyle.top                           = params[1];\r
991                 areaStyle.width                         = params[2];\r
992                 areaStyle.height                        = params[3];\r
993                                 \r
994                 // position the north, east, south & west handles\r
995                 var horizHandlePos = Math.ceil( (areaWidth - 6) / 2 ) + px;\r
996                 var vertHandlePos = Math.ceil( (areaHeight - 6) / 2 ) + px;\r
997                 \r
998                 this.handleN.style.left         = horizHandlePos;\r
999                 this.handleE.style.top          = vertHandlePos;\r
1000                 this.handleS.style.left         = horizHandlePos;\r
1001                 this.handleW.style.top          = vertHandlePos;\r
1002                 \r
1003                 // draw the four overlays\r
1004                 this.north.style.height         = params[1];\r
1005                 \r
1006                 var eastStyle                           = this.east.style;\r
1007                 eastStyle.top                           = params[1];\r
1008                 eastStyle.height                        = params[3];\r
1009                 eastStyle.left                          = params[4];\r
1010             eastStyle.width                             = params[6];\r
1011            \r
1012                 var southStyle                          = this.south.style;\r
1013                 southStyle.top                          = params[5];\r
1014                 southStyle.height                       = params[7];\r
1015            \r
1016             var westStyle                       = this.west.style;\r
1017             westStyle.top                               = params[1];\r
1018             westStyle.height                    = params[3];\r
1019                 westStyle.width                         = params[0];\r
1020                 \r
1021                 // call the draw method on sub classes\r
1022                 this.subDrawArea();\r
1023                 \r
1024                 this.forceReRender();\r
1025         },\r
1026         \r
1027         /**\r
1028          * Force the re-rendering of the selArea element which fixes rendering issues in Safari \r
1029          * & IE PC, especially evident when re-sizing perfectly vertical using any of the south handles\r
1030          * \r
1031          * @access private\r
1032          * @return void\r
1033          */\r
1034         forceReRender: function() {\r
1035                 if( this.isIE || this.isWebKit) {\r
1036                         var n = document.createTextNode(' ');\r
1037                         var d,el,fixEL,i;\r
1038                 \r
1039                         if( this.isIE ) fixEl = this.selArea;\r
1040                         else if( this.isWebKit ) {\r
1041                                 fixEl = document.getElementsByClassName( 'imgCrop_marqueeSouth', this.imgWrap )[0];\r
1042                                 /* we have to be a bit more forceful for Safari, otherwise the the marquee &\r
1043                                  * the south handles still don't move\r
1044                                  */ \r
1045                                 d = Builder.node( 'div', '' );\r
1046                                 d.style.visibility = 'hidden';\r
1047                                 \r
1048                                 var classList = ['SE','S','SW'];\r
1049                                 for( i = 0; i < classList.length; i++ ) {\r
1050                                         el = document.getElementsByClassName( 'imgCrop_handle' + classList[i], this.selArea )[0];\r
1051                                         if( el.childNodes.length ) el.removeChild( el.childNodes[0] );\r
1052                                         el.appendChild(d);\r
1053                                 }\r
1054                         }\r
1055                         fixEl.appendChild(n);\r
1056                         fixEl.removeChild(n);\r
1057                 }\r
1058         },\r
1059         \r
1060         /**\r
1061          * Starts the resize\r
1062          * \r
1063          * @access private\r
1064          * @param obj Event\r
1065          * @return void\r
1066          */\r
1067         startResize: function( e ) {\r
1068                 this.startCoords = this.cloneCoords( this.areaCoords );\r
1069                 \r
1070                 this.resizing = true;\r
1071                 this.resizeHandle = Event.element( e ).classNames().toString().replace(/([^N|NE|E|SE|S|SW|W|NW])+/, '');\r
1072                 // dump( 'this.resizeHandle : ' + this.resizeHandle + '\n' );\r
1073                 Event.stop( e );\r
1074         },\r
1075         \r
1076         /**\r
1077          * Starts the drag\r
1078          * \r
1079          * @access private\r
1080          * @param obj Event\r
1081          * @return void\r
1082          */\r
1083         startDrag: function( e ) {      \r
1084                 this.selArea.show();\r
1085                 this.clickCoords = this.getCurPos( e );\r
1086         \r
1087         this.setAreaCoords( { x1: this.clickCoords.x, y1: this.clickCoords.y, x2: this.clickCoords.x, y2: this.clickCoords.y }, false, false, null );\r
1088         \r
1089         this.dragging = true;\r
1090         this.onDrag( e ); // incase the user just clicks once after already making a selection\r
1091         Event.stop( e );\r
1092         },\r
1093         \r
1094         /**\r
1095          * Gets the current cursor position relative to the image\r
1096          * \r
1097          * @access private\r
1098          * @param obj Event\r
1099          * @return obj x,y pixels of the cursor\r
1100          */\r
1101         getCurPos: function( e ) {\r
1102                 // get the offsets for the wrapper within the document\r
1103                 var el = this.imgWrap, wrapOffsets = Position.cumulativeOffset( el );\r
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\r
1105                 while( el.nodeName != 'BODY' ) {\r
1106                         wrapOffsets[1] -= el.scrollTop  || 0;\r
1107                         wrapOffsets[0] -= el.scrollLeft || 0;\r
1108                         el = el.parentNode;\r
1109             }           \r
1110                 return curPos = { \r
1111                         x: Event.pointerX(e) - wrapOffsets[0],\r
1112                         y: Event.pointerY(e) - wrapOffsets[1]\r
1113                 }\r
1114         },\r
1115         \r
1116         /**\r
1117          * Performs the drag for both resize & inital draw dragging\r
1118          * \r
1119          * @access private\r
1120          * @param obj Event\r
1121          * @return void\r
1122          */\r
1123         onDrag: function( e ) {\r
1124                 if( this.dragging || this.resizing ) {  \r
1125                 \r
1126                         var resizeHandle = null;\r
1127                         var curPos = this.getCurPos( e );                       \r
1128                         var newCoords = this.cloneCoords( this.areaCoords );\r
1129                         var direction = { x: 1, y: 1 };\r
1130                                                 \r
1131                     if( this.dragging ) {\r
1132                         if( curPos.x < this.clickCoords.x ) direction.x = -1;\r
1133                         if( curPos.y < this.clickCoords.y ) direction.y = -1;\r
1134                         \r
1135                                 this.transformCoords( curPos.x, this.clickCoords.x, newCoords, 'x' );\r
1136                                 this.transformCoords( curPos.y, this.clickCoords.y, newCoords, 'y' );\r
1137                         } else if( this.resizing ) {\r
1138                                 resizeHandle = this.resizeHandle;                       \r
1139                                 // do x movements first\r
1140                                 if( resizeHandle.match(/E/) ) {\r
1141                                         // if we're moving an east handle\r
1142                                         this.transformCoords( curPos.x, this.startCoords.x1, newCoords, 'x' );  \r
1143                                         if( curPos.x < this.startCoords.x1 ) direction.x = -1;\r
1144                                 } else if( resizeHandle.match(/W/) ) {\r
1145                                         // if we're moving an west handle\r
1146                                         this.transformCoords( curPos.x, this.startCoords.x2, newCoords, 'x' );\r
1147                                         if( curPos.x < this.startCoords.x2 ) direction.x = -1;\r
1148                                 }\r
1149                                                                         \r
1150                                 // do y movements second\r
1151                                 if( resizeHandle.match(/N/) ) {\r
1152                                         // if we're moving an north handle      \r
1153                                         this.transformCoords( curPos.y, this.startCoords.y2, newCoords, 'y' );\r
1154                                         if( curPos.y < this.startCoords.y2 ) direction.y = -1;\r
1155                                 } else if( resizeHandle.match(/S/) ) {\r
1156                                         // if we're moving an south handle\r
1157                                         this.transformCoords( curPos.y, this.startCoords.y1, newCoords, 'y' );  \r
1158                                         if( curPos.y < this.startCoords.y1 ) direction.y = -1;\r
1159                                 }       \r
1160                                                         \r
1161                         }\r
1162                 \r
1163                         this.setAreaCoords( newCoords, false, e.shiftKey, direction, resizeHandle );\r
1164                         this.drawArea();\r
1165                         Event.stop( e ); // stop the default event (selecting images & text) in Safari & IE PC\r
1166                 }\r
1167         },\r
1168         \r
1169         /**\r
1170          * Applies the appropriate transform to supplied co-ordinates, on the\r
1171          * defined axis, depending on the relationship of the supplied values\r
1172          * \r
1173          * @access private\r
1174          * @param int Current value of pointer\r
1175          * @param int Base value to compare current pointer val to\r
1176          * @param obj Coordinates to apply transformation on x1, x2, y1, y2\r
1177          * @param string Axis to apply transformation on 'x' || 'y'\r
1178          * @return void\r
1179          */\r
1180         transformCoords : function( curVal, baseVal, coords, axis ) {\r
1181                 var newVals = [ curVal, baseVal ];\r
1182                 if( curVal > baseVal ) newVals.reverse();\r
1183                 coords[ axis + '1' ] = newVals[0];\r
1184                 coords[ axis + '2' ] = newVals[1];              \r
1185         },\r
1186         \r
1187         /**\r
1188          * Ends the crop & passes the values of the select area on to the appropriate \r
1189          * callback function on completion of a crop\r
1190          * \r
1191          * @access private\r
1192          * @return void\r
1193          */\r
1194         endCrop : function() {\r
1195                 this.dragging = false;\r
1196                 this.resizing = false;\r
1197                 \r
1198                 this.options.onEndCrop(\r
1199                         this.areaCoords,\r
1200                         {\r
1201                                 width: this.calcW(), \r
1202                                 height: this.calcH() \r
1203                         }\r
1204                 );\r
1205         },\r
1206         \r
1207         /**\r
1208          * Abstract method called on the end of initialization\r
1209          * \r
1210          * @access private\r
1211          * @abstract\r
1212          * @return void\r
1213          */\r
1214         subInitialize: function() {},\r
1215         \r
1216         /**\r
1217          * Abstract method called on the end of drawArea()\r
1218          * \r
1219          * @access private\r
1220          * @abstract\r
1221          * @return void\r
1222          */\r
1223         subDrawArea: function() {}\r
1224 };\r
1225 \r
1226 \r
1227 \r
1228 \r
1229 /**\r
1230  * Extend the Cropper.Img class to allow for presentation of a preview image of the resulting crop,\r
1231  * the option for displayOnInit is always overridden to true when displaying a preview image\r
1232  * \r
1233  * Usage:\r
1234  *      @param obj Image element to attach to\r
1235  *      @param obj Optional options:\r
1236  *              - see Cropper.Img for base options\r
1237  *              \r
1238  *              - previewWrap obj\r
1239  *                      HTML element that will be used as a container for the preview image             \r
1240  */\r
1241 Cropper.ImgWithPreview = Class.create();\r
1242 \r
1243 Object.extend( Object.extend( Cropper.ImgWithPreview.prototype, Cropper.Img.prototype ), {\r
1244         \r
1245         /**\r
1246          * Implements the abstract method from Cropper.Img to initialize preview image settings.\r
1247          * Will only attach a preview image is the previewWrap element is defined and the minWidth\r
1248          * & minHeight options are set.\r
1249          * \r
1250          * @see Croper.Img.subInitialize\r
1251          */\r
1252         subInitialize: function() {\r
1253                 /**\r
1254                  * Whether or not we've attached a preview image\r
1255                  * @var boolean\r
1256                  */\r
1257                 this.hasPreviewImg = false;\r
1258                 if( typeof(this.options.previewWrap) != 'undefined' \r
1259                         && this.options.minWidth > 0 \r
1260                         && this.options.minHeight > 0\r
1261                 ) {\r
1262                         /**\r
1263                          * The preview image wrapper element\r
1264                          * @var obj HTML element\r
1265                          */\r
1266                         this.previewWrap        = $( this.options.previewWrap );\r
1267                         /**\r
1268                          * The preview image element\r
1269                          * @var obj HTML IMG element\r
1270                          */\r
1271                         this.previewImg         = this.img.cloneNode( false );\r
1272                         // set the ID of the preview image to be unique\r
1273                         this.previewImg.id      = 'imgCrop_' + this.previewImg.id;\r
1274                         \r
1275                                                 \r
1276                         // set the displayOnInit option to true so we display the select area at the same time as the thumbnail\r
1277                         this.options.displayOnInit = true;\r
1278 \r
1279                         this.hasPreviewImg      = true;\r
1280                         \r
1281                         this.previewWrap.addClassName( 'imgCrop_previewWrap' );\r
1282                         \r
1283                         this.previewWrap.setStyle(\r
1284                          { \r
1285                                 width: this.options.minWidth + 'px',\r
1286                                 height: this.options.minHeight + 'px'\r
1287                          }\r
1288                         );\r
1289                         \r
1290                         this.previewWrap.appendChild( this.previewImg );\r
1291                 }\r
1292         },\r
1293         \r
1294         /**\r
1295          * Implements the abstract method from Cropper.Img to draw the preview image\r
1296          * \r
1297          * @see Croper.Img.subDrawArea\r
1298          */\r
1299         subDrawArea: function() {\r
1300                 if( this.hasPreviewImg ) {\r
1301                         // get the ratio of the select area to the src image\r
1302                         var calcWidth = this.calcW();\r
1303                         var calcHeight = this.calcH();\r
1304                         // ratios for the dimensions of the preview image\r
1305                         var dimRatio = { \r
1306                                 x: this.imgW / calcWidth, \r
1307                                 y: this.imgH / calcHeight \r
1308                         }; \r
1309                         //ratios for the positions within the preview\r
1310                         var posRatio = { \r
1311                                 x: calcWidth / this.options.minWidth, \r
1312                                 y: calcHeight / this.options.minHeight \r
1313                         };\r
1314                         \r
1315                         // setting the positions in an obj before apply styles for rendering speed increase\r
1316                         var calcPos     = {\r
1317                                 w: Math.ceil( this.options.minWidth * dimRatio.x ) + 'px',\r
1318                                 h: Math.ceil( this.options.minHeight * dimRatio.y ) + 'px',\r
1319                                 x: '-' + Math.ceil( this.areaCoords.x1 / posRatio.x )  + 'px',\r
1320                                 y: '-' + Math.ceil( this.areaCoords.y1 / posRatio.y ) + 'px'\r
1321                         }\r
1322                         \r
1323                         var previewStyle        = this.previewImg.style;\r
1324                         previewStyle.width      = calcPos.w;\r
1325                         previewStyle.height     = calcPos.h;\r
1326                         previewStyle.left       = calcPos.x;\r
1327                         previewStyle.top        = calcPos.y;\r
1328                 }\r
1329         }\r
1330         \r
1331 });\r

Benjamin Mako Hill || Want to submit a patch?