cropper.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779
  1. /*!
  2. * Cropper v0.3.2
  3. * https://github.com/fengyuanchen/cropper
  4. *
  5. * Copyright 2014 Fengyuan Chen
  6. * Released under the MIT license
  7. */
  8. (function(factory) {
  9. if (typeof define === "function" && define.amd) {
  10. // AMD. Register as anonymous module.
  11. define(["jquery"], factory);
  12. } else {
  13. // Browser globals.
  14. factory(jQuery);
  15. }
  16. }(function($) {
  17. "use strict";
  18. var $window = $(window),
  19. Cropper = function(element, options) {
  20. options = $.isPlainObject(options) ? options : {};
  21. this.$image = $(element);
  22. this.defaults = $.extend({}, Cropper.defaults, this.$image.data(), options);
  23. this.init();
  24. };
  25. Cropper.prototype = {
  26. construstor: Cropper,
  27. init: function() {
  28. this.setAspectRatio(this.defaults.aspectRatio);
  29. this.render();
  30. },
  31. render: function(callback) {
  32. var that = this,
  33. $image = this.$image,
  34. $clone,
  35. src;
  36. if (this.active) {
  37. return;
  38. }
  39. if (this.$clone) {
  40. this.$clone.remove(); // Remove the old clone
  41. }
  42. src = $image.attr("src"); // Don't use "prop"
  43. $clone = $('<img src="' + src + '">');
  44. $clone.on("load", function() {
  45. var image;
  46. $clone.off("load");
  47. if (this.naturalWidth && this.naturalHeight) {
  48. image = {
  49. naturalHeight: this.naturalHeight,
  50. naturalWidth: this.naturalWidth
  51. };
  52. } else {
  53. Cropper.fn.size($clone, {
  54. height: "auto",
  55. width: "auto"
  56. });
  57. image = Cropper.fn.size($clone);
  58. image = {
  59. naturalHeight: image.height,
  60. naturalWidth: image.width
  61. };
  62. }
  63. Cropper.fn.size($clone, {
  64. height: "100%",
  65. width: "100%"
  66. });
  67. image.aspectRatio = image.naturalWidth / image.naturalHeight;
  68. that.src = src;
  69. that.image = image;
  70. that.active = true;
  71. that.createCropper();
  72. });
  73. if ($.isFunction(callback)) {
  74. $image.on("ready.cropper", callback);
  75. }
  76. this.$clone = $clone;
  77. $image.after($clone);
  78. },
  79. unrender: function() {
  80. if (this.active) {
  81. this.active = false;
  82. this.removeCropper();
  83. this.src = "";
  84. this.image = null;
  85. this.cropper = null;
  86. this.dragger = null;
  87. }
  88. },
  89. rerender: function() {
  90. this.unrender();
  91. this.render();
  92. },
  93. resize: function() {
  94. clearTimeout(this.resizing);
  95. this.resizing = setTimeout($.proxy(this.rerender, this), 200);
  96. },
  97. createCropper: function() {
  98. this.$cropper = $(Cropper.template);
  99. this.$dragger = this.$cropper.find(".cropper-dragger");
  100. Cropper.fn.toggle(this.$image);
  101. this.$image.after(this.$cropper);
  102. this.$cropper.prepend(this.$clone);
  103. if (!this.defaults.modal) {
  104. Cropper.fn.toggle(this.$cropper.find(".cropper-modal"));
  105. }
  106. this.setPreview();
  107. this.addListener();
  108. },
  109. removeCropper: function() {
  110. this.removeListener();
  111. this.$preview = null;
  112. this.$clone.remove();
  113. this.$clone = null;
  114. this.$dragger = null;
  115. this.$cropper.remove();
  116. this.$cropper = null;
  117. Cropper.fn.toggle(this.$image);
  118. },
  119. addListener: function() {
  120. this.$cropper.bind("mousedown touchstart", $.proxy(this.dragstart, this));
  121. this.$cropper.bind("mousemove touchmove", $.proxy(this.dragmove, this));
  122. this.$cropper.bind("mouseup mouseleave touchend touchleave", $.proxy(this.dragend, this));
  123. $window.on("resize", $.proxy(this.resize, this));
  124. },
  125. removeListener: function() {
  126. this.$cropper.unbind("mousedown touchstart", this.dragstart);
  127. this.$cropper.unbind("mousemove touchmove", this.dragmove);
  128. this.$cropper.unbind("mouseup mouseleave touchend touchleave", this.dragend);
  129. $window.off("resize", this.resize);
  130. },
  131. setPreview: function() {
  132. var preview = this.defaults.preview;
  133. this.$preview = this.$cropper.find(".cropper-preview");
  134. if (typeof preview === "string" && preview.length > 0) {
  135. this.$preview = this.$preview.add(preview);
  136. }
  137. this.$preview.html('<img src="' + this.src + '">');
  138. this.setCropper();
  139. },
  140. setCropper: function() {
  141. var $container = this.$image.parent(),
  142. container = Cropper.fn.size($container),
  143. image = this.image,
  144. cropper;
  145. if (((image.naturalWidth * container.height / image.naturalHeight) - container.width) >= 0) {
  146. cropper = {
  147. height: container.width / image.aspectRatio,
  148. width: container.width,
  149. left: 0
  150. };
  151. cropper.top = (container.height - cropper.height) / 2;
  152. } else {
  153. cropper = {
  154. height: container.height,
  155. width: container.height * image.aspectRatio,
  156. top: 0
  157. };
  158. cropper.left = (container.width - cropper.width) / 2;
  159. }
  160. $.each(cropper, function(i, n) {
  161. cropper[i] = Math.round(n);
  162. });
  163. image.height = cropper.height;
  164. image.width = cropper.width;
  165. image.ratio = image.width / image.naturalWidth;
  166. Cropper.fn.position($container);
  167. this.$cropper.css({
  168. height: cropper.height,
  169. left: cropper.left,
  170. top: cropper.top,
  171. width: cropper.width
  172. });
  173. this.cropper = cropper;
  174. this.setDragger();
  175. },
  176. setDragger: function() {
  177. var cropper = this.cropper,
  178. // If not set, use the original aspect ratio of the image.
  179. aspectRatio = this.defaults.aspectRatio || this.image.aspectRatio,
  180. dragger;
  181. if (((cropper.height * aspectRatio) - cropper.width) >= 0) {
  182. dragger = {
  183. height: cropper.width / aspectRatio,
  184. width: cropper.width,
  185. left: 0,
  186. top: (cropper.height - (cropper.width / aspectRatio)) / 2,
  187. maxWidth: cropper.width,
  188. maxHeight: cropper.width / aspectRatio
  189. };
  190. } else {
  191. dragger = {
  192. height: cropper.height,
  193. width: cropper.height * aspectRatio,
  194. left: (cropper.width - (cropper.height * aspectRatio)) / 2,
  195. top: 0,
  196. maxHeight: cropper.height,
  197. maxWidth: cropper.height * aspectRatio
  198. };
  199. }
  200. dragger.height *= 0.8;
  201. dragger.width *= 0.8;
  202. dragger.left = (cropper.width - dragger.width) / 2;
  203. dragger.top = (cropper.height - dragger.height) / 2;
  204. this.dragger = Cropper.fn.round(dragger);
  205. this.setData(this.defaults.data);
  206. this.$image.trigger("ready.cropper").off("ready.cropper");
  207. },
  208. resetDragger: function() {
  209. var dragger = this.dragger,
  210. cropper = this.cropper;
  211. dragger.width = dragger.width > dragger.maxWidth ? dragger.maxWidth : Math.abs(dragger.width);
  212. dragger.height = dragger.height > dragger.maxHeight ? dragger.maxHeight : Math.abs(dragger.height);
  213. dragger.maxLeft = cropper.width - dragger.width;
  214. dragger.maxTop = cropper.height - dragger.height;
  215. dragger.left = dragger.left < 0 ? 0 : dragger.left > dragger.maxLeft ? dragger.maxLeft : dragger.left;
  216. dragger.top = dragger.top < 0 ? 0 : dragger.top > dragger.maxTop ? dragger.maxTop : dragger.top;
  217. dragger = Cropper.fn.round(dragger);
  218. this.$dragger.css({
  219. height: dragger.height,
  220. left: dragger.left,
  221. top: dragger.top,
  222. width: dragger.width
  223. });
  224. this.dragger = dragger;
  225. this.preview();
  226. this.output();
  227. },
  228. dragging: function() {
  229. var direction = this.direction,
  230. dragger = this.dragger,
  231. aspectRatio = this.defaults.aspectRatio,
  232. range = {
  233. x: this.endX - this.startX,
  234. y: this.endY - this.startY
  235. };
  236. if (aspectRatio) {
  237. range.X = range.y * aspectRatio;
  238. range.Y = range.x / aspectRatio;
  239. }
  240. switch (direction) {
  241. // dragging
  242. case "e":
  243. dragger.width += range.x;
  244. if (aspectRatio) {
  245. dragger.height = dragger.width / aspectRatio;
  246. dragger.top -= range.Y / 2;
  247. }
  248. if (dragger.width < 0) {
  249. this.direction = "w";
  250. dragger.width = 0;
  251. }
  252. break;
  253. case "n":
  254. dragger.height -= range.y;
  255. dragger.top += range.y;
  256. if (aspectRatio) {
  257. dragger.width = dragger.height * aspectRatio;
  258. dragger.left += range.X / 2;
  259. }
  260. if (dragger.height < 0) {
  261. this.direction = "s";
  262. dragger.height = 0;
  263. }
  264. break;
  265. case "w":
  266. dragger.width -= range.x;
  267. dragger.left += range.x;
  268. if (aspectRatio) {
  269. dragger.height = dragger.width / aspectRatio;
  270. dragger.top += range.Y / 2;
  271. }
  272. if (dragger.width < 0) {
  273. this.direction = "e";
  274. dragger.width = 0;
  275. }
  276. break;
  277. case "s":
  278. dragger.height += range.y;
  279. if (aspectRatio) {
  280. dragger.width = dragger.height * aspectRatio;
  281. dragger.left -= range.X / 2;
  282. }
  283. if (dragger.height < 0) {
  284. this.direction = "n";
  285. dragger.height = 0;
  286. }
  287. break;
  288. case "ne":
  289. dragger.height -= range.y;
  290. dragger.top += range.y;
  291. if (aspectRatio) {
  292. dragger.width = dragger.height * aspectRatio;
  293. } else {
  294. dragger.width += range.x;
  295. }
  296. if (dragger.height < 0) {
  297. this.direction = "sw";
  298. dragger.height = 0;
  299. dragger.width = 0;
  300. }
  301. break;
  302. case "nw":
  303. dragger.height -= range.y;
  304. dragger.top += range.y;
  305. if (aspectRatio) {
  306. dragger.width = dragger.height * aspectRatio;
  307. dragger.left += range.X;
  308. } else {
  309. dragger.width -= range.x;
  310. dragger.left += range.x;
  311. }
  312. if (dragger.height < 0) {
  313. this.direction = "se";
  314. dragger.height = 0;
  315. dragger.width = 0;
  316. }
  317. break;
  318. case "sw":
  319. dragger.width -= range.x;
  320. dragger.left += range.x;
  321. if (aspectRatio) {
  322. dragger.height = dragger.width / aspectRatio;
  323. } else {
  324. dragger.height += range.y;
  325. }
  326. if (dragger.width < 0) {
  327. this.direction = "ne";
  328. dragger.height = 0;
  329. dragger.width = 0;
  330. }
  331. break;
  332. case "se":
  333. dragger.width += range.x;
  334. if (aspectRatio) {
  335. dragger.height = dragger.width / aspectRatio;
  336. } else {
  337. dragger.height += range.y;
  338. }
  339. if (dragger.width < 0) {
  340. this.direction = "nw";
  341. dragger.height = 0;
  342. dragger.width = 0;
  343. }
  344. break;
  345. // moving
  346. default:
  347. dragger.left += range.x;
  348. dragger.top += range.y;
  349. }
  350. this.resetDragger();
  351. this.startX = this.endX;
  352. this.startY = this.endY;
  353. },
  354. output: function() {
  355. this.defaults.done(this.getData());
  356. },
  357. preview: function() {
  358. var that = this,
  359. cropper = that.cropper,
  360. dragger = that.dragger;
  361. this.$preview.each(function() {
  362. var $this = $(this),
  363. ratio = $this.outerWidth() / dragger.width,
  364. styles = {
  365. height: cropper.height,
  366. marginLeft: - dragger.left,
  367. marginTop: - dragger.top,
  368. width: cropper.width
  369. };
  370. $this.css({overflow: "hidden"});
  371. $this.find("img").css(Cropper.fn.round(styles, function(n) {
  372. return n * ratio;
  373. }));
  374. });
  375. },
  376. // Public methods
  377. enable: function(callback) {
  378. this.render(callback);
  379. },
  380. disable: function() {
  381. this.unrender();
  382. },
  383. setAspectRatio: function(aspectRatio) {
  384. if (aspectRatio === "auto" || ($.isNumeric(aspectRatio) && aspectRatio > 0)) {
  385. this.defaults.aspectRatio = aspectRatio === "auto" ? NaN : aspectRatio;
  386. if (this.active) {
  387. this.setDragger();
  388. }
  389. }
  390. },
  391. setData: function(data) {
  392. var cropper = this.cropper,
  393. dragger = this.dragger,
  394. aspectRatio = this.defaults.aspectRatio,
  395. isNumber = function(n) {
  396. return typeof n === "number";
  397. };
  398. if (!this.active) {
  399. return;
  400. }
  401. if ($.isPlainObject(data) && !$.isEmptyObject(data)) {
  402. data = Cropper.fn.transformData(data, this.image.ratio);
  403. if (isNumber(data.x1) && data.x1 <= cropper.width) {
  404. dragger.left = data.x1;
  405. }
  406. if (isNumber(data.y1) && data.y1 <= cropper.height) {
  407. dragger.top = data.y1;
  408. }
  409. if (aspectRatio){
  410. if (isNumber(data.width) && data.width <= cropper.width) {
  411. dragger.width = data.width;
  412. dragger.height = dragger.width / aspectRatio;
  413. } else if (isNumber(data.height) && data.height <= cropper.height) {
  414. dragger.height = data.height;
  415. dragger.width = dragger.height * aspectRatio;
  416. } else if (isNumber(data.x2) && data.x2 <= cropper.width) {
  417. dragger.width = data.x2 - dragger.left;
  418. dragger.height = dragger.width / aspectRatio;
  419. } else if (isNumber(data.y2) && data.y2 <= cropper.height) {
  420. dragger.height = data.y2 - dragger.top;
  421. dragger.width = dragger.height * aspectRatio;
  422. }
  423. } else {
  424. if (isNumber(data.width) && data.width <= cropper.width) {
  425. dragger.width = data.width;
  426. } else if (isNumber(data.x2) && data.x2 <= cropper.width) {
  427. dragger.width = data.x2 - dragger.left;
  428. }
  429. if (isNumber(data.height) && data.height <= cropper.height) {
  430. dragger.height = data.height;
  431. } else if (isNumber(data.y2) && data.height <= cropper.height) {
  432. dragger.height = data.y2 - dragger.top;
  433. }
  434. }
  435. }
  436. this.dragger = dragger;
  437. this.resetDragger();
  438. },
  439. getData: function() {
  440. var dragger = this.dragger,
  441. data = {};
  442. if (this.active) {
  443. data = {
  444. x1: dragger.left,
  445. y1: dragger.top,
  446. width: dragger.width,
  447. height: dragger.height,
  448. x2: dragger.left + dragger.width,
  449. y2: dragger.top + dragger.height
  450. };
  451. data = Cropper.fn.transformData(data, (1 / this.image.ratio));
  452. }
  453. return data;
  454. },
  455. setImgSrc: function(src) {
  456. if (typeof src === "string" && src.length > 0 && src !== this.src) {
  457. this.$image.attr("src", src);
  458. this.rerender();
  459. }
  460. },
  461. getImgInfo: function() {
  462. return this.image || {};
  463. },
  464. // Public events
  465. dragstart: function(event) {
  466. var touches = Cropper.fn.getOriginalEvent(event).touches,
  467. e = event,
  468. touching,
  469. direction;
  470. if (touches && touches.length === 1) {
  471. e = touches[0];
  472. this.touchId = e.identifier;
  473. touching = true;
  474. }
  475. direction = $(e.target).data().direction;
  476. if (Cropper.fn.isDirection(direction)) {
  477. this.startX = e.pageX;
  478. this.startY = e.pageY;
  479. this.direction = direction;
  480. this.$image.trigger("dragstart");
  481. touching && event.preventDefault();
  482. }
  483. },
  484. dragmove: function(event) {
  485. var touches = Cropper.fn.getOriginalEvent(event).changedTouches,
  486. e = event,
  487. touching;
  488. if (touches && touches.length === 1) {
  489. e = touches[0];
  490. touching = true;
  491. if (e.identifier !== this.touchId) {
  492. return;
  493. }
  494. }
  495. if (this.direction) {
  496. this.$image.trigger("dragmove");
  497. touching && event.preventDefault();
  498. this.endX = e.pageX;
  499. this.endY = e.pageY;
  500. this.dragging();
  501. }
  502. },
  503. dragend: function(event) {
  504. var touches = Cropper.fn.getOriginalEvent(event).changedTouches,
  505. e = event,
  506. touching;
  507. if (touches && touches.length === 1) {
  508. e = touches[0];
  509. touching = true;
  510. if (e.identifier !== this.touchId) {
  511. return;
  512. }
  513. }
  514. if (this.direction) {
  515. this.direction = "";
  516. this.$image.trigger("dragend");
  517. touching && event.preventDefault();
  518. }
  519. }
  520. };
  521. // Common methods
  522. Cropper.fn = {
  523. toggle: function($e) {
  524. $e.toggleClass("cropper-hidden");
  525. },
  526. position: function($e, option) {
  527. var position = $e.css("position");
  528. if (position === "static") {
  529. $e.css("position", option || "relative");
  530. }
  531. },
  532. size: function($e, options) {
  533. if ($.isPlainObject(options)) {
  534. $e.css(options);
  535. } else {
  536. return {
  537. height: $e.height(),
  538. width: $e.width()
  539. };
  540. }
  541. },
  542. round: function(data, fn) {
  543. var value,
  544. i;
  545. for (i in data) {
  546. value = data[i];
  547. if (data.hasOwnProperty(i) && typeof value === "number") {
  548. data[i] = Math.round($.isFunction(fn) ? fn(value) : value);
  549. }
  550. }
  551. return data;
  552. },
  553. transformData: function(data, ratio) {
  554. var that = this,
  555. result = {};
  556. $.each(data, function(i, n) {
  557. if (that.isDataOption(i) && $.isNumeric(n) && n >= 0) {
  558. result[i] = Math.round(n * ratio);
  559. }
  560. });
  561. return result;
  562. },
  563. getOriginalEvent: function(event) {
  564. if (event && typeof event.originalEvent !== "undefined") {
  565. event = event.originalEvent;
  566. }
  567. return event;
  568. },
  569. isDataOption: function(s) {
  570. return /^(x1|y1|x2|y2|width|height)$/i.test(s);
  571. },
  572. isDirection: function(s) {
  573. return /^(\*|e|n|w|s|ne|nw|sw|se)$/i.test(s);
  574. }
  575. };
  576. Cropper.template = [
  577. '<div class="cropper-container">',
  578. '<div class="cropper-modal"></div>',
  579. '<div class="cropper-dragger">',
  580. '<span class="cropper-preview"></span>',
  581. '<span class="cropper-dashed dashed-h"></span>',
  582. '<span class="cropper-dashed dashed-v"></span>',
  583. '<span class="cropper-face" data-direction="*"></span>',
  584. '<span class="cropper-line line-e" data-direction="e"></span>',
  585. '<span class="cropper-line line-n" data-direction="n"></span>',
  586. '<span class="cropper-line line-w" data-direction="w"></span>',
  587. '<span class="cropper-line line-s" data-direction="s"></span>',
  588. '<span class="cropper-point point-e" data-direction="e"></span>',
  589. '<span class="cropper-point point-n" data-direction="n"></span>',
  590. '<span class="cropper-point point-w" data-direction="w"></span>',
  591. '<span class="cropper-point point-s" data-direction="s"></span>',
  592. '<span class="cropper-point point-ne" data-direction="ne"></span>',
  593. '<span class="cropper-point point-nw" data-direction="nw"></span>',
  594. '<span class="cropper-point point-sw" data-direction="sw"></span>',
  595. '<span class="cropper-point point-se" data-direction="se"></span>',
  596. '</div>',
  597. '</div>'
  598. ].join("");
  599. Cropper.defaults = {
  600. aspectRatio: "auto",
  601. data: {},
  602. done: function(/* data */) {},
  603. modal: true,
  604. preview: ""
  605. };
  606. Cropper.setDefaults = function(options) {
  607. $.extend(Cropper.defaults, options);
  608. };
  609. // Register as jQuery plugin
  610. $.fn.cropper = function(options, settings) {
  611. var result = this;
  612. this.each(function() {
  613. var $this = $(this),
  614. data = $this.data("cropper");
  615. if (!data) {
  616. data = new Cropper(this, options);
  617. $this.data("cropper", data);
  618. }
  619. if (typeof options === "string" && $.isFunction(data[options])) {
  620. result = data[options](settings);
  621. }
  622. });
  623. return (typeof result !== "undefined" ? result : this);
  624. };
  625. $.fn.cropper.Constructor = Cropper;
  626. $.fn.cropper.setDefaults = Cropper.setDefaults;
  627. $(function() {
  628. $("img[cropper]").cropper();
  629. });
  630. }));