├── .gitattributes ├── .bowerrc ├── demo ├── .bowerrc ├── app │ ├── favicon.ico │ ├── images │ │ └── balloons.jpg │ ├── styles │ │ ├── main.css │ │ └── cropme.css │ ├── scripts │ │ ├── app.coffee │ │ ├── controllers │ │ │ └── main.coffee │ │ └── cropme.js │ ├── views │ │ └── main.html │ └── index.html ├── tasks │ └── options │ │ ├── wiredep.coffee │ │ ├── clean.coffee │ │ ├── watch.coffee │ │ ├── usemin.coffee │ │ ├── useminPrepare.coffee │ │ ├── copy.coffee │ │ ├── coffee.coffee │ │ └── connect.coffee ├── bower.json ├── Gruntfile.coffee └── package.json ├── .gitignore ├── tasks ├── options │ ├── ngannotate.coffee │ ├── clean.coffee │ ├── karma.coffee │ ├── ngdocs.coffee │ ├── concat.coffee │ ├── copy.coffee │ ├── connect.coffee │ ├── coffee.coffee │ ├── watch.coffee │ └── compass.coffee └── templates │ └── ngdocs.html ├── scripts ├── app.coffee ├── services │ ├── elementOffset.coffee │ └── canvasToBlob.coffee └── directives │ ├── dropbox.coffee │ └── cropme.coffee ├── bower.json ├── test ├── spec │ └── canvasToBlob.coffee └── karma.conf.coffee ├── package.json ├── LICENSE ├── Gruntfile.coffee ├── styles └── cropme.scss ├── cropme.css ├── README.md └── cropme.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /demo/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /demo/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standuprey/cropme/HEAD/demo/app/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/bower_components 3 | dist 4 | docs 5 | .tmp 6 | .sass-cache 7 | .DS_Store -------------------------------------------------------------------------------- /demo/app/images/balloons.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/standuprey/cropme/HEAD/demo/app/images/balloons.jpg -------------------------------------------------------------------------------- /demo/app/styles/main.css: -------------------------------------------------------------------------------- 1 | html, body, .container { 2 | height: 100%; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /demo/tasks/options/wiredep.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | dist: 3 | src: ['app/index.html'] 4 | ignorePath: /\.\.\// 5 | -------------------------------------------------------------------------------- /demo/tasks/options/clean.coffee: -------------------------------------------------------------------------------- 1 | module.exports = dist: 2 | files: [ 3 | dot: true 4 | src: [ 5 | "*.js" 6 | ".tmp" 7 | ] 8 | ] -------------------------------------------------------------------------------- /tasks/options/ngannotate.coffee: -------------------------------------------------------------------------------- 1 | module.exports = dist: 2 | files: [ 3 | expand: true 4 | cwd: "." 5 | src: "*.js" 6 | dest: "." 7 | ] -------------------------------------------------------------------------------- /demo/tasks/options/watch.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | coffee: 3 | files: ["app/scripts/{,*/}*.{coffee,litcoffee,coffee.md}"] 4 | tasks: ["coffee:dist"] 5 | -------------------------------------------------------------------------------- /tasks/options/clean.coffee: -------------------------------------------------------------------------------- 1 | module.exports = dist: 2 | files: [ 3 | dot: true 4 | src: [ 5 | "*.js" 6 | ".tmp" 7 | "docs" 8 | ] 9 | ] -------------------------------------------------------------------------------- /demo/tasks/options/usemin.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | html: ['dist/{,*/}*.html'] 3 | css: ['dist/styles/{,*/}*.css'] 4 | options: 5 | assetsDirs: ['dist','dist/images'] 6 | -------------------------------------------------------------------------------- /tasks/options/karma.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | unit: 3 | configFile: "test/karma.conf.coffee" 4 | singleRun: true 5 | debug: 6 | configFile: "test/karma.conf.coffee" 7 | singleRun: false 8 | browsers: ['Chrome'] 9 | -------------------------------------------------------------------------------- /tasks/options/ngdocs.coffee: -------------------------------------------------------------------------------- 1 | appName = require('../../bower.json').name 2 | 3 | module.exports = 4 | options: 5 | navTemplate: "tasks/templates/ngdocs.html" 6 | 7 | api: 8 | src: ["#{appName}.js"] 9 | title: appName 10 | -------------------------------------------------------------------------------- /tasks/options/concat.coffee: -------------------------------------------------------------------------------- 1 | appName = require('../../bower.json').name 2 | 3 | module.exports = 4 | options: 5 | separator: ";" 6 | 7 | dist: 8 | src: [ 9 | ".tmp/scripts/**/*.js" 10 | ] 11 | dest: "#{appName}.js" -------------------------------------------------------------------------------- /demo/app/scripts/app.coffee: -------------------------------------------------------------------------------- 1 | "use strict" 2 | angular.module("cropmeDemo", ["cropme", "ngRoute"]).config ($routeProvider) -> 3 | $routeProvider.when("/", 4 | templateUrl: "views/main.html" 5 | controller: "MainCtrl" 6 | ).otherwise redirectTo: "/" 7 | -------------------------------------------------------------------------------- /demo/tasks/options/useminPrepare.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | html: "app/index.html" 3 | options: 4 | dest: "dist" 5 | flow: 6 | html: 7 | steps: 8 | js: ["concat"] 9 | css: ["cssmin"] 10 | 11 | post: {} -------------------------------------------------------------------------------- /tasks/options/copy.coffee: -------------------------------------------------------------------------------- 1 | appName = require('../../bower.json').name 2 | 3 | module.exports = 4 | demo: 5 | files: [ 6 | dest: "demo/app/scripts/cropme.js" 7 | src: "cropme.js" 8 | , 9 | dest: "demo/app/styles/cropme.css" 10 | src: "cropme.css" 11 | ] 12 | -------------------------------------------------------------------------------- /tasks/options/connect.coffee: -------------------------------------------------------------------------------- 1 | module.exports = test: 2 | options: 3 | port: 9009 4 | middleware: (connect) -> 5 | [ 6 | connect.static(".tmp") 7 | connect.static("test") 8 | connect().use("/bower_components", connect.static("./bower_components")) 9 | ] -------------------------------------------------------------------------------- /tasks/options/coffee.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | options: 3 | sourceMap: false 4 | sourceRoot: "" 5 | 6 | dist: 7 | files: [ 8 | expand: true 9 | cwd: "scripts" 10 | src: "{,*/}*.coffee" 11 | dest: ".tmp/scripts/" 12 | ext: ".js" 13 | ] 14 | -------------------------------------------------------------------------------- /tasks/options/watch.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | coffee: 3 | files: ["scripts/{,*/}*.{coffee,litcoffee,coffee.md}"] 4 | tasks: ["coffee:dist"] 5 | 6 | coffeeTest: 7 | files: ["test/spec/{,*/}*.{coffee,litcoffee,coffee.md}"] 8 | tasks: [ 9 | "coffee:test" 10 | "test" 11 | ] -------------------------------------------------------------------------------- /demo/app/scripts/controllers/main.coffee: -------------------------------------------------------------------------------- 1 | "use strict" 2 | angular.module("cropmeDemo").controller "MainCtrl", ($scope, $timeout, $sce) -> 3 | $timeout -> 4 | $scope.src = $sce.trustAsResourceUrl "images/balloons.jpg" 5 | , 100 6 | 7 | $scope.$on "cropme:done", (e, blob, type, id) -> 8 | console.log blob, type, id 9 | -------------------------------------------------------------------------------- /demo/app/views/main.html: -------------------------------------------------------------------------------- 1 |
2 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /demo/tasks/options/copy.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | dist: 3 | files: [ 4 | { 5 | expand: true 6 | dot: true 7 | cwd: "app" 8 | dest: "dist" 9 | src: [ 10 | "*.{ico,png,txt}" 11 | "*.html" 12 | "views/{,*/}*.html" 13 | "styles/*.css" 14 | "images/*.*" 15 | ] 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /tasks/options/compass.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | options: 3 | sassDir: "styles" 4 | cssDir: "." 5 | raw: "http_images_path = \"images/\"\ngenerated_images_dir = \".tmp/images\"\nhttp_generated_images_path = \"../images/\"" 6 | 7 | # This doesn't work with relative paths. 8 | relativeAssets: false 9 | 10 | dist: {} 11 | -------------------------------------------------------------------------------- /demo/tasks/options/coffee.coffee: -------------------------------------------------------------------------------- 1 | # Compiles CoffeeScript to JavaScript 2 | module.exports = 3 | options: 4 | sourceMap: false 5 | sourceRoot: "" 6 | 7 | dist: 8 | files: [ 9 | expand: true 10 | cwd: "app/scripts" 11 | src: "{,*/}*.coffee" 12 | dest: ".tmp/scripts/" 13 | ext: ".js" 14 | ] 15 | -------------------------------------------------------------------------------- /scripts/app.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @ngdoc overview 3 | # @name cropme 4 | # @requires ngSanitize, ngTouch, superswipe 5 | # @description 6 | # Drag and drop or select an image, crop it and get the blob, that you can use to upload wherever and however you want 7 | # 8 | ### 9 | angular.module "cropme", ["ngSanitize", "ngTouch", "superswipe"] 10 | -------------------------------------------------------------------------------- /demo/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Stanislas Duprey", 3 | "name": "cropme-demo", 4 | "version": "1.0.3", 5 | "dependencies": { 6 | "angular": "^1.2.10", 7 | "angular-sanitize": "^1.2.10", 8 | "angular-route": "^1.2.10", 9 | "angular-touch": "^1.2.10", 10 | "angular-superswipe": "^1.0.0" 11 | }, 12 | "devDependencies": { 13 | "angular-mocks": "^1.2.10", 14 | "angular-scenario": "^1.2.10" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tasks/templates/ngdocs.html: -------------------------------------------------------------------------------- 1 |
2 | 13 |
-------------------------------------------------------------------------------- /demo/tasks/options/connect.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | options: 3 | port: 9001 4 | 5 | # Change this to '0.0.0.0' to access the server from outside. 6 | hostname: "localhost" 7 | livereload: 35729 8 | 9 | livereload: 10 | options: 11 | open: true 12 | middleware: (connect) -> 13 | [ 14 | connect.static(".tmp") 15 | connect().use("/bower_components", connect.static("./bower_components")) 16 | connect.static("app") 17 | ] -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Stanislas Duprey", 3 | "name": "cropme", 4 | "description": "Preview and crop a picture before upload", 5 | "homepage": "http://github.com/standup75/cropme", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/standup75/cropme.git" 9 | }, 10 | "main": [ 11 | "./cropme.js", 12 | "./cropme.css" 13 | ], 14 | "version": "1.0.5", 15 | "dependencies": { 16 | "angular": "^1.5.8", 17 | "angular-sanitize": "^1.5.8", 18 | "angular-touch": "^1.5.8", 19 | "angular-superswipe": "^1.0.0" 20 | }, 21 | "devDependencies": { 22 | "angular-mocks": "^1.5.8", 23 | "angular-scenario": "^1.5.8" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/spec/canvasToBlob.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | describe 'Service: canvasToBlob', -> 4 | 5 | beforeEach module 'cropme' 6 | 7 | it 'should create a blob with the french flag in it', inject (canvasToBlob) -> 8 | # upload the french flag 9 | uploader = (blob) -> expect(blob.size).toBe 1107 10 | canvas = document.createElement "canvas" 11 | canvas.height = 100 12 | canvas.width = 300 13 | ctx = canvas.getContext "2d" 14 | ctx.fillStyle = "#0000FF" 15 | ctx.fillRect 0, 0, 100, 100 16 | ctx.fillStyle = "#FFFFFF" 17 | ctx.fillRect 100, 0, 200, 100 18 | ctx.fillStyle = "#FF0000" 19 | ctx.fillRect 200, 0, 300, 100 20 | canvasToBlob canvas, uploader, "image/png" 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cropme", 3 | "version": "1.0.5", 4 | "dependencies": {}, 5 | "devDependencies": { 6 | "grunt": "^0.4.5", 7 | "grunt-contrib-clean": "^0.5.0", 8 | "grunt-contrib-coffee": "^0.10.1", 9 | "grunt-contrib-watch": "^0.6.1", 10 | "grunt-contrib-concat": "^0.4.0", 11 | "grunt-contrib-connect": "^0.7.1", 12 | "grunt-contrib-compass": "^1.0.1", 13 | "grunt-contrib-watch": "^0.6.1", 14 | "grunt-contrib-copy": "~0.6.0", 15 | "grunt-karma": "^0.12", 16 | "grunt-ng-annotate": "^0.3.2", 17 | "grunt-ngdocs": "^0.2.2", 18 | "karma-coverage": "^0.1", 19 | "karma-jasmine": "^0.2.2", 20 | "karma-coffee-preprocessor": "^0.2.1", 21 | "karma-phantomjs-launcher": "^0.1.4", 22 | "karma-chrome-launcher": "^0.1.4", 23 | "load-grunt-tasks": "^0.4.0" 24 | }, 25 | "engines": { 26 | "node": ">=0.10.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo/Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | "use strict" 2 | module.exports = (grunt) -> 3 | 4 | # Load grunt tasks automatically 5 | require("load-grunt-tasks") grunt 6 | 7 | # Define the configuration for all the tasks 8 | grunt.initConfig 9 | clean: require "./tasks/options/clean" 10 | coffee: require "./tasks/options/coffee" 11 | connect: require "./tasks/options/connect" 12 | copy: require "./tasks/options/copy" 13 | watch: require "./tasks/options/watch" 14 | useminPrepare: require "./tasks/options/useminPrepare" 15 | usemin: require "./tasks/options/usemin" 16 | 17 | grunt.registerTask "serve", [ 18 | "clean" 19 | "coffee" 20 | "connect:livereload" 21 | "watch" 22 | ] 23 | 24 | grunt.registerTask "build", [ 25 | "clean" 26 | "useminPrepare" 27 | "coffee" 28 | "concat" 29 | "copy" 30 | "usemin" 31 | 32 | ] 33 | 34 | grunt.registerTask "default", ["build"] 35 | return -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cropme-demo", 3 | "version": "1.0.3", 4 | "dependencies": {}, 5 | "devDependencies": { 6 | "grunt": "^0.4.1", 7 | "grunt-autoprefixer": "^0.7.3", 8 | "grunt-contrib-clean": "^0.5.0", 9 | "grunt-contrib-coffee": "^0.10.1", 10 | "grunt-contrib-compass": "^0.7.2", 11 | "grunt-contrib-concat": "^0.4.0", 12 | "grunt-contrib-connect": "^0.7.1", 13 | "grunt-contrib-copy": "^0.5.0", 14 | "grunt-contrib-cssmin": "^0.9.0", 15 | "grunt-contrib-htmlmin": "^0.3.0", 16 | "grunt-contrib-imagemin": "^0.7.0", 17 | "grunt-contrib-jshint": "^0.10.0", 18 | "grunt-contrib-uglify": "^0.4.0", 19 | "grunt-contrib-watch": "^0.6.1", 20 | "grunt-filerev": "^0.2.1", 21 | "grunt-google-cdn": "^0.4.0", 22 | "grunt-newer": "^0.7.0", 23 | "grunt-ngmin": "^0.0.3", 24 | "grunt-svgmin": "^0.4.0", 25 | "grunt-usemin": "^2.1.1", 26 | "jshint-stylish": "^0.2.0", 27 | "load-grunt-tasks": "^0.4.0", 28 | "time-grunt": "^0.3.1", 29 | "coffee-script": "^1.7.1" 30 | }, 31 | "engines": { 32 | "node": ">=0.10.0" 33 | } 34 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013-2014 standup75 https://github.com/standup75 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | "use strict" 2 | module.exports = (grunt) -> 3 | 4 | # Load grunt tasks automatically 5 | require("load-grunt-tasks") grunt 6 | 7 | # Define the configuration for all the tasks 8 | grunt.initConfig 9 | copy: require "./tasks/options/copy" 10 | clean: require "./tasks/options/clean" 11 | coffee: require "./tasks/options/coffee" 12 | compass: require "./tasks/options/compass" 13 | connect: require "./tasks/options/connect" 14 | karma: require "./tasks/options/karma" 15 | concat: require "./tasks/options/concat" 16 | ngAnnotate: require "./tasks/options/ngannotate" 17 | ngdocs: require "./tasks/options/ngdocs" 18 | watch: require "./tasks/options/watch" 19 | 20 | grunt.registerTask "build", [ 21 | "clean" 22 | "compass" 23 | "coffee" 24 | "concat" 25 | "ngAnnotate" 26 | "ngdocs" 27 | "copy" 28 | "connect:test" 29 | #"karma:unit" 30 | ] 31 | 32 | grunt.registerTask "debug", [ 33 | "clean" 34 | "compass" 35 | "coffee" 36 | "concat" 37 | "ngAnnotate" 38 | "ngdocs" 39 | "connect:test" 40 | #"karma:debug" 41 | ] 42 | grunt.registerTask "serve", [ 43 | "build" 44 | "watch" 45 | ] 46 | grunt.registerTask "default", ["build"] 47 | return -------------------------------------------------------------------------------- /scripts/services/elementOffset.coffee: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | ###* 4 | # @ngdoc service 5 | # @name elementOffset 6 | # @requires - 7 | # @description 8 | # Get the offset in pixel of an element on the screen 9 | # 10 | # @example 11 | 12 | ```js 13 | angular.module("cropme").directive "myDirective", (elementOffset) -> 14 | link: (scope, element, attributes) -> 15 | offset = elementOffset element 16 | console.log "This directive's element is #{offset.top}px away from the top of the screen" 17 | console.log "This directive's element is #{offset.left}px away from the left of the screen" 18 | console.log "This directive's element is #{offset.bottom}px away from the bottom of the screen" 19 | console.log "This directive's element is #{offset.right}px away from the right of the screen" 20 | ``` 21 | ### 22 | angular.module("cropme").service "elementOffset", -> 23 | (el) -> 24 | el = el[0] if el[0] 25 | offsetTop = 0 26 | offsetLeft = 0 27 | scrollTop = 0 28 | scrollLeft = 0 29 | width = el.offsetWidth 30 | height = el.offsetHeight 31 | while el 32 | offsetTop += el.offsetTop - el.scrollTop 33 | offsetLeft += el.offsetLeft - el.scrollLeft 34 | el = el.offsetParent 35 | top: offsetTop 36 | left: offsetLeft 37 | right: offsetLeft + width 38 | bottom: offsetTop + height 39 | -------------------------------------------------------------------------------- /demo/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Cropme Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /scripts/directives/dropbox.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @ngdoc directive 3 | # @name dropbox 4 | # @requires elementOffset 5 | # @description 6 | # Simple directive to manage drag and drop of a file in an element 7 | # 8 | ### 9 | angular.module("cropme").directive "dropbox", (elementOffset) -> 10 | restrict: "E" 11 | link: (scope, element, attributes) -> 12 | offset = elementOffset element 13 | reset = (evt) -> 14 | evt.stopPropagation() 15 | evt.preventDefault() 16 | scope.$apply -> 17 | scope.dragOver = false 18 | scope.dropText = "Drop files here" 19 | scope.dropClass = "" 20 | dragEnterLeave = (evt) -> 21 | return if evt.x > offset.left and evt.x < offset.left + element[0].offsetWidth and evt.y > offset.top and evt.y < offset.top + element[0].offsetHeight 22 | reset evt 23 | dropbox = element[0] 24 | scope.dropText = "Drop files here" 25 | scope.dragOver = false 26 | dropbox.addEventListener "dragenter", dragEnterLeave, false 27 | dropbox.addEventListener "dragleave", dragEnterLeave, false 28 | dropbox.addEventListener "dragover", ((evt) -> 29 | evt.stopPropagation() 30 | evt.preventDefault() 31 | ok = evt.dataTransfer and evt.dataTransfer.types and evt.dataTransfer.types.indexOf("Files") >= 0 32 | scope.$apply -> 33 | scope.dragOver = true 34 | scope.dropText = (if ok then "Drop now" else "Only files are allowed") 35 | scope.dropClass = (if ok then "over" else "not-available") 36 | 37 | ), false 38 | dropbox.addEventListener "drop", ((evt) -> 39 | reset evt 40 | 41 | files = evt.dataTransfer.files 42 | scope.$apply -> 43 | if files.length > 0 44 | for file in files 45 | if file.type.match /^image\// 46 | scope.dropText = "Loading image..." 47 | scope.dropClass = "loading" 48 | return scope.setFiles(file) 49 | scope.dropError = "Wrong file type, please drop at least an image." 50 | ), false 51 | -------------------------------------------------------------------------------- /test/karma.conf.coffee: -------------------------------------------------------------------------------- 1 | # Karma configuration 2 | # http://karma-runner.github.io/0.12/config/configuration-file.html 3 | # Generated on 2014-08-06 using 4 | # generator-karma 0.8.3 5 | 6 | module.exports = (config) -> 7 | config.set 8 | # base path, that will be used to resolve files and exclude 9 | basePath: '../' 10 | 11 | # testing framework to use (jasmine/mocha/qunit/...) 12 | frameworks: ['jasmine'] 13 | 14 | # list of files / patterns to load in the browser 15 | files: [ 16 | 'bower_components/angular/angular.js' 17 | 'bower_components/angular-mocks/angular-mocks.js' 18 | 'bower_components/angular-sanitize/angular-sanitize.js' 19 | 'bower_components/angular-touch/angular-touch.js' 20 | 'bower_components/angular-superswipe/superswipe.js' 21 | 'cropme.js' 22 | 'test/spec/**/*.coffee' 23 | ] 24 | 25 | # list of files / patterns to exclude 26 | exclude: [] 27 | 28 | # web server port 29 | port: 8081 30 | 31 | # level of logging 32 | # possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 33 | logLevel: config.LOG_INFO 34 | 35 | # Start these browsers, currently available: 36 | # - Chrome 37 | # - ChromeCanary 38 | # - Firefox 39 | # - Opera 40 | # - Safari (only Mac) 41 | # - PhantomJS 42 | # - IE (only Windows) 43 | browsers: [ 44 | 'Chrome' 45 | ] 46 | 47 | # Which plugins to enable 48 | plugins: [ 49 | 'karma-phantomjs-launcher' 50 | 'karma-chrome-launcher' 51 | 'karma-jasmine' 52 | 'karma-coverage' 53 | 'karma-coffee-preprocessor' 54 | ] 55 | 56 | # enable / disable watching file and executing tests whenever any file changes 57 | autoWatch: true 58 | 59 | # Continuous Integration mode 60 | # if true, it capture browsers, run tests and exit 61 | singleRun: false 62 | 63 | colors: true 64 | 65 | preprocessors: 66 | '**/*.coffee': ['coffee'] 67 | '.tmp/scripts/**/*.js': 'coverage' 68 | 69 | # Uncomment the following lines if you are using grunt's server to run the tests 70 | # proxies: '/': 'http://localhost:9000/' 71 | # URL root prevent conflicts with the site root 72 | # urlRoot: '_karma_' 73 | 74 | reporters: ['progress', 'coverage'] 75 | coverageReporter: 76 | type: 'html' 77 | dir: 'docs/coverage/' 78 | -------------------------------------------------------------------------------- /styles/cropme.scss: -------------------------------------------------------------------------------- 1 | @import "compass"; 2 | @import "compass/css3/user-interface"; 3 | 4 | $text_color: #444; 5 | $text_color_inverse: #fff; 6 | $text_color_inverse_alt: #e6fafc; 7 | $text_color_2: #ccc; 8 | $background_color: #f0f0f0; 9 | $shine_color: #ccc; 10 | $alert_color: #da4b3e; 11 | $alert_color_background: #fff0f0; 12 | $success_color_background: #e5ffea; 13 | $success_color_background_2: #aaffaa; 14 | $color: #328d99; 15 | $color_alt: #3799a6; 16 | $color_alt_2: #3ba5b3; 17 | $line_color: #ccc; 18 | 19 | cropme { 20 | display: block; 21 | width: 100%; 22 | @include user-select(none); 23 | .step-1 { 24 | font-family: helvetica, arial, sans-serif; 25 | position: relative; 26 | display: block; 27 | background-color: $background_color; 28 | border: 4px dashed $line_color; 29 | letter-spacing: 1px; 30 | font-size: 15px; 31 | @include box-sizing(border-box); 32 | } 33 | .cropme-file-input { 34 | position: absolute; 35 | top: 20%; 36 | left: 50%; 37 | width: 0; 38 | } 39 | input[type=file] { 40 | opacity: 0; 41 | position: absolute; 42 | top: 0; 43 | left: 0; 44 | right: 0; 45 | } 46 | .cropme-button { 47 | font-weight: bold; 48 | cursor: pointer; 49 | text-align: center; 50 | color: $text_color; 51 | height: 50px; 52 | width: 250px; 53 | margin-left: -125px; 54 | line-height: 50px; 55 | &.deactivated { 56 | opacity: 0.5; 57 | pointer-events: none; 58 | } 59 | } 60 | .cropme-button-decorated { 61 | color: $text_color_inverse; 62 | background: $color_alt_2; 63 | @include box-shadow($shine_color 2px 2px 1px 0, $color 0 0 2px 2px inset); 64 | &:hover { 65 | background: $color_alt; 66 | color: $text_color_inverse_alt; 67 | } 68 | } 69 | .cropme-or { 70 | pointer-events: none; 71 | text-align: center; 72 | color: $text_color_2; 73 | margin-top: 25px; 74 | width: 20px; 75 | margin-left: -10px; 76 | } 77 | .cropme-label { 78 | margin-top: 25px; 79 | width: 150px; 80 | margin-left: -75px; 81 | pointer-events: none; 82 | text-align: center; 83 | color: $text_color; 84 | } 85 | .cropme-error { 86 | margin-top: 10px; 87 | text-align: center; 88 | color: $alert_color; 89 | position: relative; 90 | } 91 | dropbox { 92 | display: block; 93 | position: absolute; 94 | top: 0; 95 | left: 0; 96 | right: 0; 97 | bottom: 0; 98 | &.not-available { 99 | background-color: $alert_color_background; 100 | } 101 | &.over { 102 | background-color: $success_color_background; 103 | } 104 | &.loading { 105 | background-color: $success_color_background_2; 106 | } 107 | } 108 | .overlay-tile { 109 | position: absolute; 110 | opacity: 0.5; 111 | background-color: #000; 112 | } 113 | .overlay-border { 114 | position: absolute; 115 | border: 2px solid $color; 116 | @include box-sizing(border-box); 117 | } 118 | .step-2 { 119 | position: relative; 120 | cursor: move; 121 | overflow: hidden; 122 | } 123 | img { float: left; } 124 | canvas { display: none; } 125 | } 126 | -------------------------------------------------------------------------------- /cropme.css: -------------------------------------------------------------------------------- 1 | /* line 19, styles/cropme.scss */ 2 | cropme { 3 | display: block; 4 | width: 100%; 5 | -moz-user-select: -moz-none; 6 | -ms-user-select: none; 7 | -webkit-user-select: none; 8 | user-select: none; 9 | } 10 | /* line 23, styles/cropme.scss */ 11 | cropme .step-1 { 12 | font-family: helvetica, arial, sans-serif; 13 | position: relative; 14 | display: block; 15 | background-color: #f0f0f0; 16 | border: 4px dashed #ccc; 17 | letter-spacing: 1px; 18 | font-size: 15px; 19 | -moz-box-sizing: border-box; 20 | -webkit-box-sizing: border-box; 21 | box-sizing: border-box; 22 | } 23 | /* line 33, styles/cropme.scss */ 24 | cropme .cropme-file-input { 25 | position: absolute; 26 | top: 20%; 27 | left: 50%; 28 | width: 0; 29 | } 30 | /* line 39, styles/cropme.scss */ 31 | cropme input[type=file] { 32 | opacity: 0; 33 | position: absolute; 34 | top: 0; 35 | left: 0; 36 | right: 0; 37 | } 38 | /* line 46, styles/cropme.scss */ 39 | cropme .cropme-button { 40 | font-weight: bold; 41 | cursor: pointer; 42 | text-align: center; 43 | color: #444; 44 | height: 50px; 45 | width: 250px; 46 | margin-left: -125px; 47 | line-height: 50px; 48 | } 49 | /* line 55, styles/cropme.scss */ 50 | cropme .cropme-button.deactivated { 51 | opacity: 0.5; 52 | pointer-events: none; 53 | } 54 | /* line 60, styles/cropme.scss */ 55 | cropme .cropme-button-decorated { 56 | color: #fff; 57 | background: #3ba5b3; 58 | -moz-box-shadow: #ccc 2px 2px 1px 0, #328d99 0 0 2px 2px inset; 59 | -webkit-box-shadow: #ccc 2px 2px 1px 0, #328d99 0 0 2px 2px inset; 60 | box-shadow: #ccc 2px 2px 1px 0, #328d99 0 0 2px 2px inset; 61 | } 62 | /* line 64, styles/cropme.scss */ 63 | cropme .cropme-button-decorated:hover { 64 | background: #3799a6; 65 | color: #e6fafc; 66 | } 67 | /* line 69, styles/cropme.scss */ 68 | cropme .cropme-or { 69 | pointer-events: none; 70 | text-align: center; 71 | color: #ccc; 72 | margin-top: 25px; 73 | width: 20px; 74 | margin-left: -10px; 75 | } 76 | /* line 77, styles/cropme.scss */ 77 | cropme .cropme-label { 78 | margin-top: 25px; 79 | width: 150px; 80 | margin-left: -75px; 81 | pointer-events: none; 82 | text-align: center; 83 | color: #444; 84 | } 85 | /* line 85, styles/cropme.scss */ 86 | cropme .cropme-error { 87 | margin-top: 10px; 88 | text-align: center; 89 | color: #da4b3e; 90 | position: relative; 91 | } 92 | /* line 91, styles/cropme.scss */ 93 | cropme dropbox { 94 | display: block; 95 | position: absolute; 96 | top: 0; 97 | left: 0; 98 | right: 0; 99 | bottom: 0; 100 | } 101 | /* line 98, styles/cropme.scss */ 102 | cropme dropbox.not-available { 103 | background-color: #fff0f0; 104 | } 105 | /* line 101, styles/cropme.scss */ 106 | cropme dropbox.over { 107 | background-color: #e5ffea; 108 | } 109 | /* line 104, styles/cropme.scss */ 110 | cropme dropbox.loading { 111 | background-color: #aaffaa; 112 | } 113 | /* line 108, styles/cropme.scss */ 114 | cropme .overlay-tile { 115 | position: absolute; 116 | opacity: 0.5; 117 | background-color: #000; 118 | } 119 | /* line 113, styles/cropme.scss */ 120 | cropme .overlay-border { 121 | position: absolute; 122 | border: 2px solid #328d99; 123 | -moz-box-sizing: border-box; 124 | -webkit-box-sizing: border-box; 125 | box-sizing: border-box; 126 | } 127 | /* line 118, styles/cropme.scss */ 128 | cropme .step-2 { 129 | position: relative; 130 | cursor: move; 131 | overflow: hidden; 132 | } 133 | /* line 123, styles/cropme.scss */ 134 | cropme img { 135 | float: left; 136 | } 137 | /* line 124, styles/cropme.scss */ 138 | cropme canvas { 139 | display: none; 140 | } 141 | -------------------------------------------------------------------------------- /demo/app/styles/cropme.css: -------------------------------------------------------------------------------- 1 | /* line 19, styles/cropme.scss */ 2 | cropme { 3 | display: block; 4 | width: 100%; 5 | -moz-user-select: -moz-none; 6 | -ms-user-select: none; 7 | -webkit-user-select: none; 8 | user-select: none; 9 | } 10 | /* line 23, styles/cropme.scss */ 11 | cropme .step-1 { 12 | font-family: helvetica, arial, sans-serif; 13 | position: relative; 14 | display: block; 15 | background-color: #f0f0f0; 16 | border: 4px dashed #ccc; 17 | letter-spacing: 1px; 18 | font-size: 15px; 19 | -moz-box-sizing: border-box; 20 | -webkit-box-sizing: border-box; 21 | box-sizing: border-box; 22 | } 23 | /* line 33, styles/cropme.scss */ 24 | cropme .cropme-file-input { 25 | position: absolute; 26 | top: 20%; 27 | left: 50%; 28 | width: 0; 29 | } 30 | /* line 39, styles/cropme.scss */ 31 | cropme input[type=file] { 32 | opacity: 0; 33 | position: absolute; 34 | top: 0; 35 | left: 0; 36 | right: 0; 37 | } 38 | /* line 46, styles/cropme.scss */ 39 | cropme .cropme-button { 40 | font-weight: bold; 41 | cursor: pointer; 42 | text-align: center; 43 | color: #444; 44 | height: 50px; 45 | width: 250px; 46 | margin-left: -125px; 47 | line-height: 50px; 48 | } 49 | /* line 55, styles/cropme.scss */ 50 | cropme .cropme-button.deactivated { 51 | opacity: 0.5; 52 | pointer-events: none; 53 | } 54 | /* line 60, styles/cropme.scss */ 55 | cropme .cropme-button-decorated { 56 | color: #fff; 57 | background: #3ba5b3; 58 | -moz-box-shadow: #ccc 2px 2px 1px 0, #328d99 0 0 2px 2px inset; 59 | -webkit-box-shadow: #ccc 2px 2px 1px 0, #328d99 0 0 2px 2px inset; 60 | box-shadow: #ccc 2px 2px 1px 0, #328d99 0 0 2px 2px inset; 61 | } 62 | /* line 64, styles/cropme.scss */ 63 | cropme .cropme-button-decorated:hover { 64 | background: #3799a6; 65 | color: #e6fafc; 66 | } 67 | /* line 69, styles/cropme.scss */ 68 | cropme .cropme-or { 69 | pointer-events: none; 70 | text-align: center; 71 | color: #ccc; 72 | margin-top: 25px; 73 | width: 20px; 74 | margin-left: -10px; 75 | } 76 | /* line 77, styles/cropme.scss */ 77 | cropme .cropme-label { 78 | margin-top: 25px; 79 | width: 150px; 80 | margin-left: -75px; 81 | pointer-events: none; 82 | text-align: center; 83 | color: #444; 84 | } 85 | /* line 85, styles/cropme.scss */ 86 | cropme .cropme-error { 87 | margin-top: 10px; 88 | text-align: center; 89 | color: #da4b3e; 90 | position: relative; 91 | } 92 | /* line 91, styles/cropme.scss */ 93 | cropme dropbox { 94 | display: block; 95 | position: absolute; 96 | top: 0; 97 | left: 0; 98 | right: 0; 99 | bottom: 0; 100 | } 101 | /* line 98, styles/cropme.scss */ 102 | cropme dropbox.not-available { 103 | background-color: #fff0f0; 104 | } 105 | /* line 101, styles/cropme.scss */ 106 | cropme dropbox.over { 107 | background-color: #e5ffea; 108 | } 109 | /* line 104, styles/cropme.scss */ 110 | cropme dropbox.loading { 111 | background-color: #aaffaa; 112 | } 113 | /* line 108, styles/cropme.scss */ 114 | cropme .overlay-tile { 115 | position: absolute; 116 | opacity: 0.5; 117 | background-color: #000; 118 | } 119 | /* line 113, styles/cropme.scss */ 120 | cropme .overlay-border { 121 | position: absolute; 122 | border: 2px solid #328d99; 123 | -moz-box-sizing: border-box; 124 | -webkit-box-sizing: border-box; 125 | box-sizing: border-box; 126 | } 127 | /* line 118, styles/cropme.scss */ 128 | cropme .step-2 { 129 | position: relative; 130 | cursor: move; 131 | overflow: hidden; 132 | } 133 | /* line 123, styles/cropme.scss */ 134 | cropme img { 135 | float: left; 136 | } 137 | /* line 124, styles/cropme.scss */ 138 | cropme canvas { 139 | display: none; 140 | } 141 | -------------------------------------------------------------------------------- /scripts/services/canvasToBlob.coffee: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | ###* 4 | # @ngdoc service 5 | # @name canvasToBlob 6 | # @requires - 7 | # @description 8 | # Service based on canvas-toBlob.js By Eli Grey, http://eligrey.com and Devin Samarin, https://github.com/eboyjr 9 | # Transform a html canvas into a blob that can then be uploaded as a file 10 | # 11 | # @example 12 | 13 | ```js 14 | angular.module("cropme").controller "myController", (canvasToBlob) -> 15 | # upload the french flag 16 | uploader = (blob) -> 17 | url = "http://my-awesome-server.com" 18 | xhr = new XMLHttpRequest 19 | xhr.setRequestHeader "Content-Type", blob.type 20 | xhr.onreadystatechange = (e) -> 21 | if @readyState is 4 and @status is 200 22 | console.log "done" 23 | else console.log "failed" if @readyState is 4 and @status isnt 200 24 | xhr.open "POST", url, true 25 | xhr.send blob 26 | canvas = document.createElement "canvas" 27 | canvas.height = 100 28 | canvas.width = 300 29 | ctx = canvas.getContext "2d" 30 | ctx.fillStyle = "#0000FF" 31 | ctx.fillRect 0, 0, 100, 100 32 | ctx.fillStyle = "#FFFFFF" 33 | ctx.fillRect 100, 0, 200, 100 34 | ctx.fillStyle = "#FF0000" 35 | ctx.fillRect 200, 0, 300, 100 36 | canvasToBlob canvas, uploader, "image/png" 37 | ``` 38 | ### 39 | angular.module("cropme").service "canvasToBlob", -> 40 | is_base64_regex = /\s*;\s*base64\s*(?:;|$)/i 41 | base64_ranks = undefined 42 | decode_base64 = (base64) -> 43 | len = base64.length 44 | buffer = new Uint8Array(len / 4 * 3 | 0) 45 | i = 0 46 | outptr = 0 47 | last = [0, 0] 48 | state = 0 49 | save = 0 50 | rank = undefined 51 | code = undefined 52 | undef = undefined 53 | while len-- 54 | code = base64.charCodeAt(i++) 55 | rank = base64_ranks[code - 43] 56 | if rank isnt 255 and rank isnt undef 57 | last[1] = last[0] 58 | last[0] = code 59 | save = (save << 6) | rank 60 | state++ 61 | if state is 4 62 | buffer[outptr++] = save >>> 16 63 | # padding character 64 | buffer[outptr++] = save >>> 8 if last[1] isnt 61 65 | # padding character 66 | buffer[outptr++] = save if last[0] isnt 61 67 | state = 0 68 | 69 | # 2/3 chance there's going to be some null bytes at the end, but that 70 | # doesn't really matter with most image formats. 71 | # If it somehow matters for you, truncate the buffer up outptr. 72 | buffer 73 | 74 | base64_ranks = new Uint8Array [62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 0, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51] 75 | 76 | (canvas, callback, type) -> 77 | type = "image/png" unless type 78 | if canvas.mozGetAsFile 79 | callback canvas.mozGetAsFile("canvas", type) 80 | return 81 | args = Array::slice.call(arguments, 1) 82 | dataURI = canvas.toDataURL type 83 | header_end = dataURI.indexOf(",") 84 | data = dataURI.substring(header_end + 1) 85 | is_base64 = is_base64_regex.test(dataURI.substring(0, header_end)) 86 | blob = undefined 87 | if Blob.fake 88 | 89 | # no reason to decode a data: URI that's just going to become a data URI again 90 | blob = new Blob 91 | if is_base64 92 | blob.encoding = "base64" 93 | else 94 | blob.encoding = "URI" 95 | blob.data = data 96 | blob.size = data.length 97 | else if Uint8Array 98 | if is_base64 99 | blob = new Blob([decode_base64(data)], 100 | type: type 101 | ) 102 | else 103 | blob = new Blob([decodeURIComponent(data)], 104 | type: type 105 | ) 106 | callback blob 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cropme Angular Module 2 | ======================== 3 | 4 | Drag and drop or select an image, crop it and get the blob, that you can use to upload wherever and however you want 5 | [Demo here!](http://standupweb.net/cropmedemo) 6 | 7 | From version 0.5 on, it will try to use [cordova-plugin-camera](https://github.com/apache/cordova-plugin-camera) if available 8 | 9 | Install 10 | ------- 11 | 12 | Copy the cropme.js and cropme.css files into your project and add the following line with the correct path: 13 | 14 | 15 | 16 | 17 | 18 | Alternatively, if you're using bower, you can add this to your component.json (or bower.json): 19 | 20 | "angular-cropme": "~1.0.3" 21 | 22 | Or simply run 23 | 24 | bower install angular-cropme 25 | 26 | Check the dependencies to your html (unless you're using wiredep): 27 | 28 | 29 | 30 | 31 | 32 | 33 | And (unless you're using wiredep): 34 | 35 | 36 | 37 | And the css: 38 | 39 | 40 | 41 | Add the module to your application 42 | 43 | angular.module("myApp", ["cropme"]) 44 | 45 | You can choose to hide the default ok and cancel buttons by adding this to your css 46 | 47 | #cropme-cancel, #cropme-ok { display: none; } 48 | 49 | 50 | Usage 51 | ----- 52 | 63 | 64 | 65 | Attributes 66 | ---------- 67 | 68 | Note: all local scope properties are defined using "@", meaning it accesses the string value, if you want a variable to be accessed, you need to use interpolation, for example, if the src of the image is in the controller variable imgSrc, you can use the src attributes like this: `src="{{imgSrc}}"` 69 | 70 | #### width (optional) 71 | Set the width of the crop space container. Omit the width to make the box fit the size of the parent container. The image you want to crop will be reduced to this width and the directive will throw an error if the image to be cropped is smaller than this width. 72 | #### height (optional, default: 300px) 73 | Set the height of the container. The image to be cropped cannot be less than this measurement. 74 | #### icon-class (optional) 75 | CSS class of the icon to be set in the middle of the drop box 76 | #### type (optional) 77 | Valid values are 'png' or 'jpeg'. Might work with webm too, haven't tried it. (default: "png") 78 | #### destination-width (optional) 79 | Set the target (cropped) picture width. 80 | destination-width="250" 81 | the cropped image will have a width of 250px. 82 | #### destination-height (optional) 83 | Set the target (cropped) picture height. Cannot be set if ratio is set. 84 | destination-height="250" 85 | the cropped image will have a height of 250px. 86 | #### ratio (optional, requires destination-width to be set) 87 | Constrict the crop area to a fixed ratio. Here are some common examples: 1 = 1:1 ratio, 0.75 = 4:3 ratio and 0.5 = 2:1 ratio. 88 | ``` 89 | ratio = destination-height / destination-width 90 | destination-height = ratio x destination-width 91 | ``` 92 | WARNING: When setting a ratio attribute you must not also set a destination-height attribute or an error will be thrown. 93 | 94 | To control the size of the cropped image you can use a combination of destination-width and ratio or destination-width and destination-height. 95 | 96 | #### src (optional) 97 | url of the image to preload (skips file selection). Note that if the url is not local, you might get the following error: 98 | `Error: [$sce:insecurl] Blocked loading resource from url not allowed by $sceDelegate policy` 99 | In this case make sure that wrap the source string with `$sce.trustAsResourceUrl` in your controller. You can see the controller of the demo for an example 100 | #### send-original (default: false) 101 | If you want to send the original file 102 | #### send-cropped (default: true) 103 | If you want to send the cropped image 104 | #### id (optional) 105 | Add id to cropme to tell which cropme element sent the done/ loaded event 106 | #### ok-label 107 | Label for the ok button (default: "Ok") 108 | #### cancel-label 109 | Label for the cancel button (default: "Cancel") 110 | #### browseLabel 111 | label for the browse picture button (default: "Browse picture") 112 | #### dropLabel 113 | label to replace "Drop picture here", not used on handheld devices (default: "Drop picture here") 114 | #### orLabel 115 | string to replace "or", not used on handheld devices (default: "or") 116 | 117 | #### crossOrigin (optional, used to handle CORS enabled image) 118 | by default value is **"true"**, if crossOrigin is passed as **"false"** we will not handle CORS enabled images. To know more about CORS enabled image you can go through this link 119 | 120 | Events Sent 121 | ---------- 122 | 123 | The blob will be sent through an event, to catch it inside your app, you can do it like this: 124 | 125 | $scope.$on("cropme:done", function(ev, result, cropmeEl) { /* do something */ }); 126 | 127 | The blob will be sent also through a progress event when you move or resize the area: 128 | 129 | $scope.$on("cropme:progress", function(ev, result, cropmeEl) { /* do something */ }); 130 | 131 | If the user click the cancel button the cropme:canceled event will be sent 132 | 133 | $scope.$on("cropme:canceled", function() { /* do something */ }); 134 | 135 | The module will also send an event when a picture has been chosen by the user: 136 | 137 | $scope.$on("cropme:loaded", function(ev, width, height, cropmeEl) { /* do something when the image is loaded */ }); 138 | 139 | Where result is an object with the following keys: 140 | 141 | x: x position of the crop image relative to the original image 142 | y: y position of the crop image relative to the original image 143 | height: height of the crop image 144 | width: width of the crop image 145 | croppedImage: crop image as a blob 146 | originalImage: original image as a blob 147 | destinationHeight: height of the cropped image 148 | destinationWidth: width of the cropped image 149 | filename: name of the original file 150 | 151 | 152 | Events Received 153 | --------------- 154 | 155 | And you can trigger ok and cancel action by broadcasting the events cropme:cancel and cropme:ok, for example: 156 | 157 | $scope.$broadcast("cropme:cancel", elementId); 158 | 159 | If an id is given cropme will check against that id on the cropme element 160 | 161 | So, now, how do I send this image to my server? 162 | ----------------------------------------------- 163 | 164 | scope.$on("cropme:done", function(ev, result, cropmeEl) { 165 | var blob = result.croppedImage; 166 | var xhr = new XMLHttpRequest; 167 | xhr.setRequestHeader("Content-Type", blob.type); 168 | xhr.onreadystatechange = function(e) { 169 | if (this.readyState === 4 && this.status === 200) { 170 | return console.log("done"); 171 | } else if (this.readyState === 4 && this.status !== 200) { 172 | return console.log("failed"); 173 | } 174 | }; 175 | xhr.open("POST", url, true); 176 | xhr.send(blob); 177 | }); 178 | 179 | 180 | Demo 181 | ---- 182 | 183 | To run it locally, run: 184 | `npm install & bower install` 185 | build the project 186 | `grunt` 187 | then go to the demo folder 188 | `cd demo` 189 | and install npm and bower again here 190 | `npm install & bower install` 191 | and start the demo 192 | `grunt serve` 193 | You should be able to then go on your browser at localhost:9001 194 | 195 | If you want to try and see what this is all about: 196 | [Demo here!](http://standupweb.net/cropmedemo) 197 | -------------------------------------------------------------------------------- /scripts/directives/cropme.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @ngdoc directive 3 | # @name cropme 4 | # @requires superswipe, $window, $timeout, $rootScope, elementOffset, canvasToBlob 5 | # @description 6 | # Main directive for the cropme module, see readme.md for the different options and example 7 | # 8 | ### 9 | angular.module("cropme").directive "cropme", (superswipe, $window, $timeout, $rootScope, $q, elementOffset, canvasToBlob) -> 10 | 11 | minHeight = 100 # if destinationHeight has not been defined, we need a default height for the crop zone 12 | borderSensitivity = 8 # grab area size around the borders in pixels 13 | 14 | template: """ 15 |
20 | 21 |
22 |
23 | 24 |
27 | {{browseLabel}} 28 |
29 |
{{orLabel}}
30 |
{{dropLabel}}
31 |
32 |
33 |
39 | 40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 52 | 53 |
54 | 58 | 59 | """ 60 | restrict: "E" 61 | priority: 99 # it needs to run after the attributes are interpolated 62 | scope: 63 | width: "@?" 64 | destinationWidth: "@" 65 | height: "@?" 66 | destinationHeight: "@?" 67 | iconClass: "@?" 68 | ratio: "@?" 69 | type: "@?" 70 | src: "@?" 71 | sendOriginal: "@?" 72 | sendCropped: "@?" 73 | id: "@?" 74 | okLabel: "@?" 75 | cancelLabel: "@?" 76 | dropLabel: "@?" 77 | browseLabel: "@?" 78 | orLabel: "@?" 79 | crossOrigin: '@?' 80 | link: (scope, element, attributes) -> 81 | scope.type ||= "png" 82 | scope.okLabel ||= "Ok" 83 | scope.cancelLabel ||= "Cancel" 84 | scope.dropLabel ||= "Drop picture here" 85 | scope.browseLabel ||= "Browse picture" 86 | scope.orLabel ||= "or" 87 | scope.crossOrigin ||= "true" 88 | scope.state = "step-1" 89 | scope.isHandheld = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) 90 | draggingFn = null 91 | grabbedBorder = null 92 | heightWithImage = null 93 | zoom = null 94 | imageEl = element.find('img')[0] 95 | canvasEl = element.find("canvas")[0] 96 | ctx = canvasEl.getContext "2d" 97 | if scope.crossOrigin == "false" 98 | element.find('img').removeAttr("crossOrigin"); 99 | 100 | sendCropped = -> scope.sendCropped is `undefined` or scope.sendCropped is "true" 101 | sendOriginal = -> scope.sendOriginal is "true" 102 | startCropping = (imageWidth, imageHeight) -> 103 | zoom = scope.width / imageWidth 104 | heightWithImage = imageHeight * zoom 105 | if scope.destinationWidth / scope.destinationHeight > scope.width / heightWithImage 106 | scope.widthCropZone = scope.width 107 | scope.heightCropZone = Math.round(scope.width * scope.destinationHeight / scope.destinationWidth) 108 | scope.xCropZone = 0 109 | scope.yCropZone = Math.round (heightWithImage - scope.heightCropZone) / 2 110 | else 111 | scope.widthCropZone = Math.round(heightWithImage * scope.destinationWidth / scope.destinationHeight) 112 | scope.heightCropZone = heightWithImage 113 | scope.xCropZone = Math.round (scope.width - scope.widthCropZone) / 2 114 | scope.yCropZone = 0 115 | 116 | scope.checkScopeVariables = -> 117 | scope.destinationHeight = parseInt(scope.destinationHeight, 10) if scope.destinationHeight 118 | scope.destinationWidth = parseInt(scope.destinationWidth, 10) if scope.destinationWidth 119 | if scope.height? 120 | scope.height = parseInt scope.height, 10 121 | if scope.width? 122 | scope.width = parseInt scope.width, 10 123 | else unless scope.height 124 | scope.width = parseInt(window.getComputedStyle(element.parent()[0]).getPropertyValue('width'), 10); 125 | if !scope.height? and !scope.ratio? and !scope.destinationHeight? 126 | scope.height = parseInt(window.getComputedStyle(element.parent()[0]).getPropertyValue('height'), 10); 127 | scope.ratio = scope.height / scope.width 128 | if scope.destinationHeight and not scope.ratio 129 | scope.ratio = scope.destinationHeight / scope.destinationWidth 130 | else if scope.ratio 131 | scope.destinationHeight = scope.destinationWidth * scope.ratio 132 | if scope.ratio and not scope.height 133 | scope.height = scope.width * scope.ratio 134 | true 135 | 136 | imageAreaEl = element[0].getElementsByClassName("step-2")[0] 137 | elOffset = -> elementOffset imageAreaEl 138 | $input = element.find("input") 139 | $input.bind "change", -> 140 | file = @files[0] 141 | scope.setFiles file 142 | $input.bind "click", (e) -> 143 | e.stopPropagation() 144 | $input.val "" 145 | scope.browseFiles = -> 146 | if navigator.camera 147 | navigator.camera.getPicture addTypeAndLoadImage, addPictureFailure, 148 | destinationType: navigator.camera.DestinationType.DATA_URL 149 | sourceType: navigator.camera.PictureSourceType.PHOTOLIBRARY 150 | else 151 | $input[0].click() 152 | scope.setFiles = (file) -> 153 | unless file.type.match /^image\// 154 | if scope.$$phase or $rootScope.$$phase 155 | scope.cancel(); 156 | return scope.dropError = "Wrong file type, please select an image."; 157 | return scope.$apply -> 158 | scope.cancel() 159 | scope.dropError = "Wrong file type, please select an image." 160 | scope.filename = file.name 161 | scope.dropError = "" 162 | reader = new FileReader 163 | reader.onload = (e) -> 164 | loadImage e.target.result 165 | reader.readAsDataURL(file); 166 | addPictureFailure = -> 167 | scope.$apply -> 168 | scope.cancel() 169 | scope.dropError = "Failed to get a picture from your gallery" 170 | addTypeAndLoadImage = (src) -> loadImage "data:image/jpeg;base64," + src 171 | loadImage = (src, base64Src = true) -> 172 | return unless src 173 | scope.state = "step-2" 174 | if src isnt scope.imgSrc 175 | scope.imgSrc = src 176 | scope.imgLoaded = false 177 | img = new Image 178 | img.onerror = -> 179 | scope.$apply -> 180 | scope.cancel() 181 | scope.dropError = "Unsupported type of image" 182 | img.onload = -> 183 | width = img.width 184 | height = img.height 185 | errors = [] 186 | scope.width = scope.height * width / height unless scope.width? 187 | if width < scope.width 188 | errors.push "The image you dropped has a width of #{width}, but the minimum is #{scope.width}." 189 | minHeight = Math.min scope.height, scope.destinationHeight 190 | if height < minHeight 191 | errors.push "The image you dropped has a height of #{height}, but the minimum is #{minHeight}." 192 | scope.$apply -> 193 | if errors.length 194 | scope.cancel() 195 | scope.dropError = errors.join "
" 196 | else 197 | scope.imgLoaded = true 198 | $rootScope.$broadcast "cropme:loaded", width, height, element 199 | sendImageEvent "progress" 200 | startCropping width, height 201 | if not base64Src and scope.crossOrigin == "true" 202 | img.crossOrigin = "anonymous" 203 | img.src = src 204 | 205 | moveCropZone = (coords) -> 206 | offset = elOffset() 207 | scope.xCropZone = coords.x - offset.left - scope.widthCropZone / 2 208 | scope.yCropZone = coords.y - offset.top - scope.heightCropZone / 2 209 | checkBoundsAndSendProgressEvent() 210 | moveBorders = 211 | top: (coords) -> 212 | y = coords.y - elOffset().top 213 | scope.heightCropZone += scope.yCropZone - y 214 | scope.yCropZone = y 215 | checkVRatio() 216 | checkBoundsAndSendProgressEvent() 217 | right: (coords) -> 218 | x = coords.x - elOffset().left 219 | scope.widthCropZone = x - scope.xCropZone 220 | checkHRatio() 221 | checkBoundsAndSendProgressEvent() 222 | bottom: (coords) -> 223 | y = coords.y - elOffset().top 224 | scope.heightCropZone = y - scope.yCropZone 225 | checkVRatio() 226 | checkBoundsAndSendProgressEvent() 227 | left: (coords) -> 228 | x = coords.x - elOffset().left 229 | scope.widthCropZone += scope.xCropZone - x 230 | scope.xCropZone = x 231 | checkHRatio() 232 | checkBoundsAndSendProgressEvent() 233 | 234 | checkHRatio = -> scope.heightCropZone = scope.widthCropZone * scope.ratio if scope.ratio 235 | checkVRatio = -> scope.widthCropZone = scope.heightCropZone / scope.ratio if scope.ratio 236 | checkBoundsAndSendProgressEvent = -> 237 | scope.xCropZone = 0 if scope.xCropZone < 0 238 | scope.yCropZone = 0 if scope.yCropZone < 0 239 | if scope.widthCropZone < scope.destinationWidth * zoom 240 | scope.widthCropZone = scope.destinationWidth * zoom 241 | checkHRatio() 242 | else if scope.destinationHeight and scope.heightCropZone < scope.destinationHeight * zoom 243 | scope.heightCropZone = scope.destinationHeight * zoom 244 | checkVRatio() 245 | if scope.xCropZone + scope.widthCropZone > scope.width 246 | scope.xCropZone = scope.width - scope.widthCropZone 247 | if scope.xCropZone < 0 248 | scope.widthCropZone = scope.width 249 | scope.xCropZone = 0 250 | checkHRatio() 251 | if scope.yCropZone + scope.heightCropZone > heightWithImage 252 | scope.yCropZone = heightWithImage - scope.heightCropZone 253 | if scope.yCropZone < 0 254 | scope.heightCropZone = heightWithImage 255 | scope.yCropZone = 0 256 | checkVRatio() 257 | roundBounds() 258 | debouncedSendImageEvent "progress" 259 | 260 | roundBounds = -> 261 | scope.yCropZone = Math.round scope.yCropZone 262 | scope.xCropZone = Math.round scope.xCropZone 263 | scope.widthCropZone = Math.round scope.widthCropZone 264 | scope.heightCropZone = Math.round scope.heightCropZone 265 | 266 | isNearBorders = (coords) -> 267 | offset = elOffset() 268 | x = scope.xCropZone + offset.left 269 | y = scope.yCropZone + offset.top 270 | w = scope.widthCropZone 271 | h = scope.heightCropZone 272 | topLeft = { x: x, y: y } 273 | topRight = { x: x + w, y: y } 274 | bottomLeft = { x: x, y: y + h } 275 | bottomRight = { x: x + w, y: y + h } 276 | nearHSegment(coords, x, w, y, "top") or nearVSegment(coords, y, h, x + w, "right") or nearHSegment(coords, x, w, y + h, "bottom") or nearVSegment(coords, y, h, x, "left") 277 | 278 | nearHSegment = (coords, x, w, y, borderName) -> 279 | borderName if coords.x >= x and coords.x <= x + w and Math.abs(coords.y - y) <= borderSensitivity 280 | nearVSegment = (coords, y, h, x, borderName) -> 281 | borderName if coords.y >= y and coords.y <= y + h and Math.abs(coords.x - x) <= borderSensitivity 282 | 283 | dragIt = (coords) -> 284 | if draggingFn 285 | scope.$apply -> draggingFn(coords) 286 | 287 | getCropPromise = -> 288 | deferred = $q.defer() 289 | if sendCropped() 290 | ctx.drawImage imageEl, scope.xCropZone / zoom, scope.yCropZone / zoom, scope.croppedWidth, scope.croppedHeight, 0, 0, scope.destinationWidth, scope.destinationHeight 291 | canvasToBlob canvasEl, ((blob) -> deferred.resolve(blob)), "image/#{scope.type}" 292 | else 293 | deferred.resolve() 294 | deferred.promise 295 | 296 | getOriginalPromise = -> 297 | deferred = $q.defer() 298 | if sendOriginal() 299 | originalCanvas = document.createElement "canvas" 300 | originalContext = originalCanvas.getContext "2d" 301 | originalCanvas.width = imageEl.naturalWidth 302 | originalCanvas.height = imageEl.naturalHeight 303 | originalContext.drawImage imageEl, 0, 0 304 | canvasToBlob originalCanvas, ((blob) -> deferred.resolve(blob)), "image/#{scope.type}" 305 | else 306 | deferred.resolve() 307 | deferred.promise 308 | 309 | sendImageEvent = (eventName) -> 310 | scope.croppedWidth = scope.widthCropZone / zoom 311 | scope.croppedHeight = scope.heightCropZone / zoom 312 | $q.all([getCropPromise(), getOriginalPromise()]).then (blobArray) -> 313 | result = 314 | x: scope.xCropZone / zoom 315 | y: scope.yCropZone / zoom 316 | height: scope.croppedHeight 317 | width: scope.croppedWidth 318 | destinationHeight: scope.destinationHeight 319 | destinationWidth: scope.destinationWidth 320 | filename: scope.filename 321 | result.croppedImage = blobArray[0] if blobArray[0] 322 | result.originalImage = blobArray[1] if blobArray[1] 323 | $rootScope.$broadcast "cropme:#{eventName}", result, element 324 | debounce = (func, wait, immediate) -> 325 | timeout = undefined 326 | -> 327 | context = this 328 | args = arguments 329 | 330 | later = -> 331 | timeout = null 332 | if !immediate 333 | func.apply context, args 334 | return 335 | 336 | callNow = immediate and !timeout 337 | clearTimeout timeout 338 | timeout = setTimeout(later, wait) 339 | if callNow 340 | func.apply context, args 341 | return 342 | 343 | scope.mousemove = (e) -> 344 | scope.colResizePointer = switch isNearBorders({x: e.pageX - window.scrollX, y:(e.pageY - window.scrollY)}) 345 | when 'top' then 'ne-resize' 346 | when 'right', 'bottom' then 'se-resize' 347 | when 'left' then 'sw-resize' 348 | else 'move' 349 | 350 | superswipe.bind angular.element(element[0].getElementsByClassName('step-2')[0]), 351 | 'start': (coords) -> 352 | grabbedBorder = isNearBorders coords 353 | if grabbedBorder 354 | draggingFn = moveBorders[grabbedBorder] 355 | else draggingFn = moveCropZone 356 | dragIt coords 357 | 'move': (coords) -> 358 | dragIt coords 359 | 'end': (coords) -> 360 | dragIt coords 361 | draggingFn = null 362 | 363 | scope.deselect = -> draggingFn = null 364 | scope.cancel = ($event, id) -> 365 | return if id and element.attr('id') isnt id 366 | $event.preventDefault() if $event 367 | scope.dropLabel = "Drop files here" 368 | scope.dropClass = "" 369 | scope.state = "step-1" 370 | $rootScope.$broadcast "cropme:canceled" 371 | delete scope.imgSrc 372 | delete scope.filename 373 | 374 | scope.ok = ($event) -> 375 | $event.preventDefault() if $event 376 | sendImageEvent "done" 377 | 378 | scope.$on "cropme:cancel", scope.cancel 379 | scope.$on "cropme:ok", scope.ok 380 | scope.$watch "src", -> 381 | if scope.src 382 | scope.filename = scope.src 383 | if scope.src.indexOf("data:image") is 0 384 | loadImage scope.src 385 | else 386 | delimit = if scope.src.match(/\?/) then "&" else "?" 387 | src = scope.src; 388 | if scope.crossOrign == "true" 389 | src += delimit + "crossOrigin" 390 | loadImage "#{src}", false 391 | debouncedSendImageEvent = debounce sendImageEvent, 300 392 | -------------------------------------------------------------------------------- /cropme.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @ngdoc overview 4 | * @name cropme 5 | * @requires ngSanitize, ngTouch, superswipe 6 | * @description 7 | * Drag and drop or select an image, crop it and get the blob, that you can use to upload wherever and however you want 8 | * 9 | */ 10 | 11 | (function() { 12 | angular.module("cropme", ["ngSanitize", "ngTouch", "superswipe"]); 13 | 14 | }).call(this); 15 | ; 16 | /** 17 | * @ngdoc directive 18 | * @name cropme 19 | * @requires superswipe, $window, $timeout, $rootScope, elementOffset, canvasToBlob 20 | * @description 21 | * Main directive for the cropme module, see readme.md for the different options and example 22 | * 23 | */ 24 | 25 | (function() { 26 | angular.module("cropme").directive("cropme", ["superswipe", "$window", "$timeout", "$rootScope", "$q", "elementOffset", "canvasToBlob", function(superswipe, $window, $timeout, $rootScope, $q, elementOffset, canvasToBlob) { 27 | var borderSensitivity, minHeight; 28 | minHeight = 100; 29 | borderSensitivity = 8; 30 | return { 31 | template: "\n \n
\n
\n \n \n {{browseLabel}}\n
\n
{{orLabel}}
\n
{{dropLabel}}
\n \n\n\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n\n
\n \n \n
\n\n", 32 | restrict: "E", 33 | priority: 99, 34 | scope: { 35 | width: "@?", 36 | destinationWidth: "@", 37 | height: "@?", 38 | destinationHeight: "@?", 39 | iconClass: "@?", 40 | ratio: "@?", 41 | type: "@?", 42 | src: "@?", 43 | sendOriginal: "@?", 44 | sendCropped: "@?", 45 | id: "@?", 46 | okLabel: "@?", 47 | cancelLabel: "@?", 48 | dropLabel: "@?", 49 | browseLabel: "@?", 50 | orLabel: "@?", 51 | crossOrigin: '@?' 52 | }, 53 | link: function(scope, element, attributes) { 54 | var $input, addPictureFailure, addTypeAndLoadImage, canvasEl, checkBoundsAndSendProgressEvent, checkHRatio, checkVRatio, ctx, debounce, debouncedSendImageEvent, dragIt, draggingFn, elOffset, getCropPromise, getOriginalPromise, grabbedBorder, heightWithImage, imageAreaEl, imageEl, isNearBorders, loadImage, moveBorders, moveCropZone, nearHSegment, nearVSegment, roundBounds, sendCropped, sendImageEvent, sendOriginal, startCropping, zoom; 55 | scope.type || (scope.type = "png"); 56 | scope.okLabel || (scope.okLabel = "Ok"); 57 | scope.cancelLabel || (scope.cancelLabel = "Cancel"); 58 | scope.dropLabel || (scope.dropLabel = "Drop picture here"); 59 | scope.browseLabel || (scope.browseLabel = "Browse picture"); 60 | scope.orLabel || (scope.orLabel = "or"); 61 | scope.crossOrigin || (scope.crossOrigin = "true"); 62 | scope.state = "step-1"; 63 | scope.isHandheld = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 64 | draggingFn = null; 65 | grabbedBorder = null; 66 | heightWithImage = null; 67 | zoom = null; 68 | imageEl = element.find('img')[0]; 69 | canvasEl = element.find("canvas")[0]; 70 | ctx = canvasEl.getContext("2d"); 71 | if (scope.crossOrigin === "false") { 72 | element.find('img').removeAttr("crossOrigin"); 73 | } 74 | sendCropped = function() { 75 | return scope.sendCropped === undefined || scope.sendCropped === "true"; 76 | }; 77 | sendOriginal = function() { 78 | return scope.sendOriginal === "true"; 79 | }; 80 | startCropping = function(imageWidth, imageHeight) { 81 | zoom = scope.width / imageWidth; 82 | heightWithImage = imageHeight * zoom; 83 | if (scope.destinationWidth / scope.destinationHeight > scope.width / heightWithImage) { 84 | scope.widthCropZone = scope.width; 85 | scope.heightCropZone = Math.round(scope.width * scope.destinationHeight / scope.destinationWidth); 86 | scope.xCropZone = 0; 87 | return scope.yCropZone = Math.round((heightWithImage - scope.heightCropZone) / 2); 88 | } else { 89 | scope.widthCropZone = Math.round(heightWithImage * scope.destinationWidth / scope.destinationHeight); 90 | scope.heightCropZone = heightWithImage; 91 | scope.xCropZone = Math.round((scope.width - scope.widthCropZone) / 2); 92 | return scope.yCropZone = 0; 93 | } 94 | }; 95 | scope.checkScopeVariables = function() { 96 | if (scope.destinationHeight) { 97 | scope.destinationHeight = parseInt(scope.destinationHeight, 10); 98 | } 99 | if (scope.destinationWidth) { 100 | scope.destinationWidth = parseInt(scope.destinationWidth, 10); 101 | } 102 | if (scope.height != null) { 103 | scope.height = parseInt(scope.height, 10); 104 | } 105 | if (scope.width != null) { 106 | scope.width = parseInt(scope.width, 10); 107 | } else if (!scope.height) { 108 | scope.width = parseInt(window.getComputedStyle(element.parent()[0]).getPropertyValue('width'), 10); 109 | } 110 | if ((scope.height == null) && (scope.ratio == null) && (scope.destinationHeight == null)) { 111 | scope.height = parseInt(window.getComputedStyle(element.parent()[0]).getPropertyValue('height'), 10); 112 | scope.ratio = scope.height / scope.width; 113 | } 114 | if (scope.destinationHeight && !scope.ratio) { 115 | scope.ratio = scope.destinationHeight / scope.destinationWidth; 116 | } else if (scope.ratio) { 117 | scope.destinationHeight = scope.destinationWidth * scope.ratio; 118 | } 119 | if (scope.ratio && !scope.height) { 120 | scope.height = scope.width * scope.ratio; 121 | } 122 | return true; 123 | }; 124 | imageAreaEl = element[0].getElementsByClassName("step-2")[0]; 125 | elOffset = function() { 126 | return elementOffset(imageAreaEl); 127 | }; 128 | $input = element.find("input"); 129 | $input.bind("change", function() { 130 | var file; 131 | file = this.files[0]; 132 | return scope.setFiles(file); 133 | }); 134 | $input.bind("click", function(e) { 135 | e.stopPropagation(); 136 | return $input.val(""); 137 | }); 138 | scope.browseFiles = function() { 139 | if (navigator.camera) { 140 | return navigator.camera.getPicture(addTypeAndLoadImage, addPictureFailure, { 141 | destinationType: navigator.camera.DestinationType.DATA_URL, 142 | sourceType: navigator.camera.PictureSourceType.PHOTOLIBRARY 143 | }); 144 | } else { 145 | return $input[0].click(); 146 | } 147 | }; 148 | scope.setFiles = function(file) { 149 | var reader; 150 | if (!file.type.match(/^image\//)) { 151 | if (scope.$$phase || $rootScope.$$phase) { 152 | scope.cancel(); 153 | return scope.dropError = "Wrong file type, please select an image."; 154 | } 155 | return scope.$apply(function() { 156 | scope.cancel(); 157 | return scope.dropError = "Wrong file type, please select an image."; 158 | }); 159 | } 160 | scope.filename = file.name; 161 | scope.dropError = ""; 162 | reader = new FileReader; 163 | reader.onload = function(e) { 164 | return loadImage(e.target.result); 165 | }; 166 | return reader.readAsDataURL(file); 167 | }; 168 | addPictureFailure = function() { 169 | return scope.$apply(function() { 170 | scope.cancel(); 171 | return scope.dropError = "Failed to get a picture from your gallery"; 172 | }); 173 | }; 174 | addTypeAndLoadImage = function(src) { 175 | return loadImage("data:image/jpeg;base64," + src); 176 | }; 177 | loadImage = function(src, base64Src) { 178 | var img; 179 | if (base64Src == null) { 180 | base64Src = true; 181 | } 182 | if (!src) { 183 | return; 184 | } 185 | scope.state = "step-2"; 186 | if (src !== scope.imgSrc) { 187 | scope.imgSrc = src; 188 | scope.imgLoaded = false; 189 | img = new Image; 190 | img.onerror = function() { 191 | return scope.$apply(function() { 192 | scope.cancel(); 193 | return scope.dropError = "Unsupported type of image"; 194 | }); 195 | }; 196 | img.onload = function() { 197 | var errors, height, width; 198 | width = img.width; 199 | height = img.height; 200 | errors = []; 201 | if (scope.width == null) { 202 | scope.width = scope.height * width / height; 203 | } 204 | if (width < scope.width) { 205 | errors.push("The image you dropped has a width of " + width + ", but the minimum is " + scope.width + "."); 206 | } 207 | minHeight = Math.min(scope.height, scope.destinationHeight); 208 | if (height < minHeight) { 209 | errors.push("The image you dropped has a height of " + height + ", but the minimum is " + minHeight + "."); 210 | } 211 | return scope.$apply(function() { 212 | if (errors.length) { 213 | scope.cancel(); 214 | return scope.dropError = errors.join("
"); 215 | } else { 216 | scope.imgLoaded = true; 217 | $rootScope.$broadcast("cropme:loaded", width, height, element); 218 | sendImageEvent("progress"); 219 | return startCropping(width, height); 220 | } 221 | }); 222 | }; 223 | if (!base64Src && scope.crossOrigin === "true") { 224 | img.crossOrigin = "anonymous"; 225 | } 226 | return img.src = src; 227 | } 228 | }; 229 | moveCropZone = function(coords) { 230 | var offset; 231 | offset = elOffset(); 232 | scope.xCropZone = coords.x - offset.left - scope.widthCropZone / 2; 233 | scope.yCropZone = coords.y - offset.top - scope.heightCropZone / 2; 234 | return checkBoundsAndSendProgressEvent(); 235 | }; 236 | moveBorders = { 237 | top: function(coords) { 238 | var y; 239 | y = coords.y - elOffset().top; 240 | scope.heightCropZone += scope.yCropZone - y; 241 | scope.yCropZone = y; 242 | checkVRatio(); 243 | return checkBoundsAndSendProgressEvent(); 244 | }, 245 | right: function(coords) { 246 | var x; 247 | x = coords.x - elOffset().left; 248 | scope.widthCropZone = x - scope.xCropZone; 249 | checkHRatio(); 250 | return checkBoundsAndSendProgressEvent(); 251 | }, 252 | bottom: function(coords) { 253 | var y; 254 | y = coords.y - elOffset().top; 255 | scope.heightCropZone = y - scope.yCropZone; 256 | checkVRatio(); 257 | return checkBoundsAndSendProgressEvent(); 258 | }, 259 | left: function(coords) { 260 | var x; 261 | x = coords.x - elOffset().left; 262 | scope.widthCropZone += scope.xCropZone - x; 263 | scope.xCropZone = x; 264 | checkHRatio(); 265 | return checkBoundsAndSendProgressEvent(); 266 | } 267 | }; 268 | checkHRatio = function() { 269 | if (scope.ratio) { 270 | return scope.heightCropZone = scope.widthCropZone * scope.ratio; 271 | } 272 | }; 273 | checkVRatio = function() { 274 | if (scope.ratio) { 275 | return scope.widthCropZone = scope.heightCropZone / scope.ratio; 276 | } 277 | }; 278 | checkBoundsAndSendProgressEvent = function() { 279 | if (scope.xCropZone < 0) { 280 | scope.xCropZone = 0; 281 | } 282 | if (scope.yCropZone < 0) { 283 | scope.yCropZone = 0; 284 | } 285 | if (scope.widthCropZone < scope.destinationWidth * zoom) { 286 | scope.widthCropZone = scope.destinationWidth * zoom; 287 | checkHRatio(); 288 | } else if (scope.destinationHeight && scope.heightCropZone < scope.destinationHeight * zoom) { 289 | scope.heightCropZone = scope.destinationHeight * zoom; 290 | checkVRatio(); 291 | } 292 | if (scope.xCropZone + scope.widthCropZone > scope.width) { 293 | scope.xCropZone = scope.width - scope.widthCropZone; 294 | if (scope.xCropZone < 0) { 295 | scope.widthCropZone = scope.width; 296 | scope.xCropZone = 0; 297 | checkHRatio(); 298 | } 299 | } 300 | if (scope.yCropZone + scope.heightCropZone > heightWithImage) { 301 | scope.yCropZone = heightWithImage - scope.heightCropZone; 302 | if (scope.yCropZone < 0) { 303 | scope.heightCropZone = heightWithImage; 304 | scope.yCropZone = 0; 305 | checkVRatio(); 306 | } 307 | } 308 | roundBounds(); 309 | return debouncedSendImageEvent("progress"); 310 | }; 311 | roundBounds = function() { 312 | scope.yCropZone = Math.round(scope.yCropZone); 313 | scope.xCropZone = Math.round(scope.xCropZone); 314 | scope.widthCropZone = Math.round(scope.widthCropZone); 315 | return scope.heightCropZone = Math.round(scope.heightCropZone); 316 | }; 317 | isNearBorders = function(coords) { 318 | var bottomLeft, bottomRight, h, offset, topLeft, topRight, w, x, y; 319 | offset = elOffset(); 320 | x = scope.xCropZone + offset.left; 321 | y = scope.yCropZone + offset.top; 322 | w = scope.widthCropZone; 323 | h = scope.heightCropZone; 324 | topLeft = { 325 | x: x, 326 | y: y 327 | }; 328 | topRight = { 329 | x: x + w, 330 | y: y 331 | }; 332 | bottomLeft = { 333 | x: x, 334 | y: y + h 335 | }; 336 | bottomRight = { 337 | x: x + w, 338 | y: y + h 339 | }; 340 | return nearHSegment(coords, x, w, y, "top") || nearVSegment(coords, y, h, x + w, "right") || nearHSegment(coords, x, w, y + h, "bottom") || nearVSegment(coords, y, h, x, "left"); 341 | }; 342 | nearHSegment = function(coords, x, w, y, borderName) { 343 | if (coords.x >= x && coords.x <= x + w && Math.abs(coords.y - y) <= borderSensitivity) { 344 | return borderName; 345 | } 346 | }; 347 | nearVSegment = function(coords, y, h, x, borderName) { 348 | if (coords.y >= y && coords.y <= y + h && Math.abs(coords.x - x) <= borderSensitivity) { 349 | return borderName; 350 | } 351 | }; 352 | dragIt = function(coords) { 353 | if (draggingFn) { 354 | return scope.$apply(function() { 355 | return draggingFn(coords); 356 | }); 357 | } 358 | }; 359 | getCropPromise = function() { 360 | var deferred; 361 | deferred = $q.defer(); 362 | if (sendCropped()) { 363 | ctx.drawImage(imageEl, scope.xCropZone / zoom, scope.yCropZone / zoom, scope.croppedWidth, scope.croppedHeight, 0, 0, scope.destinationWidth, scope.destinationHeight); 364 | canvasToBlob(canvasEl, (function(blob) { 365 | return deferred.resolve(blob); 366 | }), "image/" + scope.type); 367 | } else { 368 | deferred.resolve(); 369 | } 370 | return deferred.promise; 371 | }; 372 | getOriginalPromise = function() { 373 | var deferred, originalCanvas, originalContext; 374 | deferred = $q.defer(); 375 | if (sendOriginal()) { 376 | originalCanvas = document.createElement("canvas"); 377 | originalContext = originalCanvas.getContext("2d"); 378 | originalCanvas.width = imageEl.naturalWidth; 379 | originalCanvas.height = imageEl.naturalHeight; 380 | originalContext.drawImage(imageEl, 0, 0); 381 | canvasToBlob(originalCanvas, (function(blob) { 382 | return deferred.resolve(blob); 383 | }), "image/" + scope.type); 384 | } else { 385 | deferred.resolve(); 386 | } 387 | return deferred.promise; 388 | }; 389 | sendImageEvent = function(eventName) { 390 | scope.croppedWidth = scope.widthCropZone / zoom; 391 | scope.croppedHeight = scope.heightCropZone / zoom; 392 | return $q.all([getCropPromise(), getOriginalPromise()]).then(function(blobArray) { 393 | var result; 394 | result = { 395 | x: scope.xCropZone / zoom, 396 | y: scope.yCropZone / zoom, 397 | height: scope.croppedHeight, 398 | width: scope.croppedWidth, 399 | destinationHeight: scope.destinationHeight, 400 | destinationWidth: scope.destinationWidth, 401 | filename: scope.filename 402 | }; 403 | if (blobArray[0]) { 404 | result.croppedImage = blobArray[0]; 405 | } 406 | if (blobArray[1]) { 407 | result.originalImage = blobArray[1]; 408 | } 409 | return $rootScope.$broadcast("cropme:" + eventName, result, element); 410 | }); 411 | }; 412 | debounce = function(func, wait, immediate) { 413 | var timeout; 414 | timeout = void 0; 415 | return function() { 416 | var args, callNow, context, later; 417 | context = this; 418 | args = arguments; 419 | later = function() { 420 | timeout = null; 421 | if (!immediate) { 422 | func.apply(context, args); 423 | } 424 | }; 425 | callNow = immediate && !timeout; 426 | clearTimeout(timeout); 427 | timeout = setTimeout(later, wait); 428 | if (callNow) { 429 | func.apply(context, args); 430 | } 431 | }; 432 | }; 433 | scope.mousemove = function(e) { 434 | return scope.colResizePointer = (function() { 435 | switch (isNearBorders({ 436 | x: e.pageX - window.scrollX, 437 | y: e.pageY - window.scrollY 438 | })) { 439 | case 'top': 440 | return 'ne-resize'; 441 | case 'right': 442 | case 'bottom': 443 | return 'se-resize'; 444 | case 'left': 445 | return 'sw-resize'; 446 | default: 447 | return 'move'; 448 | } 449 | })(); 450 | }; 451 | superswipe.bind(angular.element(element[0].getElementsByClassName('step-2')[0]), { 452 | 'start': function(coords) { 453 | grabbedBorder = isNearBorders(coords); 454 | if (grabbedBorder) { 455 | draggingFn = moveBorders[grabbedBorder]; 456 | } else { 457 | draggingFn = moveCropZone; 458 | } 459 | return dragIt(coords); 460 | }, 461 | 'move': function(coords) { 462 | return dragIt(coords); 463 | }, 464 | 'end': function(coords) { 465 | dragIt(coords); 466 | return draggingFn = null; 467 | } 468 | }); 469 | scope.deselect = function() { 470 | return draggingFn = null; 471 | }; 472 | scope.cancel = function($event, id) { 473 | if (id && element.attr('id') !== id) { 474 | return; 475 | } 476 | if ($event) { 477 | $event.preventDefault(); 478 | } 479 | scope.dropLabel = "Drop files here"; 480 | scope.dropClass = ""; 481 | scope.state = "step-1"; 482 | $rootScope.$broadcast("cropme:canceled"); 483 | delete scope.imgSrc; 484 | return delete scope.filename; 485 | }; 486 | scope.ok = function($event) { 487 | if ($event) { 488 | $event.preventDefault(); 489 | } 490 | return sendImageEvent("done"); 491 | }; 492 | scope.$on("cropme:cancel", scope.cancel); 493 | scope.$on("cropme:ok", scope.ok); 494 | scope.$watch("src", function() { 495 | var delimit, src; 496 | if (scope.src) { 497 | scope.filename = scope.src; 498 | if (scope.src.indexOf("data:image") === 0) { 499 | return loadImage(scope.src); 500 | } else { 501 | delimit = scope.src.match(/\?/) ? "&" : "?"; 502 | src = scope.src; 503 | if (scope.crossOrign === "true") { 504 | src += delimit + "crossOrigin"; 505 | } 506 | return loadImage("" + src, false); 507 | } 508 | } 509 | }); 510 | return debouncedSendImageEvent = debounce(sendImageEvent, 300); 511 | } 512 | }; 513 | }]); 514 | 515 | }).call(this); 516 | ; 517 | /** 518 | * @ngdoc directive 519 | * @name dropbox 520 | * @requires elementOffset 521 | * @description 522 | * Simple directive to manage drag and drop of a file in an element 523 | * 524 | */ 525 | 526 | (function() { 527 | angular.module("cropme").directive("dropbox", ["elementOffset", function(elementOffset) { 528 | return { 529 | restrict: "E", 530 | link: function(scope, element, attributes) { 531 | var dragEnterLeave, dropbox, offset, reset; 532 | offset = elementOffset(element); 533 | reset = function(evt) { 534 | evt.stopPropagation(); 535 | evt.preventDefault(); 536 | return scope.$apply(function() { 537 | scope.dragOver = false; 538 | scope.dropText = "Drop files here"; 539 | return scope.dropClass = ""; 540 | }); 541 | }; 542 | dragEnterLeave = function(evt) { 543 | if (evt.x > offset.left && evt.x < offset.left + element[0].offsetWidth && evt.y > offset.top && evt.y < offset.top + element[0].offsetHeight) { 544 | return; 545 | } 546 | return reset(evt); 547 | }; 548 | dropbox = element[0]; 549 | scope.dropText = "Drop files here"; 550 | scope.dragOver = false; 551 | dropbox.addEventListener("dragenter", dragEnterLeave, false); 552 | dropbox.addEventListener("dragleave", dragEnterLeave, false); 553 | dropbox.addEventListener("dragover", (function(evt) { 554 | var ok; 555 | evt.stopPropagation(); 556 | evt.preventDefault(); 557 | ok = evt.dataTransfer && evt.dataTransfer.types && evt.dataTransfer.types.indexOf("Files") >= 0; 558 | return scope.$apply(function() { 559 | scope.dragOver = true; 560 | scope.dropText = (ok ? "Drop now" : "Only files are allowed"); 561 | return scope.dropClass = (ok ? "over" : "not-available"); 562 | }); 563 | }), false); 564 | return dropbox.addEventListener("drop", (function(evt) { 565 | var files; 566 | reset(evt); 567 | files = evt.dataTransfer.files; 568 | return scope.$apply(function() { 569 | var file, _i, _len; 570 | if (files.length > 0) { 571 | for (_i = 0, _len = files.length; _i < _len; _i++) { 572 | file = files[_i]; 573 | if (file.type.match(/^image\//)) { 574 | scope.dropText = "Loading image..."; 575 | scope.dropClass = "loading"; 576 | return scope.setFiles(file); 577 | } 578 | scope.dropError = "Wrong file type, please drop at least an image."; 579 | } 580 | } 581 | }); 582 | }), false); 583 | } 584 | }; 585 | }]); 586 | 587 | }).call(this); 588 | ;(function() { 589 | "use strict"; 590 | 591 | /** 592 | * @ngdoc service 593 | * @name canvasToBlob 594 | * @requires - 595 | * @description 596 | * Service based on canvas-toBlob.js By Eli Grey, http://eligrey.com and Devin Samarin, https://github.com/eboyjr 597 | * Transform a html canvas into a blob that can then be uploaded as a file 598 | * 599 | * @example 600 | 601 | ```js 602 | angular.module("cropme").controller "myController", (canvasToBlob) -> 603 | * upload the french flag 604 | uploader = (blob) -> 605 | url = "http://my-awesome-server.com" 606 | xhr = new XMLHttpRequest 607 | xhr.setRequestHeader "Content-Type", blob.type 608 | xhr.onreadystatechange = (e) -> 609 | if @readyState is 4 and @status is 200 610 | console.log "done" 611 | else console.log "failed" if @readyState is 4 and @status isnt 200 612 | xhr.open "POST", url, true 613 | xhr.send blob 614 | canvas = document.createElement "canvas" 615 | canvas.height = 100 616 | canvas.width = 300 617 | ctx = canvas.getContext "2d" 618 | ctx.fillStyle = "#0000FF" 619 | ctx.fillRect 0, 0, 100, 100 620 | ctx.fillStyle = "#FFFFFF" 621 | ctx.fillRect 100, 0, 200, 100 622 | ctx.fillStyle = "#FF0000" 623 | ctx.fillRect 200, 0, 300, 100 624 | canvasToBlob canvas, uploader, "image/png" 625 | ``` 626 | */ 627 | angular.module("cropme").service("canvasToBlob", function() { 628 | var base64_ranks, decode_base64, is_base64_regex; 629 | is_base64_regex = /\s*;\s*base64\s*(?:;|$)/i; 630 | base64_ranks = void 0; 631 | decode_base64 = function(base64) { 632 | var buffer, code, i, last, len, outptr, rank, save, state, undef; 633 | len = base64.length; 634 | buffer = new Uint8Array(len / 4 * 3 | 0); 635 | i = 0; 636 | outptr = 0; 637 | last = [0, 0]; 638 | state = 0; 639 | save = 0; 640 | rank = void 0; 641 | code = void 0; 642 | undef = void 0; 643 | while (len--) { 644 | code = base64.charCodeAt(i++); 645 | rank = base64_ranks[code - 43]; 646 | if (rank !== 255 && rank !== undef) { 647 | last[1] = last[0]; 648 | last[0] = code; 649 | save = (save << 6) | rank; 650 | state++; 651 | if (state === 4) { 652 | buffer[outptr++] = save >>> 16; 653 | if (last[1] !== 61) { 654 | buffer[outptr++] = save >>> 8; 655 | } 656 | if (last[0] !== 61) { 657 | buffer[outptr++] = save; 658 | } 659 | state = 0; 660 | } 661 | } 662 | } 663 | return buffer; 664 | }; 665 | base64_ranks = new Uint8Array([62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 0, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]); 666 | return function(canvas, callback, type) { 667 | var args, blob, data, dataURI, header_end, is_base64; 668 | if (!type) { 669 | type = "image/png"; 670 | } 671 | if (canvas.mozGetAsFile) { 672 | callback(canvas.mozGetAsFile("canvas", type)); 673 | return; 674 | } 675 | args = Array.prototype.slice.call(arguments, 1); 676 | dataURI = canvas.toDataURL(type); 677 | header_end = dataURI.indexOf(","); 678 | data = dataURI.substring(header_end + 1); 679 | is_base64 = is_base64_regex.test(dataURI.substring(0, header_end)); 680 | blob = void 0; 681 | if (Blob.fake) { 682 | blob = new Blob; 683 | if (is_base64) { 684 | blob.encoding = "base64"; 685 | } else { 686 | blob.encoding = "URI"; 687 | } 688 | blob.data = data; 689 | blob.size = data.length; 690 | } else if (Uint8Array) { 691 | if (is_base64) { 692 | blob = new Blob([decode_base64(data)], { 693 | type: type 694 | }); 695 | } else { 696 | blob = new Blob([decodeURIComponent(data)], { 697 | type: type 698 | }); 699 | } 700 | } 701 | return callback(blob); 702 | }; 703 | }); 704 | 705 | }).call(this); 706 | ;(function() { 707 | "use strict"; 708 | 709 | /** 710 | * @ngdoc service 711 | * @name elementOffset 712 | * @requires - 713 | * @description 714 | * Get the offset in pixel of an element on the screen 715 | * 716 | * @example 717 | 718 | ```js 719 | angular.module("cropme").directive "myDirective", (elementOffset) -> 720 | link: (scope, element, attributes) -> 721 | offset = elementOffset element 722 | console.log "This directive's element is #{offset.top}px away from the top of the screen" 723 | console.log "This directive's element is #{offset.left}px away from the left of the screen" 724 | console.log "This directive's element is #{offset.bottom}px away from the bottom of the screen" 725 | console.log "This directive's element is #{offset.right}px away from the right of the screen" 726 | ``` 727 | */ 728 | angular.module("cropme").service("elementOffset", function() { 729 | return function(el) { 730 | var height, offsetLeft, offsetTop, scrollLeft, scrollTop, width; 731 | if (el[0]) { 732 | el = el[0]; 733 | } 734 | offsetTop = 0; 735 | offsetLeft = 0; 736 | scrollTop = 0; 737 | scrollLeft = 0; 738 | width = el.offsetWidth; 739 | height = el.offsetHeight; 740 | while (el) { 741 | offsetTop += el.offsetTop - el.scrollTop; 742 | offsetLeft += el.offsetLeft - el.scrollLeft; 743 | el = el.offsetParent; 744 | } 745 | return { 746 | top: offsetTop, 747 | left: offsetLeft, 748 | right: offsetLeft + width, 749 | bottom: offsetTop + height 750 | }; 751 | }; 752 | }); 753 | 754 | }).call(this); 755 | -------------------------------------------------------------------------------- /demo/app/scripts/cropme.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @ngdoc overview 4 | * @name cropme 5 | * @requires ngSanitize, ngTouch, superswipe 6 | * @description 7 | * Drag and drop or select an image, crop it and get the blob, that you can use to upload wherever and however you want 8 | * 9 | */ 10 | 11 | (function() { 12 | angular.module("cropme", ["ngSanitize", "ngTouch", "superswipe"]); 13 | 14 | }).call(this); 15 | ; 16 | /** 17 | * @ngdoc directive 18 | * @name cropme 19 | * @requires superswipe, $window, $timeout, $rootScope, elementOffset, canvasToBlob 20 | * @description 21 | * Main directive for the cropme module, see readme.md for the different options and example 22 | * 23 | */ 24 | 25 | (function() { 26 | angular.module("cropme").directive("cropme", ["superswipe", "$window", "$timeout", "$rootScope", "$q", "elementOffset", "canvasToBlob", function(superswipe, $window, $timeout, $rootScope, $q, elementOffset, canvasToBlob) { 27 | var borderSensitivity, minHeight; 28 | minHeight = 100; 29 | borderSensitivity = 8; 30 | return { 31 | template: "\n \n
\n
\n \n \n {{browseLabel}}\n
\n
{{orLabel}}
\n
{{dropLabel}}
\n \n\n\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n\n
\n \n \n
\n\n", 32 | restrict: "E", 33 | priority: 99, 34 | scope: { 35 | width: "@?", 36 | destinationWidth: "@", 37 | height: "@?", 38 | destinationHeight: "@?", 39 | iconClass: "@?", 40 | ratio: "@?", 41 | type: "@?", 42 | src: "@?", 43 | sendOriginal: "@?", 44 | sendCropped: "@?", 45 | id: "@?", 46 | okLabel: "@?", 47 | cancelLabel: "@?", 48 | dropLabel: "@?", 49 | browseLabel: "@?", 50 | orLabel: "@?", 51 | crossOrigin: '@?' 52 | }, 53 | link: function(scope, element, attributes) { 54 | var $input, addPictureFailure, addTypeAndLoadImage, canvasEl, checkBoundsAndSendProgressEvent, checkHRatio, checkVRatio, ctx, debounce, debouncedSendImageEvent, dragIt, draggingFn, elOffset, getCropPromise, getOriginalPromise, grabbedBorder, heightWithImage, imageAreaEl, imageEl, isNearBorders, loadImage, moveBorders, moveCropZone, nearHSegment, nearVSegment, roundBounds, sendCropped, sendImageEvent, sendOriginal, startCropping, zoom; 55 | scope.type || (scope.type = "png"); 56 | scope.okLabel || (scope.okLabel = "Ok"); 57 | scope.cancelLabel || (scope.cancelLabel = "Cancel"); 58 | scope.dropLabel || (scope.dropLabel = "Drop picture here"); 59 | scope.browseLabel || (scope.browseLabel = "Browse picture"); 60 | scope.orLabel || (scope.orLabel = "or"); 61 | scope.crossOrigin || (scope.crossOrigin = "true"); 62 | scope.state = "step-1"; 63 | scope.isHandheld = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 64 | draggingFn = null; 65 | grabbedBorder = null; 66 | heightWithImage = null; 67 | zoom = null; 68 | imageEl = element.find('img')[0]; 69 | canvasEl = element.find("canvas")[0]; 70 | ctx = canvasEl.getContext("2d"); 71 | if (scope.crossOrigin === "false") { 72 | element.find('img').removeAttr("crossOrigin"); 73 | } 74 | sendCropped = function() { 75 | return scope.sendCropped === undefined || scope.sendCropped === "true"; 76 | }; 77 | sendOriginal = function() { 78 | return scope.sendOriginal === "true"; 79 | }; 80 | startCropping = function(imageWidth, imageHeight) { 81 | zoom = scope.width / imageWidth; 82 | heightWithImage = imageHeight * zoom; 83 | if (scope.destinationWidth / scope.destinationHeight > scope.width / heightWithImage) { 84 | scope.widthCropZone = scope.width; 85 | scope.heightCropZone = Math.round(scope.width * scope.destinationHeight / scope.destinationWidth); 86 | scope.xCropZone = 0; 87 | return scope.yCropZone = Math.round((heightWithImage - scope.heightCropZone) / 2); 88 | } else { 89 | scope.widthCropZone = Math.round(heightWithImage * scope.destinationWidth / scope.destinationHeight); 90 | scope.heightCropZone = heightWithImage; 91 | scope.xCropZone = Math.round((scope.width - scope.widthCropZone) / 2); 92 | return scope.yCropZone = 0; 93 | } 94 | }; 95 | scope.checkScopeVariables = function() { 96 | if (scope.destinationHeight) { 97 | scope.destinationHeight = parseInt(scope.destinationHeight, 10); 98 | } 99 | if (scope.destinationWidth) { 100 | scope.destinationWidth = parseInt(scope.destinationWidth, 10); 101 | } 102 | if (scope.height != null) { 103 | scope.height = parseInt(scope.height, 10); 104 | } 105 | if (scope.width != null) { 106 | scope.width = parseInt(scope.width, 10); 107 | } else if (!scope.height) { 108 | scope.width = parseInt(window.getComputedStyle(element.parent()[0]).getPropertyValue('width'), 10); 109 | } 110 | if ((scope.height == null) && (scope.ratio == null) && (scope.destinationHeight == null)) { 111 | scope.height = parseInt(window.getComputedStyle(element.parent()[0]).getPropertyValue('height'), 10); 112 | scope.ratio = scope.height / scope.width; 113 | } 114 | if (scope.destinationHeight && !scope.ratio) { 115 | scope.ratio = scope.destinationHeight / scope.destinationWidth; 116 | } else if (scope.ratio) { 117 | scope.destinationHeight = scope.destinationWidth * scope.ratio; 118 | } 119 | if (scope.ratio && !scope.height) { 120 | scope.height = scope.width * scope.ratio; 121 | } 122 | return true; 123 | }; 124 | imageAreaEl = element[0].getElementsByClassName("step-2")[0]; 125 | elOffset = function() { 126 | return elementOffset(imageAreaEl); 127 | }; 128 | $input = element.find("input"); 129 | $input.bind("change", function() { 130 | var file; 131 | file = this.files[0]; 132 | return scope.setFiles(file); 133 | }); 134 | $input.bind("click", function(e) { 135 | e.stopPropagation(); 136 | return $input.val(""); 137 | }); 138 | scope.browseFiles = function() { 139 | if (navigator.camera) { 140 | return navigator.camera.getPicture(addTypeAndLoadImage, addPictureFailure, { 141 | destinationType: navigator.camera.DestinationType.DATA_URL, 142 | sourceType: navigator.camera.PictureSourceType.PHOTOLIBRARY 143 | }); 144 | } else { 145 | return $input[0].click(); 146 | } 147 | }; 148 | scope.setFiles = function(file) { 149 | var reader; 150 | if (!file.type.match(/^image\//)) { 151 | if (scope.$$phase || $rootScope.$$phase) { 152 | scope.cancel(); 153 | return scope.dropError = "Wrong file type, please select an image."; 154 | } 155 | return scope.$apply(function() { 156 | scope.cancel(); 157 | return scope.dropError = "Wrong file type, please select an image."; 158 | }); 159 | } 160 | scope.filename = file.name; 161 | scope.dropError = ""; 162 | reader = new FileReader; 163 | reader.onload = function(e) { 164 | return loadImage(e.target.result); 165 | }; 166 | return reader.readAsDataURL(file); 167 | }; 168 | addPictureFailure = function() { 169 | return scope.$apply(function() { 170 | scope.cancel(); 171 | return scope.dropError = "Failed to get a picture from your gallery"; 172 | }); 173 | }; 174 | addTypeAndLoadImage = function(src) { 175 | return loadImage("data:image/jpeg;base64," + src); 176 | }; 177 | loadImage = function(src, base64Src) { 178 | var img; 179 | if (base64Src == null) { 180 | base64Src = true; 181 | } 182 | if (!src) { 183 | return; 184 | } 185 | scope.state = "step-2"; 186 | if (src !== scope.imgSrc) { 187 | scope.imgSrc = src; 188 | scope.imgLoaded = false; 189 | img = new Image; 190 | img.onerror = function() { 191 | return scope.$apply(function() { 192 | scope.cancel(); 193 | return scope.dropError = "Unsupported type of image"; 194 | }); 195 | }; 196 | img.onload = function() { 197 | var errors, height, width; 198 | width = img.width; 199 | height = img.height; 200 | errors = []; 201 | if (scope.width == null) { 202 | scope.width = scope.height * width / height; 203 | } 204 | if (width < scope.width) { 205 | errors.push("The image you dropped has a width of " + width + ", but the minimum is " + scope.width + "."); 206 | } 207 | minHeight = Math.min(scope.height, scope.destinationHeight); 208 | if (height < minHeight) { 209 | errors.push("The image you dropped has a height of " + height + ", but the minimum is " + minHeight + "."); 210 | } 211 | return scope.$apply(function() { 212 | if (errors.length) { 213 | scope.cancel(); 214 | return scope.dropError = errors.join("
"); 215 | } else { 216 | scope.imgLoaded = true; 217 | $rootScope.$broadcast("cropme:loaded", width, height, element); 218 | sendImageEvent("progress"); 219 | return startCropping(width, height); 220 | } 221 | }); 222 | }; 223 | if (!base64Src && scope.crossOrigin === "true") { 224 | img.crossOrigin = "anonymous"; 225 | } 226 | return img.src = src; 227 | } 228 | }; 229 | moveCropZone = function(coords) { 230 | var offset; 231 | offset = elOffset(); 232 | scope.xCropZone = coords.x - offset.left - scope.widthCropZone / 2; 233 | scope.yCropZone = coords.y - offset.top - scope.heightCropZone / 2; 234 | return checkBoundsAndSendProgressEvent(); 235 | }; 236 | moveBorders = { 237 | top: function(coords) { 238 | var y; 239 | y = coords.y - elOffset().top; 240 | scope.heightCropZone += scope.yCropZone - y; 241 | scope.yCropZone = y; 242 | checkVRatio(); 243 | return checkBoundsAndSendProgressEvent(); 244 | }, 245 | right: function(coords) { 246 | var x; 247 | x = coords.x - elOffset().left; 248 | scope.widthCropZone = x - scope.xCropZone; 249 | checkHRatio(); 250 | return checkBoundsAndSendProgressEvent(); 251 | }, 252 | bottom: function(coords) { 253 | var y; 254 | y = coords.y - elOffset().top; 255 | scope.heightCropZone = y - scope.yCropZone; 256 | checkVRatio(); 257 | return checkBoundsAndSendProgressEvent(); 258 | }, 259 | left: function(coords) { 260 | var x; 261 | x = coords.x - elOffset().left; 262 | scope.widthCropZone += scope.xCropZone - x; 263 | scope.xCropZone = x; 264 | checkHRatio(); 265 | return checkBoundsAndSendProgressEvent(); 266 | } 267 | }; 268 | checkHRatio = function() { 269 | if (scope.ratio) { 270 | return scope.heightCropZone = scope.widthCropZone * scope.ratio; 271 | } 272 | }; 273 | checkVRatio = function() { 274 | if (scope.ratio) { 275 | return scope.widthCropZone = scope.heightCropZone / scope.ratio; 276 | } 277 | }; 278 | checkBoundsAndSendProgressEvent = function() { 279 | if (scope.xCropZone < 0) { 280 | scope.xCropZone = 0; 281 | } 282 | if (scope.yCropZone < 0) { 283 | scope.yCropZone = 0; 284 | } 285 | if (scope.widthCropZone < scope.destinationWidth * zoom) { 286 | scope.widthCropZone = scope.destinationWidth * zoom; 287 | checkHRatio(); 288 | } else if (scope.destinationHeight && scope.heightCropZone < scope.destinationHeight * zoom) { 289 | scope.heightCropZone = scope.destinationHeight * zoom; 290 | checkVRatio(); 291 | } 292 | if (scope.xCropZone + scope.widthCropZone > scope.width) { 293 | scope.xCropZone = scope.width - scope.widthCropZone; 294 | if (scope.xCropZone < 0) { 295 | scope.widthCropZone = scope.width; 296 | scope.xCropZone = 0; 297 | checkHRatio(); 298 | } 299 | } 300 | if (scope.yCropZone + scope.heightCropZone > heightWithImage) { 301 | scope.yCropZone = heightWithImage - scope.heightCropZone; 302 | if (scope.yCropZone < 0) { 303 | scope.heightCropZone = heightWithImage; 304 | scope.yCropZone = 0; 305 | checkVRatio(); 306 | } 307 | } 308 | roundBounds(); 309 | return debouncedSendImageEvent("progress"); 310 | }; 311 | roundBounds = function() { 312 | scope.yCropZone = Math.round(scope.yCropZone); 313 | scope.xCropZone = Math.round(scope.xCropZone); 314 | scope.widthCropZone = Math.round(scope.widthCropZone); 315 | return scope.heightCropZone = Math.round(scope.heightCropZone); 316 | }; 317 | isNearBorders = function(coords) { 318 | var bottomLeft, bottomRight, h, offset, topLeft, topRight, w, x, y; 319 | offset = elOffset(); 320 | x = scope.xCropZone + offset.left; 321 | y = scope.yCropZone + offset.top; 322 | w = scope.widthCropZone; 323 | h = scope.heightCropZone; 324 | topLeft = { 325 | x: x, 326 | y: y 327 | }; 328 | topRight = { 329 | x: x + w, 330 | y: y 331 | }; 332 | bottomLeft = { 333 | x: x, 334 | y: y + h 335 | }; 336 | bottomRight = { 337 | x: x + w, 338 | y: y + h 339 | }; 340 | return nearHSegment(coords, x, w, y, "top") || nearVSegment(coords, y, h, x + w, "right") || nearHSegment(coords, x, w, y + h, "bottom") || nearVSegment(coords, y, h, x, "left"); 341 | }; 342 | nearHSegment = function(coords, x, w, y, borderName) { 343 | if (coords.x >= x && coords.x <= x + w && Math.abs(coords.y - y) <= borderSensitivity) { 344 | return borderName; 345 | } 346 | }; 347 | nearVSegment = function(coords, y, h, x, borderName) { 348 | if (coords.y >= y && coords.y <= y + h && Math.abs(coords.x - x) <= borderSensitivity) { 349 | return borderName; 350 | } 351 | }; 352 | dragIt = function(coords) { 353 | if (draggingFn) { 354 | return scope.$apply(function() { 355 | return draggingFn(coords); 356 | }); 357 | } 358 | }; 359 | getCropPromise = function() { 360 | var deferred; 361 | deferred = $q.defer(); 362 | if (sendCropped()) { 363 | ctx.drawImage(imageEl, scope.xCropZone / zoom, scope.yCropZone / zoom, scope.croppedWidth, scope.croppedHeight, 0, 0, scope.destinationWidth, scope.destinationHeight); 364 | canvasToBlob(canvasEl, (function(blob) { 365 | return deferred.resolve(blob); 366 | }), "image/" + scope.type); 367 | } else { 368 | deferred.resolve(); 369 | } 370 | return deferred.promise; 371 | }; 372 | getOriginalPromise = function() { 373 | var deferred, originalCanvas, originalContext; 374 | deferred = $q.defer(); 375 | if (sendOriginal()) { 376 | originalCanvas = document.createElement("canvas"); 377 | originalContext = originalCanvas.getContext("2d"); 378 | originalCanvas.width = imageEl.naturalWidth; 379 | originalCanvas.height = imageEl.naturalHeight; 380 | originalContext.drawImage(imageEl, 0, 0); 381 | canvasToBlob(originalCanvas, (function(blob) { 382 | return deferred.resolve(blob); 383 | }), "image/" + scope.type); 384 | } else { 385 | deferred.resolve(); 386 | } 387 | return deferred.promise; 388 | }; 389 | sendImageEvent = function(eventName) { 390 | scope.croppedWidth = scope.widthCropZone / zoom; 391 | scope.croppedHeight = scope.heightCropZone / zoom; 392 | return $q.all([getCropPromise(), getOriginalPromise()]).then(function(blobArray) { 393 | var result; 394 | result = { 395 | x: scope.xCropZone / zoom, 396 | y: scope.yCropZone / zoom, 397 | height: scope.croppedHeight, 398 | width: scope.croppedWidth, 399 | destinationHeight: scope.destinationHeight, 400 | destinationWidth: scope.destinationWidth, 401 | filename: scope.filename 402 | }; 403 | if (blobArray[0]) { 404 | result.croppedImage = blobArray[0]; 405 | } 406 | if (blobArray[1]) { 407 | result.originalImage = blobArray[1]; 408 | } 409 | return $rootScope.$broadcast("cropme:" + eventName, result, element); 410 | }); 411 | }; 412 | debounce = function(func, wait, immediate) { 413 | var timeout; 414 | timeout = void 0; 415 | return function() { 416 | var args, callNow, context, later; 417 | context = this; 418 | args = arguments; 419 | later = function() { 420 | timeout = null; 421 | if (!immediate) { 422 | func.apply(context, args); 423 | } 424 | }; 425 | callNow = immediate && !timeout; 426 | clearTimeout(timeout); 427 | timeout = setTimeout(later, wait); 428 | if (callNow) { 429 | func.apply(context, args); 430 | } 431 | }; 432 | }; 433 | scope.mousemove = function(e) { 434 | return scope.colResizePointer = (function() { 435 | switch (isNearBorders({ 436 | x: e.pageX - window.scrollX, 437 | y: e.pageY - window.scrollY 438 | })) { 439 | case 'top': 440 | return 'ne-resize'; 441 | case 'right': 442 | case 'bottom': 443 | return 'se-resize'; 444 | case 'left': 445 | return 'sw-resize'; 446 | default: 447 | return 'move'; 448 | } 449 | })(); 450 | }; 451 | superswipe.bind(angular.element(element[0].getElementsByClassName('step-2')[0]), { 452 | 'start': function(coords) { 453 | grabbedBorder = isNearBorders(coords); 454 | if (grabbedBorder) { 455 | draggingFn = moveBorders[grabbedBorder]; 456 | } else { 457 | draggingFn = moveCropZone; 458 | } 459 | return dragIt(coords); 460 | }, 461 | 'move': function(coords) { 462 | return dragIt(coords); 463 | }, 464 | 'end': function(coords) { 465 | dragIt(coords); 466 | return draggingFn = null; 467 | } 468 | }); 469 | scope.deselect = function() { 470 | return draggingFn = null; 471 | }; 472 | scope.cancel = function($event, id) { 473 | if (id && element.attr('id') !== id) { 474 | return; 475 | } 476 | if ($event) { 477 | $event.preventDefault(); 478 | } 479 | scope.dropLabel = "Drop files here"; 480 | scope.dropClass = ""; 481 | scope.state = "step-1"; 482 | $rootScope.$broadcast("cropme:canceled"); 483 | delete scope.imgSrc; 484 | return delete scope.filename; 485 | }; 486 | scope.ok = function($event) { 487 | if ($event) { 488 | $event.preventDefault(); 489 | } 490 | return sendImageEvent("done"); 491 | }; 492 | scope.$on("cropme:cancel", scope.cancel); 493 | scope.$on("cropme:ok", scope.ok); 494 | scope.$watch("src", function() { 495 | var delimit, src; 496 | if (scope.src) { 497 | scope.filename = scope.src; 498 | if (scope.src.indexOf("data:image") === 0) { 499 | return loadImage(scope.src); 500 | } else { 501 | delimit = scope.src.match(/\?/) ? "&" : "?"; 502 | src = scope.src; 503 | if (scope.crossOrign === "true") { 504 | src += delimit + "crossOrigin"; 505 | } 506 | return loadImage("" + src, false); 507 | } 508 | } 509 | }); 510 | return debouncedSendImageEvent = debounce(sendImageEvent, 300); 511 | } 512 | }; 513 | }]); 514 | 515 | }).call(this); 516 | ; 517 | /** 518 | * @ngdoc directive 519 | * @name dropbox 520 | * @requires elementOffset 521 | * @description 522 | * Simple directive to manage drag and drop of a file in an element 523 | * 524 | */ 525 | 526 | (function() { 527 | angular.module("cropme").directive("dropbox", ["elementOffset", function(elementOffset) { 528 | return { 529 | restrict: "E", 530 | link: function(scope, element, attributes) { 531 | var dragEnterLeave, dropbox, offset, reset; 532 | offset = elementOffset(element); 533 | reset = function(evt) { 534 | evt.stopPropagation(); 535 | evt.preventDefault(); 536 | return scope.$apply(function() { 537 | scope.dragOver = false; 538 | scope.dropText = "Drop files here"; 539 | return scope.dropClass = ""; 540 | }); 541 | }; 542 | dragEnterLeave = function(evt) { 543 | if (evt.x > offset.left && evt.x < offset.left + element[0].offsetWidth && evt.y > offset.top && evt.y < offset.top + element[0].offsetHeight) { 544 | return; 545 | } 546 | return reset(evt); 547 | }; 548 | dropbox = element[0]; 549 | scope.dropText = "Drop files here"; 550 | scope.dragOver = false; 551 | dropbox.addEventListener("dragenter", dragEnterLeave, false); 552 | dropbox.addEventListener("dragleave", dragEnterLeave, false); 553 | dropbox.addEventListener("dragover", (function(evt) { 554 | var ok; 555 | evt.stopPropagation(); 556 | evt.preventDefault(); 557 | ok = evt.dataTransfer && evt.dataTransfer.types && evt.dataTransfer.types.indexOf("Files") >= 0; 558 | return scope.$apply(function() { 559 | scope.dragOver = true; 560 | scope.dropText = (ok ? "Drop now" : "Only files are allowed"); 561 | return scope.dropClass = (ok ? "over" : "not-available"); 562 | }); 563 | }), false); 564 | return dropbox.addEventListener("drop", (function(evt) { 565 | var files; 566 | reset(evt); 567 | files = evt.dataTransfer.files; 568 | return scope.$apply(function() { 569 | var file, _i, _len; 570 | if (files.length > 0) { 571 | for (_i = 0, _len = files.length; _i < _len; _i++) { 572 | file = files[_i]; 573 | if (file.type.match(/^image\//)) { 574 | scope.dropText = "Loading image..."; 575 | scope.dropClass = "loading"; 576 | return scope.setFiles(file); 577 | } 578 | scope.dropError = "Wrong file type, please drop at least an image."; 579 | } 580 | } 581 | }); 582 | }), false); 583 | } 584 | }; 585 | }]); 586 | 587 | }).call(this); 588 | ;(function() { 589 | "use strict"; 590 | 591 | /** 592 | * @ngdoc service 593 | * @name canvasToBlob 594 | * @requires - 595 | * @description 596 | * Service based on canvas-toBlob.js By Eli Grey, http://eligrey.com and Devin Samarin, https://github.com/eboyjr 597 | * Transform a html canvas into a blob that can then be uploaded as a file 598 | * 599 | * @example 600 | 601 | ```js 602 | angular.module("cropme").controller "myController", (canvasToBlob) -> 603 | * upload the french flag 604 | uploader = (blob) -> 605 | url = "http://my-awesome-server.com" 606 | xhr = new XMLHttpRequest 607 | xhr.setRequestHeader "Content-Type", blob.type 608 | xhr.onreadystatechange = (e) -> 609 | if @readyState is 4 and @status is 200 610 | console.log "done" 611 | else console.log "failed" if @readyState is 4 and @status isnt 200 612 | xhr.open "POST", url, true 613 | xhr.send blob 614 | canvas = document.createElement "canvas" 615 | canvas.height = 100 616 | canvas.width = 300 617 | ctx = canvas.getContext "2d" 618 | ctx.fillStyle = "#0000FF" 619 | ctx.fillRect 0, 0, 100, 100 620 | ctx.fillStyle = "#FFFFFF" 621 | ctx.fillRect 100, 0, 200, 100 622 | ctx.fillStyle = "#FF0000" 623 | ctx.fillRect 200, 0, 300, 100 624 | canvasToBlob canvas, uploader, "image/png" 625 | ``` 626 | */ 627 | angular.module("cropme").service("canvasToBlob", function() { 628 | var base64_ranks, decode_base64, is_base64_regex; 629 | is_base64_regex = /\s*;\s*base64\s*(?:;|$)/i; 630 | base64_ranks = void 0; 631 | decode_base64 = function(base64) { 632 | var buffer, code, i, last, len, outptr, rank, save, state, undef; 633 | len = base64.length; 634 | buffer = new Uint8Array(len / 4 * 3 | 0); 635 | i = 0; 636 | outptr = 0; 637 | last = [0, 0]; 638 | state = 0; 639 | save = 0; 640 | rank = void 0; 641 | code = void 0; 642 | undef = void 0; 643 | while (len--) { 644 | code = base64.charCodeAt(i++); 645 | rank = base64_ranks[code - 43]; 646 | if (rank !== 255 && rank !== undef) { 647 | last[1] = last[0]; 648 | last[0] = code; 649 | save = (save << 6) | rank; 650 | state++; 651 | if (state === 4) { 652 | buffer[outptr++] = save >>> 16; 653 | if (last[1] !== 61) { 654 | buffer[outptr++] = save >>> 8; 655 | } 656 | if (last[0] !== 61) { 657 | buffer[outptr++] = save; 658 | } 659 | state = 0; 660 | } 661 | } 662 | } 663 | return buffer; 664 | }; 665 | base64_ranks = new Uint8Array([62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 0, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]); 666 | return function(canvas, callback, type) { 667 | var args, blob, data, dataURI, header_end, is_base64; 668 | if (!type) { 669 | type = "image/png"; 670 | } 671 | if (canvas.mozGetAsFile) { 672 | callback(canvas.mozGetAsFile("canvas", type)); 673 | return; 674 | } 675 | args = Array.prototype.slice.call(arguments, 1); 676 | dataURI = canvas.toDataURL(type); 677 | header_end = dataURI.indexOf(","); 678 | data = dataURI.substring(header_end + 1); 679 | is_base64 = is_base64_regex.test(dataURI.substring(0, header_end)); 680 | blob = void 0; 681 | if (Blob.fake) { 682 | blob = new Blob; 683 | if (is_base64) { 684 | blob.encoding = "base64"; 685 | } else { 686 | blob.encoding = "URI"; 687 | } 688 | blob.data = data; 689 | blob.size = data.length; 690 | } else if (Uint8Array) { 691 | if (is_base64) { 692 | blob = new Blob([decode_base64(data)], { 693 | type: type 694 | }); 695 | } else { 696 | blob = new Blob([decodeURIComponent(data)], { 697 | type: type 698 | }); 699 | } 700 | } 701 | return callback(blob); 702 | }; 703 | }); 704 | 705 | }).call(this); 706 | ;(function() { 707 | "use strict"; 708 | 709 | /** 710 | * @ngdoc service 711 | * @name elementOffset 712 | * @requires - 713 | * @description 714 | * Get the offset in pixel of an element on the screen 715 | * 716 | * @example 717 | 718 | ```js 719 | angular.module("cropme").directive "myDirective", (elementOffset) -> 720 | link: (scope, element, attributes) -> 721 | offset = elementOffset element 722 | console.log "This directive's element is #{offset.top}px away from the top of the screen" 723 | console.log "This directive's element is #{offset.left}px away from the left of the screen" 724 | console.log "This directive's element is #{offset.bottom}px away from the bottom of the screen" 725 | console.log "This directive's element is #{offset.right}px away from the right of the screen" 726 | ``` 727 | */ 728 | angular.module("cropme").service("elementOffset", function() { 729 | return function(el) { 730 | var height, offsetLeft, offsetTop, scrollLeft, scrollTop, width; 731 | if (el[0]) { 732 | el = el[0]; 733 | } 734 | offsetTop = 0; 735 | offsetLeft = 0; 736 | scrollTop = 0; 737 | scrollLeft = 0; 738 | width = el.offsetWidth; 739 | height = el.offsetHeight; 740 | while (el) { 741 | offsetTop += el.offsetTop - el.scrollTop; 742 | offsetLeft += el.offsetLeft - el.scrollLeft; 743 | el = el.offsetParent; 744 | } 745 | return { 746 | top: offsetTop, 747 | left: offsetLeft, 748 | right: offsetLeft + width, 749 | bottom: offsetTop + height 750 | }; 751 | }; 752 | }); 753 | 754 | }).call(this); 755 | --------------------------------------------------------------------------------