├── .gitignore ├── images ├── cross.png ├── accept.png ├── delete.png ├── asterisk_yellow.png └── trafalgar-square-annotated.jpg ├── php-api ├── get.php ├── delete.php ├── save.php └── jQueryAnnotate.class.php ├── example_usage.html ├── README.markdown ├── css └── annotation.css └── js ├── jquery.annotate.js └── jquery-1.3.2.js /.gitignore: -------------------------------------------------------------------------------- 1 | php-api/*.csv 2 | -------------------------------------------------------------------------------- /images/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stas/jquery-image-annotate-php-fork/HEAD/images/cross.png -------------------------------------------------------------------------------- /images/accept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stas/jquery-image-annotate-php-fork/HEAD/images/accept.png -------------------------------------------------------------------------------- /images/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stas/jquery-image-annotate-php-fork/HEAD/images/delete.png -------------------------------------------------------------------------------- /images/asterisk_yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stas/jquery-image-annotate-php-fork/HEAD/images/asterisk_yellow.png -------------------------------------------------------------------------------- /php-api/get.php: -------------------------------------------------------------------------------- 1 | get(); 5 | ?> 6 | -------------------------------------------------------------------------------- /php-api/delete.php: -------------------------------------------------------------------------------- 1 | delete(); 5 | ?> 6 | -------------------------------------------------------------------------------- /php-api/save.php: -------------------------------------------------------------------------------- 1 | save(); 5 | ?> 6 | -------------------------------------------------------------------------------- /images/trafalgar-square-annotated.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stas/jquery-image-annotate-php-fork/HEAD/images/trafalgar-square-annotated.jpg -------------------------------------------------------------------------------- /example_usage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Image Annotations 4 | 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 |
22 | Trafalgar Square 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ## This is a fork of jQuery Image Annotation plugin with PHP support 2 | 3 | Original author is Chris from [http://blog.flipbit.co.uk](http://blog.flipbit.co.uk) 4 | 5 | Project webpage can be found at [http://code.google.com/p/jquery-image-annotate/](http://code.google.com/p/jquery-image-annotate/) 6 | 7 | Code uses the same licence the plugin was released: GPL 2. 8 | 9 | 10 | ### How it works 11 | 12 | I wrote a class which stores annotations into a csv file (php-api/data.csv) 13 | with full support to delete, save and get the information from it to server 14 | json. 15 | 16 | 17 | ### Who uses it? 18 | 19 | * [Romanian Free Software Group](http://softwareliber.ro) for it's [FLOSSCamp Project](http://camp.softwareliber.ro/2009/membri) 20 | 21 | 22 | ### Future plans 23 | 24 | I hope this fork to be merged into upstream release, otherway I'm not sure I'll have enough time to support it. 25 | 26 | 27 | ### Requirements 28 | 29 | * PHP 5 30 | -------------------------------------------------------------------------------- /php-api/jQueryAnnotate.class.php: -------------------------------------------------------------------------------- 1 | 5 | * @version 0.2 6 | * @since PHP 5.2 7 | */ 8 | 9 | class jQueryAnnotate { 10 | 11 | /** 12 | * Constructor 13 | * Opens and reads items from the data file 14 | * 15 | * @param string $filename of the data file, default is 'data.csv' 16 | * 17 | * @access public 18 | * @return void 19 | */ 20 | function __construct($filename = "data.csv") { 21 | $this->filename = $filename; 22 | $this->file = @fopen($filename, "r+"); 23 | $this->items = $this->csv_to_array(); 24 | } 25 | 26 | /** 27 | * Outputs data file content in a JSON format 28 | * 29 | * @access public 30 | * @return void 31 | */ 32 | function get() { 33 | $items = $this->items; 34 | echo "[\n"; 35 | foreach($items as $item) { 36 | if($item) { 37 | $a = explode("\t", $item); 38 | echo $this->format($a); 39 | } 40 | } 41 | echo "]\n"; 42 | } 43 | 44 | /** 45 | * Adds a new annotation into the data file 46 | * All HTML and special characters are stripped out 47 | * As a result outputs the new saved id of the annotation 48 | * 49 | * @access public 50 | * @return void 51 | */ 52 | function save() { 53 | $data = array( 54 | $_GET["top"], 55 | $_GET["left"], 56 | $_GET["width"], 57 | $_GET["height"], 58 | $this->html2txt($_GET["text"]), 59 | "id_".md5($_GET["text"]), 60 | "true" 61 | ); 62 | 63 | $this->delete($data); // Delete previous entry 64 | if($data[4] && file_put_contents( $this->filename, implode("\t",$data)."\n", FILE_APPEND | LOCK_EX )) 65 | echo '{ "annotation_id": "id_'.md5($_GET["text"]).'" }'; 66 | } 67 | 68 | /** 69 | * Deletes an annotation from the data file 70 | * As a result outputs a 200 HTTP header 71 | * 72 | * @access public 73 | * @return void 74 | */ 75 | function delete($to_delete_old = null) { 76 | if(!$to_delete_old) 77 | $to_delete = array( 78 | $_GET["top"], 79 | $_GET["left"], 80 | $_GET["width"], 81 | $_GET["height"], 82 | $_GET["text"], 83 | $_GET["id"] 84 | ); 85 | 86 | $items = $this->items; 87 | $i = 0; 88 | foreach($items as $item) { 89 | if($item) { 90 | $item = explode("\t", $item); 91 | if( !$to_delete_old ) { 92 | if( ($item[5] == $to_delete[5]) && ($item[6] == "true\n") ) 93 | unset($items[$i]); 94 | } 95 | else { 96 | // If top and left exist already, than delete them due to exclude clones 97 | if( ($item[0] == $to_delete_old[0]) && ($item[1] == $to_delete_old[1]) && ($item[6] == "true\n") ) 98 | unset($items[$i]); 99 | } 100 | } 101 | $i++; 102 | } 103 | if( file_put_contents( $this->filename, implode("",$items), LOCK_EX )) 104 | header("HTTP/1.0 200 OK"); 105 | } 106 | 107 | /** 108 | * Converts the csv content from data file to an array 109 | * 110 | * @access public 111 | * @return array 112 | */ 113 | function csv_to_array() { 114 | if($this->file) { 115 | $csv_array = array(); 116 | while (!feof($this->file)) { 117 | $csv_array[] = fgets($this->file); 118 | } 119 | } 120 | else 121 | $this->print_error("Could not open file."); 122 | return $csv_array; 123 | } 124 | 125 | /** 126 | * Converts an array to a JSON required format 127 | * 128 | * @param array $t item from the data file content array 129 | * @access public 130 | * @return string 131 | */ 132 | function format($t) { 133 | if(is_array($t) && count($t) == 7) 134 | return " 135 | { 136 | \"top\": $t[0], 137 | \"left\": $t[1], 138 | \"width\": $t[2], 139 | \"height\": $t[3], 140 | \"text\": \"$t[4]\", 141 | \"id\": \"$t[5]\", 142 | \"editable\": $t[6] 143 | }, 144 | "; 145 | } 146 | 147 | /** 148 | * Strip a string from unwanted tags and special characters 149 | * 150 | * @param string $text to be cleared 151 | * @access public 152 | * @return string 153 | */ 154 | function html2txt($text) { 155 | $search = array ('@]*?>.*?@si', // Strip out javascript 156 | '@<[\/\!]*?[^<>]*?>@si', // Strip out HTML tags 157 | '@([\r\n])[\s]+@', // Strip out white space 158 | '@&(quot|#34);@i', // Replace HTML entities 159 | '@&(lt|#60);@i', 160 | '@&(gt|#62);@i', 161 | '@&(nbsp|#160);@i', 162 | '@&#(\d+);@e'); // evaluate as php 163 | 164 | $replace = array ('', 165 | '', 166 | '\1', 167 | '"', 168 | '<', 169 | '>', 170 | ' ', 171 | 'chr(\1)'); 172 | 173 | return trim(preg_replace($search, $replace, $text)); 174 | } 175 | 176 | /** 177 | * Prints a
 formated error message
178 | 	 *
179 | 	 * @param string $m message to be printed as an error
180 | 	 * @access public
181 | 	 * @return string
182 | 	 */
183 | 	function print_error($m) {
184 | 		die("
$m
\n"); 185 | } 186 | 187 | /** 188 | * Destructor 189 | * Closes the file the constructor opened. 190 | * 191 | * @access public 192 | * @return void 193 | */ 194 | function __destruct() { 195 | fclose($this->file); 196 | } 197 | } 198 | ?> 199 | -------------------------------------------------------------------------------- /css/annotation.css: -------------------------------------------------------------------------------- 1 | .image-annotate-add { 2 | background: #fff url(../images/asterisk_yellow.png) no-repeat 3px 3px; 3 | border: solid 1px #ccc !important; 4 | color: #000 !important; 5 | cursor: pointer; 6 | display: block; 7 | float: left; 8 | font-family: Verdana, Sans-Serif; 9 | font-size: 12px; 10 | height: 18px; 11 | line-height: 18px; 12 | padding: 2px 0 2px 24px; 13 | margin: 5px 0; 14 | width: 64px; 15 | text-decoration: none; 16 | } 17 | .image-annotate-add:hover { 18 | background-color: #eee; 19 | } 20 | .image-annotate-canvas { 21 | border: solid 1px #ccc; 22 | background-position: left top; 23 | background-repeat: no-repeat; 24 | display: block; 25 | margin: 0; 26 | position: relative; 27 | } 28 | .image-annotate-view { 29 | display: none; 30 | position: relative; 31 | } 32 | .image-annotate-area { 33 | border: 1px solid #000000; 34 | position: absolute; 35 | } 36 | .image-annotate-area div { 37 | border: 1px solid #FFFFFF; 38 | display: block; 39 | } 40 | .image-annotate-area-hover div { 41 | border-color: yellow !important; 42 | } 43 | .image-annotate-area-editable { 44 | cursor: pointer; 45 | } 46 | .image-annotate-area-editable-hover div { 47 | border-color: #00AD00 !important; 48 | } 49 | .image-annotate-note { 50 | background: #E7FFE7 none repeat scroll 0 0; 51 | border: solid 1px #397F39; 52 | color: #000; 53 | display: none; 54 | font-family: Verdana, Sans-Serif; 55 | font-size: 12px; 56 | max-width: 200px; 57 | padding: 3px 7px; 58 | position: absolute; 59 | } 60 | .image-annotate-note .actions { 61 | display: block; 62 | font-size: 80%; 63 | } 64 | .image-annotate-edit { 65 | display: none; 66 | } 67 | #image-annotate-edit-form { 68 | background: #FFFEE3 none repeat scroll 0 0; 69 | border: 1px solid #000000; 70 | height: 78px; 71 | padding: 7px; 72 | position: absolute; 73 | width: 250px; 74 | } 75 | #image-annotate-edit-form form { 76 | clear: right; 77 | margin: 0 !important; 78 | padding: 0; 79 | z-index: 999; 80 | } 81 | #image-annotate-edit-form .box { 82 | margin: 0; 83 | } 84 | #image-annotate-edit-form input.form-text, #image-annotate-edit-form #edit-comment-wrapper textarea { 85 | width: 90%; 86 | } 87 | #image-annotate-edit-form textarea { 88 | height: 50px; 89 | font-family: Verdana, Sans-Serif; 90 | font-size: 12px; 91 | width: 248px; 92 | } 93 | #image-annotate-edit-form fieldset { 94 | background: transparent none repeat scroll 0 0; 95 | } 96 | #image-annotate-edit-form .form-item { 97 | margin: 0 0 5px; 98 | } 99 | #image-annotate-edit-form .form-button, #image-annotate-edit-form .form-submit { 100 | margin: 0; 101 | } 102 | #image-annotate-edit-form a { 103 | background-color: #fff; 104 | background-repeat: no-repeat; 105 | background-position: 3px 3px; 106 | border: solid 1px #ccc; 107 | color: #333; 108 | cursor: pointer; 109 | display: block; 110 | float: left; 111 | font-family: Verdana, Sans-Serif; 112 | font-size: 12px; 113 | height: 18px; 114 | line-height: 18px; 115 | padding: 2px 0 2px 24px; 116 | margin: 3px 6px 3px 0; 117 | width: 48px; 118 | } 119 | #image-annotate-edit-form a:hover { 120 | background-color: #eee; 121 | } 122 | .image-annotate-edit-area { 123 | border: 1px solid black; 124 | cursor: move; 125 | display: block; 126 | height: 60px; 127 | left: 10px; 128 | margin: 0; 129 | padding: 0; 130 | position: absolute; 131 | top: 10px; 132 | width: 60px; 133 | } 134 | .image-annotate-edit-area .ui-resizable-handle { 135 | opacity: 0.8; 136 | } 137 | .image-annotate-edit-ok { 138 | background-image: url(../images/accept.png); 139 | } 140 | .image-annotate-edit-delete { 141 | background-image: url(../images/delete.png); 142 | } 143 | .image-annotate-edit-close { 144 | background-image: url(../images/cross.png); 145 | } 146 | .ui-resizable { 147 | position: relative; 148 | } 149 | .ui-resizable-handle { 150 | position: absolute; 151 | font-size: 0.1px; 152 | z-index: 99999; 153 | display: block; 154 | } 155 | .ui-resizable-disabled .ui-resizable-handle, .ui-resizable- autohide .ui-resizable-handle { 156 | display: block; 157 | } 158 | .ui-resizable-n { 159 | cursor: n-resize; 160 | height: 7px; 161 | width: 100%; 162 | top: -5px; 163 | left: 0px; 164 | } 165 | .ui-resizable-s { 166 | cursor: s-resize; 167 | height: 7px; 168 | width: 100%; 169 | bottom: -5px; 170 | left: 0px; 171 | } 172 | .ui-resizable-e { 173 | cursor: e-resize; 174 | width: 7px; 175 | right: -5px; 176 | top: 0px; 177 | height: 100%; 178 | } 179 | .ui-resizable-w { 180 | cursor: w-resize; 181 | width: 7px; 182 | left: -5px; 183 | top: 0px; 184 | height: 100%; 185 | } 186 | .ui-resizable-se { 187 | cursor: se-resize; 188 | width: 12px; 189 | height: 12px; 190 | right: 1px; 191 | bottom: 1px; 192 | } 193 | .ui-resizable-sw { 194 | cursor: sw-resize; 195 | width: 9px; 196 | height: 9px; 197 | left: -5px; 198 | bottom: -5px; 199 | } 200 | .ui-resizable-nw { 201 | cursor: nw-resize; 202 | width: 9px; 203 | height: 9px; 204 | left: -5px; 205 | top: -5px; 206 | } 207 | .ui-resizable-ne { 208 | cursor: ne-resize; 209 | width: 9px; 210 | height: 9px; 211 | right: -5px; 212 | top: -5px; 213 | } -------------------------------------------------------------------------------- /js/jquery.annotate.js: -------------------------------------------------------------------------------- 1 | /// 2 | (function($) { 3 | 4 | $.fn.annotateImage = function(options) { 5 | /// 6 | /// Creates annotations on the given image. 7 | /// Images are loaded from the "getUrl" propety passed into the options. 8 | /// 9 | var opts = $.extend({}, $.fn.annotateImage.defaults, options); 10 | var image = this; 11 | 12 | this.image = this; 13 | this.mode = 'view'; 14 | 15 | // Assign defaults 16 | this.getUrl = opts.getUrl; 17 | this.saveUrl = opts.saveUrl; 18 | this.deleteUrl = opts.deleteUrl; 19 | this.editable = opts.editable; 20 | this.useAjax = opts.useAjax; 21 | this.notes = opts.notes; 22 | 23 | // Add the canvas 24 | this.canvas = $('
'); 25 | this.canvas.children('.image-annotate-edit').hide(); 26 | this.canvas.children('.image-annotate-view').hide(); 27 | this.image.after(this.canvas); 28 | 29 | // Give the canvas and the container their size and background 30 | this.canvas.height(this.height()); 31 | this.canvas.width(this.width()); 32 | this.canvas.css('background-image', 'url("' + this.attr('src') + '")'); 33 | this.canvas.children('.image-annotate-view, .image-annotate-edit').height(this.height()); 34 | this.canvas.children('.image-annotate-view, .image-annotate-edit').width(this.width()); 35 | 36 | // Add the behavior: hide/show the notes when hovering the picture 37 | this.canvas.hover(function() { 38 | if ($(this).children('.image-annotate-edit').css('display') == 'none') { 39 | $(this).children('.image-annotate-view').show(); 40 | } 41 | }, function() { 42 | $(this).children('.image-annotate-view').hide(); 43 | }); 44 | 45 | this.canvas.children('.image-annotate-view').hover(function() { 46 | $(this).show(); 47 | }, function() { 48 | $(this).hide(); 49 | }); 50 | 51 | // load the notes 52 | if (this.useAjax) { 53 | $.fn.annotateImage.ajaxLoad(this); 54 | } else { 55 | $.fn.annotateImage.load(this); 56 | } 57 | 58 | // Add the "Add a note" button 59 | if (this.editable) { 60 | this.button = $('Add Note'); 61 | this.button.click(function() { 62 | $.fn.annotateImage.add(image); 63 | }); 64 | this.canvas.after(this.button); 65 | } 66 | 67 | // Hide the original 68 | this.hide(); 69 | 70 | return this; 71 | }; 72 | 73 | /** 74 | * Plugin Defaults 75 | **/ 76 | $.fn.annotateImage.defaults = { 77 | getUrl: 'your-get.rails', 78 | saveUrl: 'your-save.rails', 79 | deleteUrl: 'your-delete.rails', 80 | editable: true, 81 | useAjax: true, 82 | notes: new Array() 83 | }; 84 | 85 | $.fn.annotateImage.clear = function(image) { 86 | /// 87 | /// Clears all existing annotations from the image. 88 | /// 89 | for (var i = 0; i < image.notes.length; i++) { 90 | image.notes[image.notes[i]].destroy(); 91 | } 92 | image.notes = new Array(); 93 | }; 94 | 95 | $.fn.annotateImage.ajaxLoad = function(image) { 96 | /// 97 | /// Loads the annotations from the "getUrl" property passed in on the 98 | /// options object. 99 | /// 100 | $.getJSON(image.getUrl + '?ticks=' + $.fn.annotateImage.getTicks(), function(data) { 101 | image.notes = data; 102 | $.fn.annotateImage.load(image); 103 | }); 104 | }; 105 | 106 | $.fn.annotateImage.load = function(image) { 107 | /// 108 | /// Loads the annotations from the notes property passed in on the 109 | /// options object. 110 | /// 111 | for (var i = 0; i < image.notes.length; i++) { 112 | image.notes[image.notes[i]] = new $.fn.annotateView(image, image.notes[i]); 113 | } 114 | }; 115 | 116 | $.fn.annotateImage.getTicks = function() { 117 | /// 118 | /// Gets a count og the ticks for the current date. 119 | /// This is used to ensure that URLs are always unique and not cached by the browser. 120 | /// 121 | var now = new Date(); 122 | return now.getTime(); 123 | }; 124 | 125 | $.fn.annotateImage.add = function(image) { 126 | /// 127 | /// Adds a note to the image. 128 | /// 129 | if (image.mode == 'view') { 130 | image.mode = 'edit'; 131 | 132 | // Create/prepare the editable note elements 133 | var editable = new $.fn.annotateEdit(image); 134 | 135 | $.fn.annotateImage.createSaveButton(editable, image); 136 | $.fn.annotateImage.createCancelButton(editable, image); 137 | } 138 | }; 139 | 140 | $.fn.annotateImage.createSaveButton = function(editable, image, note) { 141 | /// 142 | /// Creates a Save button on the editable note. 143 | /// 144 | var ok = $('OK'); 145 | 146 | ok.click(function() { 147 | var form = $('#image-annotate-edit-form form'); 148 | var text = $('#image-annotate-text').val(); 149 | $.fn.annotateImage.appendPosition(form, editable) 150 | image.mode = 'view'; 151 | 152 | // Save via AJAX 153 | if (image.useAjax) { 154 | $.ajax({ 155 | url: image.saveUrl, 156 | data: form.serialize(), 157 | error: function(e) { alert("An error occured saving that note.") }, 158 | success: function(data) { 159 | if (data.annotation_id != undefined) { 160 | editable.note.id = data.annotation_id; 161 | } 162 | }, 163 | dataType: "json" 164 | }); 165 | } 166 | 167 | // Add to canvas 168 | if (note) { 169 | note.resetPosition(editable, text); 170 | } else { 171 | editable.note.editable = true; 172 | note = new $.fn.annotateView(image, editable.note) 173 | note.resetPosition(editable, text); 174 | image.notes.push(editable.note); 175 | } 176 | 177 | editable.destroy(); 178 | }); 179 | editable.form.append(ok); 180 | }; 181 | 182 | $.fn.annotateImage.createCancelButton = function(editable, image) { 183 | /// 184 | /// Creates a Cancel button on the editable note. 185 | /// 186 | var cancel = $('Cancel'); 187 | cancel.click(function() { 188 | editable.destroy(); 189 | image.mode = 'view'; 190 | }); 191 | editable.form.append(cancel); 192 | }; 193 | 194 | $.fn.annotateImage.saveAsHtml = function(image, target) { 195 | var element = $(target); 196 | var html = ""; 197 | for (var i = 0; i < image.notes.length; i++) { 198 | html += $.fn.annotateImage.createHiddenField("text_" + i, image.notes[i].text); 199 | html += $.fn.annotateImage.createHiddenField("top_" + i, image.notes[i].top); 200 | html += $.fn.annotateImage.createHiddenField("left_" + i, image.notes[i].left); 201 | html += $.fn.annotateImage.createHiddenField("height_" + i, image.notes[i].height); 202 | html += $.fn.annotateImage.createHiddenField("width_" + i, image.notes[i].width); 203 | } 204 | element.html(html); 205 | }; 206 | 207 | $.fn.annotateImage.createHiddenField = function(name, value) { 208 | return '<input type="hidden" name="' + name + '" value="' + value + '" />
'; 209 | }; 210 | 211 | $.fn.annotateEdit = function(image, note) { 212 | /// 213 | /// Defines an editable annotation area. 214 | /// 215 | this.image = image; 216 | 217 | if (note) { 218 | this.note = note; 219 | } else { 220 | var newNote = new Object(); 221 | newNote.id = "new"; 222 | newNote.top = 30; 223 | newNote.left = 30; 224 | newNote.width = 30; 225 | newNote.height = 30; 226 | newNote.text = ""; 227 | this.note = newNote; 228 | } 229 | 230 | // Set area 231 | var area = image.canvas.children('.image-annotate-edit').children('.image-annotate-edit-area'); 232 | this.area = area; 233 | this.area.css('height', this.note.height + 'px'); 234 | this.area.css('width', this.note.width + 'px'); 235 | this.area.css('left', this.note.left + 'px'); 236 | this.area.css('top', this.note.top + 'px'); 237 | 238 | // Show the edition canvas and hide the view canvas 239 | image.canvas.children('.image-annotate-view').hide(); 240 | image.canvas.children('.image-annotate-edit').show(); 241 | 242 | // Add the note (which we'll load with the form afterwards) 243 | var form = $('
'); 244 | this.form = form; 245 | 246 | $('body').append(this.form); 247 | this.form.css('left', this.area.offset().left + 'px'); 248 | this.form.css('top', (parseInt(this.area.offset().top) + parseInt(this.area.height()) + 7) + 'px'); 249 | 250 | // Set the area as a draggable/resizable element contained in the image canvas. 251 | // Would be better to use the containment option for resizable but buggy 252 | area.resizable({ 253 | handles: 'all', 254 | 255 | stop: function(e, ui) { 256 | form.css('left', area.offset().left + 'px'); 257 | form.css('top', (parseInt(area.offset().top) + parseInt(area.height()) + 2) + 'px'); 258 | } 259 | }) 260 | .draggable({ 261 | containment: image.canvas, 262 | drag: function(e, ui) { 263 | form.css('left', area.offset().left + 'px'); 264 | form.css('top', (parseInt(area.offset().top) + parseInt(area.height()) + 2) + 'px'); 265 | }, 266 | stop: function(e, ui) { 267 | form.css('left', area.offset().left + 'px'); 268 | form.css('top', (parseInt(area.offset().top) + parseInt(area.height()) + 2) + 'px'); 269 | } 270 | }); 271 | return this; 272 | }; 273 | 274 | $.fn.annotateEdit.prototype.destroy = function() { 275 | /// 276 | /// Destroys an editable annotation area. 277 | /// 278 | this.image.canvas.children('.image-annotate-edit').hide(); 279 | this.area.resizable('destroy'); 280 | this.area.draggable('destroy'); 281 | this.area.css('height', ''); 282 | this.area.css('width', ''); 283 | this.area.css('left', ''); 284 | this.area.css('top', ''); 285 | this.form.remove(); 286 | } 287 | 288 | $.fn.annotateView = function(image, note) { 289 | /// 290 | /// Defines a annotation area. 291 | /// 292 | this.image = image; 293 | 294 | this.note = note; 295 | 296 | this.editable = (note.editable && image.editable); 297 | 298 | // Add the area 299 | this.area = $('
'); 300 | image.canvas.children('.image-annotate-view').prepend(this.area); 301 | 302 | // Add the note 303 | this.form = $('
' + note.text + '
'); 304 | this.form.hide(); 305 | image.canvas.children('.image-annotate-view').append(this.form); 306 | this.form.children('span.actions').hide(); 307 | 308 | // Set the position and size of the note 309 | this.setPosition(); 310 | 311 | // Add the behavior: hide/display the note when hovering the area 312 | var annotation = this; 313 | this.area.hover(function() { 314 | annotation.show(); 315 | }, function() { 316 | annotation.hide(); 317 | }); 318 | 319 | // Edit a note feature 320 | if (this.editable) { 321 | var form = this; 322 | this.area.click(function() { 323 | form.edit(); 324 | }); 325 | } 326 | }; 327 | 328 | $.fn.annotateView.prototype.setPosition = function() { 329 | /// 330 | /// Sets the position of an annotation. 331 | /// 332 | this.area.children('div').height((parseInt(this.note.height) - 2) + 'px'); 333 | this.area.children('div').width((parseInt(this.note.width) - 2) + 'px'); 334 | this.area.css('left', (this.note.left) + 'px'); 335 | this.area.css('top', (this.note.top) + 'px'); 336 | this.form.css('left', (this.note.left) + 'px'); 337 | this.form.css('top', (parseInt(this.note.top) + parseInt(this.note.height) + 7) + 'px'); 338 | }; 339 | 340 | $.fn.annotateView.prototype.show = function() { 341 | /// 342 | /// Highlights the annotation 343 | /// 344 | this.form.fadeIn(250); 345 | if (!this.editable) { 346 | this.area.addClass('image-annotate-area-hover'); 347 | } else { 348 | this.area.addClass('image-annotate-area-editable-hover'); 349 | } 350 | }; 351 | 352 | $.fn.annotateView.prototype.hide = function() { 353 | /// 354 | /// Removes the highlight from the annotation. 355 | /// 356 | this.form.fadeOut(250); 357 | this.area.removeClass('image-annotate-area-hover'); 358 | this.area.removeClass('image-annotate-area-editable-hover'); 359 | }; 360 | 361 | $.fn.annotateView.prototype.destroy = function() { 362 | /// 363 | /// Destroys the annotation. 364 | /// 365 | this.area.remove(); 366 | this.form.remove(); 367 | } 368 | 369 | $.fn.annotateView.prototype.edit = function() { 370 | /// 371 | /// Edits the annotation. 372 | /// 373 | if (this.image.mode == 'view') { 374 | this.image.mode = 'edit'; 375 | var annotation = this; 376 | 377 | // Create/prepare the editable note elements 378 | var editable = new $.fn.annotateEdit(this.image, this.note); 379 | 380 | $.fn.annotateImage.createSaveButton(editable, this.image, annotation); 381 | 382 | // Add the delete button 383 | var del = $('Delete'); 384 | del.click(function() { 385 | var form = $('#image-annotate-edit-form form'); 386 | 387 | $.fn.annotateImage.appendPosition(form, editable) 388 | 389 | if (annotation.image.useAjax) { 390 | $.ajax({ 391 | url: annotation.image.deleteUrl, 392 | data: form.serialize(), 393 | error: function(e) { alert("An error occured deleting that note.") } 394 | }); 395 | } 396 | 397 | annotation.image.mode = 'view'; 398 | editable.destroy(); 399 | annotation.destroy(); 400 | }); 401 | editable.form.append(del); 402 | 403 | $.fn.annotateImage.createCancelButton(editable, this.image); 404 | } 405 | }; 406 | 407 | $.fn.annotateImage.appendPosition = function(form, editable) { 408 | /// 409 | /// Appends the annotations coordinates to the given form that is posted to the server. 410 | /// 411 | var areaFields = $('' + 412 | '' + 413 | '' + 414 | '' + 415 | ''); 416 | form.append(areaFields); 417 | } 418 | 419 | $.fn.annotateView.prototype.resetPosition = function(editable, text) { 420 | /// 421 | /// Sets the position of an annotation. 422 | /// 423 | this.form.html(text); 424 | this.form.hide(); 425 | 426 | // Resize 427 | this.area.children('div').height(editable.area.height() + 'px'); 428 | this.area.children('div').width((editable.area.width() - 2) + 'px'); 429 | this.area.css('left', (editable.area.position().left) + 'px'); 430 | this.area.css('top', (editable.area.position().top) + 'px'); 431 | this.form.css('left', (editable.area.position().left) + 'px'); 432 | this.form.css('top', (parseInt(editable.area.position().top) + parseInt(editable.area.height()) + 7) + 'px'); 433 | 434 | // Save new position to note 435 | this.note.top = editable.area.position().top; 436 | this.note.left = editable.area.position().left; 437 | this.note.height = editable.area.height(); 438 | this.note.width = editable.area.width(); 439 | this.note.text = text; 440 | this.note.id = editable.note.id; 441 | this.editable = true; 442 | }; 443 | 444 | })(jQuery); -------------------------------------------------------------------------------- /js/jquery-1.3.2.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery JavaScript Library v1.3.2 3 | * http://jquery.com/ 4 | * 5 | * Copyright (c) 2009 John Resig 6 | * Dual licensed under the MIT and GPL licenses. 7 | * http://docs.jquery.com/License 8 | * 9 | * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009) 10 | * Revision: 6246 11 | */ 12 | (function(){ 13 | 14 | var 15 | // Will speed up references to window, and allows munging its name. 16 | window = this, 17 | // Will speed up references to undefined, and allows munging its name. 18 | undefined, 19 | // Map over jQuery in case of overwrite 20 | _jQuery = window.jQuery, 21 | // Map over the $ in case of overwrite 22 | _$ = window.$, 23 | 24 | jQuery = window.jQuery = window.$ = function( selector, context ) { 25 | // The jQuery object is actually just the init constructor 'enhanced' 26 | return new jQuery.fn.init( selector, context ); 27 | }, 28 | 29 | // A simple way to check for HTML strings or ID strings 30 | // (both of which we optimize for) 31 | quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/, 32 | // Is it a simple selector 33 | isSimple = /^.[^:#\[\.,]*$/; 34 | 35 | jQuery.fn = jQuery.prototype = { 36 | init: function( selector, context ) { 37 | // Make sure that a selection was provided 38 | selector = selector || document; 39 | 40 | // Handle $(DOMElement) 41 | if ( selector.nodeType ) { 42 | this[0] = selector; 43 | this.length = 1; 44 | this.context = selector; 45 | return this; 46 | } 47 | // Handle HTML strings 48 | if ( typeof selector === "string" ) { 49 | // Are we dealing with HTML string or an ID? 50 | var match = quickExpr.exec( selector ); 51 | 52 | // Verify a match, and that no context was specified for #id 53 | if ( match && (match[1] || !context) ) { 54 | 55 | // HANDLE: $(html) -> $(array) 56 | if ( match[1] ) 57 | selector = jQuery.clean( [ match[1] ], context ); 58 | 59 | // HANDLE: $("#id") 60 | else { 61 | var elem = document.getElementById( match[3] ); 62 | 63 | // Handle the case where IE and Opera return items 64 | // by name instead of ID 65 | if ( elem && elem.id != match[3] ) 66 | return jQuery().find( selector ); 67 | 68 | // Otherwise, we inject the element directly into the jQuery object 69 | var ret = jQuery( elem || [] ); 70 | ret.context = document; 71 | ret.selector = selector; 72 | return ret; 73 | } 74 | 75 | // HANDLE: $(expr, [context]) 76 | // (which is just equivalent to: $(content).find(expr) 77 | } else 78 | return jQuery( context ).find( selector ); 79 | 80 | // HANDLE: $(function) 81 | // Shortcut for document ready 82 | } else if ( jQuery.isFunction( selector ) ) 83 | return jQuery( document ).ready( selector ); 84 | 85 | // Make sure that old selector state is passed along 86 | if ( selector.selector && selector.context ) { 87 | this.selector = selector.selector; 88 | this.context = selector.context; 89 | } 90 | 91 | return this.setArray(jQuery.isArray( selector ) ? 92 | selector : 93 | jQuery.makeArray(selector)); 94 | }, 95 | 96 | // Start with an empty selector 97 | selector: "", 98 | 99 | // The current version of jQuery being used 100 | jquery: "1.3.2", 101 | 102 | // The number of elements contained in the matched element set 103 | size: function() { 104 | return this.length; 105 | }, 106 | 107 | // Get the Nth element in the matched element set OR 108 | // Get the whole matched element set as a clean array 109 | get: function( num ) { 110 | return num === undefined ? 111 | 112 | // Return a 'clean' array 113 | Array.prototype.slice.call( this ) : 114 | 115 | // Return just the object 116 | this[ num ]; 117 | }, 118 | 119 | // Take an array of elements and push it onto the stack 120 | // (returning the new matched element set) 121 | pushStack: function( elems, name, selector ) { 122 | // Build a new jQuery matched element set 123 | var ret = jQuery( elems ); 124 | 125 | // Add the old object onto the stack (as a reference) 126 | ret.prevObject = this; 127 | 128 | ret.context = this.context; 129 | 130 | if ( name === "find" ) 131 | ret.selector = this.selector + (this.selector ? " " : "") + selector; 132 | else if ( name ) 133 | ret.selector = this.selector + "." + name + "(" + selector + ")"; 134 | 135 | // Return the newly-formed element set 136 | return ret; 137 | }, 138 | 139 | // Force the current matched set of elements to become 140 | // the specified array of elements (destroying the stack in the process) 141 | // You should use pushStack() in order to do this, but maintain the stack 142 | setArray: function( elems ) { 143 | // Resetting the length to 0, then using the native Array push 144 | // is a super-fast way to populate an object with array-like properties 145 | this.length = 0; 146 | Array.prototype.push.apply( this, elems ); 147 | 148 | return this; 149 | }, 150 | 151 | // Execute a callback for every element in the matched set. 152 | // (You can seed the arguments with an array of args, but this is 153 | // only used internally.) 154 | each: function( callback, args ) { 155 | return jQuery.each( this, callback, args ); 156 | }, 157 | 158 | // Determine the position of an element within 159 | // the matched set of elements 160 | index: function( elem ) { 161 | // Locate the position of the desired element 162 | return jQuery.inArray( 163 | // If it receives a jQuery object, the first element is used 164 | elem && elem.jquery ? elem[0] : elem 165 | , this ); 166 | }, 167 | 168 | attr: function( name, value, type ) { 169 | var options = name; 170 | 171 | // Look for the case where we're accessing a style value 172 | if ( typeof name === "string" ) 173 | if ( value === undefined ) 174 | return this[0] && jQuery[ type || "attr" ]( this[0], name ); 175 | 176 | else { 177 | options = {}; 178 | options[ name ] = value; 179 | } 180 | 181 | // Check to see if we're setting style values 182 | return this.each(function(i){ 183 | // Set all the styles 184 | for ( name in options ) 185 | jQuery.attr( 186 | type ? 187 | this.style : 188 | this, 189 | name, jQuery.prop( this, options[ name ], type, i, name ) 190 | ); 191 | }); 192 | }, 193 | 194 | css: function( key, value ) { 195 | // ignore negative width and height values 196 | if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 ) 197 | value = undefined; 198 | return this.attr( key, value, "curCSS" ); 199 | }, 200 | 201 | text: function( text ) { 202 | if ( typeof text !== "object" && text != null ) 203 | return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); 204 | 205 | var ret = ""; 206 | 207 | jQuery.each( text || this, function(){ 208 | jQuery.each( this.childNodes, function(){ 209 | if ( this.nodeType != 8 ) 210 | ret += this.nodeType != 1 ? 211 | this.nodeValue : 212 | jQuery.fn.text( [ this ] ); 213 | }); 214 | }); 215 | 216 | return ret; 217 | }, 218 | 219 | wrapAll: function( html ) { 220 | if ( this[0] ) { 221 | // The elements to wrap the target around 222 | var wrap = jQuery( html, this[0].ownerDocument ).clone(); 223 | 224 | if ( this[0].parentNode ) 225 | wrap.insertBefore( this[0] ); 226 | 227 | wrap.map(function(){ 228 | var elem = this; 229 | 230 | while ( elem.firstChild ) 231 | elem = elem.firstChild; 232 | 233 | return elem; 234 | }).append(this); 235 | } 236 | 237 | return this; 238 | }, 239 | 240 | wrapInner: function( html ) { 241 | return this.each(function(){ 242 | jQuery( this ).contents().wrapAll( html ); 243 | }); 244 | }, 245 | 246 | wrap: function( html ) { 247 | return this.each(function(){ 248 | jQuery( this ).wrapAll( html ); 249 | }); 250 | }, 251 | 252 | append: function() { 253 | return this.domManip(arguments, true, function(elem){ 254 | if (this.nodeType == 1) 255 | this.appendChild( elem ); 256 | }); 257 | }, 258 | 259 | prepend: function() { 260 | return this.domManip(arguments, true, function(elem){ 261 | if (this.nodeType == 1) 262 | this.insertBefore( elem, this.firstChild ); 263 | }); 264 | }, 265 | 266 | before: function() { 267 | return this.domManip(arguments, false, function(elem){ 268 | this.parentNode.insertBefore( elem, this ); 269 | }); 270 | }, 271 | 272 | after: function() { 273 | return this.domManip(arguments, false, function(elem){ 274 | this.parentNode.insertBefore( elem, this.nextSibling ); 275 | }); 276 | }, 277 | 278 | end: function() { 279 | return this.prevObject || jQuery( [] ); 280 | }, 281 | 282 | // For internal use only. 283 | // Behaves like an Array's method, not like a jQuery method. 284 | push: [].push, 285 | sort: [].sort, 286 | splice: [].splice, 287 | 288 | find: function( selector ) { 289 | if ( this.length === 1 ) { 290 | var ret = this.pushStack( [], "find", selector ); 291 | ret.length = 0; 292 | jQuery.find( selector, this[0], ret ); 293 | return ret; 294 | } else { 295 | return this.pushStack( jQuery.unique(jQuery.map(this, function(elem){ 296 | return jQuery.find( selector, elem ); 297 | })), "find", selector ); 298 | } 299 | }, 300 | 301 | clone: function( events ) { 302 | // Do the clone 303 | var ret = this.map(function(){ 304 | if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { 305 | // IE copies events bound via attachEvent when 306 | // using cloneNode. Calling detachEvent on the 307 | // clone will also remove the events from the orignal 308 | // In order to get around this, we use innerHTML. 309 | // Unfortunately, this means some modifications to 310 | // attributes in IE that are actually only stored 311 | // as properties will not be copied (such as the 312 | // the name attribute on an input). 313 | var html = this.outerHTML; 314 | if ( !html ) { 315 | var div = this.ownerDocument.createElement("div"); 316 | div.appendChild( this.cloneNode(true) ); 317 | html = div.innerHTML; 318 | } 319 | 320 | return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0]; 321 | } else 322 | return this.cloneNode(true); 323 | }); 324 | 325 | // Copy the events from the original to the clone 326 | if ( events === true ) { 327 | var orig = this.find("*").andSelf(), i = 0; 328 | 329 | ret.find("*").andSelf().each(function(){ 330 | if ( this.nodeName !== orig[i].nodeName ) 331 | return; 332 | 333 | var events = jQuery.data( orig[i], "events" ); 334 | 335 | for ( var type in events ) { 336 | for ( var handler in events[ type ] ) { 337 | jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data ); 338 | } 339 | } 340 | 341 | i++; 342 | }); 343 | } 344 | 345 | // Return the cloned set 346 | return ret; 347 | }, 348 | 349 | filter: function( selector ) { 350 | return this.pushStack( 351 | jQuery.isFunction( selector ) && 352 | jQuery.grep(this, function(elem, i){ 353 | return selector.call( elem, i ); 354 | }) || 355 | 356 | jQuery.multiFilter( selector, jQuery.grep(this, function(elem){ 357 | return elem.nodeType === 1; 358 | }) ), "filter", selector ); 359 | }, 360 | 361 | closest: function( selector ) { 362 | var pos = jQuery.expr.match.POS.test( selector ) ? jQuery(selector) : null, 363 | closer = 0; 364 | 365 | return this.map(function(){ 366 | var cur = this; 367 | while ( cur && cur.ownerDocument ) { 368 | if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selector) ) { 369 | jQuery.data(cur, "closest", closer); 370 | return cur; 371 | } 372 | cur = cur.parentNode; 373 | closer++; 374 | } 375 | }); 376 | }, 377 | 378 | not: function( selector ) { 379 | if ( typeof selector === "string" ) 380 | // test special case where just one selector is passed in 381 | if ( isSimple.test( selector ) ) 382 | return this.pushStack( jQuery.multiFilter( selector, this, true ), "not", selector ); 383 | else 384 | selector = jQuery.multiFilter( selector, this ); 385 | 386 | var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; 387 | return this.filter(function() { 388 | return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector; 389 | }); 390 | }, 391 | 392 | add: function( selector ) { 393 | return this.pushStack( jQuery.unique( jQuery.merge( 394 | this.get(), 395 | typeof selector === "string" ? 396 | jQuery( selector ) : 397 | jQuery.makeArray( selector ) 398 | ))); 399 | }, 400 | 401 | is: function( selector ) { 402 | return !!selector && jQuery.multiFilter( selector, this ).length > 0; 403 | }, 404 | 405 | hasClass: function( selector ) { 406 | return !!selector && this.is( "." + selector ); 407 | }, 408 | 409 | val: function( value ) { 410 | if ( value === undefined ) { 411 | var elem = this[0]; 412 | 413 | if ( elem ) { 414 | if( jQuery.nodeName( elem, 'option' ) ) 415 | return (elem.attributes.value || {}).specified ? elem.value : elem.text; 416 | 417 | // We need to handle select boxes special 418 | if ( jQuery.nodeName( elem, "select" ) ) { 419 | var index = elem.selectedIndex, 420 | values = [], 421 | options = elem.options, 422 | one = elem.type == "select-one"; 423 | 424 | // Nothing was selected 425 | if ( index < 0 ) 426 | return null; 427 | 428 | // Loop through all the selected options 429 | for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { 430 | var option = options[ i ]; 431 | 432 | if ( option.selected ) { 433 | // Get the specifc value for the option 434 | value = jQuery(option).val(); 435 | 436 | // We don't need an array for one selects 437 | if ( one ) 438 | return value; 439 | 440 | // Multi-Selects return an array 441 | values.push( value ); 442 | } 443 | } 444 | 445 | return values; 446 | } 447 | 448 | // Everything else, we just grab the value 449 | return (elem.value || "").replace(/\r/g, ""); 450 | 451 | } 452 | 453 | return undefined; 454 | } 455 | 456 | if ( typeof value === "number" ) 457 | value += ''; 458 | 459 | return this.each(function(){ 460 | if ( this.nodeType != 1 ) 461 | return; 462 | 463 | if ( jQuery.isArray(value) && /radio|checkbox/.test( this.type ) ) 464 | this.checked = (jQuery.inArray(this.value, value) >= 0 || 465 | jQuery.inArray(this.name, value) >= 0); 466 | 467 | else if ( jQuery.nodeName( this, "select" ) ) { 468 | var values = jQuery.makeArray(value); 469 | 470 | jQuery( "option", this ).each(function(){ 471 | this.selected = (jQuery.inArray( this.value, values ) >= 0 || 472 | jQuery.inArray( this.text, values ) >= 0); 473 | }); 474 | 475 | if ( !values.length ) 476 | this.selectedIndex = -1; 477 | 478 | } else 479 | this.value = value; 480 | }); 481 | }, 482 | 483 | html: function( value ) { 484 | return value === undefined ? 485 | (this[0] ? 486 | this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") : 487 | null) : 488 | this.empty().append( value ); 489 | }, 490 | 491 | replaceWith: function( value ) { 492 | return this.after( value ).remove(); 493 | }, 494 | 495 | eq: function( i ) { 496 | return this.slice( i, +i + 1 ); 497 | }, 498 | 499 | slice: function() { 500 | return this.pushStack( Array.prototype.slice.apply( this, arguments ), 501 | "slice", Array.prototype.slice.call(arguments).join(",") ); 502 | }, 503 | 504 | map: function( callback ) { 505 | return this.pushStack( jQuery.map(this, function(elem, i){ 506 | return callback.call( elem, i, elem ); 507 | })); 508 | }, 509 | 510 | andSelf: function() { 511 | return this.add( this.prevObject ); 512 | }, 513 | 514 | domManip: function( args, table, callback ) { 515 | if ( this[0] ) { 516 | var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(), 517 | scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ), 518 | first = fragment.firstChild; 519 | 520 | if ( first ) 521 | for ( var i = 0, l = this.length; i < l; i++ ) 522 | callback.call( root(this[i], first), this.length > 1 || i > 0 ? 523 | fragment.cloneNode(true) : fragment ); 524 | 525 | if ( scripts ) 526 | jQuery.each( scripts, evalScript ); 527 | } 528 | 529 | return this; 530 | 531 | function root( elem, cur ) { 532 | return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ? 533 | (elem.getElementsByTagName("tbody")[0] || 534 | elem.appendChild(elem.ownerDocument.createElement("tbody"))) : 535 | elem; 536 | } 537 | } 538 | }; 539 | 540 | // Give the init function the jQuery prototype for later instantiation 541 | jQuery.fn.init.prototype = jQuery.fn; 542 | 543 | function evalScript( i, elem ) { 544 | if ( elem.src ) 545 | jQuery.ajax({ 546 | url: elem.src, 547 | async: false, 548 | dataType: "script" 549 | }); 550 | 551 | else 552 | jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); 553 | 554 | if ( elem.parentNode ) 555 | elem.parentNode.removeChild( elem ); 556 | } 557 | 558 | function now(){ 559 | return +new Date; 560 | } 561 | 562 | jQuery.extend = jQuery.fn.extend = function() { 563 | // copy reference to target object 564 | var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; 565 | 566 | // Handle a deep copy situation 567 | if ( typeof target === "boolean" ) { 568 | deep = target; 569 | target = arguments[1] || {}; 570 | // skip the boolean and the target 571 | i = 2; 572 | } 573 | 574 | // Handle case when target is a string or something (possible in deep copy) 575 | if ( typeof target !== "object" && !jQuery.isFunction(target) ) 576 | target = {}; 577 | 578 | // extend jQuery itself if only one argument is passed 579 | if ( length == i ) { 580 | target = this; 581 | --i; 582 | } 583 | 584 | for ( ; i < length; i++ ) 585 | // Only deal with non-null/undefined values 586 | if ( (options = arguments[ i ]) != null ) 587 | // Extend the base object 588 | for ( var name in options ) { 589 | var src = target[ name ], copy = options[ name ]; 590 | 591 | // Prevent never-ending loop 592 | if ( target === copy ) 593 | continue; 594 | 595 | // Recurse if we're merging object values 596 | if ( deep && copy && typeof copy === "object" && !copy.nodeType ) 597 | target[ name ] = jQuery.extend( deep, 598 | // Never move original objects, clone them 599 | src || ( copy.length != null ? [ ] : { } ) 600 | , copy ); 601 | 602 | // Don't bring in undefined values 603 | else if ( copy !== undefined ) 604 | target[ name ] = copy; 605 | 606 | } 607 | 608 | // Return the modified object 609 | return target; 610 | }; 611 | 612 | // exclude the following css properties to add px 613 | var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, 614 | // cache defaultView 615 | defaultView = document.defaultView || {}, 616 | toString = Object.prototype.toString; 617 | 618 | jQuery.extend({ 619 | noConflict: function( deep ) { 620 | window.$ = _$; 621 | 622 | if ( deep ) 623 | window.jQuery = _jQuery; 624 | 625 | return jQuery; 626 | }, 627 | 628 | // See test/unit/core.js for details concerning isFunction. 629 | // Since version 1.3, DOM methods and functions like alert 630 | // aren't supported. They return false on IE (#2968). 631 | isFunction: function( obj ) { 632 | return toString.call(obj) === "[object Function]"; 633 | }, 634 | 635 | isArray: function( obj ) { 636 | return toString.call(obj) === "[object Array]"; 637 | }, 638 | 639 | // check if an element is in a (or is an) XML document 640 | isXMLDoc: function( elem ) { 641 | return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || 642 | !!elem.ownerDocument && jQuery.isXMLDoc( elem.ownerDocument ); 643 | }, 644 | 645 | // Evalulates a script in a global context 646 | globalEval: function( data ) { 647 | if ( data && /\S/.test(data) ) { 648 | // Inspired by code by Andrea Giammarchi 649 | // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html 650 | var head = document.getElementsByTagName("head")[0] || document.documentElement, 651 | script = document.createElement("script"); 652 | 653 | script.type = "text/javascript"; 654 | if ( jQuery.support.scriptEval ) 655 | script.appendChild( document.createTextNode( data ) ); 656 | else 657 | script.text = data; 658 | 659 | // Use insertBefore instead of appendChild to circumvent an IE6 bug. 660 | // This arises when a base node is used (#2709). 661 | head.insertBefore( script, head.firstChild ); 662 | head.removeChild( script ); 663 | } 664 | }, 665 | 666 | nodeName: function( elem, name ) { 667 | return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); 668 | }, 669 | 670 | // args is for internal usage only 671 | each: function( object, callback, args ) { 672 | var name, i = 0, length = object.length; 673 | 674 | if ( args ) { 675 | if ( length === undefined ) { 676 | for ( name in object ) 677 | if ( callback.apply( object[ name ], args ) === false ) 678 | break; 679 | } else 680 | for ( ; i < length; ) 681 | if ( callback.apply( object[ i++ ], args ) === false ) 682 | break; 683 | 684 | // A special, fast, case for the most common use of each 685 | } else { 686 | if ( length === undefined ) { 687 | for ( name in object ) 688 | if ( callback.call( object[ name ], name, object[ name ] ) === false ) 689 | break; 690 | } else 691 | for ( var value = object[0]; 692 | i < length && callback.call( value, i, value ) !== false; value = object[++i] ){} 693 | } 694 | 695 | return object; 696 | }, 697 | 698 | prop: function( elem, value, type, i, name ) { 699 | // Handle executable functions 700 | if ( jQuery.isFunction( value ) ) 701 | value = value.call( elem, i ); 702 | 703 | // Handle passing in a number to a CSS property 704 | return typeof value === "number" && type == "curCSS" && !exclude.test( name ) ? 705 | value + "px" : 706 | value; 707 | }, 708 | 709 | className: { 710 | // internal only, use addClass("class") 711 | add: function( elem, classNames ) { 712 | jQuery.each((classNames || "").split(/\s+/), function(i, className){ 713 | if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) ) 714 | elem.className += (elem.className ? " " : "") + className; 715 | }); 716 | }, 717 | 718 | // internal only, use removeClass("class") 719 | remove: function( elem, classNames ) { 720 | if (elem.nodeType == 1) 721 | elem.className = classNames !== undefined ? 722 | jQuery.grep(elem.className.split(/\s+/), function(className){ 723 | return !jQuery.className.has( classNames, className ); 724 | }).join(" ") : 725 | ""; 726 | }, 727 | 728 | // internal only, use hasClass("class") 729 | has: function( elem, className ) { 730 | return elem && jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1; 731 | } 732 | }, 733 | 734 | // A method for quickly swapping in/out CSS properties to get correct calculations 735 | swap: function( elem, options, callback ) { 736 | var old = {}; 737 | // Remember the old values, and insert the new ones 738 | for ( var name in options ) { 739 | old[ name ] = elem.style[ name ]; 740 | elem.style[ name ] = options[ name ]; 741 | } 742 | 743 | callback.call( elem ); 744 | 745 | // Revert the old values 746 | for ( var name in options ) 747 | elem.style[ name ] = old[ name ]; 748 | }, 749 | 750 | css: function( elem, name, force, extra ) { 751 | if ( name == "width" || name == "height" ) { 752 | var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ]; 753 | 754 | function getWH() { 755 | val = name == "width" ? elem.offsetWidth : elem.offsetHeight; 756 | 757 | if ( extra === "border" ) 758 | return; 759 | 760 | jQuery.each( which, function() { 761 | if ( !extra ) 762 | val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0; 763 | if ( extra === "margin" ) 764 | val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0; 765 | else 766 | val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0; 767 | }); 768 | } 769 | 770 | if ( elem.offsetWidth !== 0 ) 771 | getWH(); 772 | else 773 | jQuery.swap( elem, props, getWH ); 774 | 775 | return Math.max(0, Math.round(val)); 776 | } 777 | 778 | return jQuery.curCSS( elem, name, force ); 779 | }, 780 | 781 | curCSS: function( elem, name, force ) { 782 | var ret, style = elem.style; 783 | 784 | // We need to handle opacity special in IE 785 | if ( name == "opacity" && !jQuery.support.opacity ) { 786 | ret = jQuery.attr( style, "opacity" ); 787 | 788 | return ret == "" ? 789 | "1" : 790 | ret; 791 | } 792 | 793 | // Make sure we're using the right name for getting the float value 794 | if ( name.match( /float/i ) ) 795 | name = styleFloat; 796 | 797 | if ( !force && style && style[ name ] ) 798 | ret = style[ name ]; 799 | 800 | else if ( defaultView.getComputedStyle ) { 801 | 802 | // Only "float" is needed here 803 | if ( name.match( /float/i ) ) 804 | name = "float"; 805 | 806 | name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase(); 807 | 808 | var computedStyle = defaultView.getComputedStyle( elem, null ); 809 | 810 | if ( computedStyle ) 811 | ret = computedStyle.getPropertyValue( name ); 812 | 813 | // We should always get a number back from opacity 814 | if ( name == "opacity" && ret == "" ) 815 | ret = "1"; 816 | 817 | } else if ( elem.currentStyle ) { 818 | var camelCase = name.replace(/\-(\w)/g, function(all, letter){ 819 | return letter.toUpperCase(); 820 | }); 821 | 822 | ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; 823 | 824 | // From the awesome hack by Dean Edwards 825 | // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 826 | 827 | // If we're not dealing with a regular pixel number 828 | // but a number that has a weird ending, we need to convert it to pixels 829 | if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) { 830 | // Remember the original values 831 | var left = style.left, rsLeft = elem.runtimeStyle.left; 832 | 833 | // Put in the new values to get a computed value out 834 | elem.runtimeStyle.left = elem.currentStyle.left; 835 | style.left = ret || 0; 836 | ret = style.pixelLeft + "px"; 837 | 838 | // Revert the changed values 839 | style.left = left; 840 | elem.runtimeStyle.left = rsLeft; 841 | } 842 | } 843 | 844 | return ret; 845 | }, 846 | 847 | clean: function( elems, context, fragment ) { 848 | context = context || document; 849 | 850 | // !context.createElement fails in IE with an error but returns typeof 'object' 851 | if ( typeof context.createElement === "undefined" ) 852 | context = context.ownerDocument || context[0] && context[0].ownerDocument || document; 853 | 854 | // If a single string is passed in and it's a single tag 855 | // just do a createElement and skip the rest 856 | if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) { 857 | var match = /^<(\w+)\s*\/?>$/.exec(elems[0]); 858 | if ( match ) 859 | return [ context.createElement( match[1] ) ]; 860 | } 861 | 862 | var ret = [], scripts = [], div = context.createElement("div"); 863 | 864 | jQuery.each(elems, function(i, elem){ 865 | if ( typeof elem === "number" ) 866 | elem += ''; 867 | 868 | if ( !elem ) 869 | return; 870 | 871 | // Convert html string into DOM nodes 872 | if ( typeof elem === "string" ) { 873 | // Fix "XHTML"-style tags in all browsers 874 | elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){ 875 | return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? 876 | all : 877 | front + ">"; 878 | }); 879 | 880 | // Trim whitespace, otherwise indexOf won't work as expected 881 | var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase(); 882 | 883 | var wrap = 884 | // option or optgroup 885 | !tags.indexOf("", "" ] || 887 | 888 | !tags.indexOf("", "" ] || 890 | 891 | tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && 892 | [ 1, "", "
" ] || 893 | 894 | !tags.indexOf("", "" ] || 896 | 897 | // matched above 898 | (!tags.indexOf("", "" ] || 900 | 901 | !tags.indexOf("", "" ] || 903 | 904 | // IE can't serialize and