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.

1248 lines
38 KiB

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