jquery.imgareaselect.js 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234
  1. /*
  2. * imgAreaSelect jQuery plugin
  3. * version 0.9.10-wp
  4. *
  5. * Copyright (c) 2008-2013 Michal Wojciechowski (odyniec.net)
  6. *
  7. * Dual licensed under the MIT (MIT-LICENSE.txt)
  8. * and GPL (GPL-LICENSE.txt) licenses.
  9. *
  10. * https://github.com/odyniec/imgareaselect
  11. *
  12. */
  13. (function($) {
  14. /*
  15. * Math functions will be used extensively, so it's convenient to make a few
  16. * shortcuts
  17. */
  18. var abs = Math.abs,
  19. max = Math.max,
  20. min = Math.min,
  21. round = Math.round;
  22. /**
  23. * Create a new HTML div element
  24. *
  25. * @return A jQuery object representing the new element
  26. */
  27. function div() {
  28. return $('<div/>');
  29. }
  30. /**
  31. * imgAreaSelect initialization
  32. *
  33. * @param img
  34. * A HTML image element to attach the plugin to
  35. * @param options
  36. * An options object
  37. */
  38. $.imgAreaSelect = function (img, options) {
  39. var
  40. /* jQuery object representing the image */
  41. $img = $(img),
  42. /* Has the image finished loading? */
  43. imgLoaded,
  44. /* Plugin elements */
  45. /* Container box */
  46. $box = div(),
  47. /* Selection area */
  48. $area = div(),
  49. /* Border (four divs) */
  50. $border = div().add(div()).add(div()).add(div()),
  51. /* Outer area (four divs) */
  52. $outer = div().add(div()).add(div()).add(div()),
  53. /* Handles (empty by default, initialized in setOptions()) */
  54. $handles = $([]),
  55. /*
  56. * Additional element to work around a cursor problem in Opera
  57. * (explained later)
  58. */
  59. $areaOpera,
  60. /* Image position (relative to viewport) */
  61. left, top,
  62. /* Image offset (as returned by .offset()) */
  63. imgOfs = { left: 0, top: 0 },
  64. /* Image dimensions (as returned by .width() and .height()) */
  65. imgWidth, imgHeight,
  66. /*
  67. * jQuery object representing the parent element that the plugin
  68. * elements are appended to
  69. */
  70. $parent,
  71. /* Parent element offset (as returned by .offset()) */
  72. parOfs = { left: 0, top: 0 },
  73. /* Base z-index for plugin elements */
  74. zIndex = 0,
  75. /* Plugin elements position */
  76. position = 'absolute',
  77. /* X/Y coordinates of the starting point for move/resize operations */
  78. startX, startY,
  79. /* Horizontal and vertical scaling factors */
  80. scaleX, scaleY,
  81. /* Current resize mode ("nw", "se", etc.) */
  82. resize,
  83. /* Selection area constraints */
  84. minWidth, minHeight, maxWidth, maxHeight,
  85. /* Aspect ratio to maintain (floating point number) */
  86. aspectRatio,
  87. /* Are the plugin elements currently displayed? */
  88. shown,
  89. /* Current selection (relative to parent element) */
  90. x1, y1, x2, y2,
  91. /* Current selection (relative to scaled image) */
  92. selection = { x1: 0, y1: 0, x2: 0, y2: 0, width: 0, height: 0 },
  93. /* Document element */
  94. docElem = document.documentElement,
  95. /* User agent */
  96. ua = navigator.userAgent,
  97. /* Various helper variables used throughout the code */
  98. $p, d, i, o, w, h, adjusted;
  99. /*
  100. * Translate selection coordinates (relative to scaled image) to viewport
  101. * coordinates (relative to parent element)
  102. */
  103. /**
  104. * Translate selection X to viewport X
  105. *
  106. * @param x
  107. * Selection X
  108. * @return Viewport X
  109. */
  110. function viewX(x) {
  111. return x + imgOfs.left - parOfs.left;
  112. }
  113. /**
  114. * Translate selection Y to viewport Y
  115. *
  116. * @param y
  117. * Selection Y
  118. * @return Viewport Y
  119. */
  120. function viewY(y) {
  121. return y + imgOfs.top - parOfs.top;
  122. }
  123. /*
  124. * Translate viewport coordinates to selection coordinates
  125. */
  126. /**
  127. * Translate viewport X to selection X
  128. *
  129. * @param x
  130. * Viewport X
  131. * @return Selection X
  132. */
  133. function selX(x) {
  134. return x - imgOfs.left + parOfs.left;
  135. }
  136. /**
  137. * Translate viewport Y to selection Y
  138. *
  139. * @param y
  140. * Viewport Y
  141. * @return Selection Y
  142. */
  143. function selY(y) {
  144. return y - imgOfs.top + parOfs.top;
  145. }
  146. /*
  147. * Translate event coordinates (relative to document) to viewport
  148. * coordinates
  149. */
  150. /**
  151. * Get event X and translate it to viewport X
  152. *
  153. * @param event
  154. * The event object
  155. * @return Viewport X
  156. */
  157. function evX(event) {
  158. return max(event.pageX || 0, touchCoords(event).x) - parOfs.left;
  159. }
  160. /**
  161. * Get event Y and translate it to viewport Y
  162. *
  163. * @param event
  164. * The event object
  165. * @return Viewport Y
  166. */
  167. function evY(event) {
  168. return max(event.pageY || 0, touchCoords(event).y) - parOfs.top;
  169. }
  170. /**
  171. * Get X and Y coordinates of a touch event
  172. *
  173. * @param event
  174. * The event object
  175. * @return Coordinates object
  176. */
  177. function touchCoords(event) {
  178. var oev = event.originalEvent || {};
  179. if (oev.touches && oev.touches.length)
  180. return { x: oev.touches[0].pageX, y: oev.touches[0].pageY };
  181. else
  182. return { x: 0, y: 0 };
  183. }
  184. /**
  185. * Get the current selection
  186. *
  187. * @param noScale
  188. * If set to <code>true</code>, scaling is not applied to the
  189. * returned selection
  190. * @return Selection object
  191. */
  192. function getSelection(noScale) {
  193. var sx = noScale || scaleX, sy = noScale || scaleY;
  194. return { x1: round(selection.x1 * sx),
  195. y1: round(selection.y1 * sy),
  196. x2: round(selection.x2 * sx),
  197. y2: round(selection.y2 * sy),
  198. width: round(selection.x2 * sx) - round(selection.x1 * sx),
  199. height: round(selection.y2 * sy) - round(selection.y1 * sy) };
  200. }
  201. /**
  202. * Set the current selection
  203. *
  204. * @param x1
  205. * X coordinate of the upper left corner of the selection area
  206. * @param y1
  207. * Y coordinate of the upper left corner of the selection area
  208. * @param x2
  209. * X coordinate of the lower right corner of the selection area
  210. * @param y2
  211. * Y coordinate of the lower right corner of the selection area
  212. * @param noScale
  213. * If set to <code>true</code>, scaling is not applied to the
  214. * new selection
  215. */
  216. function setSelection(x1, y1, x2, y2, noScale) {
  217. var sx = noScale || scaleX, sy = noScale || scaleY;
  218. selection = {
  219. x1: round(x1 / sx || 0),
  220. y1: round(y1 / sy || 0),
  221. x2: round(x2 / sx || 0),
  222. y2: round(y2 / sy || 0)
  223. };
  224. selection.width = selection.x2 - selection.x1;
  225. selection.height = selection.y2 - selection.y1;
  226. }
  227. /**
  228. * Recalculate image and parent offsets
  229. */
  230. function adjust() {
  231. /*
  232. * Do not adjust if image has not yet loaded or if width is not a
  233. * positive number. The latter might happen when imgAreaSelect is put
  234. * on a parent element which is then hidden.
  235. */
  236. if (!imgLoaded || !$img.width())
  237. return;
  238. /*
  239. * Get image offset. The .offset() method returns float values, so they
  240. * need to be rounded.
  241. */
  242. imgOfs = { left: round($img.offset().left), top: round($img.offset().top) };
  243. /* Get image dimensions */
  244. imgWidth = $img.innerWidth();
  245. imgHeight = $img.innerHeight();
  246. imgOfs.top += ($img.outerHeight() - imgHeight) >> 1;
  247. imgOfs.left += ($img.outerWidth() - imgWidth) >> 1;
  248. /* Set minimum and maximum selection area dimensions */
  249. minWidth = round(options.minWidth / scaleX) || 0;
  250. minHeight = round(options.minHeight / scaleY) || 0;
  251. maxWidth = round(min(options.maxWidth / scaleX || 1<<24, imgWidth));
  252. maxHeight = round(min(options.maxHeight / scaleY || 1<<24, imgHeight));
  253. /*
  254. * Workaround for jQuery 1.3.2 incorrect offset calculation, originally
  255. * observed in Safari 3. Firefox 2 is also affected.
  256. */
  257. if ($().jquery == '1.3.2' && position == 'fixed' &&
  258. !docElem['getBoundingClientRect'])
  259. {
  260. imgOfs.top += max(document.body.scrollTop, docElem.scrollTop);
  261. imgOfs.left += max(document.body.scrollLeft, docElem.scrollLeft);
  262. }
  263. /* Determine parent element offset */
  264. parOfs = /absolute|relative/.test($parent.css('position')) ?
  265. { left: round($parent.offset().left) - $parent.scrollLeft(),
  266. top: round($parent.offset().top) - $parent.scrollTop() } :
  267. position == 'fixed' ?
  268. { left: $(document).scrollLeft(), top: $(document).scrollTop() } :
  269. { left: 0, top: 0 };
  270. left = viewX(0);
  271. top = viewY(0);
  272. /*
  273. * Check if selection area is within image boundaries, adjust if
  274. * necessary
  275. */
  276. if (selection.x2 > imgWidth || selection.y2 > imgHeight)
  277. doResize();
  278. }
  279. /**
  280. * Update plugin elements
  281. *
  282. * @param resetKeyPress
  283. * If set to <code>false</code>, this instance's keypress
  284. * event handler is not activated
  285. */
  286. function update(resetKeyPress) {
  287. /* If plugin elements are hidden, do nothing */
  288. if (!shown) return;
  289. /*
  290. * Set the position and size of the container box and the selection area
  291. * inside it
  292. */
  293. $box.css({ left: viewX(selection.x1), top: viewY(selection.y1) })
  294. .add($area).width(w = selection.width).height(h = selection.height);
  295. /*
  296. * Reset the position of selection area, borders, and handles (IE6/IE7
  297. * position them incorrectly if we don't do this)
  298. */
  299. $area.add($border).add($handles).css({ left: 0, top: 0 });
  300. /* Set border dimensions */
  301. $border
  302. .width(max(w - $border.outerWidth() + $border.innerWidth(), 0))
  303. .height(max(h - $border.outerHeight() + $border.innerHeight(), 0));
  304. /* Arrange the outer area elements */
  305. $($outer[0]).css({ left: left, top: top,
  306. width: selection.x1, height: imgHeight });
  307. $($outer[1]).css({ left: left + selection.x1, top: top,
  308. width: w, height: selection.y1 });
  309. $($outer[2]).css({ left: left + selection.x2, top: top,
  310. width: imgWidth - selection.x2, height: imgHeight });
  311. $($outer[3]).css({ left: left + selection.x1, top: top + selection.y2,
  312. width: w, height: imgHeight - selection.y2 });
  313. w -= $handles.outerWidth();
  314. h -= $handles.outerHeight();
  315. /* Arrange handles */
  316. switch ($handles.length) {
  317. case 8:
  318. $($handles[4]).css({ left: w >> 1 });
  319. $($handles[5]).css({ left: w, top: h >> 1 });
  320. $($handles[6]).css({ left: w >> 1, top: h });
  321. $($handles[7]).css({ top: h >> 1 });
  322. case 4:
  323. $handles.slice(1,3).css({ left: w });
  324. $handles.slice(2,4).css({ top: h });
  325. }
  326. if (resetKeyPress !== false) {
  327. /*
  328. * Need to reset the document keypress event handler -- unbind the
  329. * current handler
  330. */
  331. if ($.imgAreaSelect.onKeyPress != docKeyPress)
  332. $(document).off($.imgAreaSelect.keyPress,
  333. $.imgAreaSelect.onKeyPress);
  334. if (options.keys)
  335. /*
  336. * Set the document keypress event handler to this instance's
  337. * docKeyPress() function
  338. */
  339. $(document).on( $.imgAreaSelect.keyPress, function() {
  340. $.imgAreaSelect.onKeyPress = docKeyPress;
  341. });
  342. }
  343. /*
  344. * Internet Explorer displays 1px-wide dashed borders incorrectly by
  345. * filling the spaces between dashes with white. Toggling the margin
  346. * property between 0 and "auto" fixes this in IE6 and IE7 (IE8 is still
  347. * broken). This workaround is not perfect, as it requires setTimeout()
  348. * and thus causes the border to flicker a bit, but I haven't found a
  349. * better solution.
  350. *
  351. * Note: This only happens with CSS borders, set with the borderWidth,
  352. * borderOpacity, borderColor1, and borderColor2 options (which are now
  353. * deprecated). Borders created with GIF background images are fine.
  354. */
  355. if (msie && $border.outerWidth() - $border.innerWidth() == 2) {
  356. $border.css('margin', 0);
  357. setTimeout(function () { $border.css('margin', 'auto'); }, 0);
  358. }
  359. }
  360. /**
  361. * Do the complete update sequence: recalculate offsets, update the
  362. * elements, and set the correct values of x1, y1, x2, and y2.
  363. *
  364. * @param resetKeyPress
  365. * If set to <code>false</code>, this instance's keypress
  366. * event handler is not activated
  367. */
  368. function doUpdate(resetKeyPress) {
  369. adjust();
  370. update(resetKeyPress);
  371. x1 = viewX(selection.x1); y1 = viewY(selection.y1);
  372. x2 = viewX(selection.x2); y2 = viewY(selection.y2);
  373. }
  374. /**
  375. * Hide or fade out an element (or multiple elements)
  376. *
  377. * @param $elem
  378. * A jQuery object containing the element(s) to hide/fade out
  379. * @param fn
  380. * Callback function to be called when fadeOut() completes
  381. */
  382. function hide($elem, fn) {
  383. options.fadeSpeed ? $elem.fadeOut(options.fadeSpeed, fn) : $elem.hide();
  384. }
  385. /**
  386. * Selection area mousemove event handler
  387. *
  388. * @param event
  389. * The event object
  390. */
  391. function areaMouseMove(event) {
  392. var x = selX(evX(event)) - selection.x1,
  393. y = selY(evY(event)) - selection.y1;
  394. if (!adjusted) {
  395. adjust();
  396. adjusted = true;
  397. $box.one('mouseout', function () { adjusted = false; });
  398. }
  399. /* Clear the resize mode */
  400. resize = '';
  401. if (options.resizable) {
  402. /*
  403. * Check if the mouse pointer is over the resize margin area and set
  404. * the resize mode accordingly
  405. */
  406. if (y <= options.resizeMargin)
  407. resize = 'n';
  408. else if (y >= selection.height - options.resizeMargin)
  409. resize = 's';
  410. if (x <= options.resizeMargin)
  411. resize += 'w';
  412. else if (x >= selection.width - options.resizeMargin)
  413. resize += 'e';
  414. }
  415. $box.css('cursor', resize ? resize + '-resize' :
  416. options.movable ? 'move' : '');
  417. if ($areaOpera)
  418. $areaOpera.toggle();
  419. }
  420. /**
  421. * Document mouseup event handler
  422. *
  423. * @param event
  424. * The event object
  425. */
  426. function docMouseUp(event) {
  427. /* Set back the default cursor */
  428. $('body').css('cursor', '');
  429. /*
  430. * If autoHide is enabled, or if the selection has zero width/height,
  431. * hide the selection and the outer area
  432. */
  433. if (options.autoHide || selection.width * selection.height == 0)
  434. hide($box.add($outer), function () { $(this).hide(); });
  435. $(document).off('mousemove touchmove', selectingMouseMove);
  436. $box.on('mousemove touchmove', areaMouseMove);
  437. options.onSelectEnd(img, getSelection());
  438. }
  439. /**
  440. * Selection area mousedown event handler
  441. *
  442. * @param event
  443. * The event object
  444. * @return false
  445. */
  446. function areaMouseDown(event) {
  447. if (event.type == 'mousedown' && event.which != 1) return false;
  448. /*
  449. * With mobile browsers, there is no "moving the pointer over" action,
  450. * so we need to simulate one mousemove event happening prior to
  451. * mousedown/touchstart.
  452. */
  453. areaMouseMove(event);
  454. adjust();
  455. if (resize) {
  456. /* Resize mode is in effect */
  457. $('body').css('cursor', resize + '-resize');
  458. x1 = viewX(selection[/w/.test(resize) ? 'x2' : 'x1']);
  459. y1 = viewY(selection[/n/.test(resize) ? 'y2' : 'y1']);
  460. $(document).on('mousemove touchmove', selectingMouseMove)
  461. .one('mouseup touchend', docMouseUp);
  462. $box.off('mousemove touchmove', areaMouseMove);
  463. }
  464. else if (options.movable) {
  465. startX = left + selection.x1 - evX(event);
  466. startY = top + selection.y1 - evY(event);
  467. $box.off('mousemove touchmove', areaMouseMove);
  468. $(document).on('mousemove touchmove', movingMouseMove)
  469. .one('mouseup touchend', function () {
  470. options.onSelectEnd(img, getSelection());
  471. $(document).off('mousemove touchmove', movingMouseMove);
  472. $box.on('mousemove touchmove', areaMouseMove);
  473. });
  474. }
  475. else
  476. $img.mousedown(event);
  477. return false;
  478. }
  479. /**
  480. * Adjust the x2/y2 coordinates to maintain aspect ratio (if defined)
  481. *
  482. * @param xFirst
  483. * If set to <code>true</code>, calculate x2 first. Otherwise,
  484. * calculate y2 first.
  485. */
  486. function fixAspectRatio(xFirst) {
  487. if (aspectRatio)
  488. if (xFirst) {
  489. x2 = max(left, min(left + imgWidth,
  490. x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1)));
  491. y2 = round(max(top, min(top + imgHeight,
  492. y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1))));
  493. x2 = round(x2);
  494. }
  495. else {
  496. y2 = max(top, min(top + imgHeight,
  497. y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1)));
  498. x2 = round(max(left, min(left + imgWidth,
  499. x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1))));
  500. y2 = round(y2);
  501. }
  502. }
  503. /**
  504. * Resize the selection area respecting the minimum/maximum dimensions and
  505. * aspect ratio
  506. */
  507. function doResize() {
  508. /*
  509. * Make sure the top left corner of the selection area stays within
  510. * image boundaries (it might not if the image source was dynamically
  511. * changed).
  512. */
  513. x1 = min(x1, left + imgWidth);
  514. y1 = min(y1, top + imgHeight);
  515. if (abs(x2 - x1) < minWidth) {
  516. /* Selection width is smaller than minWidth */
  517. x2 = x1 - minWidth * (x2 < x1 || -1);
  518. if (x2 < left)
  519. x1 = left + minWidth;
  520. else if (x2 > left + imgWidth)
  521. x1 = left + imgWidth - minWidth;
  522. }
  523. if (abs(y2 - y1) < minHeight) {
  524. /* Selection height is smaller than minHeight */
  525. y2 = y1 - minHeight * (y2 < y1 || -1);
  526. if (y2 < top)
  527. y1 = top + minHeight;
  528. else if (y2 > top + imgHeight)
  529. y1 = top + imgHeight - minHeight;
  530. }
  531. x2 = max(left, min(x2, left + imgWidth));
  532. y2 = max(top, min(y2, top + imgHeight));
  533. fixAspectRatio(abs(x2 - x1) < abs(y2 - y1) * aspectRatio);
  534. if (abs(x2 - x1) > maxWidth) {
  535. /* Selection width is greater than maxWidth */
  536. x2 = x1 - maxWidth * (x2 < x1 || -1);
  537. fixAspectRatio();
  538. }
  539. if (abs(y2 - y1) > maxHeight) {
  540. /* Selection height is greater than maxHeight */
  541. y2 = y1 - maxHeight * (y2 < y1 || -1);
  542. fixAspectRatio(true);
  543. }
  544. selection = { x1: selX(min(x1, x2)), x2: selX(max(x1, x2)),
  545. y1: selY(min(y1, y2)), y2: selY(max(y1, y2)),
  546. width: abs(x2 - x1), height: abs(y2 - y1) };
  547. update();
  548. options.onSelectChange(img, getSelection());
  549. }
  550. /**
  551. * Mousemove event handler triggered when the user is selecting an area
  552. *
  553. * @param event
  554. * The event object
  555. * @return false
  556. */
  557. function selectingMouseMove(event) {
  558. x2 = /w|e|^$/.test(resize) || aspectRatio ? evX(event) : viewX(selection.x2);
  559. y2 = /n|s|^$/.test(resize) || aspectRatio ? evY(event) : viewY(selection.y2);
  560. doResize();
  561. return false;
  562. }
  563. /**
  564. * Move the selection area
  565. *
  566. * @param newX1
  567. * New viewport X1
  568. * @param newY1
  569. * New viewport Y1
  570. */
  571. function doMove(newX1, newY1) {
  572. x2 = (x1 = newX1) + selection.width;
  573. y2 = (y1 = newY1) + selection.height;
  574. $.extend(selection, { x1: selX(x1), y1: selY(y1), x2: selX(x2),
  575. y2: selY(y2) });
  576. update();
  577. options.onSelectChange(img, getSelection());
  578. }
  579. /**
  580. * Mousemove event handler triggered when the selection area is being moved
  581. *
  582. * @param event
  583. * The event object
  584. * @return false
  585. */
  586. function movingMouseMove(event) {
  587. x1 = max(left, min(startX + evX(event), left + imgWidth - selection.width));
  588. y1 = max(top, min(startY + evY(event), top + imgHeight - selection.height));
  589. doMove(x1, y1);
  590. event.preventDefault();
  591. return false;
  592. }
  593. /**
  594. * Start selection
  595. */
  596. function startSelection() {
  597. $(document).off('mousemove touchmove', startSelection);
  598. adjust();
  599. x2 = x1;
  600. y2 = y1;
  601. doResize();
  602. resize = '';
  603. if (!$outer.is(':visible'))
  604. /* Show the plugin elements */
  605. $box.add($outer).hide().fadeIn(options.fadeSpeed||0);
  606. shown = true;
  607. $(document).off('mouseup touchend', cancelSelection)
  608. .on('mousemove touchmove', selectingMouseMove)
  609. .one('mouseup touchend', docMouseUp);
  610. $box.off('mousemove touchmove', areaMouseMove);
  611. options.onSelectStart(img, getSelection());
  612. }
  613. /**
  614. * Cancel selection
  615. */
  616. function cancelSelection() {
  617. $(document).off('mousemove touchmove', startSelection)
  618. .off('mouseup touchend', cancelSelection);
  619. hide($box.add($outer));
  620. setSelection(selX(x1), selY(y1), selX(x1), selY(y1));
  621. /* If this is an API call, callback functions should not be triggered */
  622. if (!(this instanceof $.imgAreaSelect)) {
  623. options.onSelectChange(img, getSelection());
  624. options.onSelectEnd(img, getSelection());
  625. }
  626. }
  627. /**
  628. * Image mousedown event handler
  629. *
  630. * @param event
  631. * The event object
  632. * @return false
  633. */
  634. function imgMouseDown(event) {
  635. /* Ignore the event if animation is in progress */
  636. if (event.which > 1 || $outer.is(':animated')) return false;
  637. adjust();
  638. startX = x1 = evX(event);
  639. startY = y1 = evY(event);
  640. /* Selection will start when the mouse is moved */
  641. $(document).on({ 'mousemove touchmove': startSelection,
  642. 'mouseup touchend': cancelSelection });
  643. return false;
  644. }
  645. /**
  646. * Window resize event handler
  647. */
  648. function windowResize() {
  649. doUpdate(false);
  650. }
  651. /**
  652. * Image load event handler. This is the final part of the initialization
  653. * process.
  654. */
  655. function imgLoad() {
  656. imgLoaded = true;
  657. /* Set options */
  658. setOptions(options = $.extend({
  659. classPrefix: 'imgareaselect',
  660. movable: true,
  661. parent: 'body',
  662. resizable: true,
  663. resizeMargin: 10,
  664. onInit: function () {},
  665. onSelectStart: function () {},
  666. onSelectChange: function () {},
  667. onSelectEnd: function () {}
  668. }, options));
  669. $box.add($outer).css({ visibility: '' });
  670. if (options.show) {
  671. shown = true;
  672. adjust();
  673. update();
  674. $box.add($outer).hide().fadeIn(options.fadeSpeed||0);
  675. }
  676. /*
  677. * Call the onInit callback. The setTimeout() call is used to ensure
  678. * that the plugin has been fully initialized and the object instance is
  679. * available (so that it can be obtained in the callback).
  680. */
  681. setTimeout(function () { options.onInit(img, getSelection()); }, 0);
  682. }
  683. /**
  684. * Document keypress event handler
  685. *
  686. * @param event
  687. * The event object
  688. * @return false
  689. */
  690. var docKeyPress = function(event) {
  691. var k = options.keys, d, t, key = event.keyCode;
  692. d = !isNaN(k.alt) && (event.altKey || event.originalEvent.altKey) ? k.alt :
  693. !isNaN(k.ctrl) && event.ctrlKey ? k.ctrl :
  694. !isNaN(k.shift) && event.shiftKey ? k.shift :
  695. !isNaN(k.arrows) ? k.arrows : 10;
  696. if (k.arrows == 'resize' || (k.shift == 'resize' && event.shiftKey) ||
  697. (k.ctrl == 'resize' && event.ctrlKey) ||
  698. (k.alt == 'resize' && (event.altKey || event.originalEvent.altKey)))
  699. {
  700. /* Resize selection */
  701. switch (key) {
  702. case 37:
  703. /* Left */
  704. d = -d;
  705. case 39:
  706. /* Right */
  707. t = max(x1, x2);
  708. x1 = min(x1, x2);
  709. x2 = max(t + d, x1);
  710. fixAspectRatio();
  711. break;
  712. case 38:
  713. /* Up */
  714. d = -d;
  715. case 40:
  716. /* Down */
  717. t = max(y1, y2);
  718. y1 = min(y1, y2);
  719. y2 = max(t + d, y1);
  720. fixAspectRatio(true);
  721. break;
  722. default:
  723. return;
  724. }
  725. doResize();
  726. }
  727. else {
  728. /* Move selection */
  729. x1 = min(x1, x2);
  730. y1 = min(y1, y2);
  731. switch (key) {
  732. case 37:
  733. /* Left */
  734. doMove(max(x1 - d, left), y1);
  735. break;
  736. case 38:
  737. /* Up */
  738. doMove(x1, max(y1 - d, top));
  739. break;
  740. case 39:
  741. /* Right */
  742. doMove(x1 + min(d, imgWidth - selX(x2)), y1);
  743. break;
  744. case 40:
  745. /* Down */
  746. doMove(x1, y1 + min(d, imgHeight - selY(y2)));
  747. break;
  748. default:
  749. return;
  750. }
  751. }
  752. return false;
  753. };
  754. /**
  755. * Apply style options to plugin element (or multiple elements)
  756. *
  757. * @param $elem
  758. * A jQuery object representing the element(s) to style
  759. * @param props
  760. * An object that maps option names to corresponding CSS
  761. * properties
  762. */
  763. function styleOptions($elem, props) {
  764. for (var option in props)
  765. if (options[option] !== undefined)
  766. $elem.css(props[option], options[option]);
  767. }
  768. /**
  769. * Set plugin options
  770. *
  771. * @param newOptions
  772. * The new options object
  773. */
  774. function setOptions(newOptions) {
  775. if (newOptions.parent)
  776. ($parent = $(newOptions.parent)).append($box.add($outer));
  777. /* Merge the new options with the existing ones */
  778. $.extend(options, newOptions);
  779. adjust();
  780. if (newOptions.handles != null) {
  781. /* Recreate selection area handles */
  782. $handles.remove();
  783. $handles = $([]);
  784. i = newOptions.handles ? newOptions.handles == 'corners' ? 4 : 8 : 0;
  785. while (i--)
  786. $handles = $handles.add(div());
  787. /* Add a class to handles and set the CSS properties */
  788. $handles.addClass(options.classPrefix + '-handle').css({
  789. position: 'absolute',
  790. /*
  791. * The font-size property needs to be set to zero, otherwise
  792. * Internet Explorer makes the handles too large
  793. */
  794. fontSize: '0',
  795. zIndex: zIndex + 1 || 1
  796. });
  797. /*
  798. * If handle width/height has not been set with CSS rules, set the
  799. * default 5px
  800. */
  801. if (!parseInt($handles.css('width')) >= 0)
  802. $handles.width(5).height(5);
  803. /*
  804. * If the borderWidth option is in use, add a solid border to
  805. * handles
  806. */
  807. if (o = options.borderWidth)
  808. $handles.css({ borderWidth: o, borderStyle: 'solid' });
  809. /* Apply other style options */
  810. styleOptions($handles, { borderColor1: 'border-color',
  811. borderColor2: 'background-color',
  812. borderOpacity: 'opacity' });
  813. }
  814. /* Calculate scale factors */
  815. scaleX = options.imageWidth / imgWidth || 1;
  816. scaleY = options.imageHeight / imgHeight || 1;
  817. /* Set selection */
  818. if (newOptions.x1 != null) {
  819. setSelection(newOptions.x1, newOptions.y1, newOptions.x2,
  820. newOptions.y2);
  821. newOptions.show = !newOptions.hide;
  822. }
  823. if (newOptions.keys)
  824. /* Enable keyboard support */
  825. options.keys = $.extend({ shift: 1, ctrl: 'resize' },
  826. newOptions.keys);
  827. /* Add classes to plugin elements */
  828. $outer.addClass(options.classPrefix + '-outer');
  829. $area.addClass(options.classPrefix + '-selection');
  830. for (i = 0; i++ < 4;)
  831. $($border[i-1]).addClass(options.classPrefix + '-border' + i);
  832. /* Apply style options */
  833. styleOptions($area, { selectionColor: 'background-color',
  834. selectionOpacity: 'opacity' });
  835. styleOptions($border, { borderOpacity: 'opacity',
  836. borderWidth: 'border-width' });
  837. styleOptions($outer, { outerColor: 'background-color',
  838. outerOpacity: 'opacity' });
  839. if (o = options.borderColor1)
  840. $($border[0]).css({ borderStyle: 'solid', borderColor: o });
  841. if (o = options.borderColor2)
  842. $($border[1]).css({ borderStyle: 'dashed', borderColor: o });
  843. /* Append all the selection area elements to the container box */
  844. $box.append($area.add($border).add($areaOpera)).append($handles);
  845. if (msie) {
  846. if (o = ($outer.css('filter')||'').match(/opacity=(\d+)/))
  847. $outer.css('opacity', o[1]/100);
  848. if (o = ($border.css('filter')||'').match(/opacity=(\d+)/))
  849. $border.css('opacity', o[1]/100);
  850. }
  851. if (newOptions.hide)
  852. hide($box.add($outer));
  853. else if (newOptions.show && imgLoaded) {
  854. shown = true;
  855. $box.add($outer).fadeIn(options.fadeSpeed||0);
  856. doUpdate();
  857. }
  858. /* Calculate the aspect ratio factor */
  859. aspectRatio = (d = (options.aspectRatio || '').split(/:/))[0] / d[1];
  860. $img.add($outer).off('mousedown', imgMouseDown);
  861. if (options.disable || options.enable === false) {
  862. /* Disable the plugin */
  863. $box.off({ 'mousemove touchmove': areaMouseMove,
  864. 'mousedown touchstart': areaMouseDown });
  865. $(window).off('resize', windowResize);
  866. }
  867. else {
  868. if (options.enable || options.disable === false) {
  869. /* Enable the plugin */
  870. if (options.resizable || options.movable)
  871. $box.on({ 'mousemove touchmove': areaMouseMove,
  872. 'mousedown touchstart': areaMouseDown });
  873. $(window).on( 'resize', windowResize);
  874. }
  875. if (!options.persistent)
  876. $img.add($outer).on('mousedown touchstart', imgMouseDown);
  877. }
  878. options.enable = options.disable = undefined;
  879. }
  880. /**
  881. * Remove plugin completely
  882. */
  883. this.remove = function () {
  884. /*
  885. * Call setOptions with { disable: true } to unbind the event handlers
  886. */
  887. setOptions({ disable: true });
  888. $box.add($outer).remove();
  889. };
  890. /*
  891. * Public API
  892. */
  893. /**
  894. * Get current options
  895. *
  896. * @return An object containing the set of options currently in use
  897. */
  898. this.getOptions = function () { return options; };
  899. /**
  900. * Set plugin options
  901. *
  902. * @param newOptions
  903. * The new options object
  904. */
  905. this.setOptions = setOptions;
  906. /**
  907. * Get the current selection
  908. *
  909. * @param noScale
  910. * If set to <code>true</code>, scaling is not applied to the
  911. * returned selection
  912. * @return Selection object
  913. */
  914. this.getSelection = getSelection;
  915. /**
  916. * Set the current selection
  917. *
  918. * @param x1
  919. * X coordinate of the upper left corner of the selection area
  920. * @param y1
  921. * Y coordinate of the upper left corner of the selection area
  922. * @param x2
  923. * X coordinate of the lower right corner of the selection area
  924. * @param y2
  925. * Y coordinate of the lower right corner of the selection area
  926. * @param noScale
  927. * If set to <code>true</code>, scaling is not applied to the
  928. * new selection
  929. */
  930. this.setSelection = setSelection;
  931. /**
  932. * Cancel selection
  933. */
  934. this.cancelSelection = cancelSelection;
  935. /**
  936. * Update plugin elements
  937. *
  938. * @param resetKeyPress
  939. * If set to <code>false</code>, this instance's keypress
  940. * event handler is not activated
  941. */
  942. this.update = doUpdate;
  943. /* Do the dreaded browser detection */
  944. var msie = (/msie ([\w.]+)/i.exec(ua)||[])[1],
  945. opera = /opera/i.test(ua),
  946. safari = /webkit/i.test(ua) && !/chrome/i.test(ua);
  947. /*
  948. * Traverse the image's parent elements (up to <body>) and find the
  949. * highest z-index
  950. */
  951. $p = $img;
  952. while ($p.length) {
  953. zIndex = max(zIndex,
  954. !isNaN($p.css('z-index')) ? $p.css('z-index') : zIndex);
  955. /* Also check if any of the ancestor elements has fixed position */
  956. if ($p.css('position') == 'fixed')
  957. position = 'fixed';
  958. $p = $p.parent(':not(body)');
  959. }
  960. /*
  961. * If z-index is given as an option, it overrides the one found by the
  962. * above loop
  963. */
  964. zIndex = options.zIndex || zIndex;
  965. if (msie)
  966. $img.attr('unselectable', 'on');
  967. /*
  968. * In MSIE and WebKit, we need to use the keydown event instead of keypress
  969. */
  970. $.imgAreaSelect.keyPress = msie || safari ? 'keydown' : 'keypress';
  971. /*
  972. * There is a bug affecting the CSS cursor property in Opera (observed in
  973. * versions up to 10.00) that prevents the cursor from being updated unless
  974. * the mouse leaves and enters the element again. To trigger the mouseover
  975. * event, we're adding an additional div to $box and we're going to toggle
  976. * it when mouse moves inside the selection area.
  977. */
  978. if (opera)
  979. $areaOpera = div().css({ width: '100%', height: '100%',
  980. position: 'absolute', zIndex: zIndex + 2 || 2 });
  981. /*
  982. * We initially set visibility to "hidden" as a workaround for a weird
  983. * behaviour observed in Google Chrome 1.0.154.53 (on Windows XP). Normally
  984. * we would just set display to "none", but, for some reason, if we do so
  985. * then Chrome refuses to later display the element with .show() or
  986. * .fadeIn().
  987. */
  988. $box.add($outer).css({ visibility: 'hidden', position: position,
  989. overflow: 'hidden', zIndex: zIndex || '0' });
  990. $box.css({ zIndex: zIndex + 2 || 2 });
  991. $area.add($border).css({ position: 'absolute', fontSize: '0' });
  992. /*
  993. * If the image has been fully loaded, or if it is not really an image (eg.
  994. * a div), call imgLoad() immediately; otherwise, bind it to be called once
  995. * on image load event.
  996. */
  997. img.complete || img.readyState == 'complete' || !$img.is('img') ?
  998. imgLoad() : $img.one('load', imgLoad);
  999. /*
  1000. * MSIE 9.0 doesn't always fire the image load event -- resetting the src
  1001. * attribute seems to trigger it. The check is for version 7 and above to
  1002. * accommodate for MSIE 9 running in compatibility mode.
  1003. */
  1004. if (!imgLoaded && msie && msie >= 7)
  1005. img.src = img.src;
  1006. };
  1007. /**
  1008. * Invoke imgAreaSelect on a jQuery object containing the image(s)
  1009. *
  1010. * @param options
  1011. * Options object
  1012. * @return The jQuery object or a reference to imgAreaSelect instance (if the
  1013. * <code>instance</code> option was specified)
  1014. */
  1015. $.fn.imgAreaSelect = function (options) {
  1016. options = options || {};
  1017. this.each(function () {
  1018. /* Is there already an imgAreaSelect instance bound to this element? */
  1019. if ($(this).data('imgAreaSelect')) {
  1020. /* Yes there is -- is it supposed to be removed? */
  1021. if (options.remove) {
  1022. /* Remove the plugin */
  1023. $(this).data('imgAreaSelect').remove();
  1024. $(this).removeData('imgAreaSelect');
  1025. }
  1026. else
  1027. /* Reset options */
  1028. $(this).data('imgAreaSelect').setOptions(options);
  1029. }
  1030. else if (!options.remove) {
  1031. /* No exising instance -- create a new one */
  1032. /*
  1033. * If neither the "enable" nor the "disable" option is present, add
  1034. * "enable" as the default
  1035. */
  1036. if (options.enable === undefined && options.disable === undefined)
  1037. options.enable = true;
  1038. $(this).data('imgAreaSelect', new $.imgAreaSelect(this, options));
  1039. }
  1040. });
  1041. if (options.instance)
  1042. /*
  1043. * Return the imgAreaSelect instance bound to the first element in the
  1044. * set
  1045. */
  1046. return $(this).data('imgAreaSelect');
  1047. return this;
  1048. };
  1049. })(jQuery);