aboutsummaryrefslogtreecommitdiffstats
path: root/cropper
diff options
context:
space:
mode:
Diffstat (limited to 'cropper')
-rw-r--r--cropper/copper.html227
-rw-r--r--cropper/cropper.css182
-rw-r--r--cropper/cropper.js566
-rw-r--r--cropper/cropper.uncompressed.js1331
-rw-r--r--cropper/lib/builder.js101
-rw-r--r--cropper/lib/controls.js815
-rw-r--r--cropper/lib/dragdrop.js915
-rw-r--r--cropper/lib/effects.js958
-rw-r--r--cropper/lib/prototype.js2006
-rw-r--r--cropper/lib/scriptaculous.js47
-rw-r--r--cropper/lib/slider.js283
-rw-r--r--cropper/lib/unittest.js383
-rw-r--r--cropper/licence.txt12
-rw-r--r--cropper/marqueeHoriz.gifbin0 -> 1125 bytes
-rw-r--r--cropper/marqueeVert.gifbin0 -> 1141 bytes
-rw-r--r--cropper/tests/castle.jpgbin0 -> 34429 bytes
-rw-r--r--cropper/tests/castleMed.jpgbin0 -> 50584 bytes
-rw-r--r--cropper/tests/example-Basic.htm106
-rw-r--r--cropper/tests/example-CSS-Absolute.htm162
-rw-r--r--cropper/tests/example-CSS-Float.htm124
-rw-r--r--cropper/tests/example-CSS-Relative.htm116
-rw-r--r--cropper/tests/example-CoordsOnLoad.htm108
-rw-r--r--cropper/tests/example-CoordsOnLoadWithRatio.htm109
-rw-r--r--cropper/tests/example-Dimensions.htm225
-rw-r--r--cropper/tests/example-DynamicImage.htm203
-rw-r--r--cropper/tests/example-FixedRatio.htm104
-rw-r--r--cropper/tests/example-MinimumDimensions.htm105
-rw-r--r--cropper/tests/example-MinimumWidth.htm105
-rw-r--r--cropper/tests/example-Preview.htm117
-rw-r--r--cropper/tests/poppy.jpgbin0 -> 18338 bytes
-rw-r--r--cropper/tests/staticHTMLStructure.htm236
31 files changed, 9646 insertions, 0 deletions
diff --git a/cropper/copper.html b/cropper/copper.html
new file mode 100644
index 000000000..6e3f09ebc
--- /dev/null
+++ b/cropper/copper.html
@@ -0,0 +1,227 @@
+ 1.
+ <script type="text/javascript" src="scripts/cropper/lib/prototype.js" language="javascript"></script>
+ 2.
+ <script type="text/javascript" src="scripts/cropper/lib/scriptaculous.js?load=builder,dragdrop" language="javascript"></script>
+ 3.
+ <script type="text/javascript" src="scripts/cropper/cropper.js" language="javascript"></script>
+
+Options
+
+ratioDim obj
+ The pixel dimensions to apply as a restrictive ratio, with properties x & y.
+minWidth int
+ The minimum width for the select area in pixels.
+minHeight int
+ The mimimum height for the select area in pixels.
+maxWidth int
+ The maximum width for the select areas in pixels (if both minWidth & maxWidth set to same the width of the cropper will be fixed)
+maxHeight int
+ The maximum height for the select areas in pixels (if both minHeight & maxHeight set to same the height of the cropper will be fixed)
+displayOnInit int
+ Whether to display the select area on initialisation, only used when providing minimum width & height or ratio.
+onEndCrop func
+ The callback function to provide the crop details to on end of a crop.
+captureKeys boolean
+ Whether to capture the keys for moving the select area, as these can cause some problems at the moment.
+onloadCoords obj
+ A coordinates object with properties x1, y1, x2 & y2; for the coordinates of the select area to display onload
+
+The callback function
+
+The callback function is a function that allows you to capture the crop co-ordinates when the user finished a crop movement, it is passed two arguments:
+
+ * coords, obj, coordinates object with properties x1, y1, x2 & y2; for the coordinates of the select area.
+ * dimensions, obj, dimensions object with properities width & height; for the dimensions of the select area.
+
+An example function which outputs the crop values to form fields:
+Display code as plain text
+JavaScript:
+
+ 1.
+ function onEndCrop( coords, dimensions ) {
+ 2.
+ $( 'x1' ).value = coords.x1;
+ 3.
+ $( 'y1' ).value = coords.y1;
+ 4.
+ $( 'x2' ).value = coords.x2;
+ 5.
+ $( 'y2' ).value = coords.y2;
+ 6.
+ $( 'width' ).value = dimensions.width;
+ 7.
+ $( 'height' ).value = dimensions.height;
+ 8.
+ }
+
+Basic interface
+
+This basic example will attach the cropper UI to the test image and return crop results to the provided callback function.
+Display code as plain text
+HTML:
+
+ 1.
+ <img src="test.jpg" alt="Test image" id="testImage" width="500" height="333" />
+ 2.
+
+ 3.
+ <script type="text/javascript" language="javascript">
+ 4.
+ Event.observe( window, 'load', function() {
+ 5.
+ new Cropper.Img(
+ 6.
+ 'testImage',
+ 7.
+ { onEndCrop: onEndCrop }
+ 8.
+ );
+ 9.
+ } );
+ 10.
+ </script>
+
+Minimum dimensions
+
+You can apply minimum dimensions to a single axis or both, this example applies minimum dimensions to both axis.
+Display code as plain text
+HTML:
+
+ 1.
+ <img src="test.jpg" alt="Test image" id="testImage" width="500" height="333" />
+ 2.
+
+ 3.
+ <script type="text/javascript" language="javascript">
+ 4.
+ Event.observe( window, 'load', function() {
+ 5.
+ new Cropper.Img(
+ 6.
+ 'testImage',
+ 7.
+ {
+ 8.
+ minWidth: 220,
+ 9.
+ minHeight: 120,
+ 10.
+ onEndCrop: onEndCrop
+ 11.
+ }
+ 12.
+ );
+ 13.
+ } );
+ 14.
+ </script>
+
+Select area ratio
+
+You can apply a ratio to the selection area, this example applies a 4:3 ratio to the select area.
+Display code as plain text
+HTML:
+
+ 1.
+ <img src="test.jpg" alt="Test image" id="testImage" width="500" height="333" />
+ 2.
+
+ 3.
+ <script type="text/javascript" language="javascript">
+ 4.
+ Event.observe( window, 'load', function() {
+ 5.
+ new Cropper.Img(
+ 6.
+ 'testImage',
+ 7.
+ {
+ 8.
+ ratioDim: {
+ 9.
+ x: 220,
+ 10.
+ y: 165
+ 11.
+ },
+ 12.
+ displayOnInit: true,
+ 13.
+ onEndCrop: onEndCrop
+ 14.
+ }
+ 15.
+ );
+ 16.
+ } );
+ 17.
+ </script>
+
+With crop preview
+
+You can display a dynamically prouced preview of the resulting crop by using the ImgWithPreview subclass, a preview can only be displayed when we have a fixed size (set via minWidth & minHeight options). Note that the displayOnInit option is not required as this is the default behaviour when displaying a crop preview.
+Display code as plain text
+HTML:
+
+ 1.
+ <img src="test.jpg" alt="Test image" id="testImage" width="500" height="333" />
+ 2.
+ <div id="previewWrap"></div>
+ 3.
+
+ 4.
+ <script type="text/javascript" language="javascript">
+ 5.
+ Event.observe( window, 'load', function() {
+ 6.
+ new Cropper.ImgWithPreview(
+ 7.
+ 'testImage',
+ 8.
+ {
+ 9.
+ previewWrap: 'previewWrap',
+ 10.
+ minWidth: 120,
+ 11.
+ minHeight: 120,
+ 12.
+ ratioDim: { x: 200, y: 120 },
+ 13.
+ onEndCrop: onEndCrop
+ 14.
+ }
+ 15.
+ );
+ 16.
+ } );
+ 17.
+ </script>
+
+Known Issues
+
+ * Safari animated gifs, only one of each will animate, this seems to be a known Safari issue.
+ * After drawing an area and then clicking to start a new drag in IE 5.5 the rendered height appears as the last height until the user drags, this appears to be the related to another IE error (which has been fixed) where IE does not always redraw the select area properly.
+ * Lack of CSS opacity support in Opera before version 9 mean we disable those style rules, if Opera 8 support is important you & you want the overlay to work then you can use the Opera rules in the CSS to apply a black PNG with 50% alpha transparency to replicate the effect.
+ * Styling & borders on image, any CSS styling applied directly to the image itself (floats, borders, padding, margin, etc.) will cause problems with the cropper. The use of a wrapper element to apply these styles to is recommended.
+ * overflow: auto or overflow: scroll on parent will cause cropper to burst out of parent in IE and Opera when applied (maybe Mac browsers too) I'm not sure why yet.
+
+If you use CakePHP you will notice that including this in your script will break the CSS layout. This is due to the CSS rule
+
+form div{
+vertical-align: text-top;
+margin-left: 1em;
+margin-bottom:2em;
+overflow: auto;
+}
+
+A simple workaround is to add another rule directly after this like so:
+
+form div.no_cake, form div.no_cake div {
+margin:0;
+overflow:hidden;
+}
+
+and then in your code surround the img tag with a div with the class name of no_cake.
+
+Cheers \ No newline at end of file
diff --git a/cropper/cropper.css b/cropper/cropper.css
new file mode 100644
index 000000000..c2e759818
--- /dev/null
+++ b/cropper/cropper.css
@@ -0,0 +1,182 @@
+.imgCrop_wrap {
+ /* width: 500px; @done_in_js */
+ /* height: 375px; @done_in_js */
+ position: relative;
+ cursor: crosshair;
+}
+
+/* an extra classname is applied for Opera < 9.0 to fix it's lack of opacity support */
+.imgCrop_wrap.opera8 .imgCrop_overlay,
+.imgCrop_wrap.opera8 .imgCrop_clickArea {
+ background-color: transparent;
+}
+
+/* fix for IE displaying all boxes at line-height by default, although they are still 1 pixel high until we combine them with the pointless span */
+.imgCrop_wrap,
+.imgCrop_wrap * {
+ font-size: 0;
+}
+
+.imgCrop_overlay {
+ background-color: #000;
+ opacity: 0.5;
+ filter:alpha(opacity=50);
+ position: absolute;
+ width: 100%;
+ height: 100%;
+}
+
+.imgCrop_selArea {
+ position: absolute;
+ /* @done_in_js
+ top: 20px;
+ left: 20px;
+ width: 200px;
+ height: 200px;
+ background: transparent url(castle.jpg) no-repeat -210px -110px;
+ */
+ cursor: move;
+ z-index: 2;
+}
+
+/* clickArea is all a fix for IE 5.5 & 6 to allow the user to click on the given area */
+.imgCrop_clickArea {
+ width: 100%;
+ height: 100%;
+ background-color: #FFF;
+ opacity: 0.01;
+ filter:alpha(opacity=01);
+}
+
+.imgCrop_marqueeHoriz {
+ position: absolute;
+ width: 100%;
+ height: 1px;
+ background: transparent url(marqueeHoriz.gif) repeat-x 0 0;
+ z-index: 3;
+}
+
+.imgCrop_marqueeVert {
+ position: absolute;
+ height: 100%;
+ width: 1px;
+ background: transparent url(marqueeVert.gif) repeat-y 0 0;
+ z-index: 3;
+}
+
+/*
+ * FIX MARCHING ANTS IN IE
+ * As IE <6 tries to load background images we can uncomment the follwoing hack
+ * to remove that issue, not as pretty - but is anything in IE?
+ * And yes I do know that 'filter' is evil, but it will make it look semi decent in IE
+ *
+* html .imgCrop_marqueeHoriz,
+* html .imgCrop_marqueeVert {
+ background: transparent;
+ filter: Invert;
+}
+* html .imgCrop_marqueeNorth { border-top: 1px dashed #000; }
+* html .imgCrop_marqueeEast { border-right: 1px dashed #000; }
+* html .imgCrop_marqueeSouth { border-bottom: 1px dashed #000; }
+* html .imgCrop_marqueeWest { border-left: 1px dashed #000; }
+*/
+
+.imgCrop_marqueeNorth { top: 0; left: 0; }
+.imgCrop_marqueeEast { top: 0; right: 0; }
+.imgCrop_marqueeSouth { bottom: 0px; left: 0; }
+.imgCrop_marqueeWest { top: 0; left: 0; }
+
+
+.imgCrop_handle {
+ position: absolute;
+ border: 1px solid #333;
+ width: 6px;
+ height: 6px;
+ background: #FFF;
+ opacity: 0.5;
+ filter:alpha(opacity=50);
+ z-index: 4;
+}
+
+/* fix IE 5 box model */
+* html .imgCrop_handle {
+ width: 8px;
+ height: 8px;
+ wid\th: 6px;
+ hei\ght: 6px;
+}
+
+.imgCrop_handleN {
+ top: -3px;
+ left: 0;
+ /* margin-left: 49%; @done_in_js */
+ cursor: n-resize;
+}
+
+.imgCrop_handleNE {
+ top: -3px;
+ right: -3px;
+ cursor: ne-resize;
+}
+
+.imgCrop_handleE {
+ top: 0;
+ right: -3px;
+ /* margin-top: 49%; @done_in_js */
+ cursor: e-resize;
+}
+
+.imgCrop_handleSE {
+ right: -3px;
+ bottom: -3px;
+ cursor: se-resize;
+}
+
+.imgCrop_handleS {
+ right: 0;
+ bottom: -3px;
+ /* margin-right: 49%; @done_in_js */
+ cursor: s-resize;
+}
+
+.imgCrop_handleSW {
+ left: -3px;
+ bottom: -3px;
+ cursor: sw-resize;
+}
+
+.imgCrop_handleW {
+ top: 0;
+ left: -3px;
+ /* margin-top: 49%; @done_in_js */
+ cursor: w-resize;
+}
+
+.imgCrop_handleNW {
+ top: -3px;
+ left: -3px;
+ cursor: nw-resize;
+}
+
+/**
+ * Create an area to click & drag around on as the default browser behaviour is to let you drag the image
+ */
+.imgCrop_dragArea {
+ width: 100%;
+ height: 100%;
+ z-index: 200;
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+.imgCrop_previewWrap {
+ /* width: 200px; @done_in_js */
+ /* height: 200px; @done_in_js */
+ overflow: hidden;
+ position: relative;
+}
+
+.imgCrop_previewWrap img {
+ position: absolute;
+} \ No newline at end of file
diff --git a/cropper/cropper.js b/cropper/cropper.js
new file mode 100644
index 000000000..486a92ad9
--- /dev/null
+++ b/cropper/cropper.js
@@ -0,0 +1,566 @@
+/**
+ * Copyright (c) 2006, David Spurr (http://www.defusion.org.uk/)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ * * 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.
+ * * 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.
+ *
+ * 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.
+ *
+ * http://www.opensource.org/licenses/bsd-license.php
+ *
+ * See scriptaculous.js for full scriptaculous licence
+ */
+
+var CropDraggable=Class.create();
+Object.extend(Object.extend(CropDraggable.prototype,Draggable.prototype),{initialize:function(_1){
+this.options=Object.extend({drawMethod:function(){
+}},arguments[1]||{});
+this.element=$(_1);
+this.handle=this.element;
+this.delta=this.currentDelta();
+this.dragging=false;
+this.eventMouseDown=this.initDrag.bindAsEventListener(this);
+Event.observe(this.handle,"mousedown",this.eventMouseDown);
+Draggables.register(this);
+},draw:function(_2){
+var _3=Position.cumulativeOffset(this.element);
+var d=this.currentDelta();
+_3[0]-=d[0];
+_3[1]-=d[1];
+var p=[0,1].map(function(i){
+return (_2[i]-_3[i]-this.offset[i]);
+}.bind(this));
+this.options.drawMethod(p);
+}});
+var Cropper={};
+Cropper.Img=Class.create();
+Cropper.Img.prototype={initialize:function(_7,_8){
+this.options=Object.extend({ratioDim:{x:0,y:0},minWidth:0,minHeight:0,displayOnInit:false,onEndCrop:Prototype.emptyFunction,captureKeys:true,onloadCoords:null,maxWidth:0,maxHeight:0},_8||{});
+this.img=$(_7);
+this.clickCoords={x:0,y:0};
+this.dragging=false;
+this.resizing=false;
+this.isWebKit=/Konqueror|Safari|KHTML/.test(navigator.userAgent);
+this.isIE=/MSIE/.test(navigator.userAgent);
+this.isOpera8=/Opera\s[1-8]/.test(navigator.userAgent);
+this.ratioX=0;
+this.ratioY=0;
+this.attached=false;
+this.fixedWidth=(this.options.maxWidth>0&&(this.options.minWidth>=this.options.maxWidth));
+this.fixedHeight=(this.options.maxHeight>0&&(this.options.minHeight>=this.options.maxHeight));
+if(typeof this.img=="undefined"){
+return;
+}
+$A(document.getElementsByTagName("script")).each(function(s){
+if(s.src.match(/cropper\.js/)){
+var _a=s.src.replace(/cropper\.js(.*)?/,"");
+var _b=document.createElement("link");
+_b.rel="stylesheet";
+_b.type="text/css";
+_b.href=_a+"cropper.css";
+_b.media="screen";
+document.getElementsByTagName("head")[0].appendChild(_b);
+}
+});
+if(this.options.ratioDim.x>0&&this.options.ratioDim.y>0){
+var _c=this.getGCD(this.options.ratioDim.x,this.options.ratioDim.y);
+this.ratioX=this.options.ratioDim.x/_c;
+this.ratioY=this.options.ratioDim.y/_c;
+}
+this.subInitialize();
+if(this.img.complete||this.isWebKit){
+this.onLoad();
+}else{
+Event.observe(this.img,"load",this.onLoad.bindAsEventListener(this));
+}
+},getGCD:function(a,b){
+if(b==0){
+return a;
+}
+return this.getGCD(b,a%b);
+},onLoad:function(){
+var _f="imgCrop_";
+var _10=this.img.parentNode;
+var _11="";
+if(this.isOpera8){
+_11=" opera8";
+}
+this.imgWrap=Builder.node("div",{"class":_f+"wrap"+_11});
+this.north=Builder.node("div",{"class":_f+"overlay "+_f+"north"},[Builder.node("span")]);
+this.east=Builder.node("div",{"class":_f+"overlay "+_f+"east"},[Builder.node("span")]);
+this.south=Builder.node("div",{"class":_f+"overlay "+_f+"south"},[Builder.node("span")]);
+this.west=Builder.node("div",{"class":_f+"overlay "+_f+"west"},[Builder.node("span")]);
+var _12=[this.north,this.east,this.south,this.west];
+this.dragArea=Builder.node("div",{"class":_f+"dragArea"},_12);
+this.handleN=Builder.node("div",{"class":_f+"handle "+_f+"handleN"});
+this.handleNE=Builder.node("div",{"class":_f+"handle "+_f+"handleNE"});
+this.handleE=Builder.node("div",{"class":_f+"handle "+_f+"handleE"});
+this.handleSE=Builder.node("div",{"class":_f+"handle "+_f+"handleSE"});
+this.handleS=Builder.node("div",{"class":_f+"handle "+_f+"handleS"});
+this.handleSW=Builder.node("div",{"class":_f+"handle "+_f+"handleSW"});
+this.handleW=Builder.node("div",{"class":_f+"handle "+_f+"handleW"});
+this.handleNW=Builder.node("div",{"class":_f+"handle "+_f+"handleNW"});
+this.selArea=Builder.node("div",{"class":_f+"selArea"},[Builder.node("div",{"class":_f+"marqueeHoriz "+_f+"marqueeNorth"},[Builder.node("span")]),Builder.node("div",{"class":_f+"marqueeVert "+_f+"marqueeEast"},[Builder.node("span")]),Builder.node("div",{"class":_f+"marqueeHoriz "+_f+"marqueeSouth"},[Builder.node("span")]),Builder.node("div",{"class":_f+"marqueeVert "+_f+"marqueeWest"},[Builder.node("span")]),this.handleN,this.handleNE,this.handleE,this.handleSE,this.handleS,this.handleSW,this.handleW,this.handleNW,Builder.node("div",{"class":_f+"clickArea"})]);
+this.imgWrap.appendChild(this.img);
+this.imgWrap.appendChild(this.dragArea);
+this.dragArea.appendChild(this.selArea);
+this.dragArea.appendChild(Builder.node("div",{"class":_f+"clickArea"}));
+_10.appendChild(this.imgWrap);
+this.startDragBind=this.startDrag.bindAsEventListener(this);
+Event.observe(this.dragArea,"mousedown",this.startDragBind);
+this.onDragBind=this.onDrag.bindAsEventListener(this);
+Event.observe(document,"mousemove",this.onDragBind);
+this.endCropBind=this.endCrop.bindAsEventListener(this);
+Event.observe(document,"mouseup",this.endCropBind);
+this.resizeBind=this.startResize.bindAsEventListener(this);
+this.handles=[this.handleN,this.handleNE,this.handleE,this.handleSE,this.handleS,this.handleSW,this.handleW,this.handleNW];
+this.registerHandles(true);
+if(this.options.captureKeys){
+this.keysBind=this.handleKeys.bindAsEventListener(this);
+Event.observe(document,"keypress",this.keysBind);
+}
+new CropDraggable(this.selArea,{drawMethod:this.moveArea.bindAsEventListener(this)});
+this.setParams();
+},registerHandles:function(_13){
+for(var i=0;i<this.handles.length;i++){
+var _15=$(this.handles[i]);
+if(_13){
+var _16=false;
+if(this.fixedWidth&&this.fixedHeight){
+_16=true;
+}else{
+if(this.fixedWidth||this.fixedHeight){
+var _17=_15.className.match(/([S|N][E|W])$/);
+var _18=_15.className.match(/(E|W)$/);
+var _19=_15.className.match(/(N|S)$/);
+if(_17){
+_16=true;
+}else{
+if(this.fixedWidth&&_18){
+_16=true;
+}else{
+if(this.fixedHeight&&_19){
+_16=true;
+}
+}
+}
+}
+}
+if(_16){
+_15.hide();
+}else{
+Event.observe(_15,"mousedown",this.resizeBind);
+}
+}else{
+_15.show();
+Event.stopObserving(_15,"mousedown",this.resizeBind);
+}
+}
+},setParams:function(){
+this.imgW=this.img.width;
+this.imgH=this.img.height;
+$(this.north).setStyle({height:0});
+$(this.east).setStyle({width:0,height:0});
+$(this.south).setStyle({height:0});
+$(this.west).setStyle({width:0,height:0});
+$(this.imgWrap).setStyle({"width":this.imgW+"px","height":this.imgH+"px"});
+$(this.selArea).hide();
+var _1a={x1:0,y1:0,x2:0,y2:0};
+var _1b=false;
+if(this.options.onloadCoords!=null){
+_1a=this.cloneCoords(this.options.onloadCoords);
+_1b=true;
+}else{
+if(this.options.ratioDim.x>0&&this.options.ratioDim.y>0){
+_1a.x1=Math.ceil((this.imgW-this.options.ratioDim.x)/2);
+_1a.y1=Math.ceil((this.imgH-this.options.ratioDim.y)/2);
+_1a.x2=_1a.x1+this.options.ratioDim.x;
+_1a.y2=_1a.y1+this.options.ratioDim.y;
+_1b=true;
+}
+}
+this.setAreaCoords(_1a,false,false,1);
+if(this.options.displayOnInit&&_1b){
+this.selArea.show();
+this.drawArea();
+this.endCrop();
+}
+this.attached=true;
+},remove:function(){
+if(this.attached){
+this.attached=false;
+this.imgWrap.parentNode.insertBefore(this.img,this.imgWrap);
+this.imgWrap.parentNode.removeChild(this.imgWrap);
+Event.stopObserving(this.dragArea,"mousedown",this.startDragBind);
+Event.stopObserving(document,"mousemove",this.onDragBind);
+Event.stopObserving(document,"mouseup",this.endCropBind);
+this.registerHandles(false);
+if(this.options.captureKeys){
+Event.stopObserving(document,"keypress",this.keysBind);
+}
+}
+},reset:function(){
+if(!this.attached){
+this.onLoad();
+}else{
+this.setParams();
+}
+this.endCrop();
+},handleKeys:function(e){
+var dir={x:0,y:0};
+if(!this.dragging){
+switch(e.keyCode){
+case (37):
+dir.x=-1;
+break;
+case (38):
+dir.y=-1;
+break;
+case (39):
+dir.x=1;
+break;
+case (40):
+dir.y=1;
+break;
+}
+if(dir.x!=0||dir.y!=0){
+if(e.shiftKey){
+dir.x*=10;
+dir.y*=10;
+}
+this.moveArea([this.areaCoords.x1+dir.x,this.areaCoords.y1+dir.y]);
+Event.stop(e);
+}
+}
+},calcW:function(){
+return (this.areaCoords.x2-this.areaCoords.x1);
+},calcH:function(){
+return (this.areaCoords.y2-this.areaCoords.y1);
+},moveArea:function(_1e){
+this.setAreaCoords({x1:_1e[0],y1:_1e[1],x2:_1e[0]+this.calcW(),y2:_1e[1]+this.calcH()},true,false);
+this.drawArea();
+},cloneCoords:function(_1f){
+return {x1:_1f.x1,y1:_1f.y1,x2:_1f.x2,y2:_1f.y2};
+},setAreaCoords:function(_20,_21,_22,_23,_24){
+if(_21){
+var _25=_20.x2-_20.x1;
+var _26=_20.y2-_20.y1;
+if(_20.x1<0){
+_20.x1=0;
+_20.x2=_25;
+}
+if(_20.y1<0){
+_20.y1=0;
+_20.y2=_26;
+}
+if(_20.x2>this.imgW){
+_20.x2=this.imgW;
+_20.x1=this.imgW-_25;
+}
+if(_20.y2>this.imgH){
+_20.y2=this.imgH;
+_20.y1=this.imgH-_26;
+}
+}else{
+if(_20.x1<0){
+_20.x1=0;
+}
+if(_20.y1<0){
+_20.y1=0;
+}
+if(_20.x2>this.imgW){
+_20.x2=this.imgW;
+}
+if(_20.y2>this.imgH){
+_20.y2=this.imgH;
+}
+if(_23!=null){
+if(this.ratioX>0){
+this.applyRatio(_20,{x:this.ratioX,y:this.ratioY},_23,_24);
+}else{
+if(_22){
+this.applyRatio(_20,{x:1,y:1},_23,_24);
+}
+}
+var _27=[this.options.minWidth,this.options.minHeight];
+var _28=[this.options.maxWidth,this.options.maxHeight];
+if(_27[0]>0||_27[1]>0||_28[0]>0||_28[1]>0){
+var _29={a1:_20.x1,a2:_20.x2};
+var _2a={a1:_20.y1,a2:_20.y2};
+var _2b={min:0,max:this.imgW};
+var _2c={min:0,max:this.imgH};
+if((_27[0]!=0||_27[1]!=0)&&_22){
+if(_27[0]>0){
+_27[1]=_27[0];
+}else{
+if(_27[1]>0){
+_27[0]=_27[1];
+}
+}
+}
+if((_28[0]!=0||_28[0]!=0)&&_22){
+if(_28[0]>0&&_28[0]<=_28[1]){
+_28[1]=_28[0];
+}else{
+if(_28[1]>0&&_28[1]<=_28[0]){
+_28[0]=_28[1];
+}
+}
+}
+if(_27[0]>0){
+this.applyDimRestriction(_29,_27[0],_23.x,_2b,"min");
+}
+if(_27[1]>1){
+this.applyDimRestriction(_2a,_27[1],_23.y,_2c,"min");
+}
+if(_28[0]>0){
+this.applyDimRestriction(_29,_28[0],_23.x,_2b,"max");
+}
+if(_28[1]>1){
+this.applyDimRestriction(_2a,_28[1],_23.y,_2c,"max");
+}
+_20={x1:_29.a1,y1:_2a.a1,x2:_29.a2,y2:_2a.a2};
+}
+}
+}
+this.areaCoords=_20;
+},applyDimRestriction:function(_2d,val,_2f,_30,_31){
+var _32;
+if(_31=="min"){
+_32=((_2d.a2-_2d.a1)<val);
+}else{
+_32=((_2d.a2-_2d.a1)>val);
+}
+if(_32){
+if(_2f==1){
+_2d.a2=_2d.a1+val;
+}else{
+_2d.a1=_2d.a2-val;
+}
+if(_2d.a1<_30.min){
+_2d.a1=_30.min;
+_2d.a2=val;
+}else{
+if(_2d.a2>_30.max){
+_2d.a1=_30.max-val;
+_2d.a2=_30.max;
+}
+}
+}
+},applyRatio:function(_33,_34,_35,_36){
+var _37;
+if(_36=="N"||_36=="S"){
+_37=this.applyRatioToAxis({a1:_33.y1,b1:_33.x1,a2:_33.y2,b2:_33.x2},{a:_34.y,b:_34.x},{a:_35.y,b:_35.x},{min:0,max:this.imgW});
+_33.x1=_37.b1;
+_33.y1=_37.a1;
+_33.x2=_37.b2;
+_33.y2=_37.a2;
+}else{
+_37=this.applyRatioToAxis({a1:_33.x1,b1:_33.y1,a2:_33.x2,b2:_33.y2},{a:_34.x,b:_34.y},{a:_35.x,b:_35.y},{min:0,max:this.imgH});
+_33.x1=_37.a1;
+_33.y1=_37.b1;
+_33.x2=_37.a2;
+_33.y2=_37.b2;
+}
+},applyRatioToAxis:function(_38,_39,_3a,_3b){
+var _3c=Object.extend(_38,{});
+var _3d=_3c.a2-_3c.a1;
+var _3e=Math.floor(_3d*_39.b/_39.a);
+var _3f;
+var _40;
+var _41=null;
+if(_3a.b==1){
+_3f=_3c.b1+_3e;
+if(_3f>_3b.max){
+_3f=_3b.max;
+_41=_3f-_3c.b1;
+}
+_3c.b2=_3f;
+}else{
+_3f=_3c.b2-_3e;
+if(_3f<_3b.min){
+_3f=_3b.min;
+_41=_3f+_3c.b2;
+}
+_3c.b1=_3f;
+}
+if(_41!=null){
+_40=Math.floor(_41*_39.a/_39.b);
+if(_3a.a==1){
+_3c.a2=_3c.a1+_40;
+}else{
+_3c.a1=_3c.a1=_3c.a2-_40;
+}
+}
+return _3c;
+},drawArea:function(){
+var _42=this.calcW();
+var _43=this.calcH();
+var px="px";
+var _45=[this.areaCoords.x1+px,this.areaCoords.y1+px,_42+px,_43+px,this.areaCoords.x2+px,this.areaCoords.y2+px,(this.img.width-this.areaCoords.x2)+px,(this.img.height-this.areaCoords.y2)+px];
+var _46=this.selArea.style;
+_46.left=_45[0];
+_46.top=_45[1];
+_46.width=_45[2];
+_46.height=_45[3];
+var _47=Math.ceil((_42-6)/2)+px;
+var _48=Math.ceil((_43-6)/2)+px;
+this.handleN.style.left=_47;
+this.handleE.style.top=_48;
+this.handleS.style.left=_47;
+this.handleW.style.top=_48;
+this.north.style.height=_45[1];
+var _49=this.east.style;
+_49.top=_45[1];
+_49.height=_45[3];
+_49.left=_45[4];
+_49.width=_45[6];
+var _4a=this.south.style;
+_4a.top=_45[5];
+_4a.height=_45[7];
+var _4b=this.west.style;
+_4b.top=_45[1];
+_4b.height=_45[3];
+_4b.width=_45[0];
+this.subDrawArea();
+this.forceReRender();
+},forceReRender:function(){
+if(this.isIE||this.isWebKit){
+var n=document.createTextNode(" ");
+var d,el,fixEL,i;
+if(this.isIE){
+fixEl=this.selArea;
+}else{
+if(this.isWebKit){
+fixEl=document.getElementsByClassName("imgCrop_marqueeSouth",this.imgWrap)[0];
+d=Builder.node("div","");
+d.style.visibility="hidden";
+var _4e=["SE","S","SW"];
+for(i=0;i<_4e.length;i++){
+el=document.getElementsByClassName("imgCrop_handle"+_4e[i],this.selArea)[0];
+if(el.childNodes.length){
+el.removeChild(el.childNodes[0]);
+}
+el.appendChild(d);
+}
+}
+}
+fixEl.appendChild(n);
+fixEl.removeChild(n);
+}
+},startResize:function(e){
+this.startCoords=this.cloneCoords(this.areaCoords);
+this.resizing=true;
+this.resizeHandle=Event.element(e).classNames().toString().replace(/([^N|NE|E|SE|S|SW|W|NW])+/,"");
+Event.stop(e);
+},startDrag:function(e){
+this.selArea.show();
+this.clickCoords=this.getCurPos(e);
+this.setAreaCoords({x1:this.clickCoords.x,y1:this.clickCoords.y,x2:this.clickCoords.x,y2:this.clickCoords.y},false,false,null);
+this.dragging=true;
+this.onDrag(e);
+Event.stop(e);
+},getCurPos:function(e){
+var el=this.imgWrap,wrapOffsets=Position.cumulativeOffset(el);
+while(el.nodeName!="BODY"){
+wrapOffsets[1]-=el.scrollTop||0;
+wrapOffsets[0]-=el.scrollLeft||0;
+el=el.parentNode;
+}
+return curPos={x:Event.pointerX(e)-wrapOffsets[0],y:Event.pointerY(e)-wrapOffsets[1]};
+},onDrag:function(e){
+if(this.dragging||this.resizing){
+var _54=null;
+var _55=this.getCurPos(e);
+var _56=this.cloneCoords(this.areaCoords);
+var _57={x:1,y:1};
+if(this.dragging){
+if(_55.x<this.clickCoords.x){
+_57.x=-1;
+}
+if(_55.y<this.clickCoords.y){
+_57.y=-1;
+}
+this.transformCoords(_55.x,this.clickCoords.x,_56,"x");
+this.transformCoords(_55.y,this.clickCoords.y,_56,"y");
+}else{
+if(this.resizing){
+_54=this.resizeHandle;
+if(_54.match(/E/)){
+this.transformCoords(_55.x,this.startCoords.x1,_56,"x");
+if(_55.x<this.startCoords.x1){
+_57.x=-1;
+}
+}else{
+if(_54.match(/W/)){
+this.transformCoords(_55.x,this.startCoords.x2,_56,"x");
+if(_55.x<this.startCoords.x2){
+_57.x=-1;
+}
+}
+}
+if(_54.match(/N/)){
+this.transformCoords(_55.y,this.startCoords.y2,_56,"y");
+if(_55.y<this.startCoords.y2){
+_57.y=-1;
+}
+}else{
+if(_54.match(/S/)){
+this.transformCoords(_55.y,this.startCoords.y1,_56,"y");
+if(_55.y<this.startCoords.y1){
+_57.y=-1;
+}
+}
+}
+}
+}
+this.setAreaCoords(_56,false,e.shiftKey,_57,_54);
+this.drawArea();
+Event.stop(e);
+}
+},transformCoords:function(_58,_59,_5a,_5b){
+var _5c=[_58,_59];
+if(_58>_59){
+_5c.reverse();
+}
+_5a[_5b+"1"]=_5c[0];
+_5a[_5b+"2"]=_5c[1];
+},endCrop:function(){
+this.dragging=false;
+this.resizing=false;
+this.options.onEndCrop(this.areaCoords,{width:this.calcW(),height:this.calcH()});
+},subInitialize:function(){
+},subDrawArea:function(){
+}};
+Cropper.ImgWithPreview=Class.create();
+Object.extend(Object.extend(Cropper.ImgWithPreview.prototype,Cropper.Img.prototype),{subInitialize:function(){
+this.hasPreviewImg=false;
+if(typeof (this.options.previewWrap)!="undefined"&&this.options.minWidth>0&&this.options.minHeight>0){
+this.previewWrap=$(this.options.previewWrap);
+this.previewImg=this.img.cloneNode(false);
+this.previewImg.id="imgCrop_"+this.previewImg.id;
+this.options.displayOnInit=true;
+this.hasPreviewImg=true;
+this.previewWrap.addClassName("imgCrop_previewWrap");
+this.previewWrap.setStyle({width:this.options.minWidth+"px",height:this.options.minHeight+"px"});
+this.previewWrap.appendChild(this.previewImg);
+}
+},subDrawArea:function(){
+if(this.hasPreviewImg){
+var _5d=this.calcW();
+var _5e=this.calcH();
+var _5f={x:this.imgW/_5d,y:this.imgH/_5e};
+var _60={x:_5d/this.options.minWidth,y:_5e/this.options.minHeight};
+var _61={w:Math.ceil(this.options.minWidth*_5f.x)+"px",h:Math.ceil(this.options.minHeight*_5f.y)+"px",x:"-"+Math.ceil(this.areaCoords.x1/_60.x)+"px",y:"-"+Math.ceil(this.areaCoords.y1/_60.y)+"px"};
+var _62=this.previewImg.style;
+_62.width=_61.w;
+_62.height=_61.h;
+_62.left=_61.x;
+_62.top=_61.y;
+}
+}});
+
diff --git a/cropper/cropper.uncompressed.js b/cropper/cropper.uncompressed.js
new file mode 100644
index 000000000..66185546e
--- /dev/null
+++ b/cropper/cropper.uncompressed.js
@@ -0,0 +1,1331 @@
+/**
+ * Image Cropper (v. 1.2.0 - 2006-10-30 )
+ * Copyright (c) 2006 David Spurr (http://www.defusion.org.uk/)
+ *
+ * The image cropper provides a way to draw a crop area on an image and capture
+ * the coordinates of the drawn crop area.
+ *
+ * Features include:
+ * - Based on Prototype and Scriptaculous
+ * - Image editing package styling, the crop area functions and looks
+ * like those found in popular image editing software
+ * - Dynamic inclusion of required styles
+ * - Drag to draw areas
+ * - Shift drag to draw/resize areas as squares
+ * - Selection area can be moved
+ * - Seleciton area can be resized using resize handles
+ * - Allows dimension ratio limited crop areas
+ * - Allows minimum dimension crop areas
+ * - Allows maximum dimesion crop areas
+ * - If both min & max dimension options set to the same value for a single axis,then the cropper will not
+ * display the resize handles as appropriate (when min & max dimensions are passed for both axes this
+ * results in a 'fixed size' crop area)
+ * - Allows dynamic preview of resultant crop ( if minimum width & height are provided ), this is
+ * implemented as a subclass so can be excluded when not required
+ * - Movement of selection area by arrow keys ( shift + arrow key will move selection area by
+ * 10 pixels )
+ * - All operations stay within bounds of image
+ * - All functionality & display compatible with most popular browsers supported by Prototype:
+ * PC: IE 7, 6 & 5.5, Firefox 1.5, Opera 8.5 (see known issues) & 9.0b
+ * MAC: Camino 1.0, Firefox 1.5, Safari 2.0
+ *
+ * Requires:
+ * - Prototype v. 1.5.0_rc0 > (as packaged with Scriptaculous 1.6.1)
+ * - Scriptaculous v. 1.6.1 > modules: builder, dragdrop
+ *
+ * Known issues:
+ * - Safari animated gifs, only one of each will animate, this seems to be a known Safari issue
+ *
+ * - After drawing an area and then clicking to start a new drag in IE 5.5 the rendered height
+ * appears as the last height until the user drags, this appears to be the related to the error
+ * that the forceReRender() method fixes for IE 6, i.e. IE 5.5 is not redrawing the box properly.
+ *
+ * - Lack of CSS opacity support in Opera before version 9 mean we disable those style rules, these
+ * could be fixed by using PNGs with transparency if Opera 8.5 support is high priority for you
+ *
+ * - Marching ants keep reloading in IE <6 (not tested in IE7), it is a known issue in IE and I have
+ * found no viable workarounds that can be included in the release. If this really is an issue for you
+ * either try this post: http://mir.aculo.us/articles/2005/08/28/internet-explorer-and-ajax-image-caching-woes
+ * or uncomment the 'FIX MARCHING ANTS IN IE' rules in the CSS file
+ *
+ * - Styling & borders on image, any CSS styling applied directly to the image itself (floats, borders, padding, margin, etc.) will
+ * cause problems with the cropper. The use of a wrapper element to apply these styles to is recommended.
+ *
+ * - overflow: auto or overflow: scroll on parent will cause cropper to burst out of parent in IE and Opera (maybe Mac browsers too)
+ * I'm not sure why yet.
+ *
+ * Usage:
+ * See Cropper.Img & Cropper.ImgWithPreview for usage details
+ *
+ * Changelog:
+ * v1.2.0 - 2006-10-30
+ * + Added id to the preview image element using 'imgCrop_[originalImageID]'
+ * * #00001 - Fixed bug: Doesn't account for scroll offsets
+ * * #00009 - Fixed bug: Placing the cropper inside differently positioned elements causes incorrect co-ordinates and display
+ * * #00013 - Fixed bug: I-bar cursor appears on drag plane
+ * * #00014 - Fixed bug: If ID for image tag is not found in document script throws error
+ * * Fixed bug with drag start co-ordinates if wrapper element has moved in browser (e.g. dragged to a new position)
+ * * Fixed bug with drag start co-ordinates if image contained in a wrapper with scrolling - this may be buggy if image
+ * has other ancestors with scrolling applied (except the body)
+ * * #00015 - Fixed bug: When cropper removed and then reapplied onEndCrop callback gets called multiple times, solution suggestion from Bill Smith
+ * * Various speed increases & code cleanup which meant improved performance in Mac - which allowed removal of different overlay methods for
+ * IE and all other browsers, which led to a fix for:
+ * * #00010 - Fixed bug: Select area doesn't adhere to image size when image resized using img attributes
+ * - #00006 - Removed default behaviour of automatically setting a ratio when both min width & height passed, the ratioDimensions must be passed in
+ * + #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
+ * and the resize handles will not be displayed as appropriate
+ * * Switched keydown for keypress for moving select area with cursor keys (makes for nicer action) - doesn't appear to work in Safari
+ *
+ * v1.1.3 - 2006-08-21
+ * * Fixed wrong cursor on western handle in CSS
+ * + #00008 & #00003 - Added feature: Allow to set dimensions & position for cropper on load
+ * * #00002 - Fixed bug: Pressing 'remove cropper' twice removes image in IE
+ *
+ * v1.1.2 - 2006-06-09
+ * * Fixed bugs with ratios when GCD is low (patch submitted by Andy Skelton)
+ *
+ * v1.1.1 - 2006-06-03
+ * * Fixed bug with rendering issues fix in IE 5.5
+ * * Fixed bug with endCrop callback issues once cropper had been removed & reset in IE
+ *
+ * v1.1.0 - 2006-06-02
+ * * Fixed bug with IE constantly trying to reload select area background image
+ * * Applied more robust fix to Safari & IE rendering issues
+ * + Added method to reset parameters - useful for when dynamically changing img cropper attached to
+ * + Added method to remove cropper from image
+ *
+ * v1.0.0 - 2006-05-18
+ * + Initial verison
+ *
+ *
+ * Copyright (c) 2006, David Spurr (http://www.defusion.org.uk/)
+ * All rights reserved.
+ *
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ * * 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.
+ * * 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.
+ *
+ * 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.
+ *
+ * http://www.opensource.org/licenses/bsd-license.php
+ *
+ * See scriptaculous.js for full scriptaculous licence
+ */
+
+/**
+ * Extend the Draggable class to allow us to pass the rendering
+ * down to the Cropper object.
+ */
+var CropDraggable = Class.create();
+
+Object.extend( Object.extend( CropDraggable.prototype, Draggable.prototype), {
+
+ initialize: function(element) {
+ this.options = Object.extend(
+ {
+ /**
+ * The draw method to defer drawing to
+ */
+ drawMethod: function() {}
+ },
+ arguments[1] || {}
+ );
+
+ this.element = $(element);
+
+ this.handle = this.element;
+
+ this.delta = this.currentDelta();
+ this.dragging = false;
+
+ this.eventMouseDown = this.initDrag.bindAsEventListener(this);
+ Event.observe(this.handle, "mousedown", this.eventMouseDown);
+
+ Draggables.register(this);
+ },
+
+ /**
+ * Defers the drawing of the draggable to the supplied method
+ */
+ draw: function(point) {
+ var pos = Position.cumulativeOffset(this.element);
+ var d = this.currentDelta();
+ pos[0] -= d[0];
+ pos[1] -= d[1];
+
+ var p = [0,1].map(function(i) {
+ return (point[i]-pos[i]-this.offset[i])
+ }.bind(this));
+
+ this.options.drawMethod( p );
+ }
+
+});
+
+
+/**
+ * The Cropper object, this will attach itself to the provided image by wrapping it with
+ * the generated xHTML structure required by the cropper.
+ *
+ * Usage:
+ * @param obj Image element to attach to
+ * @param obj Optional options:
+ * - ratioDim obj
+ * The pixel dimensions to apply as a restrictive ratio, with properties x & y
+ *
+ * - minWidth int
+ * The minimum width for the select area in pixels
+ *
+ * - minHeight int
+ * The mimimum height for the select area in pixels
+ *
+ * - maxWidth int
+ * The maximum width for the select areas in pixels (if both minWidth & maxWidth set to same the width of the cropper will be fixed)
+ *
+ * - maxHeight int
+ * The maximum height for the select areas in pixels (if both minHeight & maxHeight set to same the height of the cropper will be fixed)
+ *
+ * - displayOnInit int
+ * Whether to display the select area on initialisation, only used when providing minimum width & height or ratio
+ *
+ * - onEndCrop func
+ * The callback function to provide the crop details to on end of a crop (see below)
+ *
+ * - captureKeys boolean
+ * Whether to capture the keys for moving the select area, as these can cause some problems at the moment
+ *
+ * - onloadCoords obj
+ * A coordinates object with properties x1, y1, x2 & y2; for the coordinates of the select area to display onload
+ *
+ *----------------------------------------------
+ *
+ * The callback function provided via the onEndCrop option should accept the following parameters:
+ * - coords obj
+ * The coordinates object with properties x1, y1, x2 & y2; for the coordinates of the select area
+ *
+ * - dimensions obj
+ * The dimensions object with properites width & height; for the dimensions of the select area
+ *
+ *
+ * Example:
+ * function onEndCrop( coords, dimensions ) {
+ * $( 'x1' ).value = coords.x1;
+ * $( 'y1' ).value = coords.y1;
+ * $( 'x2' ).value = coords.x2;
+ * $( 'y2' ).value = coords.y2;
+ * $( 'width' ).value = dimensions.width;
+ * $( 'height' ).value = dimensions.height;
+ * }
+ *
+ */
+var Cropper = {};
+Cropper.Img = Class.create();
+Cropper.Img.prototype = {
+
+ /**
+ * Initialises the class
+ *
+ * @access public
+ * @param obj Image element to attach to
+ * @param obj Options
+ * @return void
+ */
+ initialize: function(element, options) {
+ this.options = Object.extend(
+ {
+ /**
+ * @var obj
+ * The pixel dimensions to apply as a restrictive ratio
+ */
+ ratioDim: { x: 0, y: 0 },
+ /**
+ * @var int
+ * The minimum pixel width, also used as restrictive ratio if min height passed too
+ */
+ minWidth: 0,
+ /**
+ * @var int
+ * The minimum pixel height, also used as restrictive ratio if min width passed too
+ */
+ minHeight: 0,
+ /**
+ * @var boolean
+ * Whether to display the select area on initialisation, only used when providing minimum width & height or ratio
+ */
+ displayOnInit: false,
+ /**
+ * @var function
+ * The call back function to pass the final values to
+ */
+ onEndCrop: Prototype.emptyFunction,
+ /**
+ * @var boolean
+ * Whether to capture key presses or not
+ */
+ captureKeys: true,
+ /**
+ * @var obj Coordinate object x1, y1, x2, y2
+ * The coordinates to optionally display the select area at onload
+ */
+ onloadCoords: null,
+ /**
+ * @var int
+ * The maximum width for the select areas in pixels (if both minWidth & maxWidth set to same the width of the cropper will be fixed)
+ */
+ maxWidth: 0,
+ /**
+ * @var int
+ * The maximum height for the select areas in pixels (if both minHeight & maxHeight set to same the height of the cropper will be fixed)
+ */
+ maxHeight: 0
+ },
+ options || {}
+ );
+ /**
+ * @var obj
+ * The img node to attach to
+ */
+ this.img = $( element );
+ /**
+ * @var obj
+ * The x & y coordinates of the click point
+ */
+ this.clickCoords = { x: 0, y: 0 };
+ /**
+ * @var boolean
+ * Whether the user is dragging
+ */
+ this.dragging = false;
+ /**
+ * @var boolean
+ * Whether the user is resizing
+ */
+ this.resizing = false;
+ /**
+ * @var boolean
+ * Whether the user is on a webKit browser
+ */
+ this.isWebKit = /Konqueror|Safari|KHTML/.test( navigator.userAgent );
+ /**
+ * @var boolean
+ * Whether the user is on IE
+ */
+ this.isIE = /MSIE/.test( navigator.userAgent );
+ /**
+ * @var boolean
+ * Whether the user is on Opera below version 9
+ */
+ this.isOpera8 = /Opera\s[1-8]/.test( navigator.userAgent );
+ /**
+ * @var int
+ * The x ratio
+ */
+ this.ratioX = 0;
+ /**
+ * @var int
+ * The y ratio
+ */
+ this.ratioY = 0;
+ /**
+ * @var boolean
+ * Whether we've attached sucessfully
+ */
+ this.attached = false;
+ /**
+ * @var boolean
+ * Whether we've got a fixed width (if minWidth EQ or GT maxWidth then we have a fixed width
+ * in the case of minWidth > maxWidth maxWidth wins as the fixed width)
+ */
+ this.fixedWidth = ( this.options.maxWidth > 0 && ( this.options.minWidth >= this.options.maxWidth ) );
+ /**
+ * @var boolean
+ * Whether we've got a fixed height (if minHeight EQ or GT maxHeight then we have a fixed height
+ * in the case of minHeight > maxHeight maxHeight wins as the fixed height)
+ */
+ this.fixedHeight = ( this.options.maxHeight > 0 && ( this.options.minHeight >= this.options.maxHeight ) );
+
+ // quit if the image element doesn't exist
+ if( typeof this.img == 'undefined' ) return;
+
+ // include the stylesheet
+ $A( document.getElementsByTagName( 'script' ) ).each(
+ function(s) {
+ if( s.src.match( /cropper\.js/ ) ) {
+ var path = s.src.replace( /cropper\.js(.*)?/, '' );
+ // '<link rel="stylesheet" type="text/css" href="' + path + 'cropper.css" media="screen" />';
+ var style = document.createElement( 'link' );
+ style.rel = 'stylesheet';
+ style.type = 'text/css';
+ style.href = path + 'cropper.css';
+ style.media = 'screen';
+ document.getElementsByTagName( 'head' )[0].appendChild( style );
+ }
+ }
+ );
+
+ // calculate the ratio when neccessary
+ if( this.options.ratioDim.x > 0 && this.options.ratioDim.y > 0 ) {
+ var gcd = this.getGCD( this.options.ratioDim.x, this.options.ratioDim.y );
+ this.ratioX = this.options.ratioDim.x / gcd;
+ this.ratioY = this.options.ratioDim.y / gcd;
+ // dump( 'RATIO : ' + this.ratioX + ':' + this.ratioY + '\n' );
+ }
+
+ // initialise sub classes
+ this.subInitialize();
+
+ // only load the event observers etc. once the image is loaded
+ // this is done after the subInitialize() call just in case the sub class does anything
+ // that will affect the result of the call to onLoad()
+ 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
+ else Event.observe( this.img, 'load', this.onLoad.bindAsEventListener( this) );
+ },
+
+ /**
+ * The Euclidean algorithm used to find the greatest common divisor
+ *
+ * @acces private
+ * @param int Value 1
+ * @param int Value 2
+ * @return int
+ */
+ getGCD : function( a , b ) {
+ if( b == 0 ) return a;
+ return this.getGCD(b, a % b );
+ },
+
+ /**
+ * Attaches the cropper to the image once it has loaded
+ *
+ * @access private
+ * @return void
+ */
+ onLoad: function( ) {
+ /*
+ * Build the container and all related elements, will result in the following
+ *
+ * <div class="imgCrop_wrap">
+ * <img ... this.img ... />
+ * <div class="imgCrop_dragArea">
+ * <!-- the inner spans are only required for IE to stop it making the divs 1px high/wide -->
+ * <div class="imgCrop_overlay imageCrop_north"><span></span></div>
+ * <div class="imgCrop_overlay imageCrop_east"><span></span></div>
+ * <div class="imgCrop_overlay imageCrop_south"><span></span></div>
+ * <div class="imgCrop_overlay imageCrop_west"><span></span></div>
+ * <div class="imgCrop_selArea">
+ * <!-- marquees -->
+ * <!-- the inner spans are only required for IE to stop it making the divs 1px high/wide -->
+ * <div class="imgCrop_marqueeHoriz imgCrop_marqueeNorth"><span></span></div>
+ * <div class="imgCrop_marqueeVert imgCrop_marqueeEast"><span></span></div>
+ * <div class="imgCrop_marqueeHoriz imgCrop_marqueeSouth"><span></span></div>
+ * <div class="imgCrop_marqueeVert imgCrop_marqueeWest"><span></span></div>
+ * <!-- handles -->
+ * <div class="imgCrop_handle imgCrop_handleN"></div>
+ * <div class="imgCrop_handle imgCrop_handleNE"></div>
+ * <div class="imgCrop_handle imgCrop_handleE"></div>
+ * <div class="imgCrop_handle imgCrop_handleSE"></div>
+ * <div class="imgCrop_handle imgCrop_handleS"></div>
+ * <div class="imgCrop_handle imgCrop_handleSW"></div>
+ * <div class="imgCrop_handle imgCrop_handleW"></div>
+ * <div class="imgCrop_handle imgCrop_handleNW"></div>
+ * <div class="imgCrop_clickArea"></div>
+ * </div>
+ * <div class="imgCrop_clickArea"></div>
+ * </div>
+ * </div>
+ */
+ var cNamePrefix = 'imgCrop_';
+
+ // get the point to insert the container
+ var insertPoint = this.img.parentNode;
+
+ // apply an extra class to the wrapper to fix Opera below version 9
+ var fixOperaClass = '';
+ if( this.isOpera8 ) fixOperaClass = ' opera8';
+ this.imgWrap = Builder.node( 'div', { 'class': cNamePrefix + 'wrap' + fixOperaClass } );
+
+ this.north = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'north' }, [Builder.node( 'span' )] );
+ this.east = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'east' } , [Builder.node( 'span' )] );
+ this.south = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'south' }, [Builder.node( 'span' )] );
+ this.west = Builder.node( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'west' } , [Builder.node( 'span' )] );
+
+ var overlays = [ this.north, this.east, this.south, this.west ];
+
+ this.dragArea = Builder.node( 'div', { 'class': cNamePrefix + 'dragArea' }, overlays );
+
+ this.handleN = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleN' } );
+ this.handleNE = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleNE' } );
+ this.handleE = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleE' } );
+ this.handleSE = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleSE' } );
+ this.handleS = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleS' } );
+ this.handleSW = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleSW' } );
+ this.handleW = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleW' } );
+ this.handleNW = Builder.node( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleNW' } );
+
+ this.selArea = Builder.node( 'div', { 'class': cNamePrefix + 'selArea' },
+ [
+ Builder.node( 'div', { 'class': cNamePrefix + 'marqueeHoriz ' + cNamePrefix + 'marqueeNorth' }, [Builder.node( 'span' )] ),
+ Builder.node( 'div', { 'class': cNamePrefix + 'marqueeVert ' + cNamePrefix + 'marqueeEast' } , [Builder.node( 'span' )] ),
+ Builder.node( 'div', { 'class': cNamePrefix + 'marqueeHoriz ' + cNamePrefix + 'marqueeSouth' }, [Builder.node( 'span' )] ),
+ Builder.node( 'div', { 'class': cNamePrefix + 'marqueeVert ' + cNamePrefix + 'marqueeWest' } , [Builder.node( 'span' )] ),
+ this.handleN,
+ this.handleNE,
+ this.handleE,
+ this.handleSE,
+ this.handleS,
+ this.handleSW,
+ this.handleW,
+ this.handleNW,
+ Builder.node( 'div', { 'class': cNamePrefix + 'clickArea' } )
+ ]
+ );
+
+ this.imgWrap.appendChild( this.img );
+ this.imgWrap.appendChild( this.dragArea );
+ this.dragArea.appendChild( this.selArea );
+ this.dragArea.appendChild( Builder.node( 'div', { 'class': cNamePrefix + 'clickArea' } ) );
+
+ insertPoint.appendChild( this.imgWrap );
+
+ // add event observers
+ this.startDragBind = this.startDrag.bindAsEventListener( this );
+ Event.observe( this.dragArea, 'mousedown', this.startDragBind );
+
+ this.onDragBind = this.onDrag.bindAsEventListener( this );
+ Event.observe( document, 'mousemove', this.onDragBind );
+
+ this.endCropBind = this.endCrop.bindAsEventListener( this );
+ Event.observe( document, 'mouseup', this.endCropBind );
+
+ this.resizeBind = this.startResize.bindAsEventListener( this );
+ this.handles = [ this.handleN, this.handleNE, this.handleE, this.handleSE, this.handleS, this.handleSW, this.handleW, this.handleNW ];
+ this.registerHandles( true );
+
+ if( this.options.captureKeys ) {
+ this.keysBind = this.handleKeys.bindAsEventListener( this );
+ Event.observe( document, 'keypress', this.keysBind );
+ }
+
+ // attach the dragable to the select area
+ new CropDraggable( this.selArea, { drawMethod: this.moveArea.bindAsEventListener( this ) } );
+
+ this.setParams();
+ },
+
+ /**
+ * Manages adding or removing the handle event handler and hiding or displaying them as appropriate
+ *
+ * @access private
+ * @param boolean registration true = add, false = remove
+ * @return void
+ */
+ registerHandles: function( registration ) {
+ for( var i = 0; i < this.handles.length; i++ ) {
+ var handle = $( this.handles[i] );
+
+ if( registration ) {
+ var hideHandle = false; // whether to hide the handle
+
+ // disable handles asappropriate if we've got fixed dimensions
+ // if both dimensions are fixed we don't need to do much
+ if( this.fixedWidth && this.fixedHeight ) hideHandle = true;
+ else if( this.fixedWidth || this.fixedHeight ) {
+ // if one of the dimensions is fixed then just hide those handles
+ var isCornerHandle = handle.className.match( /([S|N][E|W])$/ )
+ var isWidthHandle = handle.className.match( /(E|W)$/ );
+ var isHeightHandle = handle.className.match( /(N|S)$/ );
+ if( isCornerHandle ) hideHandle = true;
+ else if( this.fixedWidth && isWidthHandle ) hideHandle = true;
+ else if( this.fixedHeight && isHeightHandle ) hideHandle = true;
+ }
+ if( hideHandle ) handle.hide();
+ else Event.observe( handle, 'mousedown', this.resizeBind );
+ } else {
+ handle.show();
+ Event.stopObserving( handle, 'mousedown', this.resizeBind );
+ }
+ }
+ },
+
+ /**
+ * Sets up all the cropper parameters, this can be used to reset the cropper when dynamically
+ * changing the images
+ *
+ * @access private
+ * @return void
+ */
+ setParams: function() {
+ /**
+ * @var int
+ * The image width
+ */
+ this.imgW = this.img.width;
+ /**
+ * @var int
+ * The image height
+ */
+ this.imgH = this.img.height;
+
+ $( this.north ).setStyle( { height: 0 } );
+ $( this.east ).setStyle( { width: 0, height: 0 } );
+ $( this.south ).setStyle( { height: 0 } );
+ $( this.west ).setStyle( { width: 0, height: 0 } );
+
+ // resize the container to fit the image
+ $( this.imgWrap ).setStyle( { 'width': this.imgW + 'px', 'height': this.imgH + 'px' } );
+
+ // hide the select area
+ $( this.selArea ).hide();
+
+ // setup the starting position of the select area
+ var startCoords = { x1: 0, y1: 0, x2: 0, y2: 0 };
+ var validCoordsSet = false;
+
+ // display the select area
+ if( this.options.onloadCoords != null ) {
+ // if we've being given some coordinates to
+ startCoords = this.cloneCoords( this.options.onloadCoords );
+ validCoordsSet = true;
+ } else if( this.options.ratioDim.x > 0 && this.options.ratioDim.y > 0 ) {
+ // if there is a ratio limit applied and the then set it to initial ratio
+ startCoords.x1 = Math.ceil( ( this.imgW - this.options.ratioDim.x ) / 2 );
+ startCoords.y1 = Math.ceil( ( this.imgH - this.options.ratioDim.y ) / 2 );
+ startCoords.x2 = startCoords.x1 + this.options.ratioDim.x;
+ startCoords.y2 = startCoords.y1 + this.options.ratioDim.y;
+ validCoordsSet = true;
+ }
+
+ this.setAreaCoords( startCoords, false, false, 1 );
+
+ if( this.options.displayOnInit && validCoordsSet ) {
+ this.selArea.show();
+ this.drawArea();
+ this.endCrop();
+ }
+
+ this.attached = true;
+ },
+
+ /**
+ * Removes the cropper
+ *
+ * @access public
+ * @return void
+ */
+ remove: function() {
+ if( this.attached ) {
+ this.attached = false;
+
+ // remove the elements we inserted
+ this.imgWrap.parentNode.insertBefore( this.img, this.imgWrap );
+ this.imgWrap.parentNode.removeChild( this.imgWrap );
+
+ // remove the event observers
+ Event.stopObserving( this.dragArea, 'mousedown', this.startDragBind );
+ Event.stopObserving( document, 'mousemove', this.onDragBind );
+ Event.stopObserving( document, 'mouseup', this.endCropBind );
+ this.registerHandles( false );
+ if( this.options.captureKeys ) Event.stopObserving( document, 'keypress', this.keysBind );
+ }
+ },
+
+ /**
+ * Resets the cropper, can be used either after being removed or any time you wish
+ *
+ * @access public
+ * @return void
+ */
+ reset: function() {
+ if( !this.attached ) this.onLoad();
+ else this.setParams();
+ this.endCrop();
+ },
+
+ /**
+ * Handles the key functionality, currently just using arrow keys to move, if the user
+ * presses shift then the area will move by 10 pixels
+ */
+ handleKeys: function( e ) {
+ var dir = { x: 0, y: 0 }; // direction to move it in & the amount in pixels
+ if( !this.dragging ) {
+
+ // catch the arrow keys
+ switch( e.keyCode ) {
+ case( 37 ) : // left
+ dir.x = -1;
+ break;
+ case( 38 ) : // up
+ dir.y = -1;
+ break;
+ case( 39 ) : // right
+ dir.x = 1;
+ break
+ case( 40 ) : // down
+ dir.y = 1;
+ break;
+ }
+
+ if( dir.x != 0 || dir.y != 0 ) {
+ // if shift is pressed then move by 10 pixels
+ if( e.shiftKey ) {
+ dir.x *= 10;
+ dir.y *= 10;
+ }
+
+ this.moveArea( [ this.areaCoords.x1 + dir.x, this.areaCoords.y1 + dir.y ] );
+ Event.stop( e );
+ }
+ }
+ },
+
+ /**
+ * Calculates the width from the areaCoords
+ *
+ * @access private
+ * @return int
+ */
+ calcW: function() {
+ return (this.areaCoords.x2 - this.areaCoords.x1)
+ },
+
+ /**
+ * Calculates the height from the areaCoords
+ *
+ * @access private
+ * @return int
+ */
+ calcH: function() {
+ return (this.areaCoords.y2 - this.areaCoords.y1)
+ },
+
+ /**
+ * Moves the select area to the supplied point (assumes the point is x1 & y1 of the select area)
+ *
+ * @access public
+ * @param array Point for x1 & y1 to move select area to
+ * @return void
+ */
+ moveArea: function( point ) {
+ // dump( 'moveArea : ' + point[0] + ',' + point[1] + ',' + ( point[0] + ( this.areaCoords.x2 - this.areaCoords.x1 ) ) + ',' + ( point[1] + ( this.areaCoords.y2 - this.areaCoords.y1 ) ) + '\n' );
+ this.setAreaCoords(
+ {
+ x1: point[0],
+ y1: point[1],
+ x2: point[0] + this.calcW(),
+ y2: point[1] + this.calcH()
+ },
+ true,
+ false
+ );
+ this.drawArea();
+ },
+
+ /**
+ * Clones a co-ordinates object, stops problems with handling them by reference
+ *
+ * @access private
+ * @param obj Coordinate object x1, y1, x2, y2
+ * @return obj Coordinate object x1, y1, x2, y2
+ */
+ cloneCoords: function( coords ) {
+ return { x1: coords.x1, y1: coords.y1, x2: coords.x2, y2: coords.y2 };
+ },
+
+ /**
+ * Sets the select coords to those provided but ensures they don't go
+ * outside the bounding box
+ *
+ * @access private
+ * @param obj Coordinates x1, y1, x2, y2
+ * @param boolean Whether this is a move
+ * @param boolean Whether to apply squaring
+ * @param obj Direction of mouse along both axis x, y ( -1 = negative, 1 = positive ) only required when moving etc.
+ * @param string The current resize handle || null
+ * @return void
+ */
+ setAreaCoords: function( coords, moving, square, direction, resizeHandle ) {
+ // dump( 'setAreaCoords (in) : ' + coords.x1 + ',' + coords.y1 + ',' + coords.x2 + ',' + coords.y2 );
+ if( moving ) {
+ // if moving
+ var targW = coords.x2 - coords.x1;
+ var targH = coords.y2 - coords.y1;
+
+ // ensure we're within the bounds
+ if( coords.x1 < 0 ) {
+ coords.x1 = 0;
+ coords.x2 = targW;
+ }
+ if( coords.y1 < 0 ) {
+ coords.y1 = 0;
+ coords.y2 = targH;
+ }
+ if( coords.x2 > this.imgW ) {
+ coords.x2 = this.imgW;
+ coords.x1 = this.imgW - targW;
+ }
+ if( coords.y2 > this.imgH ) {
+ coords.y2 = this.imgH;
+ coords.y1 = this.imgH - targH;
+ }
+ } else {
+ // ensure we're within the bounds
+ if( coords.x1 < 0 ) coords.x1 = 0;
+ if( coords.y1 < 0 ) coords.y1 = 0;
+ if( coords.x2 > this.imgW ) coords.x2 = this.imgW;
+ if( coords.y2 > this.imgH ) coords.y2 = this.imgH;
+
+ // This is passed as null in onload
+ if( direction != null ) {
+
+ // apply the ratio or squaring where appropriate
+ if( this.ratioX > 0 ) this.applyRatio( coords, { x: this.ratioX, y: this.ratioY }, direction, resizeHandle );
+ else if( square ) this.applyRatio( coords, { x: 1, y: 1 }, direction, resizeHandle );
+
+ var mins = [ this.options.minWidth, this.options.minHeight ]; // minimum dimensions [x,y]
+ var maxs = [ this.options.maxWidth, this.options.maxHeight ]; // maximum dimensions [x,y]
+
+ // apply dimensions where appropriate
+ if( mins[0] > 0 || mins[1] > 0 || maxs[0] > 0 || maxs[1] > 0) {
+
+ var coordsTransX = { a1: coords.x1, a2: coords.x2 };
+ var coordsTransY = { a1: coords.y1, a2: coords.y2 };
+ var boundsX = { min: 0, max: this.imgW };
+ var boundsY = { min: 0, max: this.imgH };
+
+ // handle squaring properly on single axis minimum dimensions
+ if( (mins[0] != 0 || mins[1] != 0) && square ) {
+ if( mins[0] > 0 ) mins[1] = mins[0];
+ else if( mins[1] > 0 ) mins[0] = mins[1];
+ }
+
+ if( (maxs[0] != 0 || maxs[0] != 0) && square ) {
+ // 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)
+ if( maxs[0] > 0 && maxs[0] <= maxs[1] ) maxs[1] = maxs[0];
+ else if( maxs[1] > 0 && maxs[1] <= maxs[0] ) maxs[0] = maxs[1];
+ }
+
+ if( mins[0] > 0 ) this.applyDimRestriction( coordsTransX, mins[0], direction.x, boundsX, 'min' );
+ if( mins[1] > 1 ) this.applyDimRestriction( coordsTransY, mins[1], direction.y, boundsY, 'min' );
+
+ if( maxs[0] > 0 ) this.applyDimRestriction( coordsTransX, maxs[0], direction.x, boundsX, 'max' );
+ if( maxs[1] > 1 ) this.applyDimRestriction( coordsTransY, maxs[1], direction.y, boundsY, 'max' );
+
+ coords = { x1: coordsTransX.a1, y1: coordsTransY.a1, x2: coordsTransX.a2, y2: coordsTransY.a2 };
+ }
+
+ }
+ }
+
+ // dump( 'setAreaCoords (out) : ' + coords.x1 + ',' + coords.y1 + ',' + coords.x2 + ',' + coords.y2 + '\n' );
+ this.areaCoords = coords;
+ },
+
+ /**
+ * Applies the supplied dimension restriction to the supplied coordinates along a single axis
+ *
+ * @access private
+ * @param obj Single axis coordinates, a1, a2 (e.g. for the x axis a1 = x1 & a2 = x2)
+ * @param int The restriction value
+ * @param int The direction ( -1 = negative, 1 = positive )
+ * @param obj The bounds of the image ( for this axis )
+ * @param string The dimension restriction type ( 'min' | 'max' )
+ * @return void
+ */
+ applyDimRestriction: function( coords, val, direction, bounds, type ) {
+ var check;
+ if( type == 'min' ) check = ( ( coords.a2 - coords.a1 ) < val );
+ else check = ( ( coords.a2 - coords.a1 ) > val );
+ if( check ) {
+ if( direction == 1 ) coords.a2 = coords.a1 + val;
+ else coords.a1 = coords.a2 - val;
+
+ // make sure we're still in the bounds (not too pretty for the user, but needed)
+ if( coords.a1 < bounds.min ) {
+ coords.a1 = bounds.min;
+ coords.a2 = val;
+ } else if( coords.a2 > bounds.max ) {
+ coords.a1 = bounds.max - val;
+ coords.a2 = bounds.max;
+ }
+ }
+ },
+
+ /**
+ * Applies the supplied ratio to the supplied coordinates
+ *
+ * @access private
+ * @param obj Coordinates, x1, y1, x2, y2
+ * @param obj Ratio, x, y
+ * @param obj Direction of mouse, x & y : -1 == negative 1 == positive
+ * @param string The current resize handle || null
+ * @return void
+ */
+ applyRatio : function( coords, ratio, direction, resizeHandle ) {
+ // dump( 'direction.y : ' + direction.y + '\n');
+ var newCoords;
+ if( resizeHandle == 'N' || resizeHandle == 'S' ) {
+ // dump( 'north south \n');
+ // if moving on either the lone north & south handles apply the ratio on the y axis
+ newCoords = this.applyRatioToAxis(
+ { a1: coords.y1, b1: coords.x1, a2: coords.y2, b2: coords.x2 },
+ { a: ratio.y, b: ratio.x },
+ { a: direction.y, b: direction.x },
+ { min: 0, max: this.imgW }
+ );
+ coords.x1 = newCoords.b1;
+ coords.y1 = newCoords.a1;
+ coords.x2 = newCoords.b2;
+ coords.y2 = newCoords.a2;
+ } else {
+ // otherwise deal with it as if we're applying the ratio on the x axis
+ newCoords = this.applyRatioToAxis(
+ { a1: coords.x1, b1: coords.y1, a2: coords.x2, b2: coords.y2 },
+ { a: ratio.x, b: ratio.y },
+ { a: direction.x, b: direction.y },
+ { min: 0, max: this.imgH }
+ );
+ coords.x1 = newCoords.a1;
+ coords.y1 = newCoords.b1;
+ coords.x2 = newCoords.a2;
+ coords.y2 = newCoords.b2;
+ }
+
+ },
+
+ /**
+ * Applies the provided ratio to the provided coordinates based on provided direction & bounds,
+ * use to encapsulate functionality to make it easy to apply to either axis. This is probably
+ * quite hard to visualise so see the x axis example within applyRatio()
+ *
+ * Example in parameter details & comments is for requesting applying ratio to x axis.
+ *
+ * @access private
+ * @param obj Coords object (a1, b1, a2, b2) where a = x & b = y in example
+ * @param obj Ratio object (a, b) where a = x & b = y in example
+ * @param obj Direction object (a, b) where a = x & b = y in example
+ * @param obj Bounds (min, max)
+ * @return obj Coords object (a1, b1, a2, b2) where a = x & b = y in example
+ */
+ applyRatioToAxis: function( coords, ratio, direction, bounds ) {
+ var newCoords = Object.extend( coords, {} );
+ var calcDimA = newCoords.a2 - newCoords.a1; // calculate dimension a (e.g. width)
+ var targDimB = Math.floor( calcDimA * ratio.b / ratio.a ); // the target dimension b (e.g. height)
+ var targB; // to hold target b (e.g. y value)
+ var targDimA; // to hold target dimension a (e.g. width)
+ var calcDimB = null; // to hold calculated dimension b (e.g. height)
+
+ // dump( 'newCoords[0]: ' + newCoords.a1 + ',' + newCoords.b1 + ','+ newCoords.a2 + ',' + newCoords.b2 + '\n');
+
+ if( direction.b == 1 ) { // if travelling in a positive direction
+ // make sure we're not going out of bounds
+ targB = newCoords.b1 + targDimB;
+ if( targB > bounds.max ) {
+ targB = bounds.max;
+ calcDimB = targB - newCoords.b1; // calcuate dimension b (e.g. height)
+ }
+
+ newCoords.b2 = targB;
+ } else { // if travelling in a negative direction
+ // make sure we're not going out of bounds
+ targB = newCoords.b2 - targDimB;
+ if( targB < bounds.min ) {
+ targB = bounds.min;
+ calcDimB = targB + newCoords.b2; // calcuate dimension b (e.g. height)
+ }
+ newCoords.b1 = targB;
+ }
+
+ // dump( 'newCoords[1]: ' + newCoords.a1 + ',' + newCoords.b1 + ','+ newCoords.a2 + ',' + newCoords.b2 + '\n');
+
+ // apply the calculated dimensions
+ if( calcDimB != null ) {
+ targDimA = Math.floor( calcDimB * ratio.a / ratio.b );
+
+ if( direction.a == 1 ) newCoords.a2 = newCoords.a1 + targDimA;
+ else newCoords.a1 = newCoords.a1 = newCoords.a2 - targDimA;
+ }
+
+ // dump( 'newCoords[2]: ' + newCoords.a1 + ',' + newCoords.b1 + ','+ newCoords.a2 + ',' + newCoords.b2 + '\n');
+
+ return newCoords;
+ },
+
+ /**
+ * Draws the select area
+ *
+ * @access private
+ * @return void
+ */
+ drawArea: function( ) {
+ /*
+ * NOTE: I'm not using the Element.setStyle() shortcut as they make it
+ * quite sluggish on Mac based browsers
+ */
+ // dump( 'drawArea : ' + this.areaCoords.x1 + ',' + this.areaCoords.y1 + ',' + this.areaCoords.x2 + ',' + this.areaCoords.y2 + '\n' );
+ var areaWidth = this.calcW();
+ var areaHeight = this.calcH();
+
+ /*
+ * Calculate all the style strings before we use them, allows reuse & produces quicker
+ * rendering (especially noticable in Mac based browsers)
+ */
+ var px = 'px';
+ var params = [
+ this.areaCoords.x1 + px, // the left of the selArea
+ this.areaCoords.y1 + px, // the top of the selArea
+ areaWidth + px, // width of the selArea
+ areaHeight + px, // height of the selArea
+ this.areaCoords.x2 + px, // bottom of the selArea
+ this.areaCoords.y2 + px, // right of the selArea
+ (this.img.width - this.areaCoords.x2) + px, // right edge of selArea
+ (this.img.height - this.areaCoords.y2) + px // bottom edge of selArea
+ ];
+
+ // do the select area
+ var areaStyle = this.selArea.style;
+ areaStyle.left = params[0];
+ areaStyle.top = params[1];
+ areaStyle.width = params[2];
+ areaStyle.height = params[3];
+
+ // position the north, east, south & west handles
+ var horizHandlePos = Math.ceil( (areaWidth - 6) / 2 ) + px;
+ var vertHandlePos = Math.ceil( (areaHeight - 6) / 2 ) + px;
+
+ this.handleN.style.left = horizHandlePos;
+ this.handleE.style.top = vertHandlePos;
+ this.handleS.style.left = horizHandlePos;
+ this.handleW.style.top = vertHandlePos;
+
+ // draw the four overlays
+ this.north.style.height = params[1];
+
+ var eastStyle = this.east.style;
+ eastStyle.top = params[1];
+ eastStyle.height = params[3];
+ eastStyle.left = params[4];
+ eastStyle.width = params[6];
+
+ var southStyle = this.south.style;
+ southStyle.top = params[5];
+ southStyle.height = params[7];
+
+ var westStyle = this.west.style;
+ westStyle.top = params[1];
+ westStyle.height = params[3];
+ westStyle.width = params[0];
+
+ // call the draw method on sub classes
+ this.subDrawArea();
+
+ this.forceReRender();
+ },
+
+ /**
+ * Force the re-rendering of the selArea element which fixes rendering issues in Safari
+ * & IE PC, especially evident when re-sizing perfectly vertical using any of the south handles
+ *
+ * @access private
+ * @return void
+ */
+ forceReRender: function() {
+ if( this.isIE || this.isWebKit) {
+ var n = document.createTextNode(' ');
+ var d,el,fixEL,i;
+
+ if( this.isIE ) fixEl = this.selArea;
+ else if( this.isWebKit ) {
+ fixEl = document.getElementsByClassName( 'imgCrop_marqueeSouth', this.imgWrap )[0];
+ /* we have to be a bit more forceful for Safari, otherwise the the marquee &
+ * the south handles still don't move
+ */
+ d = Builder.node( 'div', '' );
+ d.style.visibility = 'hidden';
+
+ var classList = ['SE','S','SW'];
+ for( i = 0; i < classList.length; i++ ) {
+ el = document.getElementsByClassName( 'imgCrop_handle' + classList[i], this.selArea )[0];
+ if( el.childNodes.length ) el.removeChild( el.childNodes[0] );
+ el.appendChild(d);
+ }
+ }
+ fixEl.appendChild(n);
+ fixEl.removeChild(n);
+ }
+ },
+
+ /**
+ * Starts the resize
+ *
+ * @access private
+ * @param obj Event
+ * @return void
+ */
+ startResize: function( e ) {
+ this.startCoords = this.cloneCoords( this.areaCoords );
+
+ this.resizing = true;
+ this.resizeHandle = Event.element( e ).classNames().toString().replace(/([^N|NE|E|SE|S|SW|W|NW])+/, '');
+ // dump( 'this.resizeHandle : ' + this.resizeHandle + '\n' );
+ Event.stop( e );
+ },
+
+ /**
+ * Starts the drag
+ *
+ * @access private
+ * @param obj Event
+ * @return void
+ */
+ startDrag: function( e ) {
+ this.selArea.show();
+ this.clickCoords = this.getCurPos( e );
+
+ this.setAreaCoords( { x1: this.clickCoords.x, y1: this.clickCoords.y, x2: this.clickCoords.x, y2: this.clickCoords.y }, false, false, null );
+
+ this.dragging = true;
+ this.onDrag( e ); // incase the user just clicks once after already making a selection
+ Event.stop( e );
+ },
+
+ /**
+ * Gets the current cursor position relative to the image
+ *
+ * @access private
+ * @param obj Event
+ * @return obj x,y pixels of the cursor
+ */
+ getCurPos: function( e ) {
+ // get the offsets for the wrapper within the document
+ var el = this.imgWrap, wrapOffsets = Position.cumulativeOffset( el );
+ // 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
+ while( el.nodeName != 'BODY' ) {
+ wrapOffsets[1] -= el.scrollTop || 0;
+ wrapOffsets[0] -= el.scrollLeft || 0;
+ el = el.parentNode;
+ }
+ return curPos = {
+ x: Event.pointerX(e) - wrapOffsets[0],
+ y: Event.pointerY(e) - wrapOffsets[1]
+ }
+ },
+
+ /**
+ * Performs the drag for both resize & inital draw dragging
+ *
+ * @access private
+ * @param obj Event
+ * @return void
+ */
+ onDrag: function( e ) {
+ if( this.dragging || this.resizing ) {
+
+ var resizeHandle = null;
+ var curPos = this.getCurPos( e );
+ var newCoords = this.cloneCoords( this.areaCoords );
+ var direction = { x: 1, y: 1 };
+
+ if( this.dragging ) {
+ if( curPos.x < this.clickCoords.x ) direction.x = -1;
+ if( curPos.y < this.clickCoords.y ) direction.y = -1;
+
+ this.transformCoords( curPos.x, this.clickCoords.x, newCoords, 'x' );
+ this.transformCoords( curPos.y, this.clickCoords.y, newCoords, 'y' );
+ } else if( this.resizing ) {
+ resizeHandle = this.resizeHandle;
+ // do x movements first
+ if( resizeHandle.match(/E/) ) {
+ // if we're moving an east handle
+ this.transformCoords( curPos.x, this.startCoords.x1, newCoords, 'x' );
+ if( curPos.x < this.startCoords.x1 ) direction.x = -1;
+ } else if( resizeHandle.match(/W/) ) {
+ // if we're moving an west handle
+ this.transformCoords( curPos.x, this.startCoords.x2, newCoords, 'x' );
+ if( curPos.x < this.startCoords.x2 ) direction.x = -1;
+ }
+
+ // do y movements second
+ if( resizeHandle.match(/N/) ) {
+ // if we're moving an north handle
+ this.transformCoords( curPos.y, this.startCoords.y2, newCoords, 'y' );
+ if( curPos.y < this.startCoords.y2 ) direction.y = -1;
+ } else if( resizeHandle.match(/S/) ) {
+ // if we're moving an south handle
+ this.transformCoords( curPos.y, this.startCoords.y1, newCoords, 'y' );
+ if( curPos.y < this.startCoords.y1 ) direction.y = -1;
+ }
+
+ }
+
+ this.setAreaCoords( newCoords, false, e.shiftKey, direction, resizeHandle );
+ this.drawArea();
+ Event.stop( e ); // stop the default event (selecting images & text) in Safari & IE PC
+ }
+ },
+
+ /**
+ * Applies the appropriate transform to supplied co-ordinates, on the
+ * defined axis, depending on the relationship of the supplied values
+ *
+ * @access private
+ * @param int Current value of pointer
+ * @param int Base value to compare current pointer val to
+ * @param obj Coordinates to apply transformation on x1, x2, y1, y2
+ * @param string Axis to apply transformation on 'x' || 'y'
+ * @return void
+ */
+ transformCoords : function( curVal, baseVal, coords, axis ) {
+ var newVals = [ curVal, baseVal ];
+ if( curVal > baseVal ) newVals.reverse();
+ coords[ axis + '1' ] = newVals[0];
+ coords[ axis + '2' ] = newVals[1];
+ },
+
+ /**
+ * Ends the crop & passes the values of the select area on to the appropriate
+ * callback function on completion of a crop
+ *
+ * @access private
+ * @return void
+ */
+ endCrop : function() {
+ this.dragging = false;
+ this.resizing = false;
+
+ this.options.onEndCrop(
+ this.areaCoords,
+ {
+ width: this.calcW(),
+ height: this.calcH()
+ }
+ );
+ },
+
+ /**
+ * Abstract method called on the end of initialization
+ *
+ * @access private
+ * @abstract
+ * @return void
+ */
+ subInitialize: function() {},
+
+ /**
+ * Abstract method called on the end of drawArea()
+ *
+ * @access private
+ * @abstract
+ * @return void
+ */
+ subDrawArea: function() {}
+};
+
+
+
+
+/**
+ * Extend the Cropper.Img class to allow for presentation of a preview image of the resulting crop,
+ * the option for displayOnInit is always overridden to true when displaying a preview image
+ *
+ * Usage:
+ * @param obj Image element to attach to
+ * @param obj Optional options:
+ * - see Cropper.Img for base options
+ *
+ * - previewWrap obj
+ * HTML element that will be used as a container for the preview image
+ */
+Cropper.ImgWithPreview = Class.create();
+
+Object.extend( Object.extend( Cropper.ImgWithPreview.prototype, Cropper.Img.prototype ), {
+
+ /**
+ * Implements the abstract method from Cropper.Img to initialize preview image settings.
+ * Will only attach a preview image is the previewWrap element is defined and the minWidth
+ * & minHeight options are set.
+ *
+ * @see Croper.Img.subInitialize
+ */
+ subInitialize: function() {
+ /**
+ * Whether or not we've attached a preview image
+ * @var boolean
+ */
+ this.hasPreviewImg = false;
+ if( typeof(this.options.previewWrap) != 'undefined'
+ && this.options.minWidth > 0
+ && this.options.minHeight > 0
+ ) {
+ /**
+ * The preview image wrapper element
+ * @var obj HTML element
+ */
+ this.previewWrap = $( this.options.previewWrap );
+ /**
+ * The preview image element
+ * @var obj HTML IMG element
+ */
+ this.previewImg = this.img.cloneNode( false );
+ // set the ID of the preview image to be unique
+ this.previewImg.id = 'imgCrop_' + this.previewImg.id;
+
+
+ // set the displayOnInit option to true so we display the select area at the same time as the thumbnail
+ this.options.displayOnInit = true;
+
+ this.hasPreviewImg = true;
+
+ this.previewWrap.addClassName( 'imgCrop_previewWrap' );
+
+ this.previewWrap.setStyle(
+ {
+ width: this.options.minWidth + 'px',
+ height: this.options.minHeight + 'px'
+ }
+ );
+
+ this.previewWrap.appendChild( this.previewImg );
+ }
+ },
+
+ /**
+ * Implements the abstract method from Cropper.Img to draw the preview image
+ *
+ * @see Croper.Img.subDrawArea
+ */
+ subDrawArea: function() {
+ if( this.hasPreviewImg ) {
+ // get the ratio of the select area to the src image
+ var calcWidth = this.calcW();
+ var calcHeight = this.calcH();
+ // ratios for the dimensions of the preview image
+ var dimRatio = {
+ x: this.imgW / calcWidth,
+ y: this.imgH / calcHeight
+ };
+ //ratios for the positions within the preview
+ var posRatio = {
+ x: calcWidth / this.options.minWidth,
+ y: calcHeight / this.options.minHeight
+ };
+
+ // setting the positions in an obj before apply styles for rendering speed increase
+ var calcPos = {
+ w: Math.ceil( this.options.minWidth * dimRatio.x ) + 'px',
+ h: Math.ceil( this.options.minHeight * dimRatio.y ) + 'px',
+ x: '-' + Math.ceil( this.areaCoords.x1 / posRatio.x ) + 'px',
+ y: '-' + Math.ceil( this.areaCoords.y1 / posRatio.y ) + 'px'
+ }
+
+ var previewStyle = this.previewImg.style;
+ previewStyle.width = calcPos.w;
+ previewStyle.height = calcPos.h;
+ previewStyle.left = calcPos.x;
+ previewStyle.top = calcPos.y;
+ }
+ }
+
+});
diff --git a/cropper/lib/builder.js b/cropper/lib/builder.js
new file mode 100644
index 000000000..5b15ba939
--- /dev/null
+++ b/cropper/lib/builder.js
@@ -0,0 +1,101 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// See scriptaculous.js for full license.
+
+var Builder = {
+ NODEMAP: {
+ AREA: 'map',
+ CAPTION: 'table',
+ COL: 'table',
+ COLGROUP: 'table',
+ LEGEND: 'fieldset',
+ OPTGROUP: 'select',
+ OPTION: 'select',
+ PARAM: 'object',
+ TBODY: 'table',
+ TD: 'table',
+ TFOOT: 'table',
+ TH: 'table',
+ THEAD: 'table',
+ TR: 'table'
+ },
+ // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
+ // due to a Firefox bug
+ node: function(elementName) {
+ elementName = elementName.toUpperCase();
+
+ // try innerHTML approach
+ var parentTag = this.NODEMAP[elementName] || 'div';
+ var parentElement = document.createElement(parentTag);
+ try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
+ parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
+ } catch(e) {}
+ var element = parentElement.firstChild || null;
+
+ // see if browser added wrapping tags
+ if(element && (element.tagName != elementName))
+ element = element.getElementsByTagName(elementName)[0];
+
+ // fallback to createElement approach
+ if(!element) element = document.createElement(elementName);
+
+ // abort if nothing could be created
+ if(!element) return;
+
+ // attributes (or text)
+ if(arguments[1])
+ if(this._isStringOrNumber(arguments[1]) ||
+ (arguments[1] instanceof Array)) {
+ this._children(element, arguments[1]);
+ } else {
+ var attrs = this._attributes(arguments[1]);
+ if(attrs.length) {
+ try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
+ parentElement.innerHTML = "<" +elementName + " " +
+ attrs + "></" + elementName + ">";
+ } catch(e) {}
+ element = parentElement.firstChild || null;
+ // workaround firefox 1.0.X bug
+ if(!element) {
+ element = document.createElement(elementName);
+ for(attr in arguments[1])
+ element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
+ }
+ if(element.tagName != elementName)
+ element = parentElement.getElementsByTagName(elementName)[0];
+ }
+ }
+
+ // text, or array of children
+ if(arguments[2])
+ this._children(element, arguments[2]);
+
+ return element;
+ },
+ _text: function(text) {
+ return document.createTextNode(text);
+ },
+ _attributes: function(attributes) {
+ var attrs = [];
+ for(attribute in attributes)
+ attrs.push((attribute=='className' ? 'class' : attribute) +
+ '="' + attributes[attribute].toString().escapeHTML() + '"');
+ return attrs.join(" ");
+ },
+ _children: function(element, children) {
+ if(typeof children=='object') { // array can hold nodes and text
+ children.flatten().each( function(e) {
+ if(typeof e=='object')
+ element.appendChild(e)
+ else
+ if(Builder._isStringOrNumber(e))
+ element.appendChild(Builder._text(e));
+ });
+ } else
+ if(Builder._isStringOrNumber(children))
+ element.appendChild(Builder._text(children));
+ },
+ _isStringOrNumber: function(param) {
+ return(typeof param=='string' || typeof param=='number');
+ }
+} \ No newline at end of file
diff --git a/cropper/lib/controls.js b/cropper/lib/controls.js
new file mode 100644
index 000000000..de0261ed5
--- /dev/null
+++ b/cropper/lib/controls.js
@@ -0,0 +1,815 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+// (c) 2005 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+// Richard Livsey
+// Rahul Bhargava
+// Rob Wills
+//
+// See scriptaculous.js for full license.
+
+// Autocompleter.Base handles all the autocompletion functionality
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least,
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method
+// should get the text for which to provide autocompletion by
+// invoking this.getToken(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: [',', '\n'] } which
+// enables autocompletion on multiple tokens. This is most
+// useful when one of the tokens is \n (a newline), as it
+// allows smart autocompletion after linebreaks.
+
+var Autocompleter = {}
+Autocompleter.Base = function() {};
+Autocompleter.Base.prototype = {
+ baseInitialize: function(element, update, options) {
+ this.element = $(element);
+ this.update = $(update);
+ this.hasFocus = false;
+ this.changed = false;
+ this.active = false;
+ this.index = 0;
+ this.entryCount = 0;
+
+ if (this.setOptions)
+ this.setOptions(options);
+ else
+ this.options = options || {};
+
+ this.options.paramName = this.options.paramName || this.element.name;
+ this.options.tokens = this.options.tokens || [];
+ this.options.frequency = this.options.frequency || 0.4;
+ this.options.minChars = this.options.minChars || 1;
+ this.options.onShow = this.options.onShow ||
+ function(element, update){
+ if(!update.style.position || update.style.position=='absolute') {
+ update.style.position = 'absolute';
+ Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
+ }
+ Effect.Appear(update,{duration:0.15});
+ };
+ this.options.onHide = this.options.onHide ||
+ function(element, update){ new Effect.Fade(update,{duration:0.15}) };
+
+ if (typeof(this.options.tokens) == 'string')
+ this.options.tokens = new Array(this.options.tokens);
+
+ this.observer = null;
+
+ this.element.setAttribute('autocomplete','off');
+
+ Element.hide(this.update);
+
+ Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
+ Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
+ },
+
+ show: function() {
+ if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+ if(!this.iefix &&
+ (navigator.appVersion.indexOf('MSIE')>0) &&
+ (navigator.userAgent.indexOf('Opera')<0) &&
+ (Element.getStyle(this.update, 'position')=='absolute')) {
+ new Insertion.After(this.update,
+ '<iframe id="' + this.update.id + '_iefix" '+
+ 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+ 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+ this.iefix = $(this.update.id+'_iefix');
+ }
+ if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+ },
+
+ fixIEOverlapping: function() {
+ Position.clone(this.update, this.iefix);
+ this.iefix.style.zIndex = 1;
+ this.update.style.zIndex = 2;
+ Element.show(this.iefix);
+ },
+
+ hide: function() {
+ this.stopIndicator();
+ if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
+ if(this.iefix) Element.hide(this.iefix);
+ },
+
+ startIndicator: function() {
+ if(this.options.indicator) Element.show(this.options.indicator);
+ },
+
+ stopIndicator: function() {
+ if(this.options.indicator) Element.hide(this.options.indicator);
+ },
+
+ onKeyPress: function(event) {
+ if(this.active)
+ switch(event.keyCode) {
+ case Event.KEY_TAB:
+ case Event.KEY_RETURN:
+ this.selectEntry();
+ Event.stop(event);
+ case Event.KEY_ESC:
+ this.hide();
+ this.active = false;
+ Event.stop(event);
+ return;
+ case Event.KEY_LEFT:
+ case Event.KEY_RIGHT:
+ return;
+ case Event.KEY_UP:
+ this.markPrevious();
+ this.render();
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+ return;
+ case Event.KEY_DOWN:
+ this.markNext();
+ this.render();
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+ return;
+ }
+ else
+ if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
+ (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return;
+
+ this.changed = true;
+ this.hasFocus = true;
+
+ if(this.observer) clearTimeout(this.observer);
+ this.observer =
+ setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+ },
+
+ activate: function() {
+ this.changed = false;
+ this.hasFocus = true;
+ this.getUpdatedChoices();
+ },
+
+ onHover: function(event) {
+ var element = Event.findElement(event, 'LI');
+ if(this.index != element.autocompleteIndex)
+ {
+ this.index = element.autocompleteIndex;
+ this.render();
+ }
+ Event.stop(event);
+ },
+
+ onClick: function(event) {
+ var element = Event.findElement(event, 'LI');
+ this.index = element.autocompleteIndex;
+ this.selectEntry();
+ this.hide();
+ },
+
+ onBlur: function(event) {
+ // needed to make click events working
+ setTimeout(this.hide.bind(this), 250);
+ this.hasFocus = false;
+ this.active = false;
+ },
+
+ render: function() {
+ if(this.entryCount > 0) {
+ for (var i = 0; i < this.entryCount; i++)
+ this.index==i ?
+ Element.addClassName(this.getEntry(i),"selected") :
+ Element.removeClassName(this.getEntry(i),"selected");
+
+ if(this.hasFocus) {
+ this.show();
+ this.active = true;
+ }
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ },
+
+ markPrevious: function() {
+ if(this.index > 0) this.index--
+ else this.index = this.entryCount-1;
+ },
+
+ markNext: function() {
+ if(this.index < this.entryCount-1) this.index++
+ else this.index = 0;
+ },
+
+ getEntry: function(index) {
+ return this.update.firstChild.childNodes[index];
+ },
+
+ getCurrentEntry: function() {
+ return this.getEntry(this.index);
+ },
+
+ selectEntry: function() {
+ this.active = false;
+ this.updateElement(this.getCurrentEntry());
+ },
+
+ updateElement: function(selectedElement) {
+ if (this.options.updateElement) {
+ this.options.updateElement(selectedElement);
+ return;
+ }
+ var value = '';
+ if (this.options.select) {
+ var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
+ if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
+ } else
+ value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+
+ var lastTokenPos = this.findLastToken();
+ if (lastTokenPos != -1) {
+ var newValue = this.element.value.substr(0, lastTokenPos + 1);
+ var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
+ if (whitespace)
+ newValue += whitespace[0];
+ this.element.value = newValue + value;
+ } else {
+ this.element.value = value;
+ }
+ this.element.focus();
+
+ if (this.options.afterUpdateElement)
+ this.options.afterUpdateElement(this.element, selectedElement);
+ },
+
+ updateChoices: function(choices) {
+ if(!this.changed && this.hasFocus) {
+ this.update.innerHTML = choices;
+ Element.cleanWhitespace(this.update);
+ Element.cleanWhitespace(this.update.firstChild);
+
+ if(this.update.firstChild && this.update.firstChild.childNodes) {
+ this.entryCount =
+ this.update.firstChild.childNodes.length;
+ for (var i = 0; i < this.entryCount; i++) {
+ var entry = this.getEntry(i);
+ entry.autocompleteIndex = i;
+ this.addObservers(entry);
+ }
+ } else {
+ this.entryCount = 0;
+ }
+
+ this.stopIndicator();
+
+ this.index = 0;
+ this.render();
+ }
+ },
+
+ addObservers: function(element) {
+ Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+ Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+ },
+
+ onObserverEvent: function() {
+ this.changed = false;
+ if(this.getToken().length>=this.options.minChars) {
+ this.startIndicator();
+ this.getUpdatedChoices();
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ },
+
+ getToken: function() {
+ var tokenPos = this.findLastToken();
+ if (tokenPos != -1)
+ var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
+ else
+ var ret = this.element.value;
+
+ return /\n/.test(ret) ? '' : ret;
+ },
+
+ findLastToken: function() {
+ var lastTokenPos = -1;
+
+ for (var i=0; i<this.options.tokens.length; i++) {
+ var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
+ if (thisTokenPos > lastTokenPos)
+ lastTokenPos = thisTokenPos;
+ }
+ return lastTokenPos;
+ }
+}
+
+Ajax.Autocompleter = Class.create();
+Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
+ initialize: function(element, update, url, options) {
+ this.baseInitialize(element, update, options);
+ this.options.asynchronous = true;
+ this.options.onComplete = this.onComplete.bind(this);
+ this.options.defaultParams = this.options.parameters || null;
+ this.url = url;
+ },
+
+ getUpdatedChoices: function() {
+ entry = encodeURIComponent(this.options.paramName) + '=' +
+ encodeURIComponent(this.getToken());
+
+ this.options.parameters = this.options.callback ?
+ this.options.callback(this.element, entry) : entry;
+
+ if(this.options.defaultParams)
+ this.options.parameters += '&' + this.options.defaultParams;
+
+ new Ajax.Request(this.url, this.options);
+ },
+
+ onComplete: function(request) {
+ this.updateChoices(request.responseText);
+ }
+
+});
+
+// The local array autocompleter. Used when you'd prefer to
+// inject an array of autocompletion options into the page, rather
+// than sending out Ajax queries, which can be quite slow sometimes.
+//
+// The constructor takes four parameters. The first two are, as usual,
+// the id of the monitored textbox, and id of the autocompletion menu.
+// The third is the array you want to autocomplete from, and the fourth
+// is the options block.
+//
+// Extra local autocompletion options:
+// - choices - How many autocompletion choices to offer
+//
+// - partialSearch - If false, the autocompleter will match entered
+// text only at the beginning of strings in the
+// autocomplete array. Defaults to true, which will
+// match text at the beginning of any *word* in the
+// strings in the autocomplete array. If you want to
+// search anywhere in the string, additionally set
+// the option fullSearch to true (default: off).
+//
+// - fullSsearch - Search anywhere in autocomplete array strings.
+//
+// - partialChars - How many characters to enter before triggering
+// a partial match (unlike minChars, which defines
+// how many characters are required to do any match
+// at all). Defaults to 2.
+//
+// - ignoreCase - Whether to ignore case when autocompleting.
+// Defaults to true.
+//
+// It's possible to pass in a custom function as the 'selector'
+// option, if you prefer to write your own autocompletion logic.
+// In that case, the other options above will not apply unless
+// you support them.
+
+Autocompleter.Local = Class.create();
+Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
+ initialize: function(element, update, array, options) {
+ this.baseInitialize(element, update, options);
+ this.options.array = array;
+ },
+
+ getUpdatedChoices: function() {
+ this.updateChoices(this.options.selector(this));
+ },
+
+ setOptions: function(options) {
+ this.options = Object.extend({
+ choices: 10,
+ partialSearch: true,
+ partialChars: 2,
+ ignoreCase: true,
+ fullSearch: false,
+ selector: function(instance) {
+ var ret = []; // Beginning matches
+ var partial = []; // Inside matches
+ var entry = instance.getToken();
+ var count = 0;
+
+ for (var i = 0; i < instance.options.array.length &&
+ ret.length < instance.options.choices ; i++) {
+
+ var elem = instance.options.array[i];
+ var foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase()) :
+ elem.indexOf(entry);
+
+ while (foundPos != -1) {
+ if (foundPos == 0 && elem.length != entry.length) {
+ ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
+ elem.substr(entry.length) + "</li>");
+ break;
+ } else if (entry.length >= instance.options.partialChars &&
+ instance.options.partialSearch && foundPos != -1) {
+ if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+ partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
+ elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
+ foundPos + entry.length) + "</li>");
+ break;
+ }
+ }
+
+ foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
+ elem.indexOf(entry, foundPos + 1);
+
+ }
+ }
+ if (partial.length)
+ ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
+ return "<ul>" + ret.join('') + "</ul>";
+ }
+ }, options || {});
+ }
+});
+
+// AJAX in-place editor
+//
+// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
+
+// Use this if you notice weird scrolling problems on some browsers,
+// the DOM might be a bit confused when this gets called so do this
+// waits 1 ms (with setTimeout) until it does the activation
+Field.scrollFreeActivate = function(field) {
+ setTimeout(function() {
+ Field.activate(field);
+ }, 1);
+}
+
+Ajax.InPlaceEditor = Class.create();
+Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
+Ajax.InPlaceEditor.prototype = {
+ initialize: function(element, url, options) {
+ this.url = url;
+ this.element = $(element);
+
+ this.options = Object.extend({
+ okButton: true,
+ okText: "ok",
+ cancelLink: true,
+ cancelText: "cancel",
+ savingText: "Saving...",
+ clickToEditText: "Click to edit",
+ okText: "ok",
+ rows: 1,
+ onComplete: function(transport, element) {
+ new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
+ },
+ onFailure: function(transport) {
+ alert("Error communicating with the server: " + transport.responseText.stripTags());
+ },
+ callback: function(form) {
+ return Form.serialize(form);
+ },
+ handleLineBreaks: true,
+ loadingText: 'Loading...',
+ savingClassName: 'inplaceeditor-saving',
+ loadingClassName: 'inplaceeditor-loading',
+ formClassName: 'inplaceeditor-form',
+ highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
+ highlightendcolor: "#FFFFFF",
+ externalControl: null,
+ submitOnBlur: false,
+ ajaxOptions: {},
+ evalScripts: false
+ }, options || {});
+
+ if(!this.options.formId && this.element.id) {
+ this.options.formId = this.element.id + "-inplaceeditor";
+ if ($(this.options.formId)) {
+ // there's already a form with that name, don't specify an id
+ this.options.formId = null;
+ }
+ }
+
+ if (this.options.externalControl) {
+ this.options.externalControl = $(this.options.externalControl);
+ }
+
+ this.originalBackground = Element.getStyle(this.element, 'background-color');
+ if (!this.originalBackground) {
+ this.originalBackground = "transparent";
+ }
+
+ this.element.title = this.options.clickToEditText;
+
+ this.onclickListener = this.enterEditMode.bindAsEventListener(this);
+ this.mouseoverListener = this.enterHover.bindAsEventListener(this);
+ this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
+ Event.observe(this.element, 'click', this.onclickListener);
+ Event.observe(this.element, 'mouseover', this.mouseoverListener);
+ Event.observe(this.element, 'mouseout', this.mouseoutListener);
+ if (this.options.externalControl) {
+ Event.observe(this.options.externalControl, 'click', this.onclickListener);
+ Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
+ Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
+ }
+ },
+ enterEditMode: function(evt) {
+ if (this.saving) return;
+ if (this.editing) return;
+ this.editing = true;
+ this.onEnterEditMode();
+ if (this.options.externalControl) {
+ Element.hide(this.options.externalControl);
+ }
+ Element.hide(this.element);
+ this.createForm();
+ this.element.parentNode.insertBefore(this.form, this.element);
+ Field.scrollFreeActivate(this.editField);
+ // stop the event to avoid a page refresh in Safari
+ if (evt) {
+ Event.stop(evt);
+ }
+ return false;
+ },
+ createForm: function() {
+ this.form = document.createElement("form");
+ this.form.id = this.options.formId;
+ Element.addClassName(this.form, this.options.formClassName)
+ this.form.onsubmit = this.onSubmit.bind(this);
+
+ this.createEditField();
+
+ if (this.options.textarea) {
+ var br = document.createElement("br");
+ this.form.appendChild(br);
+ }
+
+ if (this.options.okButton) {
+ okButton = document.createElement("input");
+ okButton.type = "submit";
+ okButton.value = this.options.okText;
+ okButton.className = 'editor_ok_button';
+ this.form.appendChild(okButton);
+ }
+
+ if (this.options.cancelLink) {
+ cancelLink = document.createElement("a");
+ cancelLink.href = "#";
+ cancelLink.appendChild(document.createTextNode(this.options.cancelText));
+ cancelLink.onclick = this.onclickCancel.bind(this);
+ cancelLink.className = 'editor_cancel';
+ this.form.appendChild(cancelLink);
+ }
+ },
+ hasHTMLLineBreaks: function(string) {
+ if (!this.options.handleLineBreaks) return false;
+ return string.match(/<br/i) || string.match(/<p>/i);
+ },
+ convertHTMLLineBreaks: function(string) {
+ return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
+ },
+ createEditField: function() {
+ var text;
+ if(this.options.loadTextURL) {
+ text = this.options.loadingText;
+ } else {
+ text = this.getText();
+ }
+
+ var obj = this;
+
+ if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
+ this.options.textarea = false;
+ var textField = document.createElement("input");
+ textField.obj = this;
+ textField.type = "text";
+ textField.name = "value";
+ textField.value = text;
+ textField.style.backgroundColor = this.options.highlightcolor;
+ textField.className = 'editor_field';
+ var size = this.options.size || this.options.cols || 0;
+ if (size != 0) textField.size = size;
+ if (this.options.submitOnBlur)
+ textField.onblur = this.onSubmit.bind(this);
+ this.editField = textField;
+ } else {
+ this.options.textarea = true;
+ var textArea = document.createElement("textarea");
+ textArea.obj = this;
+ textArea.name = "value";
+ textArea.value = this.convertHTMLLineBreaks(text);
+ textArea.rows = this.options.rows;
+ textArea.cols = this.options.cols || 40;
+ textArea.className = 'editor_field';
+ if (this.options.submitOnBlur)
+ textArea.onblur = this.onSubmit.bind(this);
+ this.editField = textArea;
+ }
+
+ if(this.options.loadTextURL) {
+ this.loadExternalText();
+ }
+ this.form.appendChild(this.editField);
+ },
+ getText: function() {
+ return this.element.innerHTML;
+ },
+ loadExternalText: function() {
+ Element.addClassName(this.form, this.options.loadingClassName);
+ this.editField.disabled = true;
+ new Ajax.Request(
+ this.options.loadTextURL,
+ Object.extend({
+ asynchronous: true,
+ onComplete: this.onLoadedExternalText.bind(this)
+ }, this.options.ajaxOptions)
+ );
+ },
+ onLoadedExternalText: function(transport) {
+ Element.removeClassName(this.form, this.options.loadingClassName);
+ this.editField.disabled = false;
+ this.editField.value = transport.responseText.stripTags();
+ },
+ onclickCancel: function() {
+ this.onComplete();
+ this.leaveEditMode();
+ return false;
+ },
+ onFailure: function(transport) {
+ this.options.onFailure(transport);
+ if (this.oldInnerHTML) {
+ this.element.innerHTML = this.oldInnerHTML;
+ this.oldInnerHTML = null;
+ }
+ return false;
+ },
+ onSubmit: function() {
+ // onLoading resets these so we need to save them away for the Ajax call
+ var form = this.form;
+ var value = this.editField.value;
+
+ // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
+ // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
+ // to be displayed indefinitely
+ this.onLoading();
+
+ if (this.options.evalScripts) {
+ new Ajax.Request(
+ this.url, Object.extend({
+ parameters: this.options.callback(form, value),
+ onComplete: this.onComplete.bind(this),
+ onFailure: this.onFailure.bind(this),
+ asynchronous:true,
+ evalScripts:true
+ }, this.options.ajaxOptions));
+ } else {
+ new Ajax.Updater(
+ { success: this.element,
+ // don't update on failure (this could be an option)
+ failure: null },
+ this.url, Object.extend({
+ parameters: this.options.callback(form, value),
+ onComplete: this.onComplete.bind(this),
+ onFailure: this.onFailure.bind(this)
+ }, this.options.ajaxOptions));
+ }
+ // stop the event to avoid a page refresh in Safari
+ if (arguments.length > 1) {
+ Event.stop(arguments[0]);
+ }
+ return false;
+ },
+ onLoading: function() {
+ this.saving = true;
+ this.removeForm();
+ this.leaveHover();
+ this.showSaving();
+ },
+ showSaving: function() {
+ this.oldInnerHTML = this.element.innerHTML;
+ this.element.innerHTML = this.options.savingText;
+ Element.addClassName(this.element, this.options.savingClassName);
+ this.element.style.backgroundColor = this.originalBackground;
+ Element.show(this.element);
+ },
+ removeForm: function() {
+ if(this.form) {
+ if (this.form.parentNode) Element.remove(this.form);
+ this.form = null;
+ }
+ },
+ enterHover: function() {
+ if (this.saving) return;
+ this.element.style.backgroundColor = this.options.highlightcolor;
+ if (this.effect) {
+ this.effect.cancel();
+ }
+ Element.addClassName(this.element, this.options.hoverClassName)
+ },
+ leaveHover: function() {
+ if (this.options.backgroundColor) {
+ this.element.style.backgroundColor = this.oldBackground;
+ }
+ Element.removeClassName(this.element, this.options.hoverClassName)
+ if (this.saving) return;
+ this.effect = new Effect.Highlight(this.element, {
+ startcolor: this.options.highlightcolor,
+ endcolor: this.options.highlightendcolor,
+ restorecolor: this.originalBackground
+ });
+ },
+ leaveEditMode: function() {
+ Element.removeClassName(this.element, this.options.savingClassName);
+ this.removeForm();
+ this.leaveHover();
+ this.element.style.backgroundColor = this.originalBackground;
+ Element.show(this.element);
+ if (this.options.externalControl) {
+ Element.show(this.options.externalControl);
+ }
+ this.editing = false;
+ this.saving = false;
+ this.oldInnerHTML = null;
+ this.onLeaveEditMode();
+ },
+ onComplete: function(transport) {
+ this.leaveEditMode();
+ this.options.onComplete.bind(this)(transport, this.element);
+ },
+ onEnterEditMode: function() {},
+ onLeaveEditMode: function() {},
+ dispose: function() {
+ if (this.oldInnerHTML) {
+ this.element.innerHTML = this.oldInnerHTML;
+ }
+ this.leaveEditMode();
+ Event.stopObserving(this.element, 'click', this.onclickListener);
+ Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
+ Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
+ if (this.options.externalControl) {
+ Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
+ Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
+ Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
+ }
+ }
+};
+
+Ajax.InPlaceCollectionEditor = Class.create();
+Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
+Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
+ createEditField: function() {
+ if (!this.cached_selectTag) {
+ var selectTag = document.createElement("select");
+ var collection = this.options.collection || [];
+ var optionTag;
+ collection.each(function(e,i) {
+ optionTag = document.createElement("option");
+ optionTag.value = (e instanceof Array) ? e[0] : e;
+ if(this.options.value==optionTag.value) optionTag.selected = true;
+ optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
+ selectTag.appendChild(optionTag);
+ }.bind(this));
+ this.cached_selectTag = selectTag;
+ }
+
+ this.editField = this.cached_selectTag;
+ if(this.options.loadTextURL) this.loadExternalText();
+ this.form.appendChild(this.editField);
+ this.options.callback = function(form, value) {
+ return "value=" + encodeURIComponent(value);
+ }
+ }
+});
+
+// Delayed observer, like Form.Element.Observer,
+// but waits for delay after last key input
+// Ideal for live-search fields
+
+Form.Element.DelayedObserver = Class.create();
+Form.Element.DelayedObserver.prototype = {
+ initialize: function(element, delay, callback) {
+ this.delay = delay || 0.5;
+ this.element = $(element);
+ this.callback = callback;
+ this.timer = null;
+ this.lastValue = $F(this.element);
+ Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
+ },
+ delayedListener: function(event) {
+ if(this.lastValue == $F(this.element)) return;
+ if(this.timer) clearTimeout(this.timer);
+ this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
+ this.lastValue = $F(this.element);
+ },
+ onTimerEvent: function() {
+ this.timer = null;
+ this.callback(this.element, $F(this.element));
+ }
+};
diff --git a/cropper/lib/dragdrop.js b/cropper/lib/dragdrop.js
new file mode 100644
index 000000000..be2a30f53
--- /dev/null
+++ b/cropper/lib/dragdrop.js
@@ -0,0 +1,915 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
+//
+// See scriptaculous.js for full license.
+
+/*--------------------------------------------------------------------------*/
+
+var Droppables = {
+ drops: [],
+
+ remove: function(element) {
+ this.drops = this.drops.reject(function(d) { return d.element==$(element) });
+ },
+
+ add: function(element) {
+ element = $(element);
+ var options = Object.extend({
+ greedy: true,
+ hoverclass: null,
+ tree: false
+ }, arguments[1] || {});
+
+ // cache containers
+ if(options.containment) {
+ options._containers = [];
+ var containment = options.containment;
+ if((typeof containment == 'object') &&
+ (containment.constructor == Array)) {
+ containment.each( function(c) { options._containers.push($(c)) });
+ } else {
+ options._containers.push($(containment));
+ }
+ }
+
+ if(options.accept) options.accept = [options.accept].flatten();
+
+ Element.makePositioned(element); // fix IE
+ options.element = element;
+
+ this.drops.push(options);
+ },
+
+ findDeepestChild: function(drops) {
+ deepest = drops[0];
+
+ for (i = 1; i < drops.length; ++i)
+ if (Element.isParent(drops[i].element, deepest.element))
+ deepest = drops[i];
+
+ return deepest;
+ },
+
+ isContained: function(element, drop) {
+ var containmentNode;
+ if(drop.tree) {
+ containmentNode = element.treeNode;
+ } else {
+ containmentNode = element.parentNode;
+ }
+ return drop._containers.detect(function(c) { return containmentNode == c });
+ },
+
+ isAffected: function(point, element, drop) {
+ return (
+ (drop.element!=element) &&
+ ((!drop._containers) ||
+ this.isContained(element, drop)) &&
+ ((!drop.accept) ||
+ (Element.classNames(element).detect(
+ function(v) { return drop.accept.include(v) } ) )) &&
+ Position.within(drop.element, point[0], point[1]) );
+ },
+
+ deactivate: function(drop) {
+ if(drop.hoverclass)
+ Element.removeClassName(drop.element, drop.hoverclass);
+ this.last_active = null;
+ },
+
+ activate: function(drop) {
+ if(drop.hoverclass)
+ Element.addClassName(drop.element, drop.hoverclass);
+ this.last_active = drop;
+ },
+
+ show: function(point, element) {
+ if(!this.drops.length) return;
+ var affected = [];
+
+ if(this.last_active) this.deactivate(this.last_active);
+ this.drops.each( function(drop) {
+ if(Droppables.isAffected(point, element, drop))
+ affected.push(drop);
+ });
+
+ if(affected.length>0) {
+ drop = Droppables.findDeepestChild(affected);
+ Position.within(drop.element, point[0], point[1]);
+ if(drop.onHover)
+ drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
+
+ Droppables.activate(drop);
+ }
+ },
+
+ fire: function(event, element) {
+ if(!this.last_active) return;
+ Position.prepare();
+
+ if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
+ if (this.last_active.onDrop)
+ this.last_active.onDrop(element, this.last_active.element, event);
+ },
+
+ reset: function() {
+ if(this.last_active)
+ this.deactivate(this.last_active);
+ }
+}
+
+var Draggables = {
+ drags: [],
+ observers: [],
+
+ register: function(draggable) {
+ if(this.drags.length == 0) {
+ this.eventMouseUp = this.endDrag.bindAsEventListener(this);
+ this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
+ this.eventKeypress = this.keyPress.bindAsEventListener(this);
+
+ Event.observe(document, "mouseup", this.eventMouseUp);
+ Event.observe(document, "mousemove", this.eventMouseMove);
+ Event.observe(document, "keypress", this.eventKeypress);
+ }
+ this.drags.push(draggable);
+ },
+
+ unregister: function(draggable) {
+ this.drags = this.drags.reject(function(d) { return d==draggable });
+ if(this.drags.length == 0) {
+ Event.stopObserving(document, "mouseup", this.eventMouseUp);
+ Event.stopObserving(document, "mousemove", this.eventMouseMove);
+ Event.stopObserving(document, "keypress", this.eventKeypress);
+ }
+ },
+
+ activate: function(draggable) {
+ window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
+ this.activeDraggable = draggable;
+ },
+
+ deactivate: function() {
+ this.activeDraggable = null;
+ },
+
+ updateDrag: function(event) {
+ if(!this.activeDraggable) return;
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ // Mozilla-based browsers fire successive mousemove events with
+ // the same coordinates, prevent needless redrawing (moz bug?)
+ if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
+ this._lastPointer = pointer;
+ this.activeDraggable.updateDrag(event, pointer);
+ },
+
+ endDrag: function(event) {
+ if(!this.activeDraggable) return;
+ this._lastPointer = null;
+ this.activeDraggable.endDrag(event);
+ this.activeDraggable = null;
+ },
+
+ keyPress: function(event) {
+ if(this.activeDraggable)
+ this.activeDraggable.keyPress(event);
+ },
+
+ addObserver: function(observer) {
+ this.observers.push(observer);
+ this._cacheObserverCallbacks();
+ },
+
+ removeObserver: function(element) { // element instead of observer fixes mem leaks
+ this.observers = this.observers.reject( function(o) { return o.element==element });
+ this._cacheObserverCallbacks();
+ },
+
+ notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
+ if(this[eventName+'Count'] > 0)
+ this.observers.each( function(o) {
+ if(o[eventName]) o[eventName](eventName, draggable, event);
+ });
+ },
+
+ _cacheObserverCallbacks: function() {
+ ['onStart','onEnd','onDrag'].each( function(eventName) {
+ Draggables[eventName+'Count'] = Draggables.observers.select(
+ function(o) { return o[eventName]; }
+ ).length;
+ });
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Draggable = Class.create();
+Draggable.prototype = {
+ initialize: function(element) {
+ var options = Object.extend({
+ handle: false,
+ starteffect: function(element) {
+ element._opacity = Element.getOpacity(element);
+ new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
+ },
+ reverteffect: function(element, top_offset, left_offset) {
+ var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
+ element._revert = new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur});
+ },
+ endeffect: function(element) {
+ var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0
+ new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity});
+ },
+ zindex: 1000,
+ revert: false,
+ scroll: false,
+ scrollSensitivity: 20,
+ scrollSpeed: 15,
+ snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] }
+ }, arguments[1] || {});
+
+ this.element = $(element);
+
+ if(options.handle && (typeof options.handle == 'string')) {
+ var h = Element.childrenWithClassName(this.element, options.handle, true);
+ if(h.length>0) this.handle = h[0];
+ }
+ if(!this.handle) this.handle = $(options.handle);
+ if(!this.handle) this.handle = this.element;
+
+ if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML)
+ options.scroll = $(options.scroll);
+
+ Element.makePositioned(this.element); // fix IE
+
+ this.delta = this.currentDelta();
+ this.options = options;
+ this.dragging = false;
+
+ this.eventMouseDown = this.initDrag.bindAsEventListener(this);
+ Event.observe(this.handle, "mousedown", this.eventMouseDown);
+
+ Draggables.register(this);
+ },
+
+ destroy: function() {
+ Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
+ Draggables.unregister(this);
+ },
+
+ currentDelta: function() {
+ return([
+ parseInt(Element.getStyle(this.element,'left') || '0'),
+ parseInt(Element.getStyle(this.element,'top') || '0')]);
+ },
+
+ initDrag: function(event) {
+ if(Event.isLeftClick(event)) {
+ // abort on form elements, fixes a Firefox issue
+ var src = Event.element(event);
+ if(src.tagName && (
+ src.tagName=='INPUT' ||
+ src.tagName=='SELECT' ||
+ src.tagName=='OPTION' ||
+ src.tagName=='BUTTON' ||
+ src.tagName=='TEXTAREA')) return;
+
+ if(this.element._revert) {
+ this.element._revert.cancel();
+ this.element._revert = null;
+ }
+
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ var pos = Position.cumulativeOffset(this.element);
+ this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
+
+ Draggables.activate(this);
+ Event.stop(event);
+ }
+ },
+
+ startDrag: function(event) {
+ this.dragging = true;
+
+ if(this.options.zindex) {
+ this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
+ this.element.style.zIndex = this.options.zindex;
+ }
+
+ if(this.options.ghosting) {
+ this._clone = this.element.cloneNode(true);
+ Position.absolutize(this.element);
+ this.element.parentNode.insertBefore(this._clone, this.element);
+ }
+
+ if(this.options.scroll) {
+ if (this.options.scroll == window) {
+ var where = this._getWindowScroll(this.options.scroll);
+ this.originalScrollLeft = where.left;
+ this.originalScrollTop = where.top;
+ } else {
+ this.originalScrollLeft = this.options.scroll.scrollLeft;
+ this.originalScrollTop = this.options.scroll.scrollTop;
+ }
+ }
+
+ Draggables.notify('onStart', this, event);
+ if(this.options.starteffect) this.options.starteffect(this.element);
+ },
+
+ updateDrag: function(event, pointer) {
+ if(!this.dragging) this.startDrag(event);
+ Position.prepare();
+ Droppables.show(pointer, this.element);
+ Draggables.notify('onDrag', this, event);
+ this.draw(pointer);
+ if(this.options.change) this.options.change(this);
+
+ if(this.options.scroll) {
+ this.stopScrolling();
+
+ var p;
+ if (this.options.scroll == window) {
+ with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
+ } else {
+ p = Position.page(this.options.scroll);
+ p[0] += this.options.scroll.scrollLeft;
+ p[1] += this.options.scroll.scrollTop;
+ p.push(p[0]+this.options.scroll.offsetWidth);
+ p.push(p[1]+this.options.scroll.offsetHeight);
+ }
+ var speed = [0,0];
+ if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
+ if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
+ if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
+ if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
+ this.startScrolling(speed);
+ }
+
+ // fix AppleWebKit rendering
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+
+ Event.stop(event);
+ },
+
+ finishDrag: function(event, success) {
+ this.dragging = false;
+
+ if(this.options.ghosting) {
+ Position.relativize(this.element);
+ Element.remove(this._clone);
+ this._clone = null;
+ }
+
+ if(success) Droppables.fire(event, this.element);
+ Draggables.notify('onEnd', this, event);
+
+ var revert = this.options.revert;
+ if(revert && typeof revert == 'function') revert = revert(this.element);
+
+ var d = this.currentDelta();
+ if(revert && this.options.reverteffect) {
+ this.options.reverteffect(this.element,
+ d[1]-this.delta[1], d[0]-this.delta[0]);
+ } else {
+ this.delta = d;
+ }
+
+ if(this.options.zindex)
+ this.element.style.zIndex = this.originalZ;
+
+ if(this.options.endeffect)
+ this.options.endeffect(this.element);
+
+ Draggables.deactivate(this);
+ Droppables.reset();
+ },
+
+ keyPress: function(event) {
+ if(event.keyCode!=Event.KEY_ESC) return;
+ this.finishDrag(event, false);
+ Event.stop(event);
+ },
+
+ endDrag: function(event) {
+ if(!this.dragging) return;
+ this.stopScrolling();
+ this.finishDrag(event, true);
+ Event.stop(event);
+ },
+
+ draw: function(point) {
+ var pos = Position.cumulativeOffset(this.element);
+ var d = this.currentDelta();
+ pos[0] -= d[0]; pos[1] -= d[1];
+
+ if(this.options.scroll && (this.options.scroll != window)) {
+ pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
+ pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
+ }
+
+ var p = [0,1].map(function(i){
+ return (point[i]-pos[i]-this.offset[i])
+ }.bind(this));
+
+ if(this.options.snap) {
+ if(typeof this.options.snap == 'function') {
+ p = this.options.snap(p[0],p[1],this);
+ } else {
+ if(this.options.snap instanceof Array) {
+ p = p.map( function(v, i) {
+ return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
+ } else {
+ p = p.map( function(v) {
+ return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
+ }
+ }}
+
+ var style = this.element.style;
+ if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+ style.left = p[0] + "px";
+ if((!this.options.constraint) || (this.options.constraint=='vertical'))
+ style.top = p[1] + "px";
+ if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
+ },
+
+ stopScrolling: function() {
+ if(this.scrollInterval) {
+ clearInterval(this.scrollInterval);
+ this.scrollInterval = null;
+ Draggables._lastScrollPointer = null;
+ }
+ },
+
+ startScrolling: function(speed) {
+ this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
+ this.lastScrolled = new Date();
+ this.scrollInterval = setInterval(this.scroll.bind(this), 10);
+ },
+
+ scroll: function() {
+ var current = new Date();
+ var delta = current - this.lastScrolled;
+ this.lastScrolled = current;
+ if(this.options.scroll == window) {
+ with (this._getWindowScroll(this.options.scroll)) {
+ if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
+ var d = delta / 1000;
+ this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
+ }
+ }
+ } else {
+ this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
+ this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
+ }
+
+ Position.prepare();
+ Droppables.show(Draggables._lastPointer, this.element);
+ Draggables.notify('onDrag', this);
+ Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
+ Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
+ Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
+ if (Draggables._lastScrollPointer[0] < 0)
+ Draggables._lastScrollPointer[0] = 0;
+ if (Draggables._lastScrollPointer[1] < 0)
+ Draggables._lastScrollPointer[1] = 0;
+ this.draw(Draggables._lastScrollPointer);
+
+ if(this.options.change) this.options.change(this);
+ },
+
+ _getWindowScroll: function(w) {
+ var T, L, W, H;
+ with (w.document) {
+ if (w.document.documentElement && documentElement.scrollTop) {
+ T = documentElement.scrollTop;
+ L = documentElement.scrollLeft;
+ } else if (w.document.body) {
+ T = body.scrollTop;
+ L = body.scrollLeft;
+ }
+ if (w.innerWidth) {
+ W = w.innerWidth;
+ H = w.innerHeight;
+ } else if (w.document.documentElement && documentElement.clientWidth) {
+ W = documentElement.clientWidth;
+ H = documentElement.clientHeight;
+ } else {
+ W = body.offsetWidth;
+ H = body.offsetHeight
+ }
+ }
+ return { top: T, left: L, width: W, height: H };
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var SortableObserver = Class.create();
+SortableObserver.prototype = {
+ initialize: function(element, observer) {
+ this.element = $(element);
+ this.observer = observer;
+ this.lastValue = Sortable.serialize(this.element);
+ },
+
+ onStart: function() {
+ this.lastValue = Sortable.serialize(this.element);
+ },
+
+ onEnd: function() {
+ Sortable.unmark();
+ if(this.lastValue != Sortable.serialize(this.element))
+ this.observer(this.element)
+ }
+}
+
+var Sortable = {
+ sortables: {},
+
+ _findRootElement: function(element) {
+ while (element.tagName != "BODY") {
+ if(element.id && Sortable.sortables[element.id]) return element;
+ element = element.parentNode;
+ }
+ },
+
+ options: function(element) {
+ element = Sortable._findRootElement($(element));
+ if(!element) return;
+ return Sortable.sortables[element.id];
+ },
+
+ destroy: function(element){
+ var s = Sortable.options(element);
+
+ if(s) {
+ Draggables.removeObserver(s.element);
+ s.droppables.each(function(d){ Droppables.remove(d) });
+ s.draggables.invoke('destroy');
+
+ delete Sortable.sortables[s.element.id];
+ }
+ },
+
+ create: function(element) {
+ element = $(element);
+ var options = Object.extend({
+ element: element,
+ tag: 'li', // assumes li children, override with tag: 'tagname'
+ dropOnEmpty: false,
+ tree: false,
+ treeTag: 'ul',
+ overlap: 'vertical', // one of 'vertical', 'horizontal'
+ constraint: 'vertical', // one of 'vertical', 'horizontal', false
+ containment: element, // also takes array of elements (or id's); or false
+ handle: false, // or a CSS class
+ only: false,
+ hoverclass: null,
+ ghosting: false,
+ scroll: false,
+ scrollSensitivity: 20,
+ scrollSpeed: 15,
+ format: /^[^_]*_(.*)$/,
+ onChange: Prototype.emptyFunction,
+ onUpdate: Prototype.emptyFunction
+ }, arguments[1] || {});
+
+ // clear any old sortable with same element
+ this.destroy(element);
+
+ // build options for the draggables
+ var options_for_draggable = {
+ revert: true,
+ scroll: options.scroll,
+ scrollSpeed: options.scrollSpeed,
+ scrollSensitivity: options.scrollSensitivity,
+ ghosting: options.ghosting,
+ constraint: options.constraint,
+ handle: options.handle };
+
+ if(options.starteffect)
+ options_for_draggable.starteffect = options.starteffect;
+
+ if(options.reverteffect)
+ options_for_draggable.reverteffect = options.reverteffect;
+ else
+ if(options.ghosting) options_for_draggable.reverteffect = function(element) {
+ element.style.top = 0;
+ element.style.left = 0;
+ };
+
+ if(options.endeffect)
+ options_for_draggable.endeffect = options.endeffect;
+
+ if(options.zindex)
+ options_for_draggable.zindex = options.zindex;
+
+ // build options for the droppables
+ var options_for_droppable = {
+ overlap: options.overlap,
+ containment: options.containment,
+ tree: options.tree,
+ hoverclass: options.hoverclass,
+ onHover: Sortable.onHover
+ //greedy: !options.dropOnEmpty
+ }
+
+ var options_for_tree = {
+ onHover: Sortable.onEmptyHover,
+ overlap: options.overlap,
+ containment: options.containment,
+ hoverclass: options.hoverclass
+ }
+
+ // fix for gecko engine
+ Element.cleanWhitespace(element);
+
+ options.draggables = [];
+ options.droppables = [];
+
+ // drop on empty handling
+ if(options.dropOnEmpty || options.tree) {
+ Droppables.add(element, options_for_tree);
+ options.droppables.push(element);
+ }
+
+ (this.findElements(element, options) || []).each( function(e) {
+ // handles are per-draggable
+ var handle = options.handle ?
+ Element.childrenWithClassName(e, options.handle)[0] : e;
+ options.draggables.push(
+ new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
+ Droppables.add(e, options_for_droppable);
+ if(options.tree) e.treeNode = element;
+ options.droppables.push(e);
+ });
+
+ if(options.tree) {
+ (Sortable.findTreeElements(element, options) || []).each( function(e) {
+ Droppables.add(e, options_for_tree);
+ e.treeNode = element;
+ options.droppables.push(e);
+ });
+ }
+
+ // keep reference
+ this.sortables[element.id] = options;
+
+ // for onupdate
+ Draggables.addObserver(new SortableObserver(element, options.onUpdate));
+
+ },
+
+ // return all suitable-for-sortable elements in a guaranteed order
+ findElements: function(element, options) {
+ return Element.findChildren(
+ element, options.only, options.tree ? true : false, options.tag);
+ },
+
+ findTreeElements: function(element, options) {
+ return Element.findChildren(
+ element, options.only, options.tree ? true : false, options.treeTag);
+ },
+
+ onHover: function(element, dropon, overlap) {
+ if(Element.isParent(dropon, element)) return;
+
+ if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
+ return;
+ } else if(overlap>0.5) {
+ Sortable.mark(dropon, 'before');
+ if(dropon.previousSibling != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, dropon);
+ if(dropon.parentNode!=oldParentNode)
+ Sortable.options(oldParentNode).onChange(element);
+ Sortable.options(dropon.parentNode).onChange(element);
+ }
+ } else {
+ Sortable.mark(dropon, 'after');
+ var nextElement = dropon.nextSibling || null;
+ if(nextElement != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, nextElement);
+ if(dropon.parentNode!=oldParentNode)
+ Sortable.options(oldParentNode).onChange(element);
+ Sortable.options(dropon.parentNode).onChange(element);
+ }
+ }
+ },
+
+ onEmptyHover: function(element, dropon, overlap) {
+ var oldParentNode = element.parentNode;
+ var droponOptions = Sortable.options(dropon);
+
+ if(!Element.isParent(dropon, element)) {
+ var index;
+
+ var children = Sortable.findElements(dropon, {tag: droponOptions.tag});
+ var child = null;
+
+ if(children) {
+ var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
+
+ for (index = 0; index < children.length; index += 1) {
+ if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
+ offset -= Element.offsetSize (children[index], droponOptions.overlap);
+ } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
+ child = index + 1 < children.length ? children[index + 1] : null;
+ break;
+ } else {
+ child = children[index];
+ break;
+ }
+ }
+ }
+
+ dropon.insertBefore(element, child);
+
+ Sortable.options(oldParentNode).onChange(element);
+ droponOptions.onChange(element);
+ }
+ },
+
+ unmark: function() {
+ if(Sortable._marker) Element.hide(Sortable._marker);
+ },
+
+ mark: function(dropon, position) {
+ // mark on ghosting only
+ var sortable = Sortable.options(dropon.parentNode);
+ if(sortable && !sortable.ghosting) return;
+
+ if(!Sortable._marker) {
+ Sortable._marker = $('dropmarker') || document.createElement('DIV');
+ Element.hide(Sortable._marker);
+ Element.addClassName(Sortable._marker, 'dropmarker');
+ Sortable._marker.style.position = 'absolute';
+ document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
+ }
+ var offsets = Position.cumulativeOffset(dropon);
+ Sortable._marker.style.left = offsets[0] + 'px';
+ Sortable._marker.style.top = offsets[1] + 'px';
+
+ if(position=='after')
+ if(sortable.overlap == 'horizontal')
+ Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
+ else
+ Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
+
+ Element.show(Sortable._marker);
+ },
+
+ _tree: function(element, options, parent) {
+ var children = Sortable.findElements(element, options) || [];
+
+ for (var i = 0; i < children.length; ++i) {
+ var match = children[i].id.match(options.format);
+
+ if (!match) continue;
+
+ var child = {
+ id: encodeURIComponent(match ? match[1] : null),
+ element: element,
+ parent: parent,
+ children: new Array,
+ position: parent.children.length,
+ container: Sortable._findChildrenElement(children[i], options.treeTag.toUpperCase())
+ }
+
+ /* Get the element containing the children and recurse over it */
+ if (child.container)
+ this._tree(child.container, options, child)
+
+ parent.children.push (child);
+ }
+
+ return parent;
+ },
+
+ /* Finds the first element of the given tag type within a parent element.
+ Used for finding the first LI[ST] within a L[IST]I[TEM].*/
+ _findChildrenElement: function (element, containerTag) {
+ if (element && element.hasChildNodes)
+ for (var i = 0; i < element.childNodes.length; ++i)
+ if (element.childNodes[i].tagName == containerTag)
+ return element.childNodes[i];
+
+ return null;
+ },
+
+ tree: function(element) {
+ element = $(element);
+ var sortableOptions = this.options(element);
+ var options = Object.extend({
+ tag: sortableOptions.tag,
+ treeTag: sortableOptions.treeTag,
+ only: sortableOptions.only,
+ name: element.id,
+ format: sortableOptions.format
+ }, arguments[1] || {});
+
+ var root = {
+ id: null,
+ parent: null,
+ children: new Array,
+ container: element,
+ position: 0
+ }
+
+ return Sortable._tree (element, options, root);
+ },
+
+ /* Construct a [i] index for a particular node */
+ _constructIndex: function(node) {
+ var index = '';
+ do {
+ if (node.id) index = '[' + node.position + ']' + index;
+ } while ((node = node.parent) != null);
+ return index;
+ },
+
+ sequence: function(element) {
+ element = $(element);
+ var options = Object.extend(this.options(element), arguments[1] || {});
+
+ return $(this.findElements(element, options) || []).map( function(item) {
+ return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
+ });
+ },
+
+ setSequence: function(element, new_sequence) {
+ element = $(element);
+ var options = Object.extend(this.options(element), arguments[2] || {});
+
+ var nodeMap = {};
+ this.findElements(element, options).each( function(n) {
+ if (n.id.match(options.format))
+ nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
+ n.parentNode.removeChild(n);
+ });
+
+ new_sequence.each(function(ident) {
+ var n = nodeMap[ident];
+ if (n) {
+ n[1].appendChild(n[0]);
+ delete nodeMap[ident];
+ }
+ });
+ },
+
+ serialize: function(element) {
+ element = $(element);
+ var options = Object.extend(Sortable.options(element), arguments[1] || {});
+ var name = encodeURIComponent(
+ (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
+
+ if (options.tree) {
+ return Sortable.tree(element, arguments[1]).children.map( function (item) {
+ return [name + Sortable._constructIndex(item) + "=" +
+ encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
+ }).flatten().join('&');
+ } else {
+ return Sortable.sequence(element, arguments[1]).map( function(item) {
+ return name + "[]=" + encodeURIComponent(item);
+ }).join('&');
+ }
+ }
+}
+
+/* Returns true if child is contained within element */
+Element.isParent = function(child, element) {
+ if (!child.parentNode || child == element) return false;
+
+ if (child.parentNode == element) return true;
+
+ return Element.isParent(child.parentNode, element);
+}
+
+Element.findChildren = function(element, only, recursive, tagName) {
+ if(!element.hasChildNodes()) return null;
+ tagName = tagName.toUpperCase();
+ if(only) only = [only].flatten();
+ var elements = [];
+ $A(element.childNodes).each( function(e) {
+ if(e.tagName && e.tagName.toUpperCase()==tagName &&
+ (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
+ elements.push(e);
+ if(recursive) {
+ var grandchildren = Element.findChildren(e, only, recursive, tagName);
+ if(grandchildren) elements.push(grandchildren);
+ }
+ });
+
+ return (elements.length>0 ? elements.flatten() : []);
+}
+
+Element.offsetSize = function (element, type) {
+ if (type == 'vertical' || type == 'height')
+ return element.offsetHeight;
+ else
+ return element.offsetWidth;
+} \ No newline at end of file
diff --git a/cropper/lib/effects.js b/cropper/lib/effects.js
new file mode 100644
index 000000000..0864323ec
--- /dev/null
+++ b/cropper/lib/effects.js
@@ -0,0 +1,958 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Contributors:
+// Justin Palmer (http://encytemedia.com/)
+// Mark Pilgrim (http://diveintomark.org/)
+// Martin Bialasinki
+//
+// See scriptaculous.js for full license.
+
+// converts rgb() and #xxx to #xxxxxx format,
+// returns self (or first argument) if not convertable
+String.prototype.parseColor = function() {
+ var color = '#';
+ if(this.slice(0,4) == 'rgb(') {
+ var cols = this.slice(4,this.length-1).split(',');
+ var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
+ } else {
+ if(this.slice(0,1) == '#') {
+ if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
+ if(this.length==7) color = this.toLowerCase();
+ }
+ }
+ return(color.length==7 ? color : (arguments[0] || this));
+}
+
+/*--------------------------------------------------------------------------*/
+
+Element.collectTextNodes = function(element) {
+ return $A($(element).childNodes).collect( function(node) {
+ return (node.nodeType==3 ? node.nodeValue :
+ (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
+ }).flatten().join('');
+}
+
+Element.collectTextNodesIgnoreClass = function(element, className) {
+ return $A($(element).childNodes).collect( function(node) {
+ return (node.nodeType==3 ? node.nodeValue :
+ ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
+ Element.collectTextNodesIgnoreClass(node, className) : ''));
+ }).flatten().join('');
+}
+
+Element.setContentZoom = function(element, percent) {
+ element = $(element);
+ Element.setStyle(element, {fontSize: (percent/100) + 'em'});
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+}
+
+Element.getOpacity = function(element){
+ var opacity;
+ if (opacity = Element.getStyle(element, 'opacity'))
+ return parseFloat(opacity);
+ if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/))
+ if(opacity[1]) return parseFloat(opacity[1]) / 100;
+ return 1.0;
+}
+
+Element.setOpacity = function(element, value){
+ element= $(element);
+ if (value == 1){
+ Element.setStyle(element, { opacity:
+ (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ?
+ 0.999999 : null });
+ if(/MSIE/.test(navigator.userAgent))
+ Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});
+ } else {
+ if(value < 0.00001) value = 0;
+ Element.setStyle(element, {opacity: value});
+ if(/MSIE/.test(navigator.userAgent))
+ Element.setStyle(element,
+ { filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +
+ 'alpha(opacity='+value*100+')' });
+ }
+}
+
+Element.getInlineOpacity = function(element){
+ return $(element).style.opacity || '';
+}
+
+Element.childrenWithClassName = function(element, className, findFirst) {
+ var classNameRegExp = new RegExp("(^|\\s)" + className + "(\\s|$)");
+ var results = $A($(element).getElementsByTagName('*'))[findFirst ? 'detect' : 'select']( function(c) {
+ return (c.className && c.className.match(classNameRegExp));
+ });
+ if(!results) results = [];
+ return results;
+}
+
+Element.forceRerendering = function(element) {
+ try {
+ element = $(element);
+ var n = document.createTextNode(' ');
+ element.appendChild(n);
+ element.removeChild(n);
+ } catch(e) { }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Array.prototype.call = function() {
+ var args = arguments;
+ this.each(function(f){ f.apply(this, args) });
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Effect = {
+ tagifyText: function(element) {
+ var tagifyStyle = 'position:relative';
+ if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1';
+ element = $(element);
+ $A(element.childNodes).each( function(child) {
+ if(child.nodeType==3) {
+ child.nodeValue.toArray().each( function(character) {
+ element.insertBefore(
+ Builder.node('span',{style: tagifyStyle},
+ character == ' ' ? String.fromCharCode(160) : character),
+ child);
+ });
+ Element.remove(child);
+ }
+ });
+ },
+ multiple: function(element, effect) {
+ var elements;
+ if(((typeof element == 'object') ||
+ (typeof element == 'function')) &&
+ (element.length))
+ elements = element;
+ else
+ elements = $(element).childNodes;
+
+ var options = Object.extend({
+ speed: 0.1,
+ delay: 0.0
+ }, arguments[2] || {});
+ var masterDelay = options.delay;
+
+ $A(elements).each( function(element, index) {
+ new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
+ });
+ },
+ PAIRS: {
+ 'slide': ['SlideDown','SlideUp'],
+ 'blind': ['BlindDown','BlindUp'],
+ 'appear': ['Appear','Fade']
+ },
+ toggle: function(element, effect) {
+ element = $(element);
+ effect = (effect || 'appear').toLowerCase();
+ var options = Object.extend({
+ queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
+ }, arguments[2] || {});
+ Effect[element.visible() ?
+ Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
+ }
+};
+
+var Effect2 = Effect; // deprecated
+
+/* ------------- transitions ------------- */
+
+Effect.Transitions = {}
+
+Effect.Transitions.linear = function(pos) {
+ return pos;
+}
+Effect.Transitions.sinoidal = function(pos) {
+ return (-Math.cos(pos*Math.PI)/2) + 0.5;
+}
+Effect.Transitions.reverse = function(pos) {
+ return 1-pos;
+}
+Effect.Transitions.flicker = function(pos) {
+ return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
+}
+Effect.Transitions.wobble = function(pos) {
+ return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
+}
+Effect.Transitions.pulse = function(pos) {
+ return (Math.floor(pos*10) % 2 == 0 ?
+ (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
+}
+Effect.Transitions.none = function(pos) {
+ return 0;
+}
+Effect.Transitions.full = function(pos) {
+ return 1;
+}
+
+/* ------------- core effects ------------- */
+
+Effect.ScopedQueue = Class.create();
+Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
+ initialize: function() {
+ this.effects = [];
+ this.interval = null;
+ },
+ _each: function(iterator) {
+ this.effects._each(iterator);
+ },
+ add: function(effect) {
+ var timestamp = new Date().getTime();
+
+ var position = (typeof effect.options.queue == 'string') ?
+ effect.options.queue : effect.options.queue.position;
+
+ switch(position) {
+ case 'front':
+ // move unstarted effects after this effect
+ this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
+ e.startOn += effect.finishOn;
+ e.finishOn += effect.finishOn;
+ });
+ break;
+ case 'end':
+ // start effect after last queued effect has finished
+ timestamp = this.effects.pluck('finishOn').max() || timestamp;
+ break;
+ }
+
+ effect.startOn += timestamp;
+ effect.finishOn += timestamp;
+
+ if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
+ this.effects.push(effect);
+
+ if(!this.interval)
+ this.interval = setInterval(this.loop.bind(this), 40);
+ },
+ remove: function(effect) {
+ this.effects = this.effects.reject(function(e) { return e==effect });
+ if(this.effects.length == 0) {
+ clearInterval(this.interval);
+ this.interval = null;
+ }
+ },
+ loop: function() {
+ var timePos = new Date().getTime();
+ this.effects.invoke('loop', timePos);
+ }
+});
+
+Effect.Queues = {
+ instances: $H(),
+ get: function(queueName) {
+ if(typeof queueName != 'string') return queueName;
+
+ if(!this.instances[queueName])
+ this.instances[queueName] = new Effect.ScopedQueue();
+
+ return this.instances[queueName];
+ }
+}
+Effect.Queue = Effect.Queues.get('global');
+
+Effect.DefaultOptions = {
+ transition: Effect.Transitions.sinoidal,
+ duration: 1.0, // seconds
+ fps: 25.0, // max. 25fps due to Effect.Queue implementation
+ sync: false, // true for combining
+ from: 0.0,
+ to: 1.0,
+ delay: 0.0,
+ queue: 'parallel'
+}
+
+Effect.Base = function() {};
+Effect.Base.prototype = {
+ position: null,
+ start: function(options) {
+ this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
+ this.currentFrame = 0;
+ this.state = 'idle';
+ this.startOn = this.options.delay*1000;
+ this.finishOn = this.startOn + (this.options.duration*1000);
+ this.event('beforeStart');
+ if(!this.options.sync)
+ Effect.Queues.get(typeof this.options.queue == 'string' ?
+ 'global' : this.options.queue.scope).add(this);
+ },
+ loop: function(timePos) {
+ if(timePos >= this.startOn) {
+ if(timePos >= this.finishOn) {
+ this.render(1.0);
+ this.cancel();
+ this.event('beforeFinish');
+ if(this.finish) this.finish();
+ this.event('afterFinish');
+ return;
+ }
+ var pos = (timePos - this.startOn) / (this.finishOn - this.startOn);
+ var frame = Math.round(pos * this.options.fps * this.options.duration);
+ if(frame > this.currentFrame) {
+ this.render(pos);
+ this.currentFrame = frame;
+ }
+ }
+ },
+ render: function(pos) {
+ if(this.state == 'idle') {
+ this.state = 'running';
+ this.event('beforeSetup');
+ if(this.setup) this.setup();
+ this.event('afterSetup');
+ }
+ if(this.state == 'running') {
+ if(this.options.transition) pos = this.options.transition(pos);
+ pos *= (this.options.to-this.options.from);
+ pos += this.options.from;
+ this.position = pos;
+ this.event('beforeUpdate');
+ if(this.update) this.update(pos);
+ this.event('afterUpdate');
+ }
+ },
+ cancel: function() {
+ if(!this.options.sync)
+ Effect.Queues.get(typeof this.options.queue == 'string' ?
+ 'global' : this.options.queue.scope).remove(this);
+ this.state = 'finished';
+ },
+ event: function(eventName) {
+ if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
+ if(this.options[eventName]) this.options[eventName](this);
+ },
+ inspect: function() {
+ return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
+ }
+}
+
+Effect.Parallel = Class.create();
+Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
+ initialize: function(effects) {
+ this.effects = effects || [];
+ this.start(arguments[1]);
+ },
+ update: function(position) {
+ this.effects.invoke('render', position);
+ },
+ finish: function(position) {
+ this.effects.each( function(effect) {
+ effect.render(1.0);
+ effect.cancel();
+ effect.event('beforeFinish');
+ if(effect.finish) effect.finish(position);
+ effect.event('afterFinish');
+ });
+ }
+});
+
+Effect.Opacity = Class.create();
+Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ // make this work on IE on elements without 'layout'
+ if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout))
+ this.element.setStyle({zoom: 1});
+ var options = Object.extend({
+ from: this.element.getOpacity() || 0.0,
+ to: 1.0
+ }, arguments[1] || {});
+ this.start(options);
+ },
+ update: function(position) {
+ this.element.setOpacity(position);
+ }
+});
+
+Effect.Move = Class.create();
+Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ var options = Object.extend({
+ x: 0,
+ y: 0,
+ mode: 'relative'
+ }, arguments[1] || {});
+ this.start(options);
+ },
+ setup: function() {
+ // Bug in Opera: Opera returns the "real" position of a static element or
+ // relative element that does not have top/left explicitly set.
+ // ==> Always set top and left for position relative elements in your stylesheets
+ // (to 0 if you do not need them)
+ this.element.makePositioned();
+ this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
+ this.originalTop = parseFloat(this.element.getStyle('top') || '0');
+ if(this.options.mode == 'absolute') {
+ // absolute movement, so we need to calc deltaX and deltaY
+ this.options.x = this.options.x - this.originalLeft;
+ this.options.y = this.options.y - this.originalTop;
+ }
+ },
+ update: function(position) {
+ this.element.setStyle({
+ left: this.options.x * position + this.originalLeft + 'px',
+ top: this.options.y * position + this.originalTop + 'px'
+ });
+ }
+});
+
+// for backwards compatibility
+Effect.MoveBy = function(element, toTop, toLeft) {
+ return new Effect.Move(element,
+ Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
+};
+
+Effect.Scale = Class.create();
+Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
+ initialize: function(element, percent) {
+ this.element = $(element)
+ var options = Object.extend({
+ scaleX: true,
+ scaleY: true,
+ scaleContent: true,
+ scaleFromCenter: false,
+ scaleMode: 'box', // 'box' or 'contents' or {} with provided values
+ scaleFrom: 100.0,
+ scaleTo: percent
+ }, arguments[2] || {});
+ this.start(options);
+ },
+ setup: function() {
+ this.restoreAfterFinish = this.options.restoreAfterFinish || false;
+ this.elementPositioning = this.element.getStyle('position');
+
+ this.originalStyle = {};
+ ['top','left','width','height','fontSize'].each( function(k) {
+ this.originalStyle[k] = this.element.style[k];
+ }.bind(this));
+
+ this.originalTop = this.element.offsetTop;
+ this.originalLeft = this.element.offsetLeft;
+
+ var fontSize = this.element.getStyle('font-size') || '100%';
+ ['em','px','%'].each( function(fontSizeType) {
+ if(fontSize.indexOf(fontSizeType)>0) {
+ this.fontSize = parseFloat(fontSize);
+ this.fontSizeType = fontSizeType;
+ }
+ }.bind(this));
+
+ this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
+
+ this.dims = null;
+ if(this.options.scaleMode=='box')
+ this.dims = [this.element.offsetHeight, this.element.offsetWidth];
+ if(/^content/.test(this.options.scaleMode))
+ this.dims = [this.element.scrollHeight, this.element.scrollWidth];
+ if(!this.dims)
+ this.dims = [this.options.scaleMode.originalHeight,
+ this.options.scaleMode.originalWidth];
+ },
+ update: function(position) {
+ var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
+ if(this.options.scaleContent && this.fontSize)
+ this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
+ this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
+ },
+ finish: function(position) {
+ if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
+ },
+ setDimensions: function(height, width) {
+ var d = {};
+ if(this.options.scaleX) d.width = width + 'px';
+ if(this.options.scaleY) d.height = height + 'px';
+ if(this.options.scaleFromCenter) {
+ var topd = (height - this.dims[0])/2;
+ var leftd = (width - this.dims[1])/2;
+ if(this.elementPositioning == 'absolute') {
+ if(this.options.scaleY) d.top = this.originalTop-topd + 'px';
+ if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
+ } else {
+ if(this.options.scaleY) d.top = -topd + 'px';
+ if(this.options.scaleX) d.left = -leftd + 'px';
+ }
+ }
+ this.element.setStyle(d);
+ }
+});
+
+Effect.Highlight = Class.create();
+Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
+ this.start(options);
+ },
+ setup: function() {
+ // Prevent executing on elements not in the layout flow
+ if(this.element.getStyle('display')=='none') { this.cancel(); return; }
+ // Disable background image during the effect
+ this.oldStyle = {
+ backgroundImage: this.element.getStyle('background-image') };
+ this.element.setStyle({backgroundImage: 'none'});
+ if(!this.options.endcolor)
+ this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
+ if(!this.options.restorecolor)
+ this.options.restorecolor = this.element.getStyle('background-color');
+ // init color calculations
+ this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
+ this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
+ },
+ update: function(position) {
+ this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
+ return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
+ },
+ finish: function() {
+ this.element.setStyle(Object.extend(this.oldStyle, {
+ backgroundColor: this.options.restorecolor
+ }));
+ }
+});
+
+Effect.ScrollTo = Class.create();
+Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ this.start(arguments[1] || {});
+ },
+ setup: function() {
+ Position.prepare();
+ var offsets = Position.cumulativeOffset(this.element);
+ if(this.options.offset) offsets[1] += this.options.offset;
+ var max = window.innerHeight ?
+ window.height - window.innerHeight :
+ document.body.scrollHeight -
+ (document.documentElement.clientHeight ?
+ document.documentElement.clientHeight : document.body.clientHeight);
+ this.scrollStart = Position.deltaY;
+ this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
+ },
+ update: function(position) {
+ Position.prepare();
+ window.scrollTo(Position.deltaX,
+ this.scrollStart + (position*this.delta));
+ }
+});
+
+/* ------------- combination effects ------------- */
+
+Effect.Fade = function(element) {
+ element = $(element);
+ var oldOpacity = element.getInlineOpacity();
+ var options = Object.extend({
+ from: element.getOpacity() || 1.0,
+ to: 0.0,
+ afterFinishInternal: function(effect) {
+ if(effect.options.to!=0) return;
+ effect.element.hide();
+ effect.element.setStyle({opacity: oldOpacity});
+ }}, arguments[1] || {});
+ return new Effect.Opacity(element,options);
+}
+
+Effect.Appear = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
+ to: 1.0,
+ // force Safari to render floated elements properly
+ afterFinishInternal: function(effect) {
+ effect.element.forceRerendering();
+ },
+ beforeSetup: function(effect) {
+ effect.element.setOpacity(effect.options.from);
+ effect.element.show();
+ }}, arguments[1] || {});
+ return new Effect.Opacity(element,options);
+}
+
+Effect.Puff = function(element) {
+ element = $(element);
+ var oldStyle = { opacity: element.getInlineOpacity(), position: element.getStyle('position') };
+ return new Effect.Parallel(
+ [ new Effect.Scale(element, 200,
+ { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
+ Object.extend({ duration: 1.0,
+ beforeSetupInternal: function(effect) {
+ effect.effects[0].element.setStyle({position: 'absolute'}); },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide();
+ effect.effects[0].element.setStyle(oldStyle); }
+ }, arguments[1] || {})
+ );
+}
+
+Effect.BlindUp = function(element) {
+ element = $(element);
+ element.makeClipping();
+ return new Effect.Scale(element, 0,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ restoreAfterFinish: true,
+ afterFinishInternal: function(effect) {
+ effect.element.hide();
+ effect.element.undoClipping();
+ }
+ }, arguments[1] || {})
+ );
+}
+
+Effect.BlindDown = function(element) {
+ element = $(element);
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, 100,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ scaleFrom: 0,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makeClipping();
+ effect.element.setStyle({height: '0px'});
+ effect.element.show();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.undoClipping();
+ }
+ }, arguments[1] || {})
+ );
+}
+
+Effect.SwitchOff = function(element) {
+ element = $(element);
+ var oldOpacity = element.getInlineOpacity();
+ return new Effect.Appear(element, {
+ duration: 0.4,
+ from: 0,
+ transition: Effect.Transitions.flicker,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(effect.element, 1, {
+ duration: 0.3, scaleFromCenter: true,
+ scaleX: false, scaleContent: false, restoreAfterFinish: true,
+ beforeSetup: function(effect) {
+ effect.element.makePositioned();
+ effect.element.makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide();
+ effect.element.undoClipping();
+ effect.element.undoPositioned();
+ effect.element.setStyle({opacity: oldOpacity});
+ }
+ })
+ }
+ });
+}
+
+Effect.DropOut = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.getStyle('top'),
+ left: element.getStyle('left'),
+ opacity: element.getInlineOpacity() };
+ return new Effect.Parallel(
+ [ new Effect.Move(element, {x: 0, y: 100, sync: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
+ Object.extend(
+ { duration: 0.5,
+ beforeSetup: function(effect) {
+ effect.effects[0].element.makePositioned();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide();
+ effect.effects[0].element.undoPositioned();
+ effect.effects[0].element.setStyle(oldStyle);
+ }
+ }, arguments[1] || {}));
+}
+
+Effect.Shake = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.getStyle('top'),
+ left: element.getStyle('left') };
+ return new Effect.Move(element,
+ { x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+ effect.element.undoPositioned();
+ effect.element.setStyle(oldStyle);
+ }}) }}) }}) }}) }}) }});
+}
+
+Effect.SlideDown = function(element) {
+ element = $(element);
+ element.cleanWhitespace();
+ // SlideDown need to have the content of the element wrapped in a container element with fixed height!
+ var oldInnerBottom = $(element.firstChild).getStyle('bottom');
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, 100, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ scaleFrom: window.opera ? 0 : 1,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makePositioned();
+ effect.element.firstChild.makePositioned();
+ if(window.opera) effect.element.setStyle({top: ''});
+ effect.element.makeClipping();
+ effect.element.setStyle({height: '0px'});
+ effect.element.show(); },
+ afterUpdateInternal: function(effect) {
+ effect.element.firstChild.setStyle({bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' });
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.undoClipping();
+ // IE will crash if child is undoPositioned first
+ if(/MSIE/.test(navigator.userAgent)){
+ effect.element.undoPositioned();
+ effect.element.firstChild.undoPositioned();
+ }else{
+ effect.element.firstChild.undoPositioned();
+ effect.element.undoPositioned();
+ }
+ effect.element.firstChild.setStyle({bottom: oldInnerBottom}); }
+ }, arguments[1] || {})
+ );
+}
+
+Effect.SlideUp = function(element) {
+ element = $(element);
+ element.cleanWhitespace();
+ var oldInnerBottom = $(element.firstChild).getStyle('bottom');
+ return new Effect.Scale(element, window.opera ? 0 : 1,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ scaleMode: 'box',
+ scaleFrom: 100,
+ restoreAfterFinish: true,
+ beforeStartInternal: function(effect) {
+ effect.element.makePositioned();
+ effect.element.firstChild.makePositioned();
+ if(window.opera) effect.element.setStyle({top: ''});
+ effect.element.makeClipping();
+ effect.element.show(); },
+ afterUpdateInternal: function(effect) {
+ effect.element.firstChild.setStyle({bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' }); },
+ afterFinishInternal: function(effect) {
+ effect.element.hide();
+ effect.element.undoClipping();
+ effect.element.firstChild.undoPositioned();
+ effect.element.undoPositioned();
+ effect.element.setStyle({bottom: oldInnerBottom}); }
+ }, arguments[1] || {})
+ );
+}
+
+// Bug in opera makes the TD containing this element expand for a instance after finish
+Effect.Squish = function(element) {
+ return new Effect.Scale(element, window.opera ? 1 : 0,
+ { restoreAfterFinish: true,
+ beforeSetup: function(effect) {
+ effect.element.makeClipping(effect.element); },
+ afterFinishInternal: function(effect) {
+ effect.element.hide(effect.element);
+ effect.element.undoClipping(effect.element); }
+ });
+}
+
+Effect.Grow = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransition: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.full
+ }, arguments[1] || {});
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: element.getInlineOpacity() };
+
+ var dims = element.getDimensions();
+ var initialMoveX, initialMoveY;
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ initialMoveX = initialMoveY = moveX = moveY = 0;
+ break;
+ case 'top-right':
+ initialMoveX = dims.width;
+ initialMoveY = moveY = 0;
+ moveX = -dims.width;
+ break;
+ case 'bottom-left':
+ initialMoveX = moveX = 0;
+ initialMoveY = dims.height;
+ moveY = -dims.height;
+ break;
+ case 'bottom-right':
+ initialMoveX = dims.width;
+ initialMoveY = dims.height;
+ moveX = -dims.width;
+ moveY = -dims.height;
+ break;
+ case 'center':
+ initialMoveX = dims.width / 2;
+ initialMoveY = dims.height / 2;
+ moveX = -dims.width / 2;
+ moveY = -dims.height / 2;
+ break;
+ }
+
+ return new Effect.Move(element, {
+ x: initialMoveX,
+ y: initialMoveY,
+ duration: 0.01,
+ beforeSetup: function(effect) {
+ effect.element.hide();
+ effect.element.makeClipping();
+ effect.element.makePositioned();
+ },
+ afterFinishInternal: function(effect) {
+ new Effect.Parallel(
+ [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
+ new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
+ new Effect.Scale(effect.element, 100, {
+ scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
+ sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
+ ], Object.extend({
+ beforeSetup: function(effect) {
+ effect.effects[0].element.setStyle({height: '0px'});
+ effect.effects[0].element.show();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.undoClipping();
+ effect.effects[0].element.undoPositioned();
+ effect.effects[0].element.setStyle(oldStyle);
+ }
+ }, options)
+ )
+ }
+ });
+}
+
+Effect.Shrink = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransition: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.none
+ }, arguments[1] || {});
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: element.getInlineOpacity() };
+
+ var dims = element.getDimensions();
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ moveX = moveY = 0;
+ break;
+ case 'top-right':
+ moveX = dims.width;
+ moveY = 0;
+ break;
+ case 'bottom-left':
+ moveX = 0;
+ moveY = dims.height;
+ break;
+ case 'bottom-right':
+ moveX = dims.width;
+ moveY = dims.height;
+ break;
+ case 'center':
+ moveX = dims.width / 2;
+ moveY = dims.height / 2;
+ break;
+ }
+
+ return new Effect.Parallel(
+ [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
+ new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
+ new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
+ ], Object.extend({
+ beforeStartInternal: function(effect) {
+ effect.effects[0].element.makePositioned();
+ effect.effects[0].element.makeClipping(); },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide();
+ effect.effects[0].element.undoClipping();
+ effect.effects[0].element.undoPositioned();
+ effect.effects[0].element.setStyle(oldStyle); }
+ }, options)
+ );
+}
+
+Effect.Pulsate = function(element) {
+ element = $(element);
+ var options = arguments[1] || {};
+ var oldOpacity = element.getInlineOpacity();
+ var transition = options.transition || Effect.Transitions.sinoidal;
+ var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
+ reverser.bind(transition);
+ return new Effect.Opacity(element,
+ Object.extend(Object.extend({ duration: 3.0, from: 0,
+ afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
+ }, options), {transition: reverser}));
+}
+
+Effect.Fold = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ width: element.style.width,
+ height: element.style.height };
+ Element.makeClipping(element);
+ return new Effect.Scale(element, 5, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(element, 1, {
+ scaleContent: false,
+ scaleY: false,
+ afterFinishInternal: function(effect) {
+ effect.element.hide();
+ effect.element.undoClipping();
+ effect.element.setStyle(oldStyle);
+ } });
+ }}, arguments[1] || {}));
+};
+
+['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
+ 'collectTextNodes','collectTextNodesIgnoreClass','childrenWithClassName'].each(
+ function(f) { Element.Methods[f] = Element[f]; }
+);
+
+Element.Methods.visualEffect = function(element, effect, options) {
+ s = effect.gsub(/_/, '-').camelize();
+ effect_class = s.charAt(0).toUpperCase() + s.substring(1);
+ new Effect[effect_class](element, options);
+ return $(element);
+};
+
+Element.addMethods(); \ No newline at end of file
diff --git a/cropper/lib/prototype.js b/cropper/lib/prototype.js
new file mode 100644
index 000000000..0caf9cd7f
--- /dev/null
+++ b/cropper/lib/prototype.js
@@ -0,0 +1,2006 @@
+/* Prototype JavaScript framework, version 1.5.0_rc0
+ * (c) 2005 Sam Stephenson <sam@conio.net>
+ *
+ * Prototype is freely distributable under the terms of an MIT-style license.
+ * For details, see the Prototype web site: http://prototype.conio.net/
+ *
+/*--------------------------------------------------------------------------*/
+
+var Prototype = {
+ Version: '1.5.0_rc0',
+ ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
+
+ emptyFunction: function() {},
+ K: function(x) {return x}
+}
+
+var Class = {
+ create: function() {
+ return function() {
+ this.initialize.apply(this, arguments);
+ }
+ }
+}
+
+var Abstract = new Object();
+
+Object.extend = function(destination, source) {
+ for (var property in source) {
+ destination[property] = source[property];
+ }
+ return destination;
+}
+
+Object.inspect = function(object) {
+ try {
+ if (object == undefined) return 'undefined';
+ if (object == null) return 'null';
+ return object.inspect ? object.inspect() : object.toString();
+ } catch (e) {
+ if (e instanceof RangeError) return '...';
+ throw e;
+ }
+}
+
+Function.prototype.bind = function() {
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function() {
+ return __method.apply(object, args.concat($A(arguments)));
+ }
+}
+
+Function.prototype.bindAsEventListener = function(object) {
+ var __method = this;
+ return function(event) {
+ return __method.call(object, event || window.event);
+ }
+}
+
+Object.extend(Number.prototype, {
+ toColorPart: function() {
+ var digits = this.toString(16);
+ if (this < 16) return '0' + digits;
+ return digits;
+ },
+
+ succ: function() {
+ return this + 1;
+ },
+
+ times: function(iterator) {
+ $R(0, this, true).each(iterator);
+ return this;
+ }
+});
+
+var Try = {
+ these: function() {
+ var returnValue;
+
+ for (var i = 0; i < arguments.length; i++) {
+ var lambda = arguments[i];
+ try {
+ returnValue = lambda();
+ break;
+ } catch (e) {}
+ }
+
+ return returnValue;
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create();
+PeriodicalExecuter.prototype = {
+ initialize: function(callback, frequency) {
+ this.callback = callback;
+ this.frequency = frequency;
+ this.currentlyExecuting = false;
+
+ this.registerCallback();
+ },
+
+ registerCallback: function() {
+ setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+ },
+
+ onTimerEvent: function() {
+ if (!this.currentlyExecuting) {
+ try {
+ this.currentlyExecuting = true;
+ this.callback();
+ } finally {
+ this.currentlyExecuting = false;
+ }
+ }
+ }
+}
+Object.extend(String.prototype, {
+ gsub: function(pattern, replacement) {
+ var result = '', source = this, match;
+ replacement = arguments.callee.prepareReplacement(replacement);
+
+ while (source.length > 0) {
+ if (match = source.match(pattern)) {
+ result += source.slice(0, match.index);
+ result += (replacement(match) || '').toString();
+ source = source.slice(match.index + match[0].length);
+ } else {
+ result += source, source = '';
+ }
+ }
+ return result;
+ },
+
+ sub: function(pattern, replacement, count) {
+ replacement = this.gsub.prepareReplacement(replacement);
+ count = count === undefined ? 1 : count;
+
+ return this.gsub(pattern, function(match) {
+ if (--count < 0) return match[0];
+ return replacement(match);
+ });
+ },
+
+ scan: function(pattern, iterator) {
+ this.gsub(pattern, iterator);
+ return this;
+ },
+
+ truncate: function(length, truncation) {
+ length = length || 30;
+ truncation = truncation === undefined ? '...' : truncation;
+ return this.length > length ?
+ this.slice(0, length - truncation.length) + truncation : this;
+ },
+
+ strip: function() {
+ return this.replace(/^\s+/, '').replace(/\s+$/, '');
+ },
+
+ stripTags: function() {
+ return this.replace(/<\/?[^>]+>/gi, '');
+ },
+
+ stripScripts: function() {
+ return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+ },
+
+ extractScripts: function() {
+ var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+ var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+ return (this.match(matchAll) || []).map(function(scriptTag) {
+ return (scriptTag.match(matchOne) || ['', ''])[1];
+ });
+ },
+
+ evalScripts: function() {
+ return this.extractScripts().map(function(script) { return eval(script) });
+ },
+
+ escapeHTML: function() {
+ var div = document.createElement('div');
+ var text = document.createTextNode(this);
+ div.appendChild(text);
+ return div.innerHTML;
+ },
+
+ unescapeHTML: function() {
+ var div = document.createElement('div');
+ div.innerHTML = this.stripTags();
+ return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
+ },
+
+ toQueryParams: function() {
+ var pairs = this.match(/^\??(.*)$/)[1].split('&');
+ return pairs.inject({}, function(params, pairString) {
+ var pair = pairString.split('=');
+ params[pair[0]] = pair[1];
+ return params;
+ });
+ },
+
+ toArray: function() {
+ return this.split('');
+ },
+
+ camelize: function() {
+ var oStringList = this.split('-');
+ if (oStringList.length == 1) return oStringList[0];
+
+ var camelizedString = this.indexOf('-') == 0
+ ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)
+ : oStringList[0];
+
+ for (var i = 1, len = oStringList.length; i < len; i++) {
+ var s = oStringList[i];
+ camelizedString += s.charAt(0).toUpperCase() + s.substring(1);
+ }
+
+ return camelizedString;
+ },
+
+ inspect: function() {
+ return "'" + this.replace(/\\/g, '\\\\').replace(/'/g, '\\\'') + "'";
+ }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+ if (typeof replacement == 'function') return replacement;
+ var template = new Template(replacement);
+ return function(match) { return template.evaluate(match) };
+}
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+var Template = Class.create();
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+Template.prototype = {
+ initialize: function(template, pattern) {
+ this.template = template.toString();
+ this.pattern = pattern || Template.Pattern;
+ },
+
+ evaluate: function(object) {
+ return this.template.gsub(this.pattern, function(match) {
+ var before = match[1];
+ if (before == '\\') return match[2];
+ return before + (object[match[3]] || '').toString();
+ });
+ }
+}
+
+var $break = new Object();
+var $continue = new Object();
+
+var Enumerable = {
+ each: function(iterator) {
+ var index = 0;
+ try {
+ this._each(function(value) {
+ try {
+ iterator(value, index++);
+ } catch (e) {
+ if (e != $continue) throw e;
+ }
+ });
+ } catch (e) {
+ if (e != $break) throw e;
+ }
+ },
+
+ all: function(iterator) {
+ var result = true;
+ this.each(function(value, index) {
+ result = result && !!(iterator || Prototype.K)(value, index);
+ if (!result) throw $break;
+ });
+ return result;
+ },
+
+ any: function(iterator) {
+ var result = true;
+ this.each(function(value, index) {
+ if (result = !!(iterator || Prototype.K)(value, index))
+ throw $break;
+ });
+ return result;
+ },
+
+ collect: function(iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ results.push(iterator(value, index));
+ });
+ return results;
+ },
+
+ detect: function (iterator) {
+ var result;
+ this.each(function(value, index) {
+ if (iterator(value, index)) {
+ result = value;
+ throw $break;
+ }
+ });
+ return result;
+ },
+
+ findAll: function(iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ if (iterator(value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ grep: function(pattern, iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ var stringValue = value.toString();
+ if (stringValue.match(pattern))
+ results.push((iterator || Prototype.K)(value, index));
+ })
+ return results;
+ },
+
+ include: function(object) {
+ var found = false;
+ this.each(function(value) {
+ if (value == object) {
+ found = true;
+ throw $break;
+ }
+ });
+ return found;
+ },
+
+ inject: function(memo, iterator) {
+ this.each(function(value, index) {
+ memo = iterator(memo, value, index);
+ });
+ return memo;
+ },
+
+ invoke: function(method) {
+ var args = $A(arguments).slice(1);
+ return this.collect(function(value) {
+ return value[method].apply(value, args);
+ });
+ },
+
+ max: function(iterator) {
+ var result;
+ this.each(function(value, index) {
+ value = (iterator || Prototype.K)(value, index);
+ if (result == undefined || value >= result)
+ result = value;
+ });
+ return result;
+ },
+
+ min: function(iterator) {
+ var result;
+ this.each(function(value, index) {
+ value = (iterator || Prototype.K)(value, index);
+ if (result == undefined || value < result)
+ result = value;
+ });
+ return result;
+ },
+
+ partition: function(iterator) {
+ var trues = [], falses = [];
+ this.each(function(value, index) {
+ ((iterator || Prototype.K)(value, index) ?
+ trues : falses).push(value);
+ });
+ return [trues, falses];
+ },
+
+ pluck: function(property) {
+ var results = [];
+ this.each(function(value, index) {
+ results.push(value[property]);
+ });
+ return results;
+ },
+
+ reject: function(iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ if (!iterator(value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ sortBy: function(iterator) {
+ return this.collect(function(value, index) {
+ return {value: value, criteria: iterator(value, index)};
+ }).sort(function(left, right) {
+ var a = left.criteria, b = right.criteria;
+ return a < b ? -1 : a > b ? 1 : 0;
+ }).pluck('value');
+ },
+
+ toArray: function() {
+ return this.collect(Prototype.K);
+ },
+
+ zip: function() {
+ var iterator = Prototype.K, args = $A(arguments);
+ if (typeof args.last() == 'function')
+ iterator = args.pop();
+
+ var collections = [this].concat(args).map($A);
+ return this.map(function(value, index) {
+ return iterator(collections.pluck(index));
+ });
+ },
+
+ inspect: function() {
+ return '#<Enumerable:' + this.toArray().inspect() + '>';
+ }
+}
+
+Object.extend(Enumerable, {
+ map: Enumerable.collect,
+ find: Enumerable.detect,
+ select: Enumerable.findAll,
+ member: Enumerable.include,
+ entries: Enumerable.toArray
+});
+var $A = Array.from = function(iterable) {
+ if (!iterable) return [];
+ if (iterable.toArray) {
+ return iterable.toArray();
+ } else {
+ var results = [];
+ for (var i = 0; i < iterable.length; i++)
+ results.push(iterable[i]);
+ return results;
+ }
+}
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse)
+ Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+ _each: function(iterator) {
+ for (var i = 0; i < this.length; i++)
+ iterator(this[i]);
+ },
+
+ clear: function() {
+ this.length = 0;
+ return this;
+ },
+
+ first: function() {
+ return this[0];
+ },
+
+ last: function() {
+ return this[this.length - 1];
+ },
+
+ compact: function() {
+ return this.select(function(value) {
+ return value != undefined || value != null;
+ });
+ },
+
+ flatten: function() {
+ return this.inject([], function(array, value) {
+ return array.concat(value && value.constructor == Array ?
+ value.flatten() : [value]);
+ });
+ },
+
+ without: function() {
+ var values = $A(arguments);
+ return this.select(function(value) {
+ return !values.include(value);
+ });
+ },
+
+ indexOf: function(object) {
+ for (var i = 0; i < this.length; i++)
+ if (this[i] == object) return i;
+ return -1;
+ },
+
+ reverse: function(inline) {
+ return (inline !== false ? this : this.toArray())._reverse();
+ },
+
+ inspect: function() {
+ return '[' + this.map(Object.inspect).join(', ') + ']';
+ }
+});
+var Hash = {
+ _each: function(iterator) {
+ for (var key in this) {
+ var value = this[key];
+ if (typeof value == 'function') continue;
+
+ var pair = [key, value];
+ pair.key = key;
+ pair.value = value;
+ iterator(pair);
+ }
+ },
+
+ keys: function() {
+ return this.pluck('key');
+ },
+
+ values: function() {
+ return this.pluck('value');
+ },
+
+ merge: function(hash) {
+ return $H(hash).inject($H(this), function(mergedHash, pair) {
+ mergedHash[pair.key] = pair.value;
+ return mergedHash;
+ });
+ },
+
+ toQueryString: function() {
+ return this.map(function(pair) {
+ return pair.map(encodeURIComponent).join('=');
+ }).join('&');
+ },
+
+ inspect: function() {
+ return '#<Hash:{' + this.map(function(pair) {
+ return pair.map(Object.inspect).join(': ');
+ }).join(', ') + '}>';
+ }
+}
+
+function $H(object) {
+ var hash = Object.extend({}, object || {});
+ Object.extend(hash, Enumerable);
+ Object.extend(hash, Hash);
+ return hash;
+}
+ObjectRange = Class.create();
+Object.extend(ObjectRange.prototype, Enumerable);
+Object.extend(ObjectRange.prototype, {
+ initialize: function(start, end, exclusive) {
+ this.start = start;
+ this.end = end;
+ this.exclusive = exclusive;
+ },
+
+ _each: function(iterator) {
+ var value = this.start;
+ do {
+ iterator(value);
+ value = value.succ();
+ } while (this.include(value));
+ },
+
+ include: function(value) {
+ if (value < this.start)
+ return false;
+ if (this.exclusive)
+ return value < this.end;
+ return value <= this.end;
+ }
+});
+
+var $R = function(start, end, exclusive) {
+ return new ObjectRange(start, end, exclusive);
+}
+
+var Ajax = {
+ getTransport: function() {
+ return Try.these(
+ function() {return new XMLHttpRequest()},
+ function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+ function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+ ) || false;
+ },
+
+ activeRequestCount: 0
+}
+
+Ajax.Responders = {
+ responders: [],
+
+ _each: function(iterator) {
+ this.responders._each(iterator);
+ },
+
+ register: function(responderToAdd) {
+ if (!this.include(responderToAdd))
+ this.responders.push(responderToAdd);
+ },
+
+ unregister: function(responderToRemove) {
+ this.responders = this.responders.without(responderToRemove);
+ },
+
+ dispatch: function(callback, request, transport, json) {
+ this.each(function(responder) {
+ if (responder[callback] && typeof responder[callback] == 'function') {
+ try {
+ responder[callback].apply(responder, [request, transport, json]);
+ } catch (e) {}
+ }
+ });
+ }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+ onCreate: function() {
+ Ajax.activeRequestCount++;
+ },
+
+ onComplete: function() {
+ Ajax.activeRequestCount--;
+ }
+});
+
+Ajax.Base = function() {};
+Ajax.Base.prototype = {
+ setOptions: function(options) {
+ this.options = {
+ method: 'post',
+ asynchronous: true,
+ contentType: 'application/x-www-form-urlencoded',
+ parameters: ''
+ }
+ Object.extend(this.options, options || {});
+ },
+
+ responseIsSuccess: function() {
+ return this.transport.status == undefined
+ || this.transport.status == 0
+ || (this.transport.status >= 200 && this.transport.status < 300);
+ },
+
+ responseIsFailure: function() {
+ return !this.responseIsSuccess();
+ }
+}
+
+Ajax.Request = Class.create();
+Ajax.Request.Events =
+ ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+ initialize: function(url, options) {
+ this.transport = Ajax.getTransport();
+ this.setOptions(options);
+ this.request(url);
+ },
+
+ request: function(url) {
+ var parameters = this.options.parameters || '';
+ if (parameters.length > 0) parameters += '&_=';
+
+ try {
+ this.url = url;
+ if (this.options.method == 'get' && parameters.length > 0)
+ this.url += (this.url.match(/\?/) ? '&' : '?') + parameters;
+
+ Ajax.Responders.dispatch('onCreate', this, this.transport);
+
+ this.transport.open(this.options.method, this.url,
+ this.options.asynchronous);
+
+ if (this.options.asynchronous) {
+ this.transport.onreadystatechange = this.onStateChange.bind(this);
+ setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10);
+ }
+
+ this.setRequestHeaders();
+
+ var body = this.options.postBody ? this.options.postBody : parameters;
+ this.transport.send(this.options.method == 'post' ? body : null);
+
+ } catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ setRequestHeaders: function() {
+ var requestHeaders =
+ ['X-Requested-With', 'XMLHttpRequest',
+ 'X-Prototype-Version', Prototype.Version,
+ 'Accept', 'text/javascript, text/html, application/xml, text/xml, */*'];
+
+ if (this.options.method == 'post') {
+ requestHeaders.push('Content-type', this.options.contentType);
+
+ /* Force "Connection: close" for Mozilla browsers to work around
+ * a bug where XMLHttpReqeuest sends an incorrect Content-length
+ * header. See Mozilla Bugzilla #246651.
+ */
+ if (this.transport.overrideMimeType)
+ requestHeaders.push('Connection', 'close');
+ }
+
+ if (this.options.requestHeaders)
+ requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);
+
+ for (var i = 0; i < requestHeaders.length; i += 2)
+ this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
+ },
+
+ onStateChange: function() {
+ var readyState = this.transport.readyState;
+ if (readyState != 1)
+ this.respondToReadyState(this.transport.readyState);
+ },
+
+ header: function(name) {
+ try {
+ return this.transport.getResponseHeader(name);
+ } catch (e) {}
+ },
+
+ evalJSON: function() {
+ try {
+ return eval('(' + this.header('X-JSON') + ')');
+ } catch (e) {}
+ },
+
+ evalResponse: function() {
+ try {
+ return eval(this.transport.responseText);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ respondToReadyState: function(readyState) {
+ var event = Ajax.Request.Events[readyState];
+ var transport = this.transport, json = this.evalJSON();
+
+ if (event == 'Complete') {
+ try {
+ (this.options['on' + this.transport.status]
+ || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
+ || Prototype.emptyFunction)(transport, json);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ if ((this.header('Content-type') || '').match(/^text\/javascript/i))
+ this.evalResponse();
+ }
+
+ try {
+ (this.options['on' + event] || Prototype.emptyFunction)(transport, json);
+ Ajax.Responders.dispatch('on' + event, this, transport, json);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
+ if (event == 'Complete')
+ this.transport.onreadystatechange = Prototype.emptyFunction;
+ },
+
+ dispatchException: function(exception) {
+ (this.options.onException || Prototype.emptyFunction)(this, exception);
+ Ajax.Responders.dispatch('onException', this, exception);
+ }
+});
+
+Ajax.Updater = Class.create();
+
+Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
+ initialize: function(container, url, options) {
+ this.containers = {
+ success: container.success ? $(container.success) : $(container),
+ failure: container.failure ? $(container.failure) :
+ (container.success ? null : $(container))
+ }
+
+ this.transport = Ajax.getTransport();
+ this.setOptions(options);
+
+ var onComplete = this.options.onComplete || Prototype.emptyFunction;
+ this.options.onComplete = (function(transport, object) {
+ this.updateContent();
+ onComplete(transport, object);
+ }).bind(this);
+
+ this.request(url);
+ },
+
+ updateContent: function() {
+ var receiver = this.responseIsSuccess() ?
+ this.containers.success : this.containers.failure;
+ var response = this.transport.responseText;
+
+ if (!this.options.evalScripts)
+ response = response.stripScripts();
+
+ if (receiver) {
+ if (this.options.insertion) {
+ new this.options.insertion(receiver, response);
+ } else {
+ Element.update(receiver, response);
+ }
+ }
+
+ if (this.responseIsSuccess()) {
+ if (this.onComplete)
+ setTimeout(this.onComplete.bind(this), 10);
+ }
+ }
+});
+
+Ajax.PeriodicalUpdater = Class.create();
+Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
+ initialize: function(container, url, options) {
+ this.setOptions(options);
+ this.onComplete = this.options.onComplete;
+
+ this.frequency = (this.options.frequency || 2);
+ this.decay = (this.options.decay || 1);
+
+ this.updater = {};
+ this.container = container;
+ this.url = url;
+
+ this.start();
+ },
+
+ start: function() {
+ this.options.onComplete = this.updateComplete.bind(this);
+ this.onTimerEvent();
+ },
+
+ stop: function() {
+ this.updater.onComplete = undefined;
+ clearTimeout(this.timer);
+ (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+ },
+
+ updateComplete: function(request) {
+ if (this.options.decay) {
+ this.decay = (request.responseText == this.lastText ?
+ this.decay * this.options.decay : 1);
+
+ this.lastText = request.responseText;
+ }
+ this.timer = setTimeout(this.onTimerEvent.bind(this),
+ this.decay * this.frequency * 1000);
+ },
+
+ onTimerEvent: function() {
+ this.updater = new Ajax.Updater(this.container, this.url, this.options);
+ }
+});
+function $() {
+ var results = [], element;
+ for (var i = 0; i < arguments.length; i++) {
+ element = arguments[i];
+ if (typeof element == 'string')
+ element = document.getElementById(element);
+ results.push(Element.extend(element));
+ }
+ return results.length < 2 ? results[0] : results;
+}
+
+document.getElementsByClassName = function(className, parentElement) {
+ var children = ($(parentElement) || document.body).getElementsByTagName('*');
+ return $A(children).inject([], function(elements, child) {
+ if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
+ elements.push(Element.extend(child));
+ return elements;
+ });
+}
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Element)
+ var Element = new Object();
+
+Element.extend = function(element) {
+ if (!element) return;
+ if (_nativeExtensions) return element;
+
+ if (!element._extended && element.tagName && element != window) {
+ var methods = Element.Methods, cache = Element.extend.cache;
+ for (property in methods) {
+ var value = methods[property];
+ if (typeof value == 'function')
+ element[property] = cache.findOrStore(value);
+ }
+ }
+
+ element._extended = true;
+ return element;
+}
+
+Element.extend.cache = {
+ findOrStore: function(value) {
+ return this[value] = this[value] || function() {
+ return value.apply(null, [this].concat($A(arguments)));
+ }
+ }
+}
+
+Element.Methods = {
+ visible: function(element) {
+ return $(element).style.display != 'none';
+ },
+
+ toggle: function() {
+ for (var i = 0; i < arguments.length; i++) {
+ var element = $(arguments[i]);
+ Element[Element.visible(element) ? 'hide' : 'show'](element);
+ }
+ },
+
+ hide: function() {
+ for (var i = 0; i < arguments.length; i++) {
+ var element = $(arguments[i]);
+ element.style.display = 'none';
+ }
+ },
+
+ show: function() {
+ for (var i = 0; i < arguments.length; i++) {
+ var element = $(arguments[i]);
+ element.style.display = '';
+ }
+ },
+
+ remove: function(element) {
+ element = $(element);
+ element.parentNode.removeChild(element);
+ },
+
+ update: function(element, html) {
+ $(element).innerHTML = html.stripScripts();
+ setTimeout(function() {html.evalScripts()}, 10);
+ },
+
+ replace: function(element, html) {
+ element = $(element);
+ if (element.outerHTML) {
+ element.outerHTML = html.stripScripts();
+ } else {
+ var range = element.ownerDocument.createRange();
+ range.selectNodeContents(element);
+ element.parentNode.replaceChild(
+ range.createContextualFragment(html.stripScripts()), element);
+ }
+ setTimeout(function() {html.evalScripts()}, 10);
+ },
+
+ getHeight: function(element) {
+ element = $(element);
+ return element.offsetHeight;
+ },
+
+ classNames: function(element) {
+ return new Element.ClassNames(element);
+ },
+
+ hasClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ return Element.classNames(element).include(className);
+ },
+
+ addClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ return Element.classNames(element).add(className);
+ },
+
+ removeClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ return Element.classNames(element).remove(className);
+ },
+
+ // removes whitespace-only text node children
+ cleanWhitespace: function(element) {
+ element = $(element);
+ for (var i = 0; i < element.childNodes.length; i++) {
+ var node = element.childNodes[i];
+ if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+ Element.remove(node);
+ }
+ },
+
+ empty: function(element) {
+ return $(element).innerHTML.match(/^\s*$/);
+ },
+
+ childOf: function(element, ancestor) {
+ element = $(element), ancestor = $(ancestor);
+ while (element = element.parentNode)
+ if (element == ancestor) return true;
+ return false;
+ },
+
+ scrollTo: function(element) {
+ element = $(element);
+ var x = element.x ? element.x : element.offsetLeft,
+ y = element.y ? element.y : element.offsetTop;
+ window.scrollTo(x, y);
+ },
+
+ getStyle: function(element, style) {
+ element = $(element);
+ var value = element.style[style.camelize()];
+ if (!value) {
+ if (document.defaultView && document.defaultView.getComputedStyle) {
+ var css = document.defaultView.getComputedStyle(element, null);
+ value = css ? css.getPropertyValue(style) : null;
+ } else if (element.currentStyle) {
+ value = element.currentStyle[style.camelize()];
+ }
+ }
+
+ if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
+ if (Element.getStyle(element, 'position') == 'static') value = 'auto';
+
+ return value == 'auto' ? null : value;
+ },
+
+ setStyle: function(element, style) {
+ element = $(element);
+ for (var name in style)
+ element.style[name.camelize()] = style[name];
+ },
+
+ getDimensions: function(element) {
+ element = $(element);
+ if (Element.getStyle(element, 'display') != 'none')
+ return {width: element.offsetWidth, height: element.offsetHeight};
+
+ // All *Width and *Height properties give 0 on elements with display none,
+ // so enable the element temporarily
+ var els = element.style;
+ var originalVisibility = els.visibility;
+ var originalPosition = els.position;
+ els.visibility = 'hidden';
+ els.position = 'absolute';
+ els.display = '';
+ var originalWidth = element.clientWidth;
+ var originalHeight = element.clientHeight;
+ els.display = 'none';
+ els.position = originalPosition;
+ els.visibility = originalVisibility;
+ return {width: originalWidth, height: originalHeight};
+ },
+
+ makePositioned: function(element) {
+ element = $(element);
+ var pos = Element.getStyle(element, 'position');
+ if (pos == 'static' || !pos) {
+ element._madePositioned = true;
+ element.style.position = 'relative';
+ // Opera returns the offset relative to the positioning context, when an
+ // element is position relative but top and left have not been defined
+ if (window.opera) {
+ element.style.top = 0;
+ element.style.left = 0;
+ }
+ }
+ },
+
+ undoPositioned: function(element) {
+ element = $(element);
+ if (element._madePositioned) {
+ element._madePositioned = undefined;
+ element.style.position =
+ element.style.top =
+ element.style.left =
+ element.style.bottom =
+ element.style.right = '';
+ }
+ },
+
+ makeClipping: function(element) {
+ element = $(element);
+ if (element._overflow) return;
+ element._overflow = element.style.overflow;
+ if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+ element.style.overflow = 'hidden';
+ },
+
+ undoClipping: function(element) {
+ element = $(element);
+ if (element._overflow) return;
+ element.style.overflow = element._overflow;
+ element._overflow = undefined;
+ }
+}
+
+Object.extend(Element, Element.Methods);
+
+var _nativeExtensions = false;
+
+if(!HTMLElement && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
+ var HTMLElement = {}
+ HTMLElement.prototype = document.createElement('div').__proto__;
+}
+
+Element.addMethods = function(methods) {
+ Object.extend(Element.Methods, methods || {});
+
+ if(typeof HTMLElement != 'undefined') {
+ var methods = Element.Methods, cache = Element.extend.cache;
+ for (property in methods) {
+ var value = methods[property];
+ if (typeof value == 'function')
+ HTMLElement.prototype[property] = cache.findOrStore(value);
+ }
+ _nativeExtensions = true;
+ }
+}
+
+Element.addMethods();
+
+var Toggle = new Object();
+Toggle.display = Element.toggle;
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.Insertion = function(adjacency) {
+ this.adjacency = adjacency;
+}
+
+Abstract.Insertion.prototype = {
+ initialize: function(element, content) {
+ this.element = $(element);
+ this.content = content.stripScripts();
+
+ if (this.adjacency && this.element.insertAdjacentHTML) {
+ try {
+ this.element.insertAdjacentHTML(this.adjacency, this.content);
+ } catch (e) {
+ var tagName = this.element.tagName.toLowerCase();
+ if (tagName == 'tbody' || tagName == 'tr') {
+ this.insertContent(this.contentFromAnonymousTable());
+ } else {
+ throw e;
+ }
+ }
+ } else {
+ this.range = this.element.ownerDocument.createRange();
+ if (this.initializeRange) this.initializeRange();
+ this.insertContent([this.range.createContextualFragment(this.content)]);
+ }
+
+ setTimeout(function() {content.evalScripts()}, 10);
+ },
+
+ contentFromAnonymousTable: function() {
+ var div = document.createElement('div');
+ div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
+ return $A(div.childNodes[0].childNodes[0].childNodes);
+ }
+}
+
+var Insertion = new Object();
+
+Insertion.Before = Class.create();
+Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
+ initializeRange: function() {
+ this.range.setStartBefore(this.element);
+ },
+
+ insertContent: function(fragments) {
+ fragments.each((function(fragment) {
+ this.element.parentNode.insertBefore(fragment, this.element);
+ }).bind(this));
+ }
+});
+
+Insertion.Top = Class.create();
+Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
+ initializeRange: function() {
+ this.range.selectNodeContents(this.element);
+ this.range.collapse(true);
+ },
+
+ insertContent: function(fragments) {
+ fragments.reverse(false).each((function(fragment) {
+ this.element.insertBefore(fragment, this.element.firstChild);
+ }).bind(this));
+ }
+});
+
+Insertion.Bottom = Class.create();
+Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
+ initializeRange: function() {
+ this.range.selectNodeContents(this.element);
+ this.range.collapse(this.element);
+ },
+
+ insertContent: function(fragments) {
+ fragments.each((function(fragment) {
+ this.element.appendChild(fragment);
+ }).bind(this));
+ }
+});
+
+Insertion.After = Class.create();
+Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
+ initializeRange: function() {
+ this.range.setStartAfter(this.element);
+ },
+
+ insertContent: function(fragments) {
+ fragments.each((function(fragment) {
+ this.element.parentNode.insertBefore(fragment,
+ this.element.nextSibling);
+ }).bind(this));
+ }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+ initialize: function(element) {
+ this.element = $(element);
+ },
+
+ _each: function(iterator) {
+ this.element.className.split(/\s+/).select(function(name) {
+ return name.length > 0;
+ })._each(iterator);
+ },
+
+ set: function(className) {
+ this.element.className = className;
+ },
+
+ add: function(classNameToAdd) {
+ if (this.include(classNameToAdd)) return;
+ this.set(this.toArray().concat(classNameToAdd).join(' '));
+ },
+
+ remove: function(classNameToRemove) {
+ if (!this.include(classNameToRemove)) return;
+ this.set(this.select(function(className) {
+ return className != classNameToRemove;
+ }).join(' '));
+ },
+
+ toString: function() {
+ return this.toArray().join(' ');
+ }
+}
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+var Selector = Class.create();
+Selector.prototype = {
+ initialize: function(expression) {
+ this.params = {classNames: []};
+ this.expression = expression.toString().strip();
+ this.parseExpression();
+ this.compileMatcher();
+ },
+
+ parseExpression: function() {
+ function abort(message) { throw 'Parse error in selector: ' + message; }
+
+ if (this.expression == '') abort('empty expression');
+
+ var params = this.params, expr = this.expression, match, modifier, clause, rest;
+ while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) {
+ params.attributes = params.attributes || [];
+ params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''});
+ expr = match[1];
+ }
+
+ if (expr == '*') return this.params.wildcard = true;
+
+ while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) {
+ modifier = match[1], clause = match[2], rest = match[3];
+ switch (modifier) {
+ case '#': params.id = clause; break;
+ case '.': params.classNames.push(clause); break;
+ case '':
+ case undefined: params.tagName = clause.toUpperCase(); break;
+ default: abort(expr.inspect());
+ }
+ expr = rest;
+ }
+
+ if (expr.length > 0) abort(expr.inspect());
+ },
+
+ buildMatchExpression: function() {
+ var params = this.params, conditions = [], clause;
+
+ if (params.wildcard)
+ conditions.push('true');
+ if (clause = params.id)
+ conditions.push('element.id == ' + clause.inspect());
+ if (clause = params.tagName)
+ conditions.push('element.tagName.toUpperCase() == ' + clause.inspect());
+ if ((clause = params.classNames).length > 0)
+ for (var i = 0; i < clause.length; i++)
+ conditions.push('Element.hasClassName(element, ' + clause[i].inspect() + ')');
+ if (clause = params.attributes) {
+ clause.each(function(attribute) {
+ var value = 'element.getAttribute(' + attribute.name.inspect() + ')';
+ var splitValueBy = function(delimiter) {
+ return value + ' && ' + value + '.split(' + delimiter.inspect() + ')';
+ }
+
+ switch (attribute.operator) {
+ case '=': conditions.push(value + ' == ' + attribute.value.inspect()); break;
+ case '~=': conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break;
+ case '|=': conditions.push(
+ splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect()
+ ); break;
+ case '!=': conditions.push(value + ' != ' + attribute.value.inspect()); break;
+ case '':
+ case undefined: conditions.push(value + ' != null'); break;
+ default: throw 'Unknown operator ' + attribute.operator + ' in selector';
+ }
+ });
+ }
+
+ return conditions.join(' && ');
+ },
+
+ compileMatcher: function() {
+ this.match = new Function('element', 'if (!element.tagName) return false; \
+ return ' + this.buildMatchExpression());
+ },
+
+ findElements: function(scope) {
+ var element;
+
+ if (element = $(this.params.id))
+ if (this.match(element))
+ if (!scope || Element.childOf(element, scope))
+ return [element];
+
+ scope = (scope || document).getElementsByTagName(this.params.tagName || '*');
+
+ var results = [];
+ for (var i = 0; i < scope.length; i++)
+ if (this.match(element = scope[i]))
+ results.push(Element.extend(element));
+
+ return results;
+ },
+
+ toString: function() {
+ return this.expression;
+ }
+}
+
+function $$() {
+ return $A(arguments).map(function(expression) {
+ return expression.strip().split(/\s+/).inject([null], function(results, expr) {
+ var selector = new Selector(expr);
+ return results.map(selector.findElements.bind(selector)).flatten();
+ });
+ }).flatten();
+}
+var Field = {
+ clear: function() {
+ for (var i = 0; i < arguments.length; i++)
+ $(arguments[i]).value = '';
+ },
+
+ focus: function(element) {
+ $(element).focus();
+ },
+
+ present: function() {
+ for (var i = 0; i < arguments.length; i++)
+ if ($(arguments[i]).value == '') return false;
+ return true;
+ },
+
+ select: function(element) {
+ $(element).select();
+ },
+
+ activate: function(element) {
+ element = $(element);
+ element.focus();
+ if (element.select)
+ element.select();
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Form = {
+ serialize: function(form) {
+ var elements = Form.getElements($(form));
+ var queryComponents = new Array();
+
+ for (var i = 0; i < elements.length; i++) {
+ var queryComponent = Form.Element.serialize(elements[i]);
+ if (queryComponent)
+ queryComponents.push(queryComponent);
+ }
+
+ return queryComponents.join('&');
+ },
+
+ getElements: function(form) {
+ form = $(form);
+ var elements = new Array();
+
+ for (var tagName in Form.Element.Serializers) {
+ var tagElements = form.getElementsByTagName(tagName);
+ for (var j = 0; j < tagElements.length; j++)
+ elements.push(tagElements[j]);
+ }
+ return elements;
+ },
+
+ getInputs: function(form, typeName, name) {
+ form = $(form);
+ var inputs = form.getElementsByTagName('input');
+
+ if (!typeName && !name)
+ return inputs;
+
+ var matchingInputs = new Array();
+ for (var i = 0; i < inputs.length; i++) {
+ var input = inputs[i];
+ if ((typeName && input.type != typeName) ||
+ (name && input.name != name))
+ continue;
+ matchingInputs.push(input);
+ }
+
+ return matchingInputs;
+ },
+
+ disable: function(form) {
+ var elements = Form.getElements(form);
+ for (var i = 0; i < elements.length; i++) {
+ var element = elements[i];
+ element.blur();
+ element.disabled = 'true';
+ }
+ },
+
+ enable: function(form) {
+ var elements = Form.getElements(form);
+ for (var i = 0; i < elements.length; i++) {
+ var element = elements[i];
+ element.disabled = '';
+ }
+ },
+
+ findFirstElement: function(form) {
+ return Form.getElements(form).find(function(element) {
+ return element.type != 'hidden' && !element.disabled &&
+ ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+ });
+ },
+
+ focusFirstElement: function(form) {
+ Field.activate(Form.findFirstElement(form));
+ },
+
+ reset: function(form) {
+ $(form).reset();
+ }
+}
+
+Form.Element = {
+ serialize: function(element) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ var parameter = Form.Element.Serializers[method](element);
+
+ if (parameter) {
+ var key = encodeURIComponent(parameter[0]);
+ if (key.length == 0) return;
+
+ if (parameter[1].constructor != Array)
+ parameter[1] = [parameter[1]];
+
+ return parameter[1].map(function(value) {
+ return key + '=' + encodeURIComponent(value);
+ }).join('&');
+ }
+ },
+
+ getValue: function(element) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ var parameter = Form.Element.Serializers[method](element);
+
+ if (parameter)
+ return parameter[1];
+ }
+}
+
+Form.Element.Serializers = {
+ input: function(element) {
+ switch (element.type.toLowerCase()) {
+ case 'submit':
+ case 'hidden':
+ case 'password':
+ case 'text':
+ return Form.Element.Serializers.textarea(element);
+ case 'checkbox':
+ case 'radio':
+ return Form.Element.Serializers.inputSelector(element);
+ }
+ return false;
+ },
+
+ inputSelector: function(element) {
+ if (element.checked)
+ return [element.name, element.value];
+ },
+
+ textarea: function(element) {
+ return [element.name, element.value];
+ },
+
+ select: function(element) {
+ return Form.Element.Serializers[element.type == 'select-one' ?
+ 'selectOne' : 'selectMany'](element);
+ },
+
+ selectOne: function(element) {
+ var value = '', opt, index = element.selectedIndex;
+ if (index >= 0) {
+ opt = element.options[index];
+ value = opt.value || opt.text;
+ }
+ return [element.name, value];
+ },
+
+ selectMany: function(element) {
+ var value = [];
+ for (var i = 0; i < element.length; i++) {
+ var opt = element.options[i];
+ if (opt.selected)
+ value.push(opt.value || opt.text);
+ }
+ return [element.name, value];
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var $F = Form.Element.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = function() {}
+Abstract.TimedObserver.prototype = {
+ initialize: function(element, frequency, callback) {
+ this.frequency = frequency;
+ this.element = $(element);
+ this.callback = callback;
+
+ this.lastValue = this.getValue();
+ this.registerCallback();
+ },
+
+ registerCallback: function() {
+ setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+ },
+
+ onTimerEvent: function() {
+ var value = this.getValue();
+ if (this.lastValue != value) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ }
+}
+
+Form.Element.Observer = Class.create();
+Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.Observer = Class.create();
+Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = function() {}
+Abstract.EventObserver.prototype = {
+ initialize: function(element, callback) {
+ this.element = $(element);
+ this.callback = callback;
+
+ this.lastValue = this.getValue();
+ if (this.element.tagName.toLowerCase() == 'form')
+ this.registerFormCallbacks();
+ else
+ this.registerCallback(this.element);
+ },
+
+ onElementEvent: function() {
+ var value = this.getValue();
+ if (this.lastValue != value) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ },
+
+ registerFormCallbacks: function() {
+ var elements = Form.getElements(this.element);
+ for (var i = 0; i < elements.length; i++)
+ this.registerCallback(elements[i]);
+ },
+
+ registerCallback: function(element) {
+ if (element.type) {
+ switch (element.type.toLowerCase()) {
+ case 'checkbox':
+ case 'radio':
+ Event.observe(element, 'click', this.onElementEvent.bind(this));
+ break;
+ case 'password':
+ case 'text':
+ case 'textarea':
+ case 'select-one':
+ case 'select-multiple':
+ Event.observe(element, 'change', this.onElementEvent.bind(this));
+ break;
+ }
+ }
+ }
+}
+
+Form.Element.EventObserver = Class.create();
+Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.EventObserver = Class.create();
+Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+if (!window.Event) {
+ var Event = new Object();
+}
+
+Object.extend(Event, {
+ KEY_BACKSPACE: 8,
+ KEY_TAB: 9,
+ KEY_RETURN: 13,
+ KEY_ESC: 27,
+ KEY_LEFT: 37,
+ KEY_UP: 38,
+ KEY_RIGHT: 39,
+ KEY_DOWN: 40,
+ KEY_DELETE: 46,
+
+ element: function(event) {
+ return event.target || event.srcElement;
+ },
+
+ isLeftClick: function(event) {
+ return (((event.which) && (event.which == 1)) ||
+ ((event.button) && (event.button == 1)));
+ },
+
+ pointerX: function(event) {
+ return event.pageX || (event.clientX +
+ (document.documentElement.scrollLeft || document.body.scrollLeft));
+ },
+
+ pointerY: function(event) {
+ return event.pageY || (event.clientY +
+ (document.documentElement.scrollTop || document.body.scrollTop));
+ },
+
+ stop: function(event) {
+ if (event.preventDefault) {
+ event.preventDefault();
+ event.stopPropagation();
+ } else {
+ event.returnValue = false;
+ event.cancelBubble = true;
+ }
+ },
+
+ // find the first node with the given tagName, starting from the
+ // node the event was triggered on; traverses the DOM upwards
+ findElement: function(event, tagName) {
+ var element = Event.element(event);
+ while (element.parentNode && (!element.tagName ||
+ (element.tagName.toUpperCase() != tagName.toUpperCase())))
+ element = element.parentNode;
+ return element;
+ },
+
+ observers: false,
+
+ _observeAndCache: function(element, name, observer, useCapture) {
+ if (!this.observers) this.observers = [];
+ if (element.addEventListener) {
+ this.observers.push([element, name, observer, useCapture]);
+ element.addEventListener(name, observer, useCapture);
+ } else if (element.attachEvent) {
+ this.observers.push([element, name, observer, useCapture]);
+ element.attachEvent('on' + name, observer);
+ }
+ },
+
+ unloadCache: function() {
+ if (!Event.observers) return;
+ for (var i = 0; i < Event.observers.length; i++) {
+ Event.stopObserving.apply(this, Event.observers[i]);
+ Event.observers[i][0] = null;
+ }
+ Event.observers = false;
+ },
+
+ observe: function(element, name, observer, useCapture) {
+ var element = $(element);
+ useCapture = useCapture || false;
+
+ if (name == 'keypress' &&
+ (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+ || element.attachEvent))
+ name = 'keydown';
+
+ this._observeAndCache(element, name, observer, useCapture);
+ },
+
+ stopObserving: function(element, name, observer, useCapture) {
+ var element = $(element);
+ useCapture = useCapture || false;
+
+ if (name == 'keypress' &&
+ (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+ || element.detachEvent))
+ name = 'keydown';
+
+ if (element.removeEventListener) {
+ element.removeEventListener(name, observer, useCapture);
+ } else if (element.detachEvent) {
+ element.detachEvent('on' + name, observer);
+ }
+ }
+});
+
+/* prevent memory leaks in IE */
+if (navigator.appVersion.match(/\bMSIE\b/))
+ Event.observe(window, 'unload', Event.unloadCache, false);
+var Position = {
+ // set to true if needed, warning: firefox performance problems
+ // NOT neeeded for page scrolling, only if draggable contained in
+ // scrollable elements
+ includeScrollOffsets: false,
+
+ // must be called before calling withinIncludingScrolloffset, every time the
+ // page is scrolled
+ prepare: function() {
+ this.deltaX = window.pageXOffset
+ || document.documentElement.scrollLeft
+ || document.body.scrollLeft
+ || 0;
+ this.deltaY = window.pageYOffset
+ || document.documentElement.scrollTop
+ || document.body.scrollTop
+ || 0;
+ },
+
+ realOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.scrollTop || 0;
+ valueL += element.scrollLeft || 0;
+ element = element.parentNode;
+ } while (element);
+ return [valueL, valueT];
+ },
+
+ cumulativeOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ } while (element);
+ return [valueL, valueT];
+ },
+
+ positionedOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ if (element) {
+ p = Element.getStyle(element, 'position');
+ if (p == 'relative' || p == 'absolute') break;
+ }
+ } while (element);
+ return [valueL, valueT];
+ },
+
+ offsetParent: function(element) {
+ if (element.offsetParent) return element.offsetParent;
+ if (element == document.body) return element;
+
+ while ((element = element.parentNode) && element != document.body)
+ if (Element.getStyle(element, 'position') != 'static')
+ return element;
+
+ return document.body;
+ },
+
+ // caches x/y coordinate pair to use with overlap
+ within: function(element, x, y) {
+ if (this.includeScrollOffsets)
+ return this.withinIncludingScrolloffsets(element, x, y);
+ this.xcomp = x;
+ this.ycomp = y;
+ this.offset = this.cumulativeOffset(element);
+
+ return (y >= this.offset[1] &&
+ y < this.offset[1] + element.offsetHeight &&
+ x >= this.offset[0] &&
+ x < this.offset[0] + element.offsetWidth);
+ },
+
+ withinIncludingScrolloffsets: function(element, x, y) {
+ var offsetcache = this.realOffset(element);
+
+ this.xcomp = x + offsetcache[0] - this.deltaX;
+ this.ycomp = y + offsetcache[1] - this.deltaY;
+ this.offset = this.cumulativeOffset(element);
+
+ return (this.ycomp >= this.offset[1] &&
+ this.ycomp < this.offset[1] + element.offsetHeight &&
+ this.xcomp >= this.offset[0] &&
+ this.xcomp < this.offset[0] + element.offsetWidth);
+ },
+
+ // within must be called directly before
+ overlap: function(mode, element) {
+ if (!mode) return 0;
+ if (mode == 'vertical')
+ return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+ element.offsetHeight;
+ if (mode == 'horizontal')
+ return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+ element.offsetWidth;
+ },
+
+ clone: function(source, target) {
+ source = $(source);
+ target = $(target);
+ target.style.position = 'absolute';
+ var offsets = this.cumulativeOffset(source);
+ target.style.top = offsets[1] + 'px';
+ target.style.left = offsets[0] + 'px';
+ target.style.width = source.offsetWidth + 'px';
+ target.style.height = source.offsetHeight + 'px';
+ },
+
+ page: function(forElement) {
+ var valueT = 0, valueL = 0;
+
+ var element = forElement;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+
+ // Safari fix
+ if (element.offsetParent==document.body)
+ if (Element.getStyle(element,'position')=='absolute') break;
+
+ } while (element = element.offsetParent);
+
+ element = forElement;
+ do {
+ valueT -= element.scrollTop || 0;
+ valueL -= element.scrollLeft || 0;
+ } while (element = element.parentNode);
+
+ return [valueL, valueT];
+ },
+
+ clone: function(source, target) {
+ var options = Object.extend({
+ setLeft: true,
+ setTop: true,
+ setWidth: true,
+ setHeight: true,
+ offsetTop: 0,
+ offsetLeft: 0
+ }, arguments[2] || {})
+
+ // find page position of source
+ source = $(source);
+ var p = Position.page(source);
+
+ // find coordinate system to use
+ target = $(target);
+ var delta = [0, 0];
+ var parent = null;
+ // delta [0,0] will do fine with position: fixed elements,
+ // position:absolute needs offsetParent deltas
+ if (Element.getStyle(target,'position') == 'absolute') {
+ parent = Position.offsetParent(target);
+ delta = Position.page(parent);
+ }
+
+ // correct by body offsets (fixes Safari)
+ if (parent == document.body) {
+ delta[0] -= document.body.offsetLeft;
+ delta[1] -= document.body.offsetTop;
+ }
+
+ // set position
+ if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px';
+ if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px';
+ if(options.setWidth) target.style.width = source.offsetWidth + 'px';
+ if(options.setHeight) target.style.height = source.offsetHeight + 'px';
+ },
+
+ absolutize: function(element) {
+ element = $(element);
+ if (element.style.position == 'absolute') return;
+ Position.prepare();
+
+ var offsets = Position.positionedOffset(element);
+ var top = offsets[1];
+ var left = offsets[0];
+ var width = element.clientWidth;
+ var height = element.clientHeight;
+
+ element._originalLeft = left - parseFloat(element.style.left || 0);
+ element._originalTop = top - parseFloat(element.style.top || 0);
+ element._originalWidth = element.style.width;
+ element._originalHeight = element.style.height;
+
+ element.style.position = 'absolute';
+ element.style.top = top + 'px';;
+ element.style.left = left + 'px';;
+ element.style.width = width + 'px';;
+ element.style.height = height + 'px';;
+ },
+
+ relativize: function(element) {
+ element = $(element);
+ if (element.style.position == 'relative') return;
+ Position.prepare();
+
+ element.style.position = 'relative';
+ var top = parseFloat(element.style.top || 0) - (element._originalTop || 0);
+ var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ element.style.height = element._originalHeight;
+ element.style.width = element._originalWidth;
+ }
+}
+
+// Safari returns margins on body which is incorrect if the child is absolutely
+// positioned. For performance reasons, redefine Position.cumulativeOffset for
+// KHTML/WebKit only.
+if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
+ Position.cumulativeOffset = function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ if (element.offsetParent == document.body)
+ if (Element.getStyle(element, 'position') == 'absolute') break;
+
+ element = element.offsetParent;
+ } while (element);
+
+ return [valueL, valueT];
+ }
+} \ No newline at end of file
diff --git a/cropper/lib/scriptaculous.js b/cropper/lib/scriptaculous.js
new file mode 100644
index 000000000..f61fc57f7
--- /dev/null
+++ b/cropper/lib/scriptaculous.js
@@ -0,0 +1,47 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var Scriptaculous = {
+ Version: '1.6.1',
+ require: function(libraryName) {
+ // inserting via DOM fails in Safari 2.0, so brute force approach
+ document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
+ },
+ load: function() {
+ if((typeof Prototype=='undefined') ||
+ (typeof Element == 'undefined') ||
+ (typeof Element.Methods=='undefined') ||
+ parseFloat(Prototype.Version.split(".")[0] + "." +
+ Prototype.Version.split(".")[1]) < 1.5)
+ throw("script.aculo.us requires the Prototype JavaScript framework >= 1.5.0");
+
+ $A(document.getElementsByTagName("script")).findAll( function(s) {
+ return (s.src && s.src.match(/scriptaculous\.js(\?.*)?$/))
+ }).each( function(s) {
+ var path = s.src.replace(/scriptaculous\.js(\?.*)?$/,'');
+ var includes = s.src.match(/\?.*load=([a-z,]*)/);
+ (includes ? includes[1] : 'builder,effects,dragdrop,controls,slider').split(',').each(
+ function(include) { Scriptaculous.require(path+include+'.js') });
+ });
+ }
+}
+
+Scriptaculous.load(); \ No newline at end of file
diff --git a/cropper/lib/slider.js b/cropper/lib/slider.js
new file mode 100644
index 000000000..c0f1fc01b
--- /dev/null
+++ b/cropper/lib/slider.js
@@ -0,0 +1,283 @@
+// Copyright (c) 2005 Marty Haught, Thomas Fuchs
+//
+// See http://script.aculo.us for more info
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+if(!Control) var Control = {};
+Control.Slider = Class.create();
+
+// options:
+// axis: 'vertical', or 'horizontal' (default)
+//
+// callbacks:
+// onChange(value)
+// onSlide(value)
+Control.Slider.prototype = {
+ initialize: function(handle, track, options) {
+ var slider = this;
+
+ if(handle instanceof Array) {
+ this.handles = handle.collect( function(e) { return $(e) });
+ } else {
+ this.handles = [$(handle)];
+ }
+
+ this.track = $(track);
+ this.options = options || {};
+
+ this.axis = this.options.axis || 'horizontal';
+ this.increment = this.options.increment || 1;
+ this.step = parseInt(this.options.step || '1');
+ this.range = this.options.range || $R(0,1);
+
+ this.value = 0; // assure backwards compat
+ this.values = this.handles.map( function() { return 0 });
+ this.spans = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false;
+ this.options.startSpan = $(this.options.startSpan || null);
+ this.options.endSpan = $(this.options.endSpan || null);
+
+ this.restricted = this.options.restricted || false;
+
+ this.maximum = this.options.maximum || this.range.end;
+ this.minimum = this.options.minimum || this.range.start;
+
+ // Will be used to align the handle onto the track, if necessary
+ this.alignX = parseInt(this.options.alignX || '0');
+ this.alignY = parseInt(this.options.alignY || '0');
+
+ this.trackLength = this.maximumOffset() - this.minimumOffset();
+ this.handleLength = this.isVertical() ? this.handles[0].offsetHeight : this.handles[0].offsetWidth;
+
+ this.active = false;
+ this.dragging = false;
+ this.disabled = false;
+
+ if(this.options.disabled) this.setDisabled();
+
+ // Allowed values array
+ this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false;
+ if(this.allowedValues) {
+ this.minimum = this.allowedValues.min();
+ this.maximum = this.allowedValues.max();
+ }
+
+ this.eventMouseDown = this.startDrag.bindAsEventListener(this);
+ this.eventMouseUp = this.endDrag.bindAsEventListener(this);
+ this.eventMouseMove = this.update.bindAsEventListener(this);
+
+ // Initialize handles in reverse (make sure first handle is active)
+ this.handles.each( function(h,i) {
+ i = slider.handles.length-1-i;
+ slider.setValue(parseFloat(
+ (slider.options.sliderValue instanceof Array ?
+ slider.options.sliderValue[i] : slider.options.sliderValue) ||
+ slider.range.start), i);
+ Element.makePositioned(h); // fix IE
+ Event.observe(h, "mousedown", slider.eventMouseDown);
+ });
+
+ Event.observe(this.track, "mousedown", this.eventMouseDown);
+ Event.observe(document, "mouseup", this.eventMouseUp);
+ Event.observe(document, "mousemove", this.eventMouseMove);
+
+ this.initialized = true;
+ },
+ dispose: function() {
+ var slider = this;
+ Event.stopObserving(this.track, "mousedown", this.eventMouseDown);
+ Event.stopObserving(document, "mouseup", this.eventMouseUp);
+ Event.stopObserving(document, "mousemove", this.eventMouseMove);
+ this.handles.each( function(h) {
+ Event.stopObserving(h, "mousedown", slider.eventMouseDown);
+ });
+ },
+ setDisabled: function(){
+ this.disabled = true;
+ },
+ setEnabled: function(){
+ this.disabled = false;
+ },
+ getNearestValue: function(value){
+ if(this.allowedValues){
+ if(value >= this.allowedValues.max()) return(this.allowedValues.max());
+ if(value <= this.allowedValues.min()) return(this.allowedValues.min());
+
+ var offset = Math.abs(this.allowedValues[0] - value);
+ var newValue = this.allowedValues[0];
+ this.allowedValues.each( function(v) {
+ var currentOffset = Math.abs(v - value);
+ if(currentOffset <= offset){
+ newValue = v;
+ offset = currentOffset;
+ }
+ });
+ return newValue;
+ }
+ if(value > this.range.end) return this.range.end;
+ if(value < this.range.start) return this.range.start;
+ return value;
+ },
+ setValue: function(sliderValue, handleIdx){
+ if(!this.active) {
+ this.activeHandle = this.handles[handleIdx];
+ this.activeHandleIdx = handleIdx;
+ this.updateStyles();
+ }
+ handleIdx = handleIdx || this.activeHandleIdx || 0;
+ if(this.initialized && this.restricted) {
+ if((handleIdx>0) && (sliderValue<this.values[handleIdx-1]))
+ sliderValue = this.values[handleIdx-1];
+ if((handleIdx < (this.handles.length-1)) && (sliderValue>this.values[handleIdx+1]))
+ sliderValue = this.values[handleIdx+1];
+ }
+ sliderValue = this.getNearestValue(sliderValue);
+ this.values[handleIdx] = sliderValue;
+ this.value = this.values[0]; // assure backwards compat
+
+ this.handles[handleIdx].style[this.isVertical() ? 'top' : 'left'] =
+ this.translateToPx(sliderValue);
+
+ this.drawSpans();
+ if(!this.dragging || !this.event) this.updateFinished();
+ },
+ setValueBy: function(delta, handleIdx) {
+ this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta,
+ handleIdx || this.activeHandleIdx || 0);
+ },
+ translateToPx: function(value) {
+ return Math.round(
+ ((this.trackLength-this.handleLength)/(this.range.end-this.range.start)) *
+ (value - this.range.start)) + "px";
+ },
+ translateToValue: function(offset) {
+ return ((offset/(this.trackLength-this.handleLength) *
+ (this.range.end-this.range.start)) + this.range.start);
+ },
+ getRange: function(range) {
+ var v = this.values.sortBy(Prototype.K);
+ range = range || 0;
+ return $R(v[range],v[range+1]);
+ },
+ minimumOffset: function(){
+ return(this.isVertical() ? this.alignY : this.alignX);
+ },
+ maximumOffset: function(){
+ return(this.isVertical() ?
+ this.track.offsetHeight - this.alignY : this.track.offsetWidth - this.alignX);
+ },
+ isVertical: function(){
+ return (this.axis == 'vertical');
+ },
+ drawSpans: function() {
+ var slider = this;
+ if(this.spans)
+ $R(0, this.spans.length-1).each(function(r) { slider.setSpan(slider.spans[r], slider.getRange(r)) });
+ if(this.options.startSpan)
+ this.setSpan(this.options.startSpan,
+ $R(0, this.values.length>1 ? this.getRange(0).min() : this.value ));
+ if(this.options.endSpan)
+ this.setSpan(this.options.endSpan,
+ $R(this.values.length>1 ? this.getRange(this.spans.length-1).max() : this.value, this.maximum));
+ },
+ setSpan: function(span, range) {
+ if(this.isVertical()) {
+ span.style.top = this.translateToPx(range.start);
+ span.style.height = this.translateToPx(range.end - range.start + this.range.start);
+ } else {
+ span.style.left = this.translateToPx(range.start);
+ span.style.width = this.translateToPx(range.end - range.start + this.range.start);
+ }
+ },
+ updateStyles: function() {
+ this.handles.each( function(h){ Element.removeClassName(h, 'selected') });
+ Element.addClassName(this.activeHandle, 'selected');
+ },
+ startDrag: function(event) {
+ if(Event.isLeftClick(event)) {
+ if(!this.disabled){
+ this.active = true;
+
+ var handle = Event.element(event);
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ if(handle==this.track) {
+ var offsets = Position.cumulativeOffset(this.track);
+ this.event = event;
+ this.setValue(this.translateToValue(
+ (this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0])-(this.handleLength/2)
+ ));
+ var offsets = Position.cumulativeOffset(this.activeHandle);
+ this.offsetX = (pointer[0] - offsets[0]);
+ this.offsetY = (pointer[1] - offsets[1]);
+ } else {
+ // find the handle (prevents issues with Safari)
+ while((this.handles.indexOf(handle) == -1) && handle.parentNode)
+ handle = handle.parentNode;
+
+ this.activeHandle = handle;
+ this.activeHandleIdx = this.handles.indexOf(this.activeHandle);
+ this.updateStyles();
+
+ var offsets = Position.cumulativeOffset(this.activeHandle);
+ this.offsetX = (pointer[0] - offsets[0]);
+ this.offsetY = (pointer[1] - offsets[1]);
+ }
+ }
+ Event.stop(event);
+ }
+ },
+ update: function(event) {
+ if(this.active) {
+ if(!this.dragging) this.dragging = true;
+ this.draw(event);
+ // fix AppleWebKit rendering
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+ Event.stop(event);
+ }
+ },
+ draw: function(event) {
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ var offsets = Position.cumulativeOffset(this.track);
+ pointer[0] -= this.offsetX + offsets[0];
+ pointer[1] -= this.offsetY + offsets[1];
+ this.event = event;
+ this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] ));
+ if(this.initialized && this.options.onSlide)
+ this.options.onSlide(this.values.length>1 ? this.values : this.value, this);
+ },
+ endDrag: function(event) {
+ if(this.active && this.dragging) {
+ this.finishDrag(event, true);
+ Event.stop(event);
+ }
+ this.active = false;
+ this.dragging = false;
+ },
+ finishDrag: function(event, success) {
+ this.active = false;
+ this.dragging = false;
+ this.updateFinished();
+ },
+ updateFinished: function() {
+ if(this.initialized && this.options.onChange)
+ this.options.onChange(this.values.length>1 ? this.values : this.value, this);
+ this.event = null;
+ }
+} \ No newline at end of file
diff --git a/cropper/lib/unittest.js b/cropper/lib/unittest.js
new file mode 100644
index 000000000..d2c2d8171
--- /dev/null
+++ b/cropper/lib/unittest.js
@@ -0,0 +1,383 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005 Jon Tirsen (http://www.tirsen.com)
+// (c) 2005 Michael Schuerig (http://www.schuerig.de/michael/)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+// experimental, Firefox-only
+Event.simulateMouse = function(element, eventName) {
+ var options = Object.extend({
+ pointerX: 0,
+ pointerY: 0,
+ buttons: 0
+ }, arguments[2] || {});
+ var oEvent = document.createEvent("MouseEvents");
+ oEvent.initMouseEvent(eventName, true, true, document.defaultView,
+ options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY,
+ false, false, false, false, 0, $(element));
+
+ if(this.mark) Element.remove(this.mark);
+ this.mark = document.createElement('div');
+ this.mark.appendChild(document.createTextNode(" "));
+ document.body.appendChild(this.mark);
+ this.mark.style.position = 'absolute';
+ this.mark.style.top = options.pointerY + "px";
+ this.mark.style.left = options.pointerX + "px";
+ this.mark.style.width = "5px";
+ this.mark.style.height = "5px;";
+ this.mark.style.borderTop = "1px solid red;"
+ this.mark.style.borderLeft = "1px solid red;"
+
+ if(this.step)
+ alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options));
+
+ $(element).dispatchEvent(oEvent);
+};
+
+// Note: Due to a fix in Firefox 1.0.5/6 that probably fixed "too much", this doesn't work in 1.0.6 or DP2.
+// You need to downgrade to 1.0.4 for now to get this working
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much
+Event.simulateKey = function(element, eventName) {
+ var options = Object.extend({
+ ctrlKey: false,
+ altKey: false,
+ shiftKey: false,
+ metaKey: false,
+ keyCode: 0,
+ charCode: 0
+ }, arguments[2] || {});
+
+ var oEvent = document.createEvent("KeyEvents");
+ oEvent.initKeyEvent(eventName, true, true, window,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
+ options.keyCode, options.charCode );
+ $(element).dispatchEvent(oEvent);
+};
+
+Event.simulateKeys = function(element, command) {
+ for(var i=0; i<command.length; i++) {
+ Event.simulateKey(element,'keypress',{charCode:command.charCodeAt(i)});
+ }
+};
+
+var Test = {}
+Test.Unit = {};
+
+// security exception workaround
+Test.Unit.inspect = Object.inspect;
+
+Test.Unit.Logger = Class.create();
+Test.Unit.Logger.prototype = {
+ initialize: function(log) {
+ this.log = $(log);
+ if (this.log) {
+ this._createLogTable();
+ }
+ },
+ start: function(testName) {
+ if (!this.log) return;
+ this.testName = testName;
+ this.lastLogLine = document.createElement('tr');
+ this.statusCell = document.createElement('td');
+ this.nameCell = document.createElement('td');
+ this.nameCell.appendChild(document.createTextNode(testName));
+ this.messageCell = document.createElement('td');
+ this.lastLogLine.appendChild(this.statusCell);
+ this.lastLogLine.appendChild(this.nameCell);
+ this.lastLogLine.appendChild(this.messageCell);
+ this.loglines.appendChild(this.lastLogLine);
+ },
+ finish: function(status, summary) {
+ if (!this.log) return;
+ this.lastLogLine.className = status;
+ this.statusCell.innerHTML = status;
+ this.messageCell.innerHTML = this._toHTML(summary);
+ },
+ message: function(message) {
+ if (!this.log) return;
+ this.messageCell.innerHTML = this._toHTML(message);
+ },
+ summary: function(summary) {
+ if (!this.log) return;
+ this.logsummary.innerHTML = this._toHTML(summary);
+ },
+ _createLogTable: function() {
+ this.log.innerHTML =
+ '<div id="logsummary"></div>' +
+ '<table id="logtable">' +
+ '<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' +
+ '<tbody id="loglines"></tbody>' +
+ '</table>';
+ this.logsummary = $('logsummary')
+ this.loglines = $('loglines');
+ },
+ _toHTML: function(txt) {
+ return txt.escapeHTML().replace(/\n/g,"<br/>");
+ }
+}
+
+Test.Unit.Runner = Class.create();
+Test.Unit.Runner.prototype = {
+ initialize: function(testcases) {
+ this.options = Object.extend({
+ testLog: 'testlog'
+ }, arguments[1] || {});
+ this.options.resultsURL = this.parseResultsURLQueryParameter();
+ if (this.options.testLog) {
+ this.options.testLog = $(this.options.testLog) || null;
+ }
+ if(this.options.tests) {
+ this.tests = [];
+ for(var i = 0; i < this.options.tests.length; i++) {
+ if(/^test/.test(this.options.tests[i])) {
+ this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"]));
+ }
+ }
+ } else {
+ if (this.options.test) {
+ this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])];
+ } else {
+ this.tests = [];
+ for(var testcase in testcases) {
+ if(/^test/.test(testcase)) {
+ this.tests.push(new Test.Unit.Testcase(testcase, testcases[testcase], testcases["setup"], testcases["teardown"]));
+ }
+ }
+ }
+ }
+ this.currentTest = 0;
+ this.logger = new Test.Unit.Logger(this.options.testLog);
+ setTimeout(this.runTests.bind(this), 1000);
+ },
+ parseResultsURLQueryParameter: function() {
+ return window.location.search.parseQuery()["resultsURL"];
+ },
+ // Returns:
+ // "ERROR" if there was an error,
+ // "FAILURE" if there was a failure, or
+ // "SUCCESS" if there was neither
+ getResult: function() {
+ var hasFailure = false;
+ for(var i=0;i<this.tests.length;i++) {
+ if (this.tests[i].errors > 0) {
+ return "ERROR";
+ }
+ if (this.tests[i].failures > 0) {
+ hasFailure = true;
+ }
+ }
+ if (hasFailure) {
+ return "FAILURE";
+ } else {
+ return "SUCCESS";
+ }
+ },
+ postResults: function() {
+ if (this.options.resultsURL) {
+ new Ajax.Request(this.options.resultsURL,
+ { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false });
+ }
+ },
+ runTests: function() {
+ var test = this.tests[this.currentTest];
+ if (!test) {
+ // finished!
+ this.postResults();
+ this.logger.summary(this.summary());
+ return;
+ }
+ if(!test.isWaiting) {
+ this.logger.start(test.name);
+ }
+ test.run();
+ if(test.isWaiting) {
+ this.logger.message("Waiting for " + test.timeToWait + "ms");
+ setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
+ } else {
+ this.logger.finish(test.status(), test.summary());
+ this.currentTest++;
+ // tail recursive, hopefully the browser will skip the stackframe
+ this.runTests();
+ }
+ },
+ summary: function() {
+ var assertions = 0;
+ var failures = 0;
+ var errors = 0;
+ var messages = [];
+ for(var i=0;i<this.tests.length;i++) {
+ assertions += this.tests[i].assertions;
+ failures += this.tests[i].failures;
+ errors += this.tests[i].errors;
+ }
+ return (
+ this.tests.length + " tests, " +
+ assertions + " assertions, " +
+ failures + " failures, " +
+ errors + " errors");
+ }
+}
+
+Test.Unit.Assertions = Class.create();
+Test.Unit.Assertions.prototype = {
+ initialize: function() {
+ this.assertions = 0;
+ this.failures = 0;
+ this.errors = 0;
+ this.messages = [];
+ },
+ summary: function() {
+ return (
+ this.assertions + " assertions, " +
+ this.failures + " failures, " +
+ this.errors + " errors" + "\n" +
+ this.messages.join("\n"));
+ },
+ pass: function() {
+ this.assertions++;
+ },
+ fail: function(message) {
+ this.failures++;
+ this.messages.push("Failure: " + message);
+ },
+ info: function(message) {
+ this.messages.push("Info: " + message);
+ },
+ error: function(error) {
+ this.errors++;
+ this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) +")");
+ },
+ status: function() {
+ if (this.failures > 0) return 'failed';
+ if (this.errors > 0) return 'error';
+ return 'passed';
+ },
+ assert: function(expression) {
+ var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"';
+ try { expression ? this.pass() :
+ this.fail(message); }
+ catch(e) { this.error(e); }
+ },
+ assertEqual: function(expected, actual) {
+ var message = arguments[2] || "assertEqual";
+ try { (expected == actual) ? this.pass() :
+ this.fail(message + ': expected "' + Test.Unit.inspect(expected) +
+ '", actual "' + Test.Unit.inspect(actual) + '"'); }
+ catch(e) { this.error(e); }
+ },
+ assertEnumEqual: function(expected, actual) {
+ var message = arguments[2] || "assertEnumEqual";
+ try { $A(expected).length == $A(actual).length &&
+ expected.zip(actual).all(function(pair) { return pair[0] == pair[1] }) ?
+ this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) +
+ ', actual ' + Test.Unit.inspect(actual)); }
+ catch(e) { this.error(e); }
+ },
+ assertNotEqual: function(expected, actual) {
+ var message = arguments[2] || "assertNotEqual";
+ try { (expected != actual) ? this.pass() :
+ this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); }
+ catch(e) { this.error(e); }
+ },
+ assertNull: function(obj) {
+ var message = arguments[1] || 'assertNull'
+ try { (obj==null) ? this.pass() :
+ this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
+ catch(e) { this.error(e); }
+ },
+ assertHidden: function(element) {
+ var message = arguments[1] || 'assertHidden';
+ this.assertEqual("none", element.style.display, message);
+ },
+ assertNotNull: function(object) {
+ var message = arguments[1] || 'assertNotNull';
+ this.assert(object != null, message);
+ },
+ assertInstanceOf: function(expected, actual) {
+ var message = arguments[2] || 'assertInstanceOf';
+ try {
+ (actual instanceof expected) ? this.pass() :
+ this.fail(message + ": object was not an instance of the expected type"); }
+ catch(e) { this.error(e); }
+ },
+ assertNotInstanceOf: function(expected, actual) {
+ var message = arguments[2] || 'assertNotInstanceOf';
+ try {
+ !(actual instanceof expected) ? this.pass() :
+ this.fail(message + ": object was an instance of the not expected type"); }
+ catch(e) { this.error(e); }
+ },
+ _isVisible: function(element) {
+ element = $(element);
+ if(!element.parentNode) return true;
+ this.assertNotNull(element);
+ if(element.style && Element.getStyle(element, 'display') == 'none')
+ return false;
+
+ return this._isVisible(element.parentNode);
+ },
+ assertNotVisible: function(element) {
+ this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1]));
+ },
+ assertVisible: function(element) {
+ this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1]));
+ },
+ benchmark: function(operation, iterations) {
+ var startAt = new Date();
+ (iterations || 1).times(operation);
+ var timeTaken = ((new Date())-startAt);
+ this.info((arguments[2] || 'Operation') + ' finished ' +
+ iterations + ' iterations in ' + (timeTaken/1000)+'s' );
+ return timeTaken;
+ }
+}
+
+Test.Unit.Testcase = Class.create();
+Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), {
+ initialize: function(name, test, setup, teardown) {
+ Test.Unit.Assertions.prototype.initialize.bind(this)();
+ this.name = name;
+ this.test = test || function() {};
+ this.setup = setup || function() {};
+ this.teardown = teardown || function() {};
+ this.isWaiting = false;
+ this.timeToWait = 1000;
+ },
+ wait: function(time, nextPart) {
+ this.isWaiting = true;
+ this.test = nextPart;
+ this.timeToWait = time;
+ },
+ run: function() {
+ try {
+ try {
+ if (!this.isWaiting) this.setup.bind(this)();
+ this.isWaiting = false;
+ this.test.bind(this)();
+ } finally {
+ if(!this.isWaiting) {
+ this.teardown.bind(this)();
+ }
+ }
+ }
+ catch(e) { this.error(e); }
+ }
+});
diff --git a/cropper/licence.txt b/cropper/licence.txt
new file mode 100644
index 000000000..b59e02917
--- /dev/null
+++ b/cropper/licence.txt
@@ -0,0 +1,12 @@
+Copyright (c) 2006, David Spurr (www.defusion.org.uk)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ * 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.
+ * 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.
+
+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.
+
+http://www.opensource.org/licenses/bsd-license.php \ No newline at end of file
diff --git a/cropper/marqueeHoriz.gif b/cropper/marqueeHoriz.gif
new file mode 100644
index 000000000..25317e573
--- /dev/null
+++ b/cropper/marqueeHoriz.gif
Binary files differ
diff --git a/cropper/marqueeVert.gif b/cropper/marqueeVert.gif
new file mode 100644
index 000000000..354070bb4
--- /dev/null
+++ b/cropper/marqueeVert.gif
Binary files differ
diff --git a/cropper/tests/castle.jpg b/cropper/tests/castle.jpg
new file mode 100644
index 000000000..e40b7e4cb
--- /dev/null
+++ b/cropper/tests/castle.jpg
Binary files differ
diff --git a/cropper/tests/castleMed.jpg b/cropper/tests/castleMed.jpg
new file mode 100644
index 000000000..c35a6f554
--- /dev/null
+++ b/cropper/tests/castleMed.jpg
Binary files differ
diff --git a/cropper/tests/example-Basic.htm b/cropper/tests/example-Basic.htm
new file mode 100644
index 000000000..391c2ecdb
--- /dev/null
+++ b/cropper/tests/example-Basic.htm
@@ -0,0 +1,106 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="Content-Language" content="en-us" />
+ <title>Basic cropper test</title>
+ <script src="../lib/prototype.js" type="text/javascript"></script>
+ <script src="../lib/scriptaculous.js?load=builder,dragdrop" type="text/javascript"></script>
+ <script src="../cropper.js" type="text/javascript"></script>
+
+
+ <script type="text/javascript" charset="utf-8">
+
+ // setup the callback function
+ function onEndCrop( coords, dimensions ) {
+ $( 'x1' ).value = coords.x1;
+ $( 'y1' ).value = coords.y1;
+ $( 'x2' ).value = coords.x2;
+ $( 'y2' ).value = coords.y2;
+ $( 'width' ).value = dimensions.width;
+ $( 'height' ).value = dimensions.height;
+ }
+
+ // basic example
+ Event.observe(
+ window,
+ 'load',
+ function() {
+ new Cropper.Img(
+ 'testImage',
+ {
+ onEndCrop: onEndCrop
+ }
+ )
+ }
+ );
+
+
+ if( typeof(dump) != 'function' ) {
+ Debug.init(true, '/');
+
+ function dump( msg ) {
+ Debug.raise( msg );
+ };
+ } else dump( '---------------------------------------\n' );
+
+
+ </script>
+ <link rel="stylesheet" type="text/css" href="debug.css" media="all" />
+ <style type="text/css">
+ label {
+ clear: left;
+ margin-left: 50px;
+ float: left;
+ width: 5em;
+ }
+
+ html, body {
+ margin: 0;
+ }
+
+ #testWrap {
+ margin: 20px 0 0 50px; /* Just while testing, to make sure we return the correct positions for the image & not the window */
+ }
+ </style>
+</head>
+<body>
+ <h2>Basic cropper test</h2>
+ <p>
+ Some test content before the image
+ </p>
+
+ <div id="testWrap">
+ <img src="castle.jpg" alt="test image" id="testImage" width="500" height="333" />
+ </div>
+
+
+ <p>
+ <label for="x1">x1:</label>
+ <input type="text" name="x1" id="x1" />
+ </p>
+ <p>
+ <label for="y1">y1:</label>
+ <input type="text" name="y1" id="y1" />
+ </p>
+ <p>
+ <label for="x2">x2:</label>
+ <input type="text" name="x2" id="x2" />
+ </p>
+ <p>
+ <label for="y2">y2:</label>
+ <input type="text" name="y2" id="y2" />
+ </p>
+ <p>
+ <label for="width">width:</label>
+ <input type="text" name="width" id="width" />
+ </p>
+ <p>
+ <label for="height">height</label>
+ <input type="text" name="height" id="height" />
+ </p>
+
+</body>
+</html>
+
+
diff --git a/cropper/tests/example-CSS-Absolute.htm b/cropper/tests/example-CSS-Absolute.htm
new file mode 100644
index 000000000..17e4c489d
--- /dev/null
+++ b/cropper/tests/example-CSS-Absolute.htm
@@ -0,0 +1,162 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="Content-Language" content="en-us" />
+ <title>CSS - Absolute positioned (and draggable) test</title>
+ <script src="../lib/prototype.js" type="text/javascript"></script>
+ <script src="../lib/scriptaculous.js?load=builder,dragdrop,effects" type="text/javascript"></script>
+ <script src="../cropper.js" type="text/javascript"></script>
+
+
+ <script type="text/javascript" charset="utf-8">
+
+ // setup the callback function
+ function onEndCrop( coords, dimensions ) {
+ $( 'x1' ).value = coords.x1;
+ $( 'y1' ).value = coords.y1;
+ $( 'x2' ).value = coords.x2;
+ $( 'y2' ).value = coords.y2;
+ $( 'width' ).value = dimensions.width;
+ $( 'height' ).value = dimensions.height;
+ }
+
+ // Absolute positioned example
+ Event.observe(
+ window,
+ 'load',
+ function() {
+ new Cropper.Img( 'testAbsImage', { onEndCrop: onEndCrop } );
+ new Draggable( 'test-abs' );
+ }
+ );
+
+ if( typeof(dump) != 'function' ) {
+ Debug.init(true, '/');
+
+ function dump( msg ) {
+ Debug.raise( msg );
+ };
+ } else dump( '---------------------------------------\n' );
+
+
+ </script>
+ <link rel="stylesheet" type="text/css" href="debug.css" media="all" />
+ <style type="text/css">
+ label {
+ clear: left;
+ margin-left: 50px;
+ float: left;
+ width: 5em;
+ }
+
+ html, body {
+ margin: 0;
+ }
+
+ #testWrap {
+ margin: 20px 0 0 50px; /* Just while testing, to make sure we return the correct positions for the image & not the window */
+ }
+
+ #test-abs {
+ width: 510px;
+ position: absolute;
+ top: 50px;
+ left: 25%;
+ background-color: #dee;
+ border: 3px solid #ccc;
+ z-index: 10;
+ }
+ </style>
+</head>
+<body>
+ <h2>CSS - Absolute positioned (and draggable) test</h2>
+ <p>
+ Some test content before the image
+ </p>
+ <p>
+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Pellentesque consequat risus cursus ipsum. Etiam libero. Integer vel mauris. Donec vulputate. In ut augue vitae nibh lobortis tempor. Aliquam hendrerit quam. Phasellus sed orci. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Ut sed urna. Donec nunc urna, porttitor a, feugiat pellentesque, varius id, justo. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla facilisi. Sed sollicitudin. Integer enim. Aenean sollicitudin.
+ </p>
+ <p>
+ Integer lorem turpis, dapibus sed, vulputate nec, volutpat a, sem. Sed malesuada laoreet lorem. Duis mauris ipsum, fringilla nec, tristique vel, imperdiet vel, neque. Nulla vel purus. Fusce non lectus. Mauris pulvinar. Curabitur eget eros. Nunc ultrices, risus vitae adipiscing scelerisque, quam mi auctor lacus, non pellentesque augue sapien a magna. Etiam rutrum posuere tortor. Mauris rhoncus sagittis dolor. Donec sed quam. Vivamus vel diam id massa adipiscing bibendum. Suspendisse potenti. Integer arcu est, adipiscing sit amet, convallis eu, sollicitudin tincidunt, quam.
+ </p>
+ <p>
+ Etiam ligula lorem, imperdiet ac, luctus eget, ultrices at, odio. Vivamus malesuada, justo eu adipiscing semper, nisi dui tempus magna, quis ultrices nunc tellus id massa. Nullam lobortis auctor sapien. Quisque non nulla. Donec lobortis pellentesque nisl. Sed lacus sapien, viverra vitae, blandit ut, fermentum quis, leo. Morbi augue turpis, hendrerit non, feugiat vel, laoreet sed, est. Nunc velit. Praesent lobortis. Integer enim. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Curabitur faucibus lacus ac ante. Donec odio odio, tincidunt a, egestas nec, scelerisque nec, dui. Cras sollicitudin. Donec lacus enim, mollis sit amet, interdum quis, euismod et, nulla. Nunc sit amet dui eu magna dapibus mollis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nulla facilisi.
+ </p>
+ <p>
+ In hac habitasse platea dictumst. Nunc neque urna, dapibus ut, tristique ut, bibendum ac, felis. Donec dictum est ut dolor. Etiam accumsan, velit sit amet blandit vestibulum, turpis quam hendrerit risus, vel interdum eros orci in nunc. Curabitur tellus sapien, rutrum ac, euismod ac, malesuada nec, pede. Proin sit amet ipsum. Praesent quam nisl, adipiscing nec, tristique eget, fermentum sed, est. Praesent ac est sit amet orci facilisis placerat. Sed consequat, est sit amet consectetuer viverra, risus urna porttitor tellus, ut convallis nibh libero in lectus. Pellentesque molestie, erat non vehicula pretium, turpis nisi eleifend eros, sed scelerisque tortor odio non tellus. Nunc leo tellus, faucibus vitae, placerat a, accumsan vel, arcu. In et orci. Ut tristique euismod nibh. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Sed nulla nunc, placerat vitae, pellentesque non, interdum non, sapien. Quisque faucibus, eros sed venenatis sagittis, leo risus rhoncus risus, in pretium sem purus a lacus. Aliquam aliquam leo et diam.
+
+ </p>
+ <p>
+ Nulla sagittis diam. Phasellus vitae enim tristique libero molestie tristique. Nam mauris sem, elementum nec, cursus in, fringilla ac, neque. Nunc metus nisi, dictum vel, vulputate quis, porttitor bibendum, tortor. Vestibulum vehicula. Nulla facilisi. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla ac magna sed purus ultricies euismod. Aliquam dictum. Sed mauris. Suspendisse justo. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi purus lorem, auctor non, porta ac, vehicula vel, orci. Morbi pharetra massa nec leo. Maecenas et mauris. Aliquam porttitor tincidunt nulla. Vestibulum pede.
+ </p>
+ <p>
+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Pellentesque consequat risus cursus ipsum. Etiam libero. Integer vel mauris. Donec vulputate. In ut augue vitae nibh lobortis tempor. Aliquam hendrerit quam. Phasellus sed orci. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Ut sed urna. Donec nunc urna, porttitor a, feugiat pellentesque, varius id, justo. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla facilisi. Sed sollicitudin. Integer enim. Aenean sollicitudin.
+ </p>
+ <p>
+ Integer lorem turpis, dapibus sed, vulputate nec, volutpat a, sem. Sed malesuada laoreet lorem. Duis mauris ipsum, fringilla nec, tristique vel, imperdiet vel, neque. Nulla vel purus. Fusce non lectus. Mauris pulvinar. Curabitur eget eros. Nunc ultrices, risus vitae adipiscing scelerisque, quam mi auctor lacus, non pellentesque augue sapien a magna. Etiam rutrum posuere tortor. Mauris rhoncus sagittis dolor. Donec sed quam. Vivamus vel diam id massa adipiscing bibendum. Suspendisse potenti. Integer arcu est, adipiscing sit amet, convallis eu, sollicitudin tincidunt, quam.
+ </p>
+ <p>
+ Etiam ligula lorem, imperdiet ac, luctus eget, ultrices at, odio. Vivamus malesuada, justo eu adipiscing semper, nisi dui tempus magna, quis ultrices nunc tellus id massa. Nullam lobortis auctor sapien. Quisque non nulla. Donec lobortis pellentesque nisl. Sed lacus sapien, viverra vitae, blandit ut, fermentum quis, leo. Morbi augue turpis, hendrerit non, feugiat vel, laoreet sed, est. Nunc velit. Praesent lobortis. Integer enim. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Curabitur faucibus lacus ac ante. Donec odio odio, tincidunt a, egestas nec, scelerisque nec, dui. Cras sollicitudin. Donec lacus enim, mollis sit amet, interdum quis, euismod et, nulla. Nunc sit amet dui eu magna dapibus mollis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nulla facilisi.
+ </p>
+ <p>
+ In hac habitasse platea dictumst. Nunc neque urna, dapibus ut, tristique ut, bibendum ac, felis. Donec dictum est ut dolor. Etiam accumsan, velit sit amet blandit vestibulum, turpis quam hendrerit risus, vel interdum eros orci in nunc. Curabitur tellus sapien, rutrum ac, euismod ac, malesuada nec, pede. Proin sit amet ipsum. Praesent quam nisl, adipiscing nec, tristique eget, fermentum sed, est. Praesent ac est sit amet orci facilisis placerat. Sed consequat, est sit amet consectetuer viverra, risus urna porttitor tellus, ut convallis nibh libero in lectus. Pellentesque molestie, erat non vehicula pretium, turpis nisi eleifend eros, sed scelerisque tortor odio non tellus. Nunc leo tellus, faucibus vitae, placerat a, accumsan vel, arcu. In et orci. Ut tristique euismod nibh. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Sed nulla nunc, placerat vitae, pellentesque non, interdum non, sapien. Quisque faucibus, eros sed venenatis sagittis, leo risus rhoncus risus, in pretium sem purus a lacus. Aliquam aliquam leo et diam.
+
+ </p>
+ <p>
+ Nulla sagittis diam. Phasellus vitae enim tristique libero molestie tristique. Nam mauris sem, elementum nec, cursus in, fringilla ac, neque. Nunc metus nisi, dictum vel, vulputate quis, porttitor bibendum, tortor. Vestibulum vehicula. Nulla facilisi. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla ac magna sed purus ultricies euismod. Aliquam dictum. Sed mauris. Suspendisse justo. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi purus lorem, auctor non, porta ac, vehicula vel, orci. Morbi pharetra massa nec leo. Maecenas et mauris. Aliquam porttitor tincidunt nulla. Vestibulum pede.
+ </p>
+ <p>
+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Pellentesque consequat risus cursus ipsum. Etiam libero. Integer vel mauris. Donec vulputate. In ut augue vitae nibh lobortis tempor. Aliquam hendrerit quam. Phasellus sed orci. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Ut sed urna. Donec nunc urna, porttitor a, feugiat pellentesque, varius id, justo. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla facilisi. Sed sollicitudin. Integer enim. Aenean sollicitudin.
+ </p>
+ <p>
+ Integer lorem turpis, dapibus sed, vulputate nec, volutpat a, sem. Sed malesuada laoreet lorem. Duis mauris ipsum, fringilla nec, tristique vel, imperdiet vel, neque. Nulla vel purus. Fusce non lectus. Mauris pulvinar. Curabitur eget eros. Nunc ultrices, risus vitae adipiscing scelerisque, quam mi auctor lacus, non pellentesque augue sapien a magna. Etiam rutrum posuere tortor. Mauris rhoncus sagittis dolor. Donec sed quam. Vivamus vel diam id massa adipiscing bibendum. Suspendisse potenti. Integer arcu est, adipiscing sit amet, convallis eu, sollicitudin tincidunt, quam.
+ </p>
+ <p>
+ Etiam ligula lorem, imperdiet ac, luctus eget, ultrices at, odio. Vivamus malesuada, justo eu adipiscing semper, nisi dui tempus magna, quis ultrices nunc tellus id massa. Nullam lobortis auctor sapien. Quisque non nulla. Donec lobortis pellentesque nisl. Sed lacus sapien, viverra vitae, blandit ut, fermentum quis, leo. Morbi augue turpis, hendrerit non, feugiat vel, laoreet sed, est. Nunc velit. Praesent lobortis. Integer enim. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Curabitur faucibus lacus ac ante. Donec odio odio, tincidunt a, egestas nec, scelerisque nec, dui. Cras sollicitudin. Donec lacus enim, mollis sit amet, interdum quis, euismod et, nulla. Nunc sit amet dui eu magna dapibus mollis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nulla facilisi.
+ </p>
+ <p>
+ In hac habitasse platea dictumst. Nunc neque urna, dapibus ut, tristique ut, bibendum ac, felis. Donec dictum est ut dolor. Etiam accumsan, velit sit amet blandit vestibulum, turpis quam hendrerit risus, vel interdum eros orci in nunc. Curabitur tellus sapien, rutrum ac, euismod ac, malesuada nec, pede. Proin sit amet ipsum. Praesent quam nisl, adipiscing nec, tristique eget, fermentum sed, est. Praesent ac est sit amet orci facilisis placerat. Sed consequat, est sit amet consectetuer viverra, risus urna porttitor tellus, ut convallis nibh libero in lectus. Pellentesque molestie, erat non vehicula pretium, turpis nisi eleifend eros, sed scelerisque tortor odio non tellus. Nunc leo tellus, faucibus vitae, placerat a, accumsan vel, arcu. In et orci. Ut tristique euismod nibh. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Sed nulla nunc, placerat vitae, pellentesque non, interdum non, sapien. Quisque faucibus, eros sed venenatis sagittis, leo risus rhoncus risus, in pretium sem purus a lacus. Aliquam aliquam leo et diam.
+
+ </p>
+ <p>
+ Nulla sagittis diam. Phasellus vitae enim tristique libero molestie tristique. Nam mauris sem, elementum nec, cursus in, fringilla ac, neque. Nunc metus nisi, dictum vel, vulputate quis, porttitor bibendum, tortor. Vestibulum vehicula. Nulla facilisi. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla ac magna sed purus ultricies euismod. Aliquam dictum. Sed mauris. Suspendisse justo. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi purus lorem, auctor non, porta ac, vehicula vel, orci. Morbi pharetra massa nec leo. Maecenas et mauris. Aliquam porttitor tincidunt nulla. Vestibulum pede.
+ </p>
+
+
+<div id="test-abs">
+ <h2>Absolute test</h2>
+ <div id="testAbsWrap">
+ <img src="castle.jpg" alt="test image" id="testAbsImage" width="500" height="333" />
+ </div>
+
+ <p>
+ <label for="x1">x1:</label>
+ <input type="text" name="x1" id="x1" />
+ </p>
+ <p>
+ <label for="y1">y1:</label>
+ <input type="text" name="y1" id="y1" />
+ </p>
+ <p>
+ <label for="x2">x2:</label>
+ <input type="text" name="x2" id="x2" />
+ </p>
+ <p>
+ <label for="y2">y2:</label>
+ <input type="text" name="y2" id="y2" />
+ </p>
+ <p>
+ <label for="width">width:</label>
+ <input type="text" name="width" id="width" />
+ </p>
+ <p>
+ <label for="height">height</label>
+ <input type="text" name="height" id="height" />
+ </p>
+</div>
+
+</body>
+</html>
+
+
diff --git a/cropper/tests/example-CSS-Float.htm b/cropper/tests/example-CSS-Float.htm
new file mode 100644
index 000000000..5066553fe
--- /dev/null
+++ b/cropper/tests/example-CSS-Float.htm
@@ -0,0 +1,124 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="Content-Language" content="en-us" />
+ <title>CSS - Float test</title>
+ <script src="../lib/prototype.js" type="text/javascript"></script>
+ <script src="../lib/scriptaculous.js?load=builder,dragdrop" type="text/javascript"></script>
+ <script src="../cropper.js" type="text/javascript"></script>
+
+
+ <script type="text/javascript" charset="utf-8">
+
+ // setup the callback function
+ function onEndCrop( coords, dimensions ) {
+ $( 'x1' ).value = coords.x1;
+ $( 'y1' ).value = coords.y1;
+ $( 'x2' ).value = coords.x2;
+ $( 'y2' ).value = coords.y2;
+ $( 'width' ).value = dimensions.width;
+ $( 'height' ).value = dimensions.height;
+ }
+
+ // float example
+ Event.observe(
+ window,
+ 'load',
+ function() {
+ new Cropper.Img(
+ 'testFloatImage',
+ {
+ onEndCrop: function( coords, dimensions ) {
+ $( 'floatX1' ).value = coords.x1;
+ $( 'floatY1' ).value = coords.y1;
+ $( 'floatX2' ).value = coords.x2;
+ $( 'floatY2' ).value = coords.y2;
+ $( 'floatWidth' ).value = dimensions.width;
+ $( 'floatHeight' ).value = dimensions.height;
+ }
+ }
+ );
+ }
+ );
+
+ if( typeof(dump) != 'function' ) {
+ Debug.init(true, '/');
+
+ function dump( msg ) {
+ Debug.raise( msg );
+ };
+ } else dump( '---------------------------------------\n' );
+
+
+ </script>
+ <link rel="stylesheet" type="text/css" href="debug.css" media="all" />
+ <style type="text/css">
+ label {
+ clear: left;
+ margin-left: 50px;
+ float: left;
+ width: 5em;
+ }
+
+ html, body {
+ margin: 0;
+ }
+
+ #testWrap {
+ margin: 20px 0 0 50px; /* Just while testing, to make sure we return the correct positions for the image & not the window */
+ }
+
+ #test-float {
+ width: 600px;
+ float: right;
+ background-color: #eee;
+ border: 3px solid #000;
+ margin: 10px;
+ padding: 5px;
+ }
+
+ </style>
+</head>
+<body>
+ <h2>Test page with floating wrapper</h2>
+ <p>
+ Some test content before the image
+ </p>
+
+<div id="test-float">
+ <h2>Float test</h2>
+ <div id="testFloatWrap">
+ <img src="castle.jpg" alt="test image" id="testFloatImage" width="500" height="333" />
+ </div>
+
+ <p>
+ <label for="floatX1">x1:</label>
+ <input type="text" name="floatX1" id="floatX1" />
+ </p>
+ <p>
+ <label for="floatY1">y1:</label>
+ <input type="text" name="floatY1" id="floatY1" />
+ </p>
+ <p>
+ <label for="floatX2">x2:</label>
+ <input type="text" name="floatX2" id="floatX2" />
+ </p>
+ <p>
+ <label for="floatY2">y2:</label>
+ <input type="text" name="floatY2" id="floatY2" />
+ </p>
+ <p>
+ <label for="floatWidth">width:</label>
+ <input type="text" name="floatWidth" id="floatWidth" />
+ </p>
+ <p>
+ <label for="floatHeight">height</label>
+ <input type="text" name="floatHeight" id="floatHeight" />
+ </p>
+</div>
+
+</body>
+</html>
+
+
diff --git a/cropper/tests/example-CSS-Relative.htm b/cropper/tests/example-CSS-Relative.htm
new file mode 100644
index 000000000..5894fe1f9
--- /dev/null
+++ b/cropper/tests/example-CSS-Relative.htm
@@ -0,0 +1,116 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="Content-Language" content="en-us" />
+ <title>CSS - Relative test</title>
+ <script src="../lib/prototype.js" type="text/javascript"></script>
+ <script src="../lib/scriptaculous.js?load=builder,dragdrop" type="text/javascript"></script>
+ <script src="../cropper.js" type="text/javascript"></script>
+
+
+ <script type="text/javascript" charset="utf-8">
+
+ // setup the callback function
+ function onEndCrop( coords, dimensions ) {
+ $( 'x1' ).value = coords.x1;
+ $( 'y1' ).value = coords.y1;
+ $( 'x2' ).value = coords.x2;
+ $( 'y2' ).value = coords.y2;
+ $( 'width' ).value = dimensions.width;
+ $( 'height' ).value = dimensions.height;
+ }
+
+ // relative example
+ Event.observe(
+ window,
+ 'load',
+ function() {
+ new Cropper.Img(
+ 'testImage',
+ {
+ onEndCrop: onEndCrop
+ }
+ )
+ }
+ );
+
+ if( typeof(dump) != 'function' ) {
+ Debug.init(true, '/');
+
+ function dump( msg ) {
+ Debug.raise( msg );
+ };
+ } else dump( '---------------------------------------\n' );
+
+
+ </script>
+ <link rel="stylesheet" type="text/css" href="debug.css" media="all" />
+ <style type="text/css">
+ label {
+ clear: left;
+ margin-left: 50px;
+ float: left;
+ width: 5em;
+ }
+
+ html, body {
+ margin: 0;
+ }
+
+ #testWrap {
+ margin: 20px 0 0 50px; /* Just while testing, to make sure we return the correct positions for the image & not the window */
+ }
+
+ #test-relative {
+ background-color: #ccc;
+ border: 3px solid #ddd;
+ position: relative;
+ top: 25px;
+ left: 25px;
+ }
+ </style>
+</head>
+<body>
+ <h2>Test page with relatively positioned wrapper</h2>
+ <p>
+ Some test content before the image
+ </p>
+
+<div id="test-relative">
+ <h2>Relative test</h2>
+ <div id="testWrap">
+ <img src="castle.jpg" alt="test image" id="testImage" width="500" height="333" />
+ </div>
+
+
+ <p>
+ <label for="x1">x1:</label>
+ <input type="text" name="x1" id="x1" />
+ </p>
+ <p>
+ <label for="y1">y1:</label>
+ <input type="text" name="y1" id="y1" />
+ </p>
+ <p>
+ <label for="x2">x2:</label>
+ <input type="text" name="x2" id="x2" />
+ </p>
+ <p>
+ <label for="y2">y2:</label>
+ <input type="text" name="y2" id="y2" />
+ </p>
+ <p>
+ <label for="width">width:</label>
+ <input type="text" name="width" id="width" />
+ </p>
+ <p>
+ <label for="height">height</label>
+ <input type="text" name="height" id="height" />
+ </p>
+</div>
+
+</body>
+</html>
+
+
diff --git a/cropper/tests/example-CoordsOnLoad.htm b/cropper/tests/example-CoordsOnLoad.htm
new file mode 100644
index 000000000..254a23472
--- /dev/null
+++ b/cropper/tests/example-CoordsOnLoad.htm
@@ -0,0 +1,108 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="Content-Language" content="en-us" />
+ <title>Loading &amp; displaying co-ordinates of crop area on attachment test</title>
+ <script src="../lib/prototype.js" type="text/javascript"></script>
+ <script src="../lib/scriptaculous.js?load=builder,dragdrop" type="text/javascript"></script>
+ <script src="../cropper.js" type="text/javascript"></script>
+
+
+ <script type="text/javascript" charset="utf-8">
+
+ // setup the callback function
+ function onEndCrop( coords, dimensions ) {
+ $( 'x1' ).value = coords.x1;
+ $( 'y1' ).value = coords.y1;
+ $( 'x2' ).value = coords.x2;
+ $( 'y2' ).value = coords.y2;
+ $( 'width' ).value = dimensions.width;
+ $( 'height' ).value = dimensions.height;
+ }
+
+ // basic example
+ Event.observe(
+ window,
+ 'load',
+ function() {
+ new Cropper.Img(
+ 'testImage',
+ {
+ onEndCrop: onEndCrop,
+ displayOnInit: true,
+ onloadCoords: { x1: 10, y1: 10, x2: 250, y2: 100 }
+ }
+ )
+ }
+ );
+
+
+ if( typeof(dump) != 'function' ) {
+ Debug.init(true, '/');
+
+ function dump( msg ) {
+ Debug.raise( msg );
+ };
+ } else dump( '---------------------------------------\n' );
+
+
+ </script>
+ <link rel="stylesheet" type="text/css" href="debug.css" media="all" />
+ <style type="text/css">
+ label {
+ clear: left;
+ margin-left: 50px;
+ float: left;
+ width: 5em;
+ }
+
+ html, body {
+ margin: 0;
+ }
+
+ #testWrap {
+ margin: 20px 0 0 50px; /* Just while testing, to make sure we return the correct positions for the image & not the window */
+ }
+ </style>
+</head>
+<body>
+ <h2>Loading &amp; displaying co-ordinates of crop area on attachment test</h2>
+ <p>
+ Some test content before the image
+ </p>
+
+ <div id="testWrap">
+ <img src="castle.jpg" alt="test image" id="testImage" width="500" height="333" />
+ </div>
+
+
+ <p>
+ <label for="x1">x1:</label>
+ <input type="text" name="x1" id="x1" />
+ </p>
+ <p>
+ <label for="y1">y1:</label>
+ <input type="text" name="y1" id="y1" />
+ </p>
+ <p>
+ <label for="x2">x2:</label>
+ <input type="text" name="x2" id="x2" />
+ </p>
+ <p>
+ <label for="y2">y2:</label>
+ <input type="text" name="y2" id="y2" />
+ </p>
+ <p>
+ <label for="width">width:</label>
+ <input type="text" name="width" id="width" />
+ </p>
+ <p>
+ <label for="height">height</label>
+ <input type="text" name="height" id="height" />
+ </p>
+
+</body>
+</html>
+
+
diff --git a/cropper/tests/example-CoordsOnLoadWithRatio.htm b/cropper/tests/example-CoordsOnLoadWithRatio.htm
new file mode 100644
index 000000000..3a696360a
--- /dev/null
+++ b/cropper/tests/example-CoordsOnLoadWithRatio.htm
@@ -0,0 +1,109 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="Content-Language" content="en-us" />
+ <title>Loading &amp; displaying co-ordinates (with ratio) of crop area on attachment test<</title>
+ <script src="../lib/prototype.js" type="text/javascript"></script>
+ <script src="../lib/scriptaculous.js?load=builder,dragdrop" type="text/javascript"></script>
+ <script src="../cropper.js" type="text/javascript"></script>
+
+
+ <script type="text/javascript" charset="utf-8">
+
+ // setup the callback function
+ function onEndCrop( coords, dimensions ) {
+ $( 'x1' ).value = coords.x1;
+ $( 'y1' ).value = coords.y1;
+ $( 'x2' ).value = coords.x2;
+ $( 'y2' ).value = coords.y2;
+ $( 'width' ).value = dimensions.width;
+ $( 'height' ).value = dimensions.height;
+ }
+
+ // basic example
+ Event.observe(
+ window,
+ 'load',
+ function() {
+ new Cropper.Img(
+ 'testImage',
+ {
+ onEndCrop: onEndCrop,
+ displayOnInit: true,
+ onloadCoords: { x1: 10, y1: 10, x2: 210, y2: 110 },
+ ratioDim: { x: 200, y: 100 }
+ }
+ )
+ }
+ );
+
+
+ if( typeof(dump) != 'function' ) {
+ Debug.init(true, '/');
+
+ function dump( msg ) {
+ Debug.raise( msg );
+ };
+ } else dump( '---------------------------------------\n' );
+
+
+ </script>
+ <link rel="stylesheet" type="text/css" href="debug.css" media="all" />
+ <style type="text/css">
+ label {
+ clear: left;
+ margin-left: 50px;
+ float: left;
+ width: 5em;
+ }
+
+ html, body {
+ margin: 0;
+ }
+
+ #testWrap {
+ margin: 20px 0 0 50px; /* Just while testing, to make sure we return the correct positions for the image & not the window */
+ }
+ </style>
+</head>
+<body>
+ <h2>Loading &amp; displaying co-ordinates (with ratio) of crop area on attachment test</h2>
+ <p>
+ Some test content before the image
+ </p>
+
+ <div id="testWrap">
+ <img src="castle.jpg" alt="test image" id="testImage" width="500" height="333" />
+ </div>
+
+
+ <p>
+ <label for="x1">x1:</label>
+ <input type="text" name="x1" id="x1" />
+ </p>
+ <p>
+ <label for="y1">y1:</label>
+ <input type="text" name="y1" id="y1" />
+ </p>
+ <p>
+ <label for="x2">x2:</label>
+ <input type="text" name="x2" id="x2" />
+ </p>
+ <p>
+ <label for="y2">y2:</label>
+ <input type="text" name="y2" id="y2" />
+ </p>
+ <p>
+ <label for="width">width:</label>
+ <input type="text" name="width" id="width" />
+ </p>
+ <p>
+ <label for="height">height</label>
+ <input type="text" name="height" id="height" />
+ </p>
+
+</body>
+</html>
+
+
diff --git a/cropper/tests/example-Dimensions.htm b/cropper/tests/example-Dimensions.htm
new file mode 100644
index 000000000..f54f99683
--- /dev/null
+++ b/cropper/tests/example-Dimensions.htm
@@ -0,0 +1,225 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="Content-Language" content="en-us" />
+ <title>Different dimensions test</title>
+ <script src="../lib/prototype.js" type="text/javascript"></script>
+ <script src="../lib/scriptaculous.js?load=builder,dragdrop" type="text/javascript"></script>
+ <script src="../cropper.js" type="text/javascript"></script>
+
+
+ <script type="text/javascript" charset="utf-8">
+
+ function onEndCrop( coords, dimensions ) {
+ $( 'x1' ).value = coords.x1;
+ $( 'y1' ).value = coords.y1;
+ $( 'x2' ).value = coords.x2;
+ $( 'y2' ).value = coords.y2;
+ $( 'width' ).value = dimensions.width;
+ $( 'height' ).value = dimensions.height;
+ }
+
+ /*
+
+ // example with minimum dimensions
+ Event.observe(
+ window,
+ 'load',
+ function() {
+ new Cropper.Img(
+ 'testImage',
+ {
+ minWidth: 200,
+ minHeight: 120,
+ maxWidth: 200,
+ //maxHeight: 120,
+ displayOnInit: true,
+ onEndCrop: onEndCrop
+ }
+ )
+ }
+ );
+ */
+
+ Event.observe( window, 'load',
+ function() {
+ Event.observe( 'dimensionsForm', 'submit', CropManager.attachCropper.bindAsEventListener( CropManager ) );
+ CropManager.attachCropper();
+ }
+ );
+
+ /**
+ * A little manager that allows us to reset the options dynamically
+ */
+ var CropManager = {
+ /**
+ * Holds the current Cropper.Img object
+ * @var obj
+ */
+ curCrop: null,
+
+ /**
+ * Gets a min/max parameter from the form
+ *
+ * @access private
+ * @param string Form element ID
+ * @return int
+ */
+ getParam: function( name ) {
+ var val = $F( name );
+ console.log( name + ' :: ' + val );
+ return parseInt( val );
+ },
+
+ /**
+ * Attaches/resets the image cropper
+ *
+ * @access private
+ * @param obj Event object
+ * @return void
+ */
+ attachCropper: function( e ) {
+ if( this.curCrop == null ) {
+ this.curCrop = new Cropper.Img(
+ 'testImage',
+ {
+ minWidth: this.getParam( 'minWidth' ),
+ minHeight: this.getParam( 'minHeight' ),
+ maxWidth: this.getParam( 'maxWidth' ),
+ maxHeight: this.getParam( 'maxHeight' ),
+ onEndCrop: onEndCrop
+ }
+ );
+ } else {
+ this.removeCropper();
+ this.curCrop.initialize(
+ 'testImage',
+ {
+ minWidth: this.getParam( 'minWidth' ),
+ minHeight: this.getParam( 'minHeight' ),
+ maxWidth: this.getParam( 'maxWidth' ),
+ maxHeight: this.getParam( 'maxHeight' ),
+ onEndCrop: onEndCrop
+ }
+ );
+ }
+ if( e != null ) Event.stop( e );
+ },
+
+ /**
+ * Removes the cropper
+ *
+ * @access public
+ * @return void
+ */
+ removeCropper: function() {
+ if( this.curCrop != null ) {
+ this.curCrop.remove();
+ }
+ },
+
+ /**
+ * Resets the cropper, either re-setting or re-applying
+ *
+ * @access public
+ * @return void
+ */
+ resetCropper: function() {
+ this.attachCropper();
+ }
+ };
+
+ /*
+ if( typeof(dump) != 'function' ) {
+ Debug.init(true, '/');
+
+ function dump( msg ) {
+ // Debug.raise( msg );
+ };
+ } else dump( '---------------------------------------\n' );
+ */
+
+ </script>
+ <link rel="stylesheet" type="text/css" href="debug.css" media="all" />
+ <style type="text/css">
+ label {
+ clear: left;
+ margin-left: 50px;
+ float: left;
+ width: 5em;
+ }
+
+ #testWrap {
+ margin: 20px 0 0 50px; /* Just while testing, to make sure we return the correct positions for the image & not the window */
+ }
+
+ #dimensionsForm {
+ float: right;
+ width: 350px;
+ }
+ </style>
+</head>
+<body>
+ <h2>Multiple dimensions tests</h2>
+ <p>
+ Test of applying different dimension restrictions to the cropper
+ </p>
+
+ <form action="#" id="dimensionsForm">
+ <fieldset>
+ Set the cropper with the following dimension restrictions:
+ <p>
+ <label for="minWidth">Min Width</label>
+ <input type="text" size="10" maxlength="3" value="200" id="minWidth" name="minWidth" />
+ </p>
+ <p>
+ <label for="maxWidth">Max Width</label>
+ <input type="text" size="10" maxlength="3" value="200" id="maxWidth" name="maxWidth" />
+ </p>
+ <p>
+ <label for="minHeight">Min Height</label>
+ <input type="text" size="10" maxlength="3" value="120" id="minHeight" name="minHeight" />
+ </p>
+ <p>
+ <label for="maxHeight">Max Height</label>
+ <input type="text" size="10" maxlength="3" value="120" id="maxHeight" name="maxHeight" />
+ </p>
+ <input type="submit" value="Set Cropper" />
+ </fieldset>
+ </form>
+
+ <div id="testWrap">
+ <img src="castle.jpg" alt="test image" id="testImage" width="500" height="333" />
+ </div>
+
+
+ <p>
+ <label for="x1">x1:</label>
+ <input type="text" name="x1" id="x1" />
+ </p>
+ <p>
+ <label for="y1">y1:</label>
+ <input type="text" name="y1" id="y1" />
+ </p>
+ <p>
+ <label for="x2">x2:</label>
+ <input type="text" name="x2" id="x2" />
+ </p>
+ <p>
+ <label for="y2">y2:</label>
+ <input type="text" name="y2" id="y2" />
+ </p>
+ <p>
+ <label for="width">width:</label>
+ <input type="text" name="width" id="width" />
+ </p>
+ <p>
+ <label for="height">height</label>
+ <input type="text" name="height" id="height" />
+ </p>
+
+</body>
+</html>
+
+
diff --git a/cropper/tests/example-DynamicImage.htm b/cropper/tests/example-DynamicImage.htm
new file mode 100644
index 000000000..898363489
--- /dev/null
+++ b/cropper/tests/example-DynamicImage.htm
@@ -0,0 +1,203 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="Content-Language" content="en-us" />
+ <title>Dynamic image test</title>
+ <script src="../lib/prototype.js" type="text/javascript"></script>
+ <script src="../lib/scriptaculous.js?load=builder,dragdrop" type="text/javascript"></script>
+ <script src="../cropper.js" type="text/javascript"></script>
+
+
+ <script type="text/javascript" charset="utf-8">
+
+ /**
+ * A little manager that allows us to swap the image dynamically
+ *
+ */
+ var CropImageManager = {
+ /**
+ * Holds the current Cropper.Img object
+ * @var obj
+ */
+ curCrop: null,
+
+ /**
+ * Initialises the cropImageManager
+ *
+ * @access public
+ * @return void
+ */
+ init: function() {
+ this.attachCropper();
+ },
+
+ /**
+ * Handles the changing of the select to change the image, the option value
+ * is a pipe seperated list of imgSrc|width|height
+ *
+ * @access public
+ * @param obj event
+ * @return void
+ */
+ onChange: function( e ) {
+ var vals = $F( Event.element( e ) ).split('|');
+ this.setImage( vals[0], vals[1], vals[2] );
+ },
+
+ /**
+ * Sets the image within the element & attaches/resets the image cropper
+ *
+ * @access private
+ * @param string Source path of new image
+ * @param int Width of new image in pixels
+ * @param int Height of new image in pixels
+ * @return void
+ */
+ setImage: function( imgSrc, w, h ) {
+ $( 'testImage' ).src = imgSrc;
+ $( 'testImage' ).width = w;
+ $( 'testImage' ).height = h;
+ this.attachCropper();
+ },
+
+ /**
+ * Attaches/resets the image cropper
+ *
+ * @access private
+ * @return void
+ */
+ attachCropper: function() {
+ if( this.curCrop == null ) this.curCrop = new Cropper.Img( 'testImage', { onEndCrop: onEndCrop } );
+ else this.curCrop.reset();
+ },
+
+ /**
+ * Removes the cropper
+ *
+ * @access public
+ * @return void
+ */
+ removeCropper: function() {
+ if( this.curCrop != null ) {
+ this.curCrop.remove();
+ }
+ },
+
+ /**
+ * Resets the cropper, either re-setting or re-applying
+ *
+ * @access public
+ * @return void
+ */
+ resetCropper: function() {
+ this.attachCropper();
+ }
+ };
+
+
+ // setup the callback function
+ function onEndCrop( coords, dimensions ) {
+ $( 'x1' ).value = coords.x1;
+ $( 'y1' ).value = coords.y1;
+ $( 'x2' ).value = coords.x2;
+ $( 'y2' ).value = coords.y2;
+ $( 'width' ).value = dimensions.width;
+ $( 'height' ).value = dimensions.height;
+ }
+
+ // basic example
+ Event.observe(
+ window,
+ 'load',
+ function() {
+ CropImageManager.init();
+ Event.observe( $('removeCropper'), 'click', CropImageManager.removeCropper.bindAsEventListener( CropImageManager ), false );
+ Event.observe( $('resetCropper'), 'click', CropImageManager.resetCropper.bindAsEventListener( CropImageManager ), false );
+ Event.observe( $('imageChoice'), 'change', CropImageManager.onChange.bindAsEventListener( CropImageManager ), false );
+ }
+ );
+
+
+ /*
+ if( typeof(dump) != 'function' ) {
+ Debug.init(true, '/');
+
+ function dump( msg ) {
+ Debug.raise( msg );
+ };
+ } else dump( '---------------------------------------\n' );
+ */
+
+ </script>
+ <link rel="stylesheet" type="text/css" href="debug.css" media="all" />
+ <style type="text/css">
+ label {
+ clear: left;
+ margin-left: 50px;
+ float: left;
+ width: 5em;
+ }
+
+ html, body {
+ margin: 0;
+ }
+
+ #testWrap {
+ margin: 20px 0 0 50px; /* Just while testing, to make sure we return the correct positions for the image & not the window */
+ }
+ </style>
+</head>
+<body>
+ <h2>Dynamic image test</h2>
+ <p>
+ Test of dynamically changing images or removing & re-applying the cropper
+ </p>
+
+ <div id="testWrap">
+ <img src="castle.jpg" alt="test image" id="testImage" width="500" height="333" />
+ </div>
+
+ <p>
+ <label for="imageChoice">image:</label>
+ <select name="imageChoice" id="imageChoice">
+ <option value="castle.jpg|500|333">Castle</option>
+ <option value="poppy.jpg|311|466">Flower</option>
+ </select>
+ </p>
+
+ <p>
+ <input type="button" id="removeCropper" value="Remove Cropper" />
+ <input type="button" id="resetCropper" value="Reset Cropper" />
+ </p>
+
+
+ <p>
+ <label for="x1">x1:</label>
+ <input type="text" name="x1" id="x1" />
+ </p>
+ <p>
+ <label for="y1">y1:</label>
+ <input type="text" name="y1" id="y1" />
+ </p>
+ <p>
+ <label for="x2">x2:</label>
+ <input type="text" name="x2" id="x2" />
+ </p>
+ <p>
+ <label for="y2">y2:</label>
+ <input type="text" name="y2" id="y2" />
+ </p>
+ <p>
+ <label for="width">width:</label>
+ <input type="text" name="width" id="width" />
+ </p>
+ <p>
+ <label for="height">height</label>
+ <input type="text" name="height" id="height" />
+ </p>
+
+</body>
+</html>
+
+
diff --git a/cropper/tests/example-FixedRatio.htm b/cropper/tests/example-FixedRatio.htm
new file mode 100644
index 000000000..973bedda9
--- /dev/null
+++ b/cropper/tests/example-FixedRatio.htm
@@ -0,0 +1,104 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="Content-Language" content="en-us" />
+ <title>Fixed ratio test</title>
+ <script src="../lib/prototype.js" type="text/javascript"></script>
+ <script src="../lib/scriptaculous.js?load=builder,dragdrop" type="text/javascript"></script>
+ <script src="../cropper.js" type="text/javascript"></script>
+
+
+ <script type="text/javascript" charset="utf-8">
+
+ function onEndCrop( coords, dimensions ) {
+ $( 'x1' ).value = coords.x1;
+ $( 'y1' ).value = coords.y1;
+ $( 'x2' ).value = coords.x2;
+ $( 'y2' ).value = coords.y2;
+ $( 'width' ).value = dimensions.width;
+ $( 'height' ).value = dimensions.height;
+ }
+
+ // with a supplied ratio
+ Event.observe(
+ window,
+ 'load',
+ function() {
+ new Cropper.Img(
+ 'testImage',
+ {
+ ratioDim: { x: 220, y: 124 },
+ displayOnInit: true,
+ onEndCrop: onEndCrop
+ }
+ )
+ }
+ );
+
+ /*
+ if( typeof(dump) != 'function' ) {
+ Debug.init(true, '/');
+
+ function dump( msg ) {
+ // Debug.raise( msg );
+ };
+ } else dump( '---------------------------------------\n' );
+ */
+
+ </script>
+ <link rel="stylesheet" type="text/css" href="debug.css" media="all" />
+ <style type="text/css">
+ label {
+ clear: left;
+ margin-left: 50px;
+ float: left;
+ width: 5em;
+ }
+
+ #testWrap {
+ margin: 20px 0 0 50px; /* Just while testing, to make sure we return the correct positions for the image & not the window */
+ }
+ </style>
+</head>
+<body>
+ <h2>Fixed ratio test</h2>
+ <p>
+ Test of applying a fixed ratio to the cropper
+ </p>
+ <br />
+
+ <div id="testWrap">
+ <img src="castle.jpg" alt="test image" id="testImage" width="500" height="333" />
+ </div>
+
+
+ <p>
+ <label for="x1">x1:</label>
+ <input type="text" name="x1" id="x1" />
+ </p>
+ <p>
+ <label for="y1">y1:</label>
+ <input type="text" name="y1" id="y1" />
+ </p>
+ <p>
+ <label for="x2">x2:</label>
+ <input type="text" name="x2" id="x2" />
+ </p>
+ <p>
+ <label for="y2">y2:</label>
+ <input type="text" name="y2" id="y2" />
+ </p>
+ <p>
+ <label for="width">width:</label>
+ <input type="text" name="width" id="width" />
+ </p>
+ <p>
+ <label for="height">height</label>
+ <input type="text" name="height" id="height" />
+ </p>
+
+</body>
+</html>
+
+
diff --git a/cropper/tests/example-MinimumDimensions.htm b/cropper/tests/example-MinimumDimensions.htm
new file mode 100644
index 000000000..3ae93c8e1
--- /dev/null
+++ b/cropper/tests/example-MinimumDimensions.htm
@@ -0,0 +1,105 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="Content-Language" content="en-us" />
+ <title>Min dimensions test</title>
+ <script src="../lib/prototype.js" type="text/javascript"></script>
+ <script src="../lib/scriptaculous.js?load=builder,dragdrop" type="text/javascript"></script>
+ <script src="../cropper.js" type="text/javascript"></script>
+
+
+ <script type="text/javascript" charset="utf-8">
+
+ function onEndCrop( coords, dimensions ) {
+ $( 'x1' ).value = coords.x1;
+ $( 'y1' ).value = coords.y1;
+ $( 'x2' ).value = coords.x2;
+ $( 'y2' ).value = coords.y2;
+ $( 'width' ).value = dimensions.width;
+ $( 'height' ).value = dimensions.height;
+ }
+
+ // example with minimum dimensions
+ Event.observe(
+ window,
+ 'load',
+ function() {
+ new Cropper.Img(
+ 'testImage',
+ {
+ minWidth: 200,
+ minHeight: 120,
+ displayOnInit: true,
+ onEndCrop: onEndCrop
+ }
+ )
+ }
+ );
+
+ /*
+ if( typeof(dump) != 'function' ) {
+ Debug.init(true, '/');
+
+ function dump( msg ) {
+ // Debug.raise( msg );
+ };
+ } else dump( '---------------------------------------\n' );
+ */
+
+ </script>
+ <link rel="stylesheet" type="text/css" href="debug.css" media="all" />
+ <style type="text/css">
+ label {
+ clear: left;
+ margin-left: 50px;
+ float: left;
+ width: 5em;
+ }
+
+ #testWrap {
+ margin: 20px 0 0 50px; /* Just while testing, to make sure we return the correct positions for the image & not the window */
+ }
+ </style>
+</head>
+<body>
+ <h2>Minimum (both axes ) dimension test</h2>
+ <p>
+ Test of applying a minimum dimension to both axes to the cropper
+ </p>
+ <br />
+
+ <div id="testWrap">
+ <img src="castle.jpg" alt="test image" id="testImage" width="500" height="333" />
+ </div>
+
+
+ <p>
+ <label for="x1">x1:</label>
+ <input type="text" name="x1" id="x1" />
+ </p>
+ <p>
+ <label for="y1">y1:</label>
+ <input type="text" name="y1" id="y1" />
+ </p>
+ <p>
+ <label for="x2">x2:</label>
+ <input type="text" name="x2" id="x2" />
+ </p>
+ <p>
+ <label for="y2">y2:</label>
+ <input type="text" name="y2" id="y2" />
+ </p>
+ <p>
+ <label for="width">width:</label>
+ <input type="text" name="width" id="width" />
+ </p>
+ <p>
+ <label for="height">height</label>
+ <input type="text" name="height" id="height" />
+ </p>
+
+</body>
+</html>
+
+
diff --git a/cropper/tests/example-MinimumWidth.htm b/cropper/tests/example-MinimumWidth.htm
new file mode 100644
index 000000000..b0576b87f
--- /dev/null
+++ b/cropper/tests/example-MinimumWidth.htm
@@ -0,0 +1,105 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="Content-Language" content="en-us" />
+ <title>Min (single axis) dimensions test</title>
+ <script src="../lib/prototype.js" type="text/javascript"></script>
+ <script src="../lib/scriptaculous.js?load=builder,dragdrop" type="text/javascript"></script>
+ <script src="../cropper.js" type="text/javascript"></script>
+
+
+ <script type="text/javascript" charset="utf-8">
+
+ function onEndCrop( coords, dimensions ) {
+ $( 'x1' ).value = coords.x1;
+ $( 'y1' ).value = coords.y1;
+ $( 'x2' ).value = coords.x2;
+ $( 'y2' ).value = coords.y2;
+ $( 'width' ).value = dimensions.width;
+ $( 'height' ).value = dimensions.height;
+ }
+
+ // example with minimum dimensions
+ Event.observe(
+ window,
+ 'load',
+ function() {
+ new Cropper.Img(
+ 'testImage',
+ {
+ minWidth: 200,
+ displayOnInit: true,
+ onEndCrop: onEndCrop
+ }
+ )
+ }
+ );
+
+ /*
+ if( typeof(dump) != 'function' ) {
+ Debug.init(true, '/');
+
+ function dump( msg ) {
+ // Debug.raise( msg );
+ };
+ } else dump( '---------------------------------------\n' );
+ */
+
+ </script>
+ <link rel="stylesheet" type="text/css" href="debug.css" media="all" />
+ <style type="text/css">
+ label {
+ clear: left;
+ margin-left: 50px;
+ float: left;
+ width: 5em;
+ }
+
+ #testWrap {
+ margin: 20px 0 0 50px; /* Just while testing, to make sure we return the correct positions for the image & not the window */
+ }
+ </style>
+</head>
+<body>
+ <h2>Minimum (single axis) dimension test</h2>
+ <p>
+ Test of applying a minimum dimension to only one axis (width in this case) to the cropper
+ </p>
+ <br />
+ <br /><br />
+
+ <div id="testWrap">
+ <img src="castle.jpg" alt="test image" id="testImage" width="500" height="333" />
+ </div>
+
+
+ <p>
+ <label for="x1">x1:</label>
+ <input type="text" name="x1" id="x1" />
+ </p>
+ <p>
+ <label for="y1">y1:</label>
+ <input type="text" name="y1" id="y1" />
+ </p>
+ <p>
+ <label for="x2">x2:</label>
+ <input type="text" name="x2" id="x2" />
+ </p>
+ <p>
+ <label for="y2">y2:</label>
+ <input type="text" name="y2" id="y2" />
+ </p>
+ <p>
+ <label for="width">width:</label>
+ <input type="text" name="width" id="width" />
+ </p>
+ <p>
+ <label for="height">height</label>
+ <input type="text" name="height" id="height" />
+ </p>
+
+</body>
+</html>
+
+
diff --git a/cropper/tests/example-Preview.htm b/cropper/tests/example-Preview.htm
new file mode 100644
index 000000000..701670c91
--- /dev/null
+++ b/cropper/tests/example-Preview.htm
@@ -0,0 +1,117 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="Content-Language" content="en-us" />
+ <title></title>
+ <script src="../lib/prototype.js" type="text/javascript"></script>
+ <script src="../lib/scriptaculous.js?load=builder,dragdrop" type="text/javascript"></script>
+ <script src="../cropper.js" type="text/javascript"></script>
+
+
+ <script type="text/javascript" charset="utf-8">
+
+ function onEndCrop( coords, dimensions ) {
+ $( 'x1' ).value = coords.x1;
+ $( 'y1' ).value = coords.y1;
+ $( 'x2' ).value = coords.x2;
+ $( 'y2' ).value = coords.y2;
+ $( 'width' ).value = dimensions.width;
+ $( 'height' ).value = dimensions.height;
+ }
+
+ // example with a preview of crop results, must have minimumm dimensions
+ Event.observe(
+ window,
+ 'load',
+ function() {
+ new Cropper.ImgWithPreview(
+ 'testImage',
+ {
+ minWidth: 200,
+ minHeight: 120,
+ ratioDim: { x: 200, y: 120 },
+ displayOnInit: true,
+ onEndCrop: onEndCrop,
+ previewWrap: 'previewArea'
+ }
+ )
+ }
+ );
+
+ /*
+ if( typeof(dump) != 'function' ) {
+ Debug.init(true, '/');
+
+ function dump( msg ) {
+ // Debug.raise( msg );
+ };
+ } else dump( '---------------------------------------\n' );
+ */
+
+ </script>
+ <link rel="stylesheet" type="text/css" href="debug.css" media="all" />
+ <style type="text/css">
+ label {
+ clear: left;
+ margin-left: 50px;
+ float: left;
+ width: 5em;
+ }
+
+ #testWrap {
+ width: 500px;
+ float: left;
+ margin: 20px 0 0 50px; /* Just while testing, to make sure we return the correct positions for the image & not the window */
+ }
+
+ #previewArea {
+ margin: 20px; 0 0 20px;
+ float: left;
+ }
+
+ #results {
+ clear: both;
+ }
+ </style>
+</head>
+<body>
+ <br /><br />
+
+ <div id="testWrap">
+ <img src="castle.jpg" alt="test image" id="testImage" width="500" height="333" />
+ </div>
+
+ <div id="previewArea"></div>
+
+ <div id="results">
+ <p>
+ <label for="x1">x1:</label>
+ <input type="text" name="x1" id="x1" />
+ </p>
+ <p>
+ <label for="y1">y1:</label>
+ <input type="text" name="y1" id="y1" />
+ </p>
+ <p>
+ <label for="x2">x2:</label>
+ <input type="text" name="x2" id="x2" />
+ </p>
+ <p>
+ <label for="y2">y2:</label>
+ <input type="text" name="y2" id="y2" />
+ </p>
+ <p>
+ <label for="width">width:</label>
+ <input type="text" name="width" id="width" />
+ </p>
+ <p>
+ <label for="height">height</label>
+ <input type="text" name="height" id="height" />
+ </p>
+ </div>
+
+</body>
+</html>
+
+
diff --git a/cropper/tests/poppy.jpg b/cropper/tests/poppy.jpg
new file mode 100644
index 000000000..1f6498584
--- /dev/null
+++ b/cropper/tests/poppy.jpg
Binary files differ
diff --git a/cropper/tests/staticHTMLStructure.htm b/cropper/tests/staticHTMLStructure.htm
new file mode 100644
index 000000000..ddb99278e
--- /dev/null
+++ b/cropper/tests/staticHTMLStructure.htm
@@ -0,0 +1,236 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="Content-Language" content="en-us" />
+ <title></title>
+</head>
+<body>
+ <!--
+
+ This is a static test file for the HTML & CSS structure employed, tested in
+ the following browsers:
+
+ PC:
+ IE 6: working
+ IE 5.5: working
+ IE 5.0: opacity issues
+ FF 1.5: working
+ Opera 9: working
+ MAC:
+ Camino 1.0: working
+ FF 1.5: working
+ Safari 2.0: working
+
+ -->
+ <style type="text/css">
+ .imgCrop_wrap {
+ width: 500px; /* @TODO IN JS */
+ height: 333px; /* @TODO IN JS */
+ position: relative;
+ cursor: crosshair;
+ }
+
+ /* fix for IE displaying all boxes at line-height by default */
+ .imgCrop_wrap,
+ .imgCrop_wrap * {
+ font-size: 0;
+ }
+
+ .imgCrop_overlay {
+ background-color: #000;
+ opacity: 0.5;
+ filter:alpha(opacity=50);
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ }
+
+ .imgCrop_selArea {
+ position: absolute;
+ cursor: move;
+ /* @TODO: rest to be done via JS when selecting areas */
+ top: 110px;
+ left: 210px;
+ width: 200px;
+ height: 200px;
+ z-index: 2;
+ background: transparent url(castle.jpg) no-repeat -210px -110px;
+ }
+
+ /* imgCrop_clickArea is all a fix for IE 5.5 & 6 to allow the user to click on the given area */
+ .imgCrop_clickArea {
+ width: 100%;
+ height: 100%;
+ background-color: #FFF;
+ opacity: 0.01;
+ filter:alpha(opacity=01);
+ }
+
+ .imgCrop_marqueeHoriz {
+ position: absolute;
+ width: 100%;
+ height: 1px;
+ background: transparent url(marqueeHoriz.gif) repeat-x 0 0;
+ }
+
+ .imgCrop_marqueeVert {
+ position: absolute;
+ height: 100%;
+ width: 1px;
+ background: transparent url(marqueeVert.gif) repeat-y 0 0;
+ }
+
+ .imgCrop_marqueeNorth { top: 0; left: 0; }
+ .imgCrop_marqueeEast { top: 0; right: 0; }
+ .imgCrop_marqueeSouth { bottom: 0px; left: 0; }
+ .imgCrop_marqueeWest { top: 0; left: 0; }
+
+
+ .imgCrop_handle {
+ position: absolute;
+ border: 1px solid #333;
+ width: 6px;
+ height: 6px;
+ background: #FFF;
+ opacity: 0.5;
+ filter:alpha(opacity=50);
+ z-index: 3;
+ }
+
+ .imgCrop_handleN {
+ top: -3px;
+ left: 0;
+ margin-left: 49%; /* @TODO : in JS */
+ cursor: n-resize;
+ }
+
+ .imgCrop_handleNE {
+ top: -3px;
+ right: -3px;
+ cursor: ne-resize;
+ }
+
+ .imgCrop_handleE {
+ top: 0;
+ right: -3px;
+ margin-top: 49%; /* @TODO : in JS */
+ cursor: e-resize;
+ }
+
+ .imgCrop_handleSE {
+ right: -3px;
+ bottom: -3px;
+ cursor: se-resize;
+ }
+
+ .imgCrop_handleS {
+ right: 0;
+ bottom: -3px;
+ margin-right: 49%; /* @TODO : in JS */
+ cursor: s-resize;
+ }
+
+ .imgCrop_handleSW {
+ left: -3px;
+ bottom: -3px;
+ cursor: sw-resize;
+ }
+
+ .imgCrop_handleW {
+ top: 0;
+ left: -3px;
+ margin-top: 49%; /* @TODO : in JS */
+ cursor: e-resize;
+ }
+
+ .imgCrop_handleNW {
+ top: -3px;
+ left: -3px;
+ cursor: nw-resize;
+ }
+
+ /**
+ * Create an area to click & drag around on as the default browser behaviour is to let you drag the image
+ */
+ .imgCrop_dragArea {
+ width: 100%;
+ height: 100%;
+ z-index: 200;
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+
+
+ .imgCrop_previewWrap {
+ width: 200px; /* @TODO : in JS */
+ height: 200px; /* @TODO : in JS */
+ overflow: hidden;
+ position: relative;
+ }
+
+ /* @TODO : all in JS */
+ .imgCrop_previewWrap img {
+ position: absolute;
+ width: 500px;
+ height: 333px;
+ left: -210px;
+ top: -110px;
+ }
+
+ /**
+ * These are just for the static test
+ */
+ .imgCrop_wrap {
+ margin: 20px 0 0 50px;
+ float: left;
+ }
+
+ #previewWrapper {
+ float: left;
+ margin-left: 20px;
+ }
+
+
+ </style>
+
+ <br /><br />
+
+ <!-- This is all attached to the image dynamically -->
+ <div class="imgCrop_wrap">
+ <img src="castle.jpg" alt="test image" id="testImage" width="500" height="333" />
+ <div class="imgCrop_dragArea">
+ <div class="imgCrop_overlay"></div>
+ <div class="imgCrop_selArea">
+ <!-- marquees -->
+ <!-- the inner spans are only required for IE to stop it making the divs 1px high/wide -->
+ <div class="imgCrop_marqueeHoriz imgCrop_marqueeNorth"><span></span></div>
+ <div class="imgCrop_marqueeVert imgCrop_marqueeEast"><span></span></div>
+ <div class="imgCrop_marqueeHoriz imgCrop_marqueeSouth"><span></span></div>
+ <div class="imgCrop_marqueeVert imgCrop_marqueeWest"><span></span></div>
+ <!-- handles -->
+ <div class="imgCrop_handle imgCrop_handleN"></div>
+ <div class="imgCrop_handle imgCrop_handleNE"></div>
+ <div class="imgCrop_handle imgCrop_handleE"></div>
+ <div class="imgCrop_handle imgCrop_handleSE"></div>
+ <div class="imgCrop_handle imgCrop_handleS"></div>
+ <div class="imgCrop_handle imgCrop_handleSW"></div>
+ <div class="imgCrop_handle imgCrop_handleW"></div>
+ <div class="imgCrop_handle imgCrop_handleNW"></div>
+ <div class="imgCrop_clickArea"></div>
+ </div>
+ <div class="imgCrop_clickArea"></div>
+ </div>
+ </div>
+
+ <div id="previewWrapper">
+ <h3>Preview:</h3>
+ <div class="imgCrop_previewWrap">
+ <img src="castle.jpg" alt="test image" id="previewImage" />
+ </div>
+ </div>
+</body>
+</html>
+
+