├── .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 |
--------------------------------------------------------------------------------
/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 |
33 |
39 |
![]()
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
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
\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
\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 |
--------------------------------------------------------------------------------