You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1233 rivejä
37 KiB

  1. /*
  2. * imgAreaSelect jQuery plugin
  3. * version 0.9.10-monkey
  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. * http://odyniec.net/projects/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).unbind($.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)[$.imgAreaSelect.keyPress](
  340. $.imgAreaSelect.onKeyPress = docKeyPress);
  341. }
  342. /*
  343. * Internet Explorer displays 1px-wide dashed borders incorrectly by
  344. * filling the spaces between dashes with white. Toggling the margin
  345. * property between 0 and "auto" fixes this in IE6 and IE7 (IE8 is still
  346. * broken). This workaround is not perfect, as it requires setTimeout()
  347. * and thus causes the border to flicker a bit, but I haven't found a
  348. * better solution.
  349. *
  350. * Note: This only happens with CSS borders, set with the borderWidth,
  351. * borderOpacity, borderColor1, and borderColor2 options (which are now
  352. * deprecated). Borders created with GIF background images are fine.
  353. */
  354. if (msie && $border.outerWidth() - $border.innerWidth() == 2) {
  355. $border.css('margin', 0);
  356. setTimeout(function () { $border.css('margin', 'auto'); }, 0);
  357. }
  358. }
  359. /**
  360. * Do the complete update sequence: recalculate offsets, update the
  361. * elements, and set the correct values of x1, y1, x2, and y2.
  362. *
  363. * @param resetKeyPress
  364. * If set to <code>false</code>, this instance's keypress
  365. * event handler is not activated
  366. */
  367. function doUpdate(resetKeyPress) {
  368. adjust();
  369. update(resetKeyPress);
  370. x1 = viewX(selection.x1); y1 = viewY(selection.y1);
  371. x2 = viewX(selection.x2); y2 = viewY(selection.y2);
  372. }
  373. /**
  374. * Hide or fade out an element (or multiple elements)
  375. *
  376. * @param $elem
  377. * A jQuery object containing the element(s) to hide/fade out
  378. * @param fn
  379. * Callback function to be called when fadeOut() completes
  380. */
  381. function hide($elem, fn) {
  382. options.fadeSpeed ? $elem.fadeOut(options.fadeSpeed, fn) : $elem.hide();
  383. }
  384. /**
  385. * Selection area mousemove event handler
  386. *
  387. * @param event
  388. * The event object
  389. */
  390. function areaMouseMove(event) {
  391. var x = selX(evX(event)) - selection.x1,
  392. y = selY(evY(event)) - selection.y1;
  393. if (!adjusted) {
  394. adjust();
  395. adjusted = true;
  396. $box.one('mouseout', function () { adjusted = false; });
  397. }
  398. /* Clear the resize mode */
  399. resize = '';
  400. if (options.resizable) {
  401. /*
  402. * Check if the mouse pointer is over the resize margin area and set
  403. * the resize mode accordingly
  404. */
  405. if (y <= options.resizeMargin)
  406. resize = 'n';
  407. else if (y >= selection.height - options.resizeMargin)
  408. resize = 's';
  409. if (x <= options.resizeMargin)
  410. resize += 'w';
  411. else if (x >= selection.width - options.resizeMargin)
  412. resize += 'e';
  413. }
  414. $box.css('cursor', resize ? resize + '-resize' :
  415. options.movable ? 'move' : '');
  416. if ($areaOpera)
  417. $areaOpera.toggle();
  418. }
  419. /**
  420. * Document mouseup event handler
  421. *
  422. * @param event
  423. * The event object
  424. */
  425. function docMouseUp(event) {
  426. /* Set back the default cursor */
  427. $('body').css('cursor', '');
  428. /*
  429. * If autoHide is enabled, or if the selection has zero width/height,
  430. * hide the selection and the outer area
  431. */
  432. if (options.autoHide || selection.width * selection.height == 0)
  433. hide($box.add($outer), function () { $(this).hide(); });
  434. $(document).off('mousemove touchmove', selectingMouseMove);
  435. $box.on('mousemove touchmove', areaMouseMove);
  436. options.onSelectEnd(img, getSelection());
  437. }
  438. /**
  439. * Selection area mousedown event handler
  440. *
  441. * @param event
  442. * The event object
  443. * @return false
  444. */
  445. function areaMouseDown(event) {
  446. if (event.type == 'mousedown' && event.which != 1) return false;
  447. /*
  448. * With mobile browsers, there is no "moving the pointer over" action,
  449. * so we need to simulate one mousemove event happening prior to
  450. * mousedown/touchstart.
  451. */
  452. areaMouseMove(event);
  453. adjust();
  454. if (resize) {
  455. /* Resize mode is in effect */
  456. $('body').css('cursor', resize + '-resize');
  457. x1 = viewX(selection[/w/.test(resize) ? 'x2' : 'x1']);
  458. y1 = viewY(selection[/n/.test(resize) ? 'y2' : 'y1']);
  459. $(document).on('mousemove touchmove', selectingMouseMove)
  460. .one('mouseup touchend', docMouseUp);
  461. $box.off('mousemove touchmove', areaMouseMove);
  462. }
  463. else if (options.movable) {
  464. startX = left + selection.x1 - evX(event);
  465. startY = top + selection.y1 - evY(event);
  466. $box.off('mousemove touchmove', areaMouseMove);
  467. $(document).on('mousemove touchmove', movingMouseMove)
  468. .one('mouseup touchend', function () {
  469. options.onSelectEnd(img, getSelection());
  470. $(document).off('mousemove touchmove', movingMouseMove);
  471. $box.on('mousemove touchmove', areaMouseMove);
  472. });
  473. }
  474. else
  475. $img.mousedown(event);
  476. return false;
  477. }
  478. /**
  479. * Adjust the x2/y2 coordinates to maintain aspect ratio (if defined)
  480. *
  481. * @param xFirst
  482. * If set to <code>true</code>, calculate x2 first. Otherwise,
  483. * calculate y2 first.
  484. */
  485. function fixAspectRatio(xFirst) {
  486. if (aspectRatio)
  487. if (xFirst) {
  488. x2 = max(left, min(left + imgWidth,
  489. x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1)));
  490. y2 = round(max(top, min(top + imgHeight,
  491. y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1))));
  492. x2 = round(x2);
  493. }
  494. else {
  495. y2 = max(top, min(top + imgHeight,
  496. y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1)));
  497. x2 = round(max(left, min(left + imgWidth,
  498. x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1))));
  499. y2 = round(y2);
  500. }
  501. }
  502. /**
  503. * Resize the selection area respecting the minimum/maximum dimensions and
  504. * aspect ratio
  505. */
  506. function doResize() {
  507. /*
  508. * Make sure the top left corner of the selection area stays within
  509. * image boundaries (it might not if the image source was dynamically
  510. * changed).
  511. */
  512. x1 = min(x1, left + imgWidth);
  513. y1 = min(y1, top + imgHeight);
  514. if (abs(x2 - x1) < minWidth) {
  515. /* Selection width is smaller than minWidth */
  516. x2 = x1 - minWidth * (x2 < x1 || -1);
  517. if (x2 < left)
  518. x1 = left + minWidth;
  519. else if (x2 > left + imgWidth)
  520. x1 = left + imgWidth - minWidth;
  521. }
  522. if (abs(y2 - y1) < minHeight) {
  523. /* Selection height is smaller than minHeight */
  524. y2 = y1 - minHeight * (y2 < y1 || -1);
  525. if (y2 < top)
  526. y1 = top + minHeight;
  527. else if (y2 > top + imgHeight)
  528. y1 = top + imgHeight - minHeight;
  529. }
  530. x2 = max(left, min(x2, left + imgWidth));
  531. y2 = max(top, min(y2, top + imgHeight));
  532. fixAspectRatio(abs(x2 - x1) < abs(y2 - y1) * aspectRatio);
  533. if (abs(x2 - x1) > maxWidth) {
  534. /* Selection width is greater than maxWidth */
  535. x2 = x1 - maxWidth * (x2 < x1 || -1);
  536. fixAspectRatio();
  537. }
  538. if (abs(y2 - y1) > maxHeight) {
  539. /* Selection height is greater than maxHeight */
  540. y2 = y1 - maxHeight * (y2 < y1 || -1);
  541. fixAspectRatio(true);
  542. }
  543. selection = { x1: selX(min(x1, x2)), x2: selX(max(x1, x2)),
  544. y1: selY(min(y1, y2)), y2: selY(max(y1, y2)),
  545. width: abs(x2 - x1), height: abs(y2 - y1) };
  546. update();
  547. options.onSelectChange(img, getSelection());
  548. }
  549. /**
  550. * Mousemove event handler triggered when the user is selecting an area
  551. *
  552. * @param event
  553. * The event object
  554. * @return false
  555. */
  556. function selectingMouseMove(event) {
  557. x2 = /w|e|^$/.test(resize) || aspectRatio ? evX(event) : viewX(selection.x2);
  558. y2 = /n|s|^$/.test(resize) || aspectRatio ? evY(event) : viewY(selection.y2);
  559. doResize();
  560. return false;
  561. }
  562. /**
  563. * Move the selection area
  564. *
  565. * @param newX1
  566. * New viewport X1
  567. * @param newY1
  568. * New viewport Y1
  569. */
  570. function doMove(newX1, newY1) {
  571. x2 = (x1 = newX1) + selection.width;
  572. y2 = (y1 = newY1) + selection.height;
  573. $.extend(selection, { x1: selX(x1), y1: selY(y1), x2: selX(x2),
  574. y2: selY(y2) });
  575. update();
  576. options.onSelectChange(img, getSelection());
  577. }
  578. /**
  579. * Mousemove event handler triggered when the selection area is being moved
  580. *
  581. * @param event
  582. * The event object
  583. * @return false
  584. */
  585. function movingMouseMove(event) {
  586. x1 = max(left, min(startX + evX(event), left + imgWidth - selection.width));
  587. y1 = max(top, min(startY + evY(event), top + imgHeight - selection.height));
  588. doMove(x1, y1);
  589. event.preventDefault();
  590. return false;
  591. }
  592. /**
  593. * Start selection
  594. */
  595. function startSelection() {
  596. $(document).off('mousemove touchmove', startSelection);
  597. adjust();
  598. x2 = x1;
  599. y2 = y1;
  600. doResize();
  601. resize = '';
  602. if (!$outer.is(':visible'))
  603. /* Show the plugin elements */
  604. $box.add($outer).hide().fadeIn(options.fadeSpeed||0);
  605. shown = true;
  606. $(document).off('mouseup touchend', cancelSelection)
  607. .on('mousemove touchmove', selectingMouseMove)
  608. .one('mouseup touchend', docMouseUp);
  609. $box.off('mousemove touchmove', areaMouseMove);
  610. options.onSelectStart(img, getSelection());
  611. }
  612. /**
  613. * Cancel selection
  614. */
  615. function cancelSelection() {
  616. $(document).off('mousemove touchmove', startSelection)
  617. .off('mouseup touchend', cancelSelection);
  618. hide($box.add($outer));
  619. setSelection(selX(x1), selY(y1), selX(x1), selY(y1));
  620. /* If this is an API call, callback functions should not be triggered */
  621. if (!(this instanceof $.imgAreaSelect)) {
  622. options.onSelectChange(img, getSelection());
  623. options.onSelectEnd(img, getSelection());
  624. }
  625. }
  626. /**
  627. * Image mousedown event handler
  628. *
  629. * @param event
  630. * The event object
  631. * @return false
  632. */
  633. function imgMouseDown(event) {
  634. /* Ignore the event if animation is in progress */
  635. if (event.which != 1 || $outer.is(':animated')) return false;
  636. adjust();
  637. startX = x1 = evX(event);
  638. startY = y1 = evY(event);
  639. /* Selection will start when the mouse is moved */
  640. $(document).on({ 'mousemove touchmove': startSelection,
  641. 'mouseup touchend': cancelSelection });
  642. return false;
  643. }
  644. /**
  645. * Window resize event handler
  646. */
  647. function windowResize() {
  648. doUpdate(false);
  649. }
  650. /**
  651. * Image load event handler. This is the final part of the initialization
  652. * process.
  653. */
  654. function imgLoad() {
  655. imgLoaded = true;
  656. /* Set options */
  657. setOptions(options = $.extend({
  658. classPrefix: 'imgareaselect',
  659. movable: true,
  660. parent: 'body',
  661. resizable: true,
  662. resizeMargin: 10,
  663. onInit: function () {},
  664. onSelectStart: function () {},
  665. onSelectChange: function () {},
  666. onSelectEnd: function () {}
  667. }, options));
  668. $box.add($outer).css({ visibility: '' });
  669. if (options.show) {
  670. shown = true;
  671. adjust();
  672. update();
  673. $box.add($outer).hide().fadeIn(options.fadeSpeed||0);
  674. }
  675. /*
  676. * Call the onInit callback. The setTimeout() call is used to ensure
  677. * that the plugin has been fully initialized and the object instance is
  678. * available (so that it can be obtained in the callback).
  679. */
  680. setTimeout(function () { options.onInit(img, getSelection()); }, 0);
  681. }
  682. /**
  683. * Document keypress event handler
  684. *
  685. * @param event
  686. * The event object
  687. * @return false
  688. */
  689. var docKeyPress = function(event) {
  690. var k = options.keys, d, t, key = event.keyCode;
  691. d = !isNaN(k.alt) && (event.altKey || event.originalEvent.altKey) ? k.alt :
  692. !isNaN(k.ctrl) && event.ctrlKey ? k.ctrl :
  693. !isNaN(k.shift) && event.shiftKey ? k.shift :
  694. !isNaN(k.arrows) ? k.arrows : 10;
  695. if (k.arrows == 'resize' || (k.shift == 'resize' && event.shiftKey) ||
  696. (k.ctrl == 'resize' && event.ctrlKey) ||
  697. (k.alt == 'resize' && (event.altKey || event.originalEvent.altKey)))
  698. {
  699. /* Resize selection */
  700. switch (key) {
  701. case 37:
  702. /* Left */
  703. d = -d;
  704. case 39:
  705. /* Right */
  706. t = max(x1, x2);
  707. x1 = min(x1, x2);
  708. x2 = max(t + d, x1);
  709. fixAspectRatio();
  710. break;
  711. case 38:
  712. /* Up */
  713. d = -d;
  714. case 40:
  715. /* Down */
  716. t = max(y1, y2);
  717. y1 = min(y1, y2);
  718. y2 = max(t + d, y1);
  719. fixAspectRatio(true);
  720. break;
  721. default:
  722. return;
  723. }
  724. doResize();
  725. }
  726. else {
  727. /* Move selection */
  728. x1 = min(x1, x2);
  729. y1 = min(y1, y2);
  730. switch (key) {
  731. case 37:
  732. /* Left */
  733. doMove(max(x1 - d, left), y1);
  734. break;
  735. case 38:
  736. /* Up */
  737. doMove(x1, max(y1 - d, top));
  738. break;
  739. case 39:
  740. /* Right */
  741. doMove(x1 + min(d, imgWidth - selX(x2)), y1);
  742. break;
  743. case 40:
  744. /* Down */
  745. doMove(x1, y1 + min(d, imgHeight - selY(y2)));
  746. break;
  747. default:
  748. return;
  749. }
  750. }
  751. return false;
  752. };
  753. /**
  754. * Apply style options to plugin element (or multiple elements)
  755. *
  756. * @param $elem
  757. * A jQuery object representing the element(s) to style
  758. * @param props
  759. * An object that maps option names to corresponding CSS
  760. * properties
  761. */
  762. function styleOptions($elem, props) {
  763. for (var option in props)
  764. if (options[option] !== undefined)
  765. $elem.css(props[option], options[option]);
  766. }
  767. /**
  768. * Set plugin options
  769. *
  770. * @param newOptions
  771. * The new options object
  772. */
  773. function setOptions(newOptions) {
  774. if (newOptions.parent)
  775. ($parent = $(newOptions.parent)).append($box.add($outer));
  776. /* Merge the new options with the existing ones */
  777. $.extend(options, newOptions);
  778. adjust();
  779. if (newOptions.handles != null) {
  780. /* Recreate selection area handles */
  781. $handles.remove();
  782. $handles = $([]);
  783. i = newOptions.handles ? newOptions.handles == 'corners' ? 4 : 8 : 0;
  784. while (i--)
  785. $handles = $handles.add(div());
  786. /* Add a class to handles and set the CSS properties */
  787. $handles.addClass(options.classPrefix + '-handle').css({
  788. position: 'absolute',
  789. /*
  790. * The font-size property needs to be set to zero, otherwise
  791. * Internet Explorer makes the handles too large
  792. */
  793. fontSize: 0,
  794. zIndex: zIndex + 1 || 1
  795. });
  796. /*
  797. * If handle width/height has not been set with CSS rules, set the
  798. * default 5px
  799. */
  800. if (!parseInt($handles.css('width')) >= 0)
  801. $handles.width(5).height(5);
  802. /*
  803. * If the borderWidth option is in use, add a solid border to
  804. * handles
  805. */
  806. if (o = options.borderWidth)
  807. $handles.css({ borderWidth: o, borderStyle: 'solid' });
  808. /* Apply other style options */
  809. styleOptions($handles, { borderColor1: 'border-color',
  810. borderColor2: 'background-color',
  811. borderOpacity: 'opacity' });
  812. }
  813. /* Calculate scale factors */
  814. scaleX = options.imageWidth / imgWidth || 1;
  815. scaleY = options.imageHeight / imgHeight || 1;
  816. /* Set selection */
  817. if (newOptions.x1 != null) {
  818. setSelection(newOptions.x1, newOptions.y1, newOptions.x2,
  819. newOptions.y2);
  820. newOptions.show = !newOptions.hide;
  821. }
  822. if (newOptions.keys)
  823. /* Enable keyboard support */
  824. options.keys = $.extend({ shift: 1, ctrl: 'resize' },
  825. newOptions.keys);
  826. /* Add classes to plugin elements */
  827. $outer.addClass(options.classPrefix + '-outer');
  828. $area.addClass(options.classPrefix + '-selection');
  829. for (i = 0; i++ < 4;)
  830. $($border[i-1]).addClass(options.classPrefix + '-border' + i);
  831. /* Apply style options */
  832. styleOptions($area, { selectionColor: 'background-color',
  833. selectionOpacity: 'opacity' });
  834. styleOptions($border, { borderOpacity: 'opacity',
  835. borderWidth: 'border-width' });
  836. styleOptions($outer, { outerColor: 'background-color',
  837. outerOpacity: 'opacity' });
  838. if (o = options.borderColor1)
  839. $($border[0]).css({ borderStyle: 'solid', borderColor: o });
  840. if (o = options.borderColor2)
  841. $($border[1]).css({ borderStyle: 'dashed', borderColor: o });
  842. /* Append all the selection area elements to the container box */
  843. $box.append($area.add($border).add($areaOpera)).append($handles);
  844. if (msie) {
  845. if (o = ($outer.css('filter')||'').match(/opacity=(\d+)/))
  846. $outer.css('opacity', o[1]/100);
  847. if (o = ($border.css('filter')||'').match(/opacity=(\d+)/))
  848. $border.css('opacity', o[1]/100);
  849. }
  850. if (newOptions.hide)
  851. hide($box.add($outer));
  852. else if (newOptions.show && imgLoaded) {
  853. shown = true;
  854. $box.add($outer).fadeIn(options.fadeSpeed||0);
  855. doUpdate();
  856. }
  857. /* Calculate the aspect ratio factor */
  858. aspectRatio = (d = (options.aspectRatio || '').split(/:/))[0] / d[1];
  859. $img.add($outer).unbind('mousedown', imgMouseDown);
  860. if (options.disable || options.enable === false) {
  861. /* Disable the plugin */
  862. $box.off({ 'mousemove touchmove': areaMouseMove,
  863. 'mousedown touchstart': areaMouseDown });
  864. $(window).off('resize', windowResize);
  865. }
  866. else {
  867. if (options.enable || options.disable === false) {
  868. /* Enable the plugin */
  869. if (options.resizable || options.movable)
  870. $box.on({ 'mousemove touchmove': areaMouseMove,
  871. 'mousedown touchstart': areaMouseDown });
  872. $(window).resize(windowResize);
  873. }
  874. if (!options.persistent)
  875. $img.add($outer).on('mousedown touchstart', imgMouseDown);
  876. }
  877. options.enable = options.disable = undefined;
  878. }
  879. /**
  880. * Remove plugin completely
  881. */
  882. this.remove = function () {
  883. /*
  884. * Call setOptions with { disable: true } to unbind the event handlers
  885. */
  886. setOptions({ disable: true });
  887. $box.add($outer).remove();
  888. };
  889. /*
  890. * Public API
  891. */
  892. /**
  893. * Get current options
  894. *
  895. * @return An object containing the set of options currently in use
  896. */
  897. this.getOptions = function () { return options; };
  898. /**
  899. * Set plugin options
  900. *
  901. * @param newOptions
  902. * The new options object
  903. */
  904. this.setOptions = setOptions;
  905. /**
  906. * Get the current selection
  907. *
  908. * @param noScale
  909. * If set to <code>true</code>, scaling is not applied to the
  910. * returned selection
  911. * @return Selection object
  912. */
  913. this.getSelection = getSelection;
  914. /**
  915. * Set the current selection
  916. *
  917. * @param x1
  918. * X coordinate of the upper left corner of the selection area
  919. * @param y1
  920. * Y coordinate of the upper left corner of the selection area
  921. * @param x2
  922. * X coordinate of the lower right corner of the selection area
  923. * @param y2
  924. * Y coordinate of the lower right corner of the selection area
  925. * @param noScale
  926. * If set to <code>true</code>, scaling is not applied to the
  927. * new selection
  928. */
  929. this.setSelection = setSelection;
  930. /**
  931. * Cancel selection
  932. */
  933. this.cancelSelection = cancelSelection;
  934. /**
  935. * Update plugin elements
  936. *
  937. * @param resetKeyPress
  938. * If set to <code>false</code>, this instance's keypress
  939. * event handler is not activated
  940. */
  941. this.update = doUpdate;
  942. /* Do the dreaded browser detection */
  943. var msie = (/msie ([\w.]+)/i.exec(ua)||[])[1],
  944. opera = /opera/i.test(ua),
  945. safari = /webkit/i.test(ua) && !/chrome/i.test(ua);
  946. /*
  947. * Traverse the image's parent elements (up to <body>) and find the
  948. * highest z-index
  949. */
  950. $p = $img;
  951. while ($p.length) {
  952. zIndex = max(zIndex,
  953. !isNaN($p.css('z-index')) ? $p.css('z-index') : zIndex);
  954. /* Also check if any of the ancestor elements has fixed position */
  955. if ($p.css('position') == 'fixed')
  956. position = 'fixed';
  957. $p = $p.parent(':not(body)');
  958. }
  959. /*
  960. * If z-index is given as an option, it overrides the one found by the
  961. * above loop
  962. */
  963. zIndex = options.zIndex || zIndex;
  964. if (msie)
  965. $img.attr('unselectable', 'on');
  966. /*
  967. * In MSIE and WebKit, we need to use the keydown event instead of keypress
  968. */
  969. $.imgAreaSelect.keyPress = msie || safari ? 'keydown' : 'keypress';
  970. /*
  971. * There is a bug affecting the CSS cursor property in Opera (observed in
  972. * versions up to 10.00) that prevents the cursor from being updated unless
  973. * the mouse leaves and enters the element again. To trigger the mouseover
  974. * event, we're adding an additional div to $box and we're going to toggle
  975. * it when mouse moves inside the selection area.
  976. */
  977. if (opera)
  978. $areaOpera = div().css({ width: '100%', height: '100%',
  979. position: 'absolute', zIndex: zIndex + 2 || 2 });
  980. /*
  981. * We initially set visibility to "hidden" as a workaround for a weird
  982. * behaviour observed in Google Chrome 1.0.154.53 (on Windows XP). Normally
  983. * we would just set display to "none", but, for some reason, if we do so
  984. * then Chrome refuses to later display the element with .show() or
  985. * .fadeIn().
  986. */
  987. $box.add($outer).css({ visibility: 'hidden', position: position,
  988. overflow: 'hidden', zIndex: zIndex || '0' });
  989. $box.css({ zIndex: zIndex + 2 || 2 });
  990. $area.add($border).css({ position: 'absolute', fontSize: 0 });
  991. /*
  992. * If the image has been fully loaded, or if it is not really an image (eg.
  993. * a div), call imgLoad() immediately; otherwise, bind it to be called once
  994. * on image load event.
  995. */
  996. img.complete || img.readyState == 'complete' || !$img.is('img') ?
  997. imgLoad() : $img.one('load', imgLoad);
  998. /*
  999. * MSIE 9.0 doesn't always fire the image load event -- resetting the src
  1000. * attribute seems to trigger it. The check is for version 7 and above to
  1001. * accommodate for MSIE 9 running in compatibility mode.
  1002. */
  1003. if (!imgLoaded && msie && msie >= 7)
  1004. img.src = img.src;
  1005. };
  1006. /**
  1007. * Invoke imgAreaSelect on a jQuery object containing the image(s)
  1008. *
  1009. * @param options
  1010. * Options object
  1011. * @return The jQuery object or a reference to imgAreaSelect instance (if the
  1012. * <code>instance</code> option was specified)
  1013. */
  1014. $.fn.imgAreaSelect = function (options) {
  1015. options = options || {};
  1016. this.each(function () {
  1017. /* Is there already an imgAreaSelect instance bound to this element? */
  1018. if ($(this).data('imgAreaSelect')) {
  1019. /* Yes there is -- is it supposed to be removed? */
  1020. if (options.remove) {
  1021. /* Remove the plugin */
  1022. $(this).data('imgAreaSelect').remove();
  1023. $(this).removeData('imgAreaSelect');
  1024. }
  1025. else
  1026. /* Reset options */
  1027. $(this).data('imgAreaSelect').setOptions(options);
  1028. }
  1029. else if (!options.remove) {
  1030. /* No exising instance -- create a new one */
  1031. /*
  1032. * If neither the "enable" nor the "disable" option is present, add
  1033. * "enable" as the default
  1034. */
  1035. if (options.enable === undefined && options.disable === undefined)
  1036. options.enable = true;
  1037. $(this).data('imgAreaSelect', new $.imgAreaSelect(this, options));
  1038. }
  1039. });
  1040. if (options.instance)
  1041. /*
  1042. * Return the imgAreaSelect instance bound to the first element in the
  1043. * set
  1044. */
  1045. return $(this).data('imgAreaSelect');
  1046. return this;
  1047. };
  1048. })(jQuery);