From 540aeed8ad98333973e29db906d81ef69716bf1d Mon Sep 17 00:00:00 2001 From: Felix Niklas Date: Wed, 29 Oct 2014 20:43:56 +0100 Subject: [PATCH] avatar picker and camera --- .../app/controllers/_profile/avatar.js.coffee | 340 ++++++- .../javascripts/app/lib/base/cropper.min.js | 9 + app/assets/javascripts/app/lib/base/exif.js | 805 +++++++++++++++++ .../app/lib/base/modernizr.custom.63196.js | 828 ++++++++++++++++++ .../javascripts/app/models/user.js.coffee | 26 +- .../app/views/generic/attachment.jst.eco | 4 +- .../javascripts/app/views/modal.jst.eco | 8 +- .../app/views/profile/avatar-holder.jst.eco | 4 + .../app/views/profile/avatar.jst.eco | 28 +- .../app/views/profile/camera.jst.eco | 4 + .../app/views/profile/imageCropper.jst.eco | 10 + app/assets/stylesheets/application.css | 1 + app/assets/stylesheets/cropper.min.css | 9 + app/assets/stylesheets/zammad.css.scss | 168 +++- 14 files changed, 2156 insertions(+), 88 deletions(-) create mode 100755 app/assets/javascripts/app/lib/base/cropper.min.js create mode 100755 app/assets/javascripts/app/lib/base/exif.js create mode 100644 app/assets/javascripts/app/lib/base/modernizr.custom.63196.js create mode 100644 app/assets/javascripts/app/views/profile/avatar-holder.jst.eco create mode 100644 app/assets/javascripts/app/views/profile/camera.jst.eco create mode 100644 app/assets/javascripts/app/views/profile/imageCropper.jst.eco create mode 100755 app/assets/stylesheets/cropper.min.css diff --git a/app/assets/javascripts/app/controllers/_profile/avatar.js.coffee b/app/assets/javascripts/app/controllers/_profile/avatar.js.coffee index 792772240..1526ebb9d 100644 --- a/app/assets/javascripts/app/controllers/_profile/avatar.js.coffee +++ b/app/assets/javascripts/app/controllers/_profile/avatar.js.coffee @@ -1,60 +1,316 @@ class Index extends App.Controller + elements: + '.js-upload': 'fileInput' + '.avatar-gallery': 'avatarGallery' + events: - 'submit form': 'update' + 'click .js-openCamera': 'openCamera' + 'change .js-upload': 'onUpload' + 'click .avatar': 'onSelect' + 'click .avatar-delete': 'onDelete' constructor: -> super return if !@authenticate() + @render() + # check if the browser supports webcam access + # doesnt render the camera button if not + hasGetUserMedia: -> + return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || + navigator.mozGetUserMedia || navigator.msGetUserMedia) + render: => - @html App.view('profile/avatar')() + @html App.view('profile/avatar') + webcamSupport: @hasGetUserMedia() + onSelect: (e) => + @pick( $(e.currentTarget) ) - update: (e) => - e.preventDefault() - params = @formParam(e.target) - error = @form.validate(params) - if error - @formValidate( form: e.target, errors: error ) - return false + onDelete: (e) => + e.stopPropagation() + if confirm App.i18n.translateInline('Delete Avatar?') + $(e.currentTarget).parent('.avatar-holder').remove() + @pick @('.avatar').last() + # remove avatar - @formDisable(e) + pick: (avatar) => + @$('.avatar').removeClass('is-active') + avatar.addClass('is-active') + # update avatar globally - # get data - @locale = params['locale'] - @ajax( - id: 'preferences' - type: 'PUT' - url: @apiPath + '/users/preferences' - data: JSON.stringify(params) - processData: true - success: @success - error: @error - ) + openCamera: => + new Camera + callback: @storeImage - success: (data, status, xhr) => - App.User.retrieve( - App.Session.get( 'id' ), - => - App.i18n.set( @locale ) - App.Event.trigger( 'ui:rerender' ) - App.Event.trigger( 'ui:page:rerender' ) - @notify( - type: 'success' - msg: App.i18n.translateContent( 'Successfully!' ) - ) - , - true - ) + storeImage: (src) => + avatarHolder = $(App.view('profile/avatar-holder') src: src) + @avatarGallery.append(avatarHolder) + @pick avatarHolder.find('.avatar') - error: (xhr, status, error) => - @render() - data = JSON.parse( xhr.responseText ) - @notify( - type: 'error' - msg: App.i18n.translateContent( data.message ) - ) + onUpload: (event) => + callback = @storeImage + EXIF.getData event.target.files[0], -> + orientation = this.exifdata.Orientation + reader = new FileReader() + reader.onload = (e) => + new ImageCropper + imageSource: e.target.result + callback: callback + orientation: orientation + + reader.readAsDataURL(this) App.Config.set( 'Avatar', { prio: 1100, name: 'Avatar', parent: '#profile', target: '#profile/avatar', controller: Index }, 'NavBarProfile' ) + +class ImageCropper extends App.ControllerModal + elements: + '.imageCropper-image': 'image' + + constructor: (options) -> + super + @head = 'Crop Image' + @cancel = true + @button = 'Save' + @buttonClass = 'btn--success' + + @show( App.view('profile/imageCropper')() ) + + @size = 256 + + orientationTransform = + 1: 0 + 3: 180 + 6: 90 + 8: -90 + + @angle = orientationTransform[ @options.orientation ] + + if @angle != 0 + @isOrientating = true + image = new Image() + image.addEventListener 'load', @orientateImage + image.src = @options.imageSource + else + @image.attr src: @options.imageSource + + orientateImage: (e) => + image = e.currentTarget + canvas = document.createElement('canvas') + ctx = canvas.getContext('2d') + + # fit image into options.max bounding box + # if image.width > @options.max + # image.height = @options.max * image.height/image.width + # image.width = @options.max + # else if image.height > @options.max + # image.width = @options.max * image.width/image.height + # image.height = @options.max + + if @angle is 180 + canvas.width = image.width + canvas.height = image.height + else + canvas.width = image.height + canvas.height = image.width + + ctx.translate(canvas.width/2, canvas.height/2) + ctx.rotate(@angle * Math.PI/180) + ctx.drawImage(image, -image.width/2, -image.height/2, image.width, image.height) + + @image.attr src: canvas.toDataURL() + @isOrientating = false + @initializeCropper() if @isShown + + onShown: => + @isShown = true + @initializeCropper() if not @isOrientating + + initializeCropper: => + @image.cropper + aspectRatio: 1, + dashed: false, + preview: ".imageCropper-preview" + + onSubmit: (e) => + e.preventDefault() + @options.callback( @image.cropper("getDataURL") ) + @image.cropper("destroy") + @hide() + + +class Camera extends App.ControllerModal + elements: + '.js-shoot': 'shootButton' + '.js-submit': 'submitButton' + '.camera-preview': 'preview' + '.camera': 'camera' + 'video': 'video' + + events: + 'click .js-shoot:not(.is-disabled)': 'onShootClick' + + constructor: (options) -> + super + @size = 256 + @photoTaken = false + @backgroundColor = 'white' + + @head = 'Camera' + @cancel = true + @button = 'Save' + @buttonClass = 'btn--success is-disabled' + @centerButtons = [{ + className: 'btn--success js-shoot', + text: 'Shoot' + }] + + @show( App.view('profile/camera')() ) + + @ctx = @preview.get(0).getContext('2d') + + requestWebcam = Modernizr.prefixed('getUserMedia', navigator) + requestWebcam({video: true}, @onWebcamReady, @onWebcamError) + + @initializeCache() + + onShootClick: => + if @photoTaken + @photoTaken = false + @countdown = 0 + @submitButton.addClass('is-disabled') + @shootButton + .removeClass('btn--danger') + .addClass('btn--success') + .text(App.i18n.translateInline('Shoot')) + @updatePreview() + else + @shoot() + @shootButton + .removeClass('btn--success') + .addClass('btn--danger') + .text(App.i18n.translateInline('Discard')) + + shoot: => + @photoTaken = true + @submitButton.removeClass('is-disabled') + + onWebcamReady: (stream) => + # in case the modal is closed before the + # request was fullfilled + if @hidden + @stream.stop() + return + + # cache stream so that we can later turn it off + @stream = stream + + @video.attr 'src', window.URL.createObjectURL(stream) + + # setup the offset to center the webcam image perfectly + # when the stream is ready + @video.on('canplay', @setupPreview) + + # start to update the preview once its playing + @video.on('play', @updatePreview) + + # start the stream + @video.get(0).play() + + onWebcamError: (error) => + # in case the modal is closed before the + # request was fullfilled + if @hidden + return + + convertToHumanReadable = + 'PermissionDeniedError': App.i18n.translateInline('You have to allow access to your webcam.') + 'ConstraintNotSatisfiedError': App.i18n.translateInline('No camera found.') + + alert convertToHumanReadable[error.name] + @hide() + + setupPreview: => + @video.attr 'height', @size + @preview.attr + width: @size + height: @size + @offsetX = (@video.width() - @size)/2 + @centerX = @size/2 + @centerY = @size/2 + + updatePreview: => + @ctx.clearRect(0, 0, @preview.width(), @preview.height()) + + # create circle clip area + @ctx.save() + + @ctx.beginPath() + @ctx.arc(@centerX, @centerY, @size/2, 0, 2 * Math.PI, false) + @ctx.closePath() + @ctx.clip() + + # flip the image to look like a mirror + @ctx.scale(-1,1) + + # draw video frame + @ctx.drawImage(@video.get(0), @offsetX, 0, -@video.width(), @size) + + # flip the image to look like a mirror + @ctx.scale(-1,1) + + # add anti-aliasing + # http://stackoverflow.com/a/12395939 + @ctx.strokeStyle = @backgroundColor + @ctx.lineWidth = 2 + @ctx.arc(@centerX, @centerY, @size/2, 0, 2 * Math.PI, false) + @ctx.stroke() + + # reset the clip area to be able to draw on the whole canvas + @ctx.restore() + + # update the preview again as soon as + # the browser is ready to draw a new frame + if not @photoTaken + requestAnimationFrame @updatePreview + else + # cache raw video data + @cacheScreenshot() + + initializeCache: -> + # create virtual canvas + @cache = $('') + @cacheCtx = @cache.get(0).getContext('2d') + + cacheScreenshot: -> + # reset video height + @video.attr height: '' + + @cache.attr + width: @video.height() + height: @video.height() + + offsetX = (@video.width() - @video.height())/2 + + # draw full resolution screenshot + @cacheCtx.save() + # flip image + @cacheCtx.scale(-1,1) + @cacheCtx.drawImage(@video.get(0), offsetX, 0, -@video.width(), @video.height()) + + @cacheCtx.restore() + + # reset video height + @video.attr height: @size + + onHide: => + @stream.stop() if @stream + @hidden = true + + onSubmit: (e) => + e.preventDefault() + # send picture to the + @options.callback( @cache.get(0).toDataURL() ) + @hide() diff --git a/app/assets/javascripts/app/lib/base/cropper.min.js b/app/assets/javascripts/app/lib/base/cropper.min.js new file mode 100755 index 000000000..3352875fc --- /dev/null +++ b/app/assets/javascripts/app/lib/base/cropper.min.js @@ -0,0 +1,9 @@ +/*! + * Cropper v0.7.0 + * https://github.com/fengyuanchen/cropper + * + * Copyright 2014 Fengyuan Chen + * Released under the MIT license + */ + +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a(jQuery)}(function(a){"use strict";var b=a(window),c=a(document),d=!0,e=!1,f=null,g=0/0,h=1/0,i="undefined",j="directive",k=".cropper",l=/^(e|n|w|s|ne|nw|sw|se|all|crop|move|zoom)$/i,m=/^(x|y|width|height|rotate)$/i,n=/^(naturalWidth|naturalHeight|width|height|aspectRatio|ratio|rotate)$/i,o="cropper-modal",p="cropper-hidden",q="cropper-invisible",r="cropper-move",s="cropper-crop",t="cropper-disabled",u="mousedown touchstart",v="mousemove touchmove",w="mouseup mouseleave touchend touchleave touchcancel",x="wheel mousewheel DOMMouseScroll",y="resize"+k,z="dblclick",A="build"+k,B="built"+k,C="dragstart"+k,D="dragmove"+k,E="dragend"+k,F=function(a){return"number"==typeof a},G=function(a){return''},H=function(a){return"rotate("+a+"deg)"},I=function(b,c){this.$element=a(b),this.defaults=a.extend({},I.DEFAULTS,a.isPlainObject(c)?c:{}),this.ready=e,this.built=e,this.cropped=e,this.disabled=e,this.init()},J=Math.round,K=Math.sqrt,L=Math.min,M=Math.max,N=Math.abs,O=parseFloat;I.prototype={constructor:I,init:function(){var b=this.defaults;a.each(b,function(a,c){switch(a){case"aspectRatio":b[a]=N(O(c))||g;break;case"minWidth":case"minHeight":b[a]=N(O(c))||0;break;case"maxWidth":case"maxHeight":b[a]=N(O(c))||h}}),this.load()},load:function(){var b,c,e=this,f=this.$element,g=f[0],h={};f.is("img")?c=f.attr("src"):f.is("canvas")&&g.getContext&&(c=g.toDataURL()),c&&(this.$clone&&this.$clone.remove(),this.$clone=b=a(G(c)),b.one("load",function(){h.naturalWidth=this.naturalWidth||b.width(),h.naturalHeight=this.naturalHeight||b.height(),h.aspectRatio=h.naturalWidth/h.naturalHeight,e.url=c,e.image=h,e.ready=d,e.build()}),b.addClass(q).prependTo("body"))},build:function(){var b,e,f=this.$element,g=this.defaults;this.ready&&(this.built&&this.unbuild(),f.one(A,g.build),b=a.Event(A),f.trigger(b),b.isDefaultPrevented()||(this.$cropper=e=a(I.TEMPLATE),f.addClass(p),this.$clone.removeClass(q).prependTo(e),this.$container=f.parent(),this.$container.append(e),this.$canvas=e.find(".cropper-canvas"),this.$dragger=e.find(".cropper-dragger"),this.$viewer=e.find(".cropper-viewer"),g.autoCrop?this.cropped=d:this.$dragger.addClass(p),g.dragCrop&&this.setDragMode("crop"),g.modal&&this.$canvas.addClass(o),!g.dashed&&this.$dragger.find(".cropper-dashed").addClass(p),!g.movable&&this.$dragger.find(".cropper-face").addClass(p),!g.resizable&&this.$dragger.find(".cropper-line, .cropper-point").addClass(p),this.$dragScope=g.multiple?this.$cropper:c,this.addListeners(),this.initPreview(),this.built=d,this.update(),f.one(B,g.built),f.trigger(B)))},unbuild:function(){this.built&&(this.built=e,this.removeListeners(),this.$preview.empty(),this.$preview=f,this.$dragger=f,this.$canvas=f,this.$container=f,this.$cropper.remove(),this.$cropper=f)},update:function(a){this.initContainer(),this.initCropper(),this.initImage(),this.initDragger(),a?(this.setData(a,d),this.setDragMode("crop")):this.setData(this.defaults.data)},resize:function(){clearTimeout(this.resizing),this.resizing=setTimeout(a.proxy(this.update,this,this.getData()),200)},preview:function(){var b=this.image,c=this.dragger,d=b.width,e=b.height,f=c.left-b.left,g=c.top-b.top,h=b.rotate;this.$viewer.find("img").css({width:J(d),height:J(e),marginLeft:-J(f),marginTop:-J(g),transform:H(h)}),this.$preview.each(function(){var b=a(this),i=b.width()/c.width;b.find("img").css({width:J(d*i),height:J(e*i),marginLeft:-J(f*i),marginTop:-J(g*i),transform:H(h)})})},addListeners:function(){var c=this.$element,d=this.defaults;c.on(C,d.dragstart).on(D,d.dragmove).on(E,d.dragend),this.$cropper.on(u,a.proxy(this.dragstart,this)).on(z,a.proxy(this.dblclick,this)),d.zoomable&&this.$cropper.on(x,a.proxy(this.wheel,this)),this.$dragScope.on(v,a.proxy(this.dragmove,this)).on(w,a.proxy(this.dragend,this)),b.on(y,a.proxy(this.resize,this))},removeListeners:function(){var a=this.$element,c=this.defaults;a.off(C,c.dragstart).off(D,c.dragmove).off(E,c.dragend),this.$cropper.off(u,this.dragstart).off(z,this.dblclick),c.zoomable&&this.$cropper.off(x,this.wheel),this.$dragScope.off(v,this.dragmove).off(w,this.dragend),b.off(y,this.resize)},initPreview:function(){var b=G(this.url);this.$preview=a(this.defaults.preview),this.$preview.html(b),this.$viewer.html(b)},initContainer:function(){var a=this.$container;this.container={width:a.width(),height:a.height()}},initCropper:function(){var a,b=this.container,c=this.image;c.naturalWidth*b.height/c.naturalHeight-b.width>=0?(a={width:b.width,height:b.width/c.aspectRatio,left:0},a.top=(b.height-a.height)/2):(a={width:b.height*c.aspectRatio,height:b.height,top:0},a.left=(b.width-a.width)/2),this.$cropper.css({width:J(a.width),height:J(a.height),left:J(a.left),top:J(a.top)}),this.cropper=a},initImage:function(){var b=this.image,c=this.cropper,d={_width:c.width,_height:c.height,width:c.width,height:c.height,left:0,top:0,ratio:c.width/b.naturalWidth,rotate:0};this.defaultImage=a.extend({},b,d),b._width!==c.width||b._height!==c.height?a.extend(b,d):b=a.extend(d,b),this.image=b,this.renderImage()},renderImage:function(a){var b=this.image;"zoom"===a&&(b.left-=(b.width-b.oldWidth)/2,b.top-=(b.height-b.oldHeight)/2),b.left=L(M(b.left,b._width-b.width),0),b.top=L(M(b.top,b._height-b.height),0),this.$clone.css({width:J(b.width),height:J(b.height),marginLeft:J(b.left),marginTop:J(b.top),transform:H(b.rotate)}),a&&(this.defaults.done(this.getData()),this.preview())},initDragger:function(){var b,c=this.defaults,d=this.cropper,e=c.aspectRatio||this.image.aspectRatio,f=this.image.ratio;b=d.height*e-d.width>=0?{height:d.width/e,width:d.width,left:0,top:(d.height-d.width/e)/2,maxWidth:d.width,maxHeight:d.width/e}:{height:d.height,width:d.height*e,left:(d.width-d.height*e)/2,top:0,maxWidth:d.height*e,maxHeight:d.height},b.minWidth=0,b.minHeight=0,c.aspectRatio?(isFinite(c.maxWidth)?(b.maxWidth=L(b.maxWidth,c.maxWidth*f),b.maxHeight=b.maxWidth/e):isFinite(c.maxHeight)&&(b.maxHeight=L(b.maxHeight,c.maxHeight*f),b.maxWidth=b.maxHeight*e),c.minWidth>0?(b.minWidth=M(0,c.minWidth*f),b.minHeight=b.minWidth/e):c.minHeight>0&&(b.minHeight=M(0,c.minHeight*f),b.minWidth=b.minHeight*e)):(b.maxWidth=L(b.maxWidth,c.maxWidth*f),b.maxHeight=L(b.maxHeight,c.maxHeight*f),b.minWidth=M(0,c.minWidth*f),b.minHeight=M(0,c.minHeight*f)),b.minWidth=L(b.maxWidth,b.minWidth),b.minHeight=L(b.maxHeight,b.minHeight),b.height*=.8,b.width*=.8,b.left=(d.width-b.width)/2,b.top=(d.height-b.height)/2,b.oldLeft=b.left,b.oldTop=b.top,this.defaultDragger=b,this.dragger=a.extend({},b)},renderDragger:function(){var a=this.dragger,b=this.cropper;a.width>a.maxWidth?(a.width=a.maxWidth,a.left=a.oldLeft):a.widtha.maxHeight?(a.height=a.maxHeight,a.top=a.oldTop):a.height=e.minWidth?(e.width=b.width,e.height=e.width/h):F(b.height)&&b.height<=e.maxHeight&&b.height>=e.minHeight&&(e.height=b.height,e.width=e.height*h):(F(b.width)&&b.width<=e.maxWidth&&b.width>=e.minWidth&&(e.width=b.width),F(b.height)&&b.height<=e.maxHeight&&b.height>=e.minHeight&&(e.height=b.height)),F(b.rotate)&&this.rotate(b.rotate)),this.dragger=e,this.renderDragger())},getData:function(){var a=this.dragger,b=this.image,c={};return this.built&&(c={x:a.left-b.left,y:a.top-b.top,width:a.width,height:a.height,rotate:b.rotate},c=this.transformData(c,d)),c},transformData:function(b,c){var d=this.image.ratio,e={};return a.each(b,function(a,b){b=O(b),m.test(a)&&!isNaN(b)&&(e[a]="rotate"===a?b:c?J(b/d):b*d)}),e},setAspectRatio:function(a){var b="auto"===a;a=O(a),(b||!isNaN(a)&&a>0)&&(this.defaults.aspectRatio=b?g:a,this.built&&(this.initDragger(),this.renderDragger()))},getImageData:function(){var b={};return this.ready&&a.each(this.image,function(a,c){n.test(a)&&(b[a]=c)}),b},getDataURL:function(a,b){var c=this.getData(),d=this.$clone[0],e=document.createElement("canvas"),f="";return this.cropped&&e.getContext&&(e.width=c.width,e.height=c.height,e.getContext("2d").drawImage(d,c.x,c.y,c.width,c.height,0,0,c.width,c.height),f=e.toDataURL(a,b)),f},setDragMode:function(a){var b=this.$canvas,c=this.defaults,f=e,g=e;if(!this.disabled){switch(a){case"crop":c.dragCrop&&(f=d,b.data(j,a));break;case"move":c.movable&&(g=d,b.data(j,a));break;default:b.removeData(j)}b.toggleClass(s,f).toggleClass(r,g)}},enable:function(){this.disabled=e,this.$cropper.removeClass(t)},disable:function(){this.disabled=d,this.$cropper.addClass(t)},rotate:function(a){var b;!this.disabled&&this.defaults.rotatable&&(b=this.image.rotate+(O(a)||0),this.image.rotate=b%360,this.renderImage("rotate"))},zoom:function(a){var b,c,d,e;!this.disabled&&this.defaults.zoomable&&(b=this.image,c=b.width*(1+a),d=b.height*(1+a),e=c/b._width,e>10||(1>e&&(c=b._width,d=b._height),this.setDragMode(1>=e?"crop":"move"),b.oldWidth=b.width,b.oldHeight=b.height,b.width=c,b.height=d,b.ratio=b.width/b.naturalWidth,this.renderImage("zoom")))},dblclick:function(){this.disabled||this.setDragMode(this.$canvas.hasClass(s)?"move":"crop")},wheel:function(a){var b,c=a.originalEvent,d=117.25,e=5,f=166.66665649414062,g=.1;this.disabled||(a.preventDefault(),c.deltaY?(b=c.deltaY,b=b%e===0?b/e:b%d===0?b/d:b/f):b=c.wheelDelta?-c.wheelDelta/120:c.detail?c.detail/3:0,this.zoom(b*g))},dragstart:function(b){var c,f,g,h=b.originalEvent.touches,i=b;if(!this.disabled){if(h){if(g=h.length,g>1){if(!this.defaults.zoomable||2!==g)return;i=h[1],this.startX2=i.pageX,this.startY2=i.pageY,c="zoom"}i=h[0]}if(c=c||a(i.target).data(j),l.test(c)){if(b.preventDefault(),f=a.Event(C),this.$element.trigger(f),f.isDefaultPrevented())return;this.directive=c,this.cropping=e,this.startX=i.pageX,this.startY=i.pageY,"crop"===c&&(this.cropping=d,this.$canvas.addClass(o))}}},dragmove:function(b){var c,d,e=b.originalEvent.touches,f=b;if(!this.disabled){if(e){if(d=e.length,d>1){if(!this.defaults.zoomable||2!==d)return;f=e[1],this.endX2=f.pageX,this.endY2=f.pageY}f=e[0]}if(this.directive){if(b.preventDefault(),c=a.Event(D),this.$element.trigger(c),c.isDefaultPrevented())return;this.endX=f.pageX,this.endY=f.pageY,this.dragging()}}},dragend:function(b){var c;if(!this.disabled&&this.directive){if(b.preventDefault(),c=a.Event(E),this.$element.trigger(c),c.isDefaultPrevented())return;this.cropping&&(this.cropping=e,this.$canvas.toggleClass(o,this.cropped&&this.defaults.modal)),this.directive=""}},dragging:function(){var a,b=this.directive,c=this.image,f=this.cropper,g=f.width,h=f.height,i=this.dragger,j=i.width,k=i.height,l=i.left,m=i.top,n=l+j,o=m+k,q=d,r=this.defaults,s=r.aspectRatio,t={x:this.endX-this.startX,y:this.endY-this.startY};switch(s&&(t.X=t.y*s,t.Y=t.x/s),b){case"all":l+=t.x,m+=t.y;break;case"e":if(t.x>=0&&(n>=g||s&&(0>=m||o>=h))){q=e;break}j+=t.x,s&&(k=j/s,m-=t.Y/2),0>j&&(b="w",j=0);break;case"n":if(t.y<=0&&(0>=m||s&&(0>=l||n>=g))){q=e;break}k-=t.y,m+=t.y,s&&(j=k*s,l+=t.X/2),0>k&&(b="s",k=0);break;case"w":if(t.x<=0&&(0>=l||s&&(0>=m||o>=h))){q=e;break}j-=t.x,l+=t.x,s&&(k=j/s,m+=t.Y/2),0>j&&(b="e",j=0);break;case"s":if(t.y>=0&&(o>=h||s&&(0>=l||n>=g))){q=e;break}k+=t.y,s&&(j=k*s,l-=t.X/2),0>k&&(b="n",k=0);break;case"ne":if(t.y<=0&&(0>=m||n>=g)){q=e;break}k-=t.y,m+=t.y,s?j=k*s:j+=t.x,0>k&&(b="sw",k=0,j=0);break;case"nw":if(t.y<=0&&(0>=m||0>=l)){q=e;break}k-=t.y,m+=t.y,s?(j=k*s,l+=t.X):(j-=t.x,l+=t.x),0>k&&(b="se",k=0,j=0);break;case"sw":if(t.x<=0&&(0>=l||o>=h)){q=e;break}j-=t.x,l+=t.x,s?k=j/s:k+=t.y,0>j&&(b="ne",k=0,j=0);break;case"se":if(t.x>=0&&(n>=g||o>=h)){q=e;break}j+=t.x,s?k=j/s:k+=t.y,0>j&&(b="nw",k=0,j=0);break;case"move":c.left+=t.x,c.top+=t.y,this.renderImage("move"),q=e;break;case"zoom":r.zoomable&&(this.zoom(function(a,b,c,d,e,f){return(K(e*e+f*f)-K(c*c+d*d))/K(a*a+b*b)}(c.width,c.height,N(this.startX-this.startX2),N(this.startY-this.startY2),N(this.endX-this.endX2),N(this.endY-this.endY2))),this.endX2=this.startX2,this.endY2=this.startY2);break;case"crop":t.x&&t.y&&(a=this.$cropper.offset(),l=this.startX-a.left,m=this.startY-a.top,j=i.minWidth,k=i.minHeight,t.x>0?t.y>0?b="se":(b="ne",m-=k):t.y>0?(b="sw",l-=j):(b="nw",l-=j,m-=k),this.cropped||(this.cropped=d,this.$dragger.removeClass(p)))}q&&(i.width=j,i.height=k,i.left=l,i.top=m,this.directive=b,this.renderDragger()),this.startX=this.endX,this.startY=this.endY}},I.TEMPLATE=function(a,b){return b=b.split(","),a.replace(/\d+/g,function(a){return b[a]})}('<0 6="5-container"><0 6="5-canvas"><0 6="5-dragger"><1 6="5-viewer"><1 6="5-8 8-h"><1 6="5-8 8-v"><1 6="5-face" 3-2="all"><1 6="5-7 7-e" 3-2="e"><1 6="5-7 7-n" 3-2="n"><1 6="5-7 7-w" 3-2="w"><1 6="5-7 7-s" 3-2="s"><1 6="5-4 4-e" 3-2="e"><1 6="5-4 4-n" 3-2="n"><1 6="5-4 4-w" 3-2="w"><1 6="5-4 4-s" 3-2="s"><1 6="5-4 4-ne" 3-2="ne"><1 6="5-4 4-nw" 3-2="nw"><1 6="5-4 4-sw" 3-2="sw"><1 6="5-4 4-se" 3-2="se">',"div,span,directive,data,point,cropper,class,line,dashed"),I.DEFAULTS={aspectRatio:"auto",data:{},done:a.noop,preview:"",multiple:e,autoCrop:d,dragCrop:d,dashed:d,modal:d,movable:d,resizable:d,zoomable:d,rotatable:d,minWidth:0,minHeight:0,maxWidth:h,maxHeight:h,build:f,built:f,dragstart:f,dragmove:f,dragend:f},I.setDefaults=function(b){a.extend(I.DEFAULTS,b)},I.other=a.fn.cropper,a.fn.cropper=function(b){var c,d=[].slice.call(arguments,1);return this.each(function(){var e,f=a(this),g=f.data("cropper");g||f.data("cropper",g=new I(this,b)),"string"==typeof b&&a.isFunction(e=g[b])&&(c=e.apply(g,d))}),typeof c!==i?c:this},a.fn.cropper.constructor=I,a.fn.cropper.setDefaults=I.setDefaults,a.fn.cropper.noConflict=function(){return a.fn.cropper=I.other,this}}); \ No newline at end of file diff --git a/app/assets/javascripts/app/lib/base/exif.js b/app/assets/javascripts/app/lib/base/exif.js new file mode 100755 index 000000000..296a338be --- /dev/null +++ b/app/assets/javascripts/app/lib/base/exif.js @@ -0,0 +1,805 @@ +(function() { + + var debug = false; + + var root = this; + + var EXIF = function(obj) { + if (obj instanceof EXIF) return obj; + if (!(this instanceof EXIF)) return new EXIF(obj); + this.EXIFwrapped = obj; + }; + + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = EXIF; + } + exports.EXIF = EXIF; + } else { + root.EXIF = EXIF; + } + + var ExifTags = EXIF.Tags = { + + // version tags + 0x9000 : "ExifVersion", // EXIF version + 0xA000 : "FlashpixVersion", // Flashpix format version + + // colorspace tags + 0xA001 : "ColorSpace", // Color space information tag + + // image configuration + 0xA002 : "PixelXDimension", // Valid width of meaningful image + 0xA003 : "PixelYDimension", // Valid height of meaningful image + 0x9101 : "ComponentsConfiguration", // Information about channels + 0x9102 : "CompressedBitsPerPixel", // Compressed bits per pixel + + // user information + 0x927C : "MakerNote", // Any desired information written by the manufacturer + 0x9286 : "UserComment", // Comments by user + + // related file + 0xA004 : "RelatedSoundFile", // Name of related sound file + + // date and time + 0x9003 : "DateTimeOriginal", // Date and time when the original image was generated + 0x9004 : "DateTimeDigitized", // Date and time when the image was stored digitally + 0x9290 : "SubsecTime", // Fractions of seconds for DateTime + 0x9291 : "SubsecTimeOriginal", // Fractions of seconds for DateTimeOriginal + 0x9292 : "SubsecTimeDigitized", // Fractions of seconds for DateTimeDigitized + + // picture-taking conditions + 0x829A : "ExposureTime", // Exposure time (in seconds) + 0x829D : "FNumber", // F number + 0x8822 : "ExposureProgram", // Exposure program + 0x8824 : "SpectralSensitivity", // Spectral sensitivity + 0x8827 : "ISOSpeedRatings", // ISO speed rating + 0x8828 : "OECF", // Optoelectric conversion factor + 0x9201 : "ShutterSpeedValue", // Shutter speed + 0x9202 : "ApertureValue", // Lens aperture + 0x9203 : "BrightnessValue", // Value of brightness + 0x9204 : "ExposureBias", // Exposure bias + 0x9205 : "MaxApertureValue", // Smallest F number of lens + 0x9206 : "SubjectDistance", // Distance to subject in meters + 0x9207 : "MeteringMode", // Metering mode + 0x9208 : "LightSource", // Kind of light source + 0x9209 : "Flash", // Flash status + 0x9214 : "SubjectArea", // Location and area of main subject + 0x920A : "FocalLength", // Focal length of the lens in mm + 0xA20B : "FlashEnergy", // Strobe energy in BCPS + 0xA20C : "SpatialFrequencyResponse", // + 0xA20E : "FocalPlaneXResolution", // Number of pixels in width direction per FocalPlaneResolutionUnit + 0xA20F : "FocalPlaneYResolution", // Number of pixels in height direction per FocalPlaneResolutionUnit + 0xA210 : "FocalPlaneResolutionUnit", // Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution + 0xA214 : "SubjectLocation", // Location of subject in image + 0xA215 : "ExposureIndex", // Exposure index selected on camera + 0xA217 : "SensingMethod", // Image sensor type + 0xA300 : "FileSource", // Image source (3 == DSC) + 0xA301 : "SceneType", // Scene type (1 == directly photographed) + 0xA302 : "CFAPattern", // Color filter array geometric pattern + 0xA401 : "CustomRendered", // Special processing + 0xA402 : "ExposureMode", // Exposure mode + 0xA403 : "WhiteBalance", // 1 = auto white balance, 2 = manual + 0xA404 : "DigitalZoomRation", // Digital zoom ratio + 0xA405 : "FocalLengthIn35mmFilm", // Equivalent foacl length assuming 35mm film camera (in mm) + 0xA406 : "SceneCaptureType", // Type of scene + 0xA407 : "GainControl", // Degree of overall image gain adjustment + 0xA408 : "Contrast", // Direction of contrast processing applied by camera + 0xA409 : "Saturation", // Direction of saturation processing applied by camera + 0xA40A : "Sharpness", // Direction of sharpness processing applied by camera + 0xA40B : "DeviceSettingDescription", // + 0xA40C : "SubjectDistanceRange", // Distance to subject + + // other tags + 0xA005 : "InteroperabilityIFDPointer", + 0xA420 : "ImageUniqueID" // Identifier assigned uniquely to each image + }; + + var TiffTags = EXIF.TiffTags = { + 0x0100 : "ImageWidth", + 0x0101 : "ImageHeight", + 0x8769 : "ExifIFDPointer", + 0x8825 : "GPSInfoIFDPointer", + 0xA005 : "InteroperabilityIFDPointer", + 0x0102 : "BitsPerSample", + 0x0103 : "Compression", + 0x0106 : "PhotometricInterpretation", + 0x0112 : "Orientation", + 0x0115 : "SamplesPerPixel", + 0x011C : "PlanarConfiguration", + 0x0212 : "YCbCrSubSampling", + 0x0213 : "YCbCrPositioning", + 0x011A : "XResolution", + 0x011B : "YResolution", + 0x0128 : "ResolutionUnit", + 0x0111 : "StripOffsets", + 0x0116 : "RowsPerStrip", + 0x0117 : "StripByteCounts", + 0x0201 : "JPEGInterchangeFormat", + 0x0202 : "JPEGInterchangeFormatLength", + 0x012D : "TransferFunction", + 0x013E : "WhitePoint", + 0x013F : "PrimaryChromaticities", + 0x0211 : "YCbCrCoefficients", + 0x0214 : "ReferenceBlackWhite", + 0x0132 : "DateTime", + 0x010E : "ImageDescription", + 0x010F : "Make", + 0x0110 : "Model", + 0x0131 : "Software", + 0x013B : "Artist", + 0x8298 : "Copyright" + }; + + var GPSTags = EXIF.GPSTags = { + 0x0000 : "GPSVersionID", + 0x0001 : "GPSLatitudeRef", + 0x0002 : "GPSLatitude", + 0x0003 : "GPSLongitudeRef", + 0x0004 : "GPSLongitude", + 0x0005 : "GPSAltitudeRef", + 0x0006 : "GPSAltitude", + 0x0007 : "GPSTimeStamp", + 0x0008 : "GPSSatellites", + 0x0009 : "GPSStatus", + 0x000A : "GPSMeasureMode", + 0x000B : "GPSDOP", + 0x000C : "GPSSpeedRef", + 0x000D : "GPSSpeed", + 0x000E : "GPSTrackRef", + 0x000F : "GPSTrack", + 0x0010 : "GPSImgDirectionRef", + 0x0011 : "GPSImgDirection", + 0x0012 : "GPSMapDatum", + 0x0013 : "GPSDestLatitudeRef", + 0x0014 : "GPSDestLatitude", + 0x0015 : "GPSDestLongitudeRef", + 0x0016 : "GPSDestLongitude", + 0x0017 : "GPSDestBearingRef", + 0x0018 : "GPSDestBearing", + 0x0019 : "GPSDestDistanceRef", + 0x001A : "GPSDestDistance", + 0x001B : "GPSProcessingMethod", + 0x001C : "GPSAreaInformation", + 0x001D : "GPSDateStamp", + 0x001E : "GPSDifferential" + }; + + var StringValues = EXIF.StringValues = { + ExposureProgram : { + 0 : "Not defined", + 1 : "Manual", + 2 : "Normal program", + 3 : "Aperture priority", + 4 : "Shutter priority", + 5 : "Creative program", + 6 : "Action program", + 7 : "Portrait mode", + 8 : "Landscape mode" + }, + MeteringMode : { + 0 : "Unknown", + 1 : "Average", + 2 : "CenterWeightedAverage", + 3 : "Spot", + 4 : "MultiSpot", + 5 : "Pattern", + 6 : "Partial", + 255 : "Other" + }, + LightSource : { + 0 : "Unknown", + 1 : "Daylight", + 2 : "Fluorescent", + 3 : "Tungsten (incandescent light)", + 4 : "Flash", + 9 : "Fine weather", + 10 : "Cloudy weather", + 11 : "Shade", + 12 : "Daylight fluorescent (D 5700 - 7100K)", + 13 : "Day white fluorescent (N 4600 - 5400K)", + 14 : "Cool white fluorescent (W 3900 - 4500K)", + 15 : "White fluorescent (WW 3200 - 3700K)", + 17 : "Standard light A", + 18 : "Standard light B", + 19 : "Standard light C", + 20 : "D55", + 21 : "D65", + 22 : "D75", + 23 : "D50", + 24 : "ISO studio tungsten", + 255 : "Other" + }, + Flash : { + 0x0000 : "Flash did not fire", + 0x0001 : "Flash fired", + 0x0005 : "Strobe return light not detected", + 0x0007 : "Strobe return light detected", + 0x0009 : "Flash fired, compulsory flash mode", + 0x000D : "Flash fired, compulsory flash mode, return light not detected", + 0x000F : "Flash fired, compulsory flash mode, return light detected", + 0x0010 : "Flash did not fire, compulsory flash mode", + 0x0018 : "Flash did not fire, auto mode", + 0x0019 : "Flash fired, auto mode", + 0x001D : "Flash fired, auto mode, return light not detected", + 0x001F : "Flash fired, auto mode, return light detected", + 0x0020 : "No flash function", + 0x0041 : "Flash fired, red-eye reduction mode", + 0x0045 : "Flash fired, red-eye reduction mode, return light not detected", + 0x0047 : "Flash fired, red-eye reduction mode, return light detected", + 0x0049 : "Flash fired, compulsory flash mode, red-eye reduction mode", + 0x004D : "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected", + 0x004F : "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected", + 0x0059 : "Flash fired, auto mode, red-eye reduction mode", + 0x005D : "Flash fired, auto mode, return light not detected, red-eye reduction mode", + 0x005F : "Flash fired, auto mode, return light detected, red-eye reduction mode" + }, + SensingMethod : { + 1 : "Not defined", + 2 : "One-chip color area sensor", + 3 : "Two-chip color area sensor", + 4 : "Three-chip color area sensor", + 5 : "Color sequential area sensor", + 7 : "Trilinear sensor", + 8 : "Color sequential linear sensor" + }, + SceneCaptureType : { + 0 : "Standard", + 1 : "Landscape", + 2 : "Portrait", + 3 : "Night scene" + }, + SceneType : { + 1 : "Directly photographed" + }, + CustomRendered : { + 0 : "Normal process", + 1 : "Custom process" + }, + WhiteBalance : { + 0 : "Auto white balance", + 1 : "Manual white balance" + }, + GainControl : { + 0 : "None", + 1 : "Low gain up", + 2 : "High gain up", + 3 : "Low gain down", + 4 : "High gain down" + }, + Contrast : { + 0 : "Normal", + 1 : "Soft", + 2 : "Hard" + }, + Saturation : { + 0 : "Normal", + 1 : "Low saturation", + 2 : "High saturation" + }, + Sharpness : { + 0 : "Normal", + 1 : "Soft", + 2 : "Hard" + }, + SubjectDistanceRange : { + 0 : "Unknown", + 1 : "Macro", + 2 : "Close view", + 3 : "Distant view" + }, + FileSource : { + 3 : "DSC" + }, + + Components : { + 0 : "", + 1 : "Y", + 2 : "Cb", + 3 : "Cr", + 4 : "R", + 5 : "G", + 6 : "B" + } + }; + + function addEvent(element, event, handler) { + if (element.addEventListener) { + element.addEventListener(event, handler, false); + } else if (element.attachEvent) { + element.attachEvent("on" + event, handler); + } + } + + function imageHasData(img) { + return !!(img.exifdata); + } + + + function base64ToArrayBuffer(base64, contentType) { + contentType = contentType || base64.match(/^data\:([^\;]+)\;base64,/mi)[1] || ''; // e.g. 'data:image/jpeg;base64,...' => 'image/jpeg' + base64 = base64.replace(/^data\:([^\;]+)\;base64,/gmi, ''); + var binary = atob(base64); + var len = binary.length; + var buffer = new ArrayBuffer(len); + var view = new Uint8Array(buffer); + for (var i = 0; i < len; i++) { + view[i] = binary.charCodeAt(i); + } + return buffer; + } + + function objectURLToBlob(url, callback) { + var http = new XMLHttpRequest(); + http.open("GET", url, true); + http.responseType = "blob"; + http.onload = function(e) { + if (this.status == 200 || this.status === 0) { + callback(this.response); + } + }; + http.send(); + } + + function getImageData(img, callback) { + function handleBinaryFile(binFile) { + var data = findEXIFinJPEG(binFile); + var iptcdata = findIPTCinJPEG(binFile); + img.exifdata = data || {}; + img.iptcdata = iptcdata || {}; + if (callback) { + callback.call(img); + } + } + + if (img.src) { + if (/^data\:/i.test(img.src)) { // Data URI + var arrayBuffer = base64ToArrayBuffer(img.src); + handleBinaryFile(arrayBuffer); + + } else if (/^blob\:/i.test(img.src)) { // Object URL + var fileReader = new FileReader(); + fileReader.onload = function(e) { + handleBinaryFile(e.target.result); + }; + objectURLToBlob(img.src, function (blob) { + fileReader.readAsArrayBuffer(blob); + }); + } else { + var http = new XMLHttpRequest(); + http.onload = function() { + if (this.status == 200 || this.status === 0) { + handleBinaryFile(http.response); + } else { + throw "Could not load image"; + } + http = null; + }; + http.open("GET", img.src, true); + http.responseType = "arraybuffer"; + http.send(null); + } + } else if (window.FileReader && (img instanceof window.Blob || img instanceof window.File)) { + var fileReader = new FileReader(); + fileReader.onload = function(e) { + if (debug) console.log("Got file of length " + e.target.result.byteLength); + handleBinaryFile(e.target.result); + }; + + fileReader.readAsArrayBuffer(img); + } + } + + function findEXIFinJPEG(file) { + var dataView = new DataView(file); + + if (debug) console.log("Got file of length " + file.byteLength); + if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { + if (debug) console.log("Not a valid JPEG"); + return false; // not a valid jpeg + } + + var offset = 2, + length = file.byteLength, + marker; + + while (offset < length) { + if (dataView.getUint8(offset) != 0xFF) { + if (debug) console.log("Not a valid marker at offset " + offset + ", found: " + dataView.getUint8(offset)); + return false; // not a valid marker, something is wrong + } + + marker = dataView.getUint8(offset + 1); + if (debug) console.log(marker); + + // we could implement handling for other markers here, + // but we're only looking for 0xFFE1 for EXIF data + + if (marker == 225) { + if (debug) console.log("Found 0xFFE1 marker"); + + return readEXIFData(dataView, offset + 4, dataView.getUint16(offset + 2) - 2); + + // offset += 2 + file.getShortAt(offset+2, true); + + } else { + offset += 2 + dataView.getUint16(offset+2); + } + + } + + } + + function findIPTCinJPEG(file) { + var dataView = new DataView(file); + + if (debug) console.log("Got file of length " + file.byteLength); + if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { + if (debug) console.log("Not a valid JPEG"); + return false; // not a valid jpeg + } + + var offset = 2, + length = file.byteLength; + + + var isFieldSegmentStart = function(dataView, offset){ + return ( + dataView.getUint8(offset) === 0x38 && + dataView.getUint8(offset+1) === 0x42 && + dataView.getUint8(offset+2) === 0x49 && + dataView.getUint8(offset+3) === 0x4D && + dataView.getUint8(offset+4) === 0x04 && + dataView.getUint8(offset+5) === 0x04 + ); + }; + + while (offset < length) { + + if ( isFieldSegmentStart(dataView, offset )){ + + // Get the length of the name header (which is padded to an even number of bytes) + var nameHeaderLength = dataView.getUint8(offset+7); + if(nameHeaderLength % 2 !== 0) nameHeaderLength += 1; + // Check for pre photoshop 6 format + if(nameHeaderLength === 0) { + // Always 4 + nameHeaderLength = 4; + } + + var startOffset = offset + 8 + nameHeaderLength; + var sectionLength = dataView.getUint16(offset + 6 + nameHeaderLength); + + return readIPTCData(file, startOffset, sectionLength); + + break; + + } + + + // Not the marker, continue searching + offset++; + + } + + } + var IptcFieldMap = { + 0x78 : 'caption', + 0x6E : 'credit', + 0x19 : 'keywords', + 0x37 : 'dateCreated', + 0x50 : 'byline', + 0x55 : 'bylineTitle', + 0x7A : 'captionWriter', + 0x69 : 'headline', + 0x74 : 'copyright', + 0x0F : 'category' + }; + function readIPTCData(file, startOffset, sectionLength){ + var dataView = new DataView(file); + var data = {}; + var fieldValue, fieldName, dataSize, segmentType, segmentSize; + var segmentStartPos = startOffset; + while(segmentStartPos < startOffset+sectionLength) { + if(dataView.getUint8(segmentStartPos) === 0x1C && dataView.getUint8(segmentStartPos+1) === 0x02){ + segmentType = dataView.getUint8(segmentStartPos+2); + if(segmentType in IptcFieldMap) { + dataSize = dataView.getInt16(segmentStartPos+3); + segmentSize = dataSize + 5; + fieldName = IptcFieldMap[segmentType]; + fieldValue = getStringFromDB(dataView, segmentStartPos+5, dataSize); + // Check if we already stored a value with this name + if(data.hasOwnProperty(fieldName)) { + // Value already stored with this name, create multivalue field + if(data[fieldName] instanceof Array) { + data[fieldName].push(fieldValue); + } + else { + data[fieldName] = [data[fieldName], fieldValue]; + } + } + else { + data[fieldName] = fieldValue; + } + } + + } + segmentStartPos++; + } + return data; + } + + + + function readTags(file, tiffStart, dirStart, strings, bigEnd) { + var entries = file.getUint16(dirStart, !bigEnd), + tags = {}, + entryOffset, tag, + i; + + for (i=0;i 4 ? valueOffset : (entryOffset + 8); + vals = []; + for (n=0;n 4 ? valueOffset : (entryOffset + 8); + return getStringFromDB(file, offset, numValues-1); + + case 3: // short, 16 bit int + if (numValues == 1) { + return file.getUint16(entryOffset + 8, !bigEnd); + } else { + offset = numValues > 2 ? valueOffset : (entryOffset + 8); + vals = []; + for (n=0;n', rule, ''].join(''); + div.id = mod; + (body ? div : fakeBody).innerHTML += style; + fakeBody.appendChild(div); + if ( !body ) { + fakeBody.style.background = ''; + fakeBody.style.overflow = 'hidden'; + docOverflow = docElement.style.overflow; + docElement.style.overflow = 'hidden'; + docElement.appendChild(fakeBody); + } + + ret = callback(div, rule); + if ( !body ) { + fakeBody.parentNode.removeChild(fakeBody); + docElement.style.overflow = docOverflow; + } else { + div.parentNode.removeChild(div); + } + + return !!ret; + + }, + + + + isEventSupported = (function() { + + var TAGNAMES = { + 'select': 'input', 'change': 'input', + 'submit': 'form', 'reset': 'form', + 'error': 'img', 'load': 'img', 'abort': 'img' + }; + + function isEventSupported( eventName, element ) { + + element = element || document.createElement(TAGNAMES[eventName] || 'div'); + eventName = 'on' + eventName; + + var isSupported = eventName in element; + + if ( !isSupported ) { + if ( !element.setAttribute ) { + element = document.createElement('div'); + } + if ( element.setAttribute && element.removeAttribute ) { + element.setAttribute(eventName, ''); + isSupported = is(element[eventName], 'function'); + + if ( !is(element[eventName], 'undefined') ) { + element[eventName] = undefined; + } + element.removeAttribute(eventName); + } + } + + element = null; + return isSupported; + } + return isEventSupported; + })(), + + + _hasOwnProperty = ({}).hasOwnProperty, hasOwnProp; + + if ( !is(_hasOwnProperty, 'undefined') && !is(_hasOwnProperty.call, 'undefined') ) { + hasOwnProp = function (object, property) { + return _hasOwnProperty.call(object, property); + }; + } + else { + hasOwnProp = function (object, property) { + return ((property in object) && is(object.constructor.prototype[property], 'undefined')); + }; + } + + + if (!Function.prototype.bind) { + Function.prototype.bind = function bind(that) { + + var target = this; + + if (typeof target != "function") { + throw new TypeError(); + } + + var args = slice.call(arguments, 1), + bound = function () { + + if (this instanceof bound) { + + var F = function(){}; + F.prototype = target.prototype; + var self = new F(); + + var result = target.apply( + self, + args.concat(slice.call(arguments)) + ); + if (Object(result) === result) { + return result; + } + return self; + + } else { + + return target.apply( + that, + args.concat(slice.call(arguments)) + ); + + } + + }; + + return bound; + }; + } + + function setCss( str ) { + mStyle.cssText = str; + } + + function setCssAll( str1, str2 ) { + return setCss(prefixes.join(str1 + ';') + ( str2 || '' )); + } + + function is( obj, type ) { + return typeof obj === type; + } + + function contains( str, substr ) { + return !!~('' + str).indexOf(substr); + } + + function testProps( props, prefixed ) { + for ( var i in props ) { + var prop = props[i]; + if ( !contains(prop, "-") && mStyle[prop] !== undefined ) { + return prefixed == 'pfx' ? prop : true; + } + } + return false; + } + + function testDOMProps( props, obj, elem ) { + for ( var i in props ) { + var item = obj[props[i]]; + if ( item !== undefined) { + + if (elem === false) return props[i]; + + if (is(item, 'function')){ + return item.bind(elem || obj); + } + + return item; + } + } + return false; + } + + function testPropsAll( prop, prefixed, elem ) { + + var ucProp = prop.charAt(0).toUpperCase() + prop.slice(1), + props = (prop + ' ' + cssomPrefixes.join(ucProp + ' ') + ucProp).split(' '); + + if(is(prefixed, "string") || is(prefixed, "undefined")) { + return testProps(props, prefixed); + + } else { + props = (prop + ' ' + (domPrefixes).join(ucProp + ' ') + ucProp).split(' '); + return testDOMProps(props, prefixed, elem); + } + } tests['flexbox'] = function() { + return testPropsAll('flexWrap'); + }; tests['canvas'] = function() { + var elem = document.createElement('canvas'); + return !!(elem.getContext && elem.getContext('2d')); + }; + + tests['canvastext'] = function() { + return !!(Modernizr['canvas'] && is(document.createElement('canvas').getContext('2d').fillText, 'function')); + }; + + + + tests['webgl'] = function() { + return !!window.WebGLRenderingContext; + }; + + + tests['touch'] = function() { + var bool; + + if(('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) { + bool = true; + } else { + injectElementWithStyles(['@media (',prefixes.join('touch-enabled),('),mod,')','{#modernizr{top:9px;position:absolute}}'].join(''), function( node ) { + bool = node.offsetTop === 9; + }); + } + + return bool; + }; + + + + tests['geolocation'] = function() { + return 'geolocation' in navigator; + }; + + + tests['postmessage'] = function() { + return !!window.postMessage; + }; + + + tests['websqldatabase'] = function() { + return !!window.openDatabase; + }; + + tests['indexedDB'] = function() { + return !!testPropsAll("indexedDB", window); + }; + + tests['hashchange'] = function() { + return isEventSupported('hashchange', window) && (document.documentMode === undefined || document.documentMode > 7); + }; + + tests['history'] = function() { + return !!(window.history && history.pushState); + }; + + tests['draganddrop'] = function() { + var div = document.createElement('div'); + return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div); + }; + + tests['websockets'] = function() { + return 'WebSocket' in window || 'MozWebSocket' in window; + }; + + + tests['rgba'] = function() { + setCss('background-color:rgba(150,255,150,.5)'); + + return contains(mStyle.backgroundColor, 'rgba'); + }; + + tests['hsla'] = function() { + setCss('background-color:hsla(120,40%,100%,.5)'); + + return contains(mStyle.backgroundColor, 'rgba') || contains(mStyle.backgroundColor, 'hsla'); + }; + + tests['multiplebgs'] = function() { + setCss('background:url(https://),url(https://),red url(https://)'); + + return (/(url\s*\(.*?){3}/).test(mStyle.background); + }; tests['backgroundsize'] = function() { + return testPropsAll('backgroundSize'); + }; + + tests['borderimage'] = function() { + return testPropsAll('borderImage'); + }; + + + + tests['borderradius'] = function() { + return testPropsAll('borderRadius'); + }; + + tests['boxshadow'] = function() { + return testPropsAll('boxShadow'); + }; + + tests['textshadow'] = function() { + return document.createElement('div').style.textShadow === ''; + }; + + + tests['opacity'] = function() { + setCssAll('opacity:.55'); + + return (/^0.55$/).test(mStyle.opacity); + }; + + + tests['cssanimations'] = function() { + return testPropsAll('animationName'); + }; + + + tests['csscolumns'] = function() { + return testPropsAll('columnCount'); + }; + + + tests['cssgradients'] = function() { + var str1 = 'background-image:', + str2 = 'gradient(linear,left top,right bottom,from(#9f9),to(white));', + str3 = 'linear-gradient(left top,#9f9, white);'; + + setCss( + (str1 + '-webkit- '.split(' ').join(str2 + str1) + + prefixes.join(str3 + str1)).slice(0, -str1.length) + ); + + return contains(mStyle.backgroundImage, 'gradient'); + }; + + + tests['cssreflections'] = function() { + return testPropsAll('boxReflect'); + }; + + + tests['csstransforms'] = function() { + return !!testPropsAll('transform'); + }; + + + tests['csstransforms3d'] = function() { + + var ret = !!testPropsAll('perspective'); + + if ( ret && 'webkitPerspective' in docElement.style ) { + + injectElementWithStyles('@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}', function( node, rule ) { + ret = node.offsetLeft === 9 && node.offsetHeight === 3; + }); + } + return ret; + }; + + + tests['csstransitions'] = function() { + return testPropsAll('transition'); + }; + + + + tests['fontface'] = function() { + var bool; + + injectElementWithStyles('@font-face {font-family:"font";src:url("https://")}', function( node, rule ) { + var style = document.getElementById('smodernizr'), + sheet = style.sheet || style.styleSheet, + cssText = sheet ? (sheet.cssRules && sheet.cssRules[0] ? sheet.cssRules[0].cssText : sheet.cssText || '') : ''; + + bool = /src/i.test(cssText) && cssText.indexOf(rule.split(' ')[0]) === 0; + }); + + return bool; + }; + + tests['generatedcontent'] = function() { + var bool; + + injectElementWithStyles(['#',mod,'{font:0/0 a}#',mod,':after{content:"',smile,'";visibility:hidden;font:3px/1 a}'].join(''), function( node ) { + bool = node.offsetHeight >= 3; + }); + + return bool; + }; + tests['video'] = function() { + var elem = document.createElement('video'), + bool = false; + + try { + if ( bool = !!elem.canPlayType ) { + bool = new Boolean(bool); + bool.ogg = elem.canPlayType('video/ogg; codecs="theora"') .replace(/^no$/,''); + + bool.h264 = elem.canPlayType('video/mp4; codecs="avc1.42E01E"') .replace(/^no$/,''); + + bool.webm = elem.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,''); + } + + } catch(e) { } + + return bool; + }; + + tests['audio'] = function() { + var elem = document.createElement('audio'), + bool = false; + + try { + if ( bool = !!elem.canPlayType ) { + bool = new Boolean(bool); + bool.ogg = elem.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,''); + bool.mp3 = elem.canPlayType('audio/mpeg;') .replace(/^no$/,''); + + bool.wav = elem.canPlayType('audio/wav; codecs="1"') .replace(/^no$/,''); + bool.m4a = ( elem.canPlayType('audio/x-m4a;') || + elem.canPlayType('audio/aac;')) .replace(/^no$/,''); + } + } catch(e) { } + + return bool; + }; + + + tests['localstorage'] = function() { + try { + localStorage.setItem(mod, mod); + localStorage.removeItem(mod); + return true; + } catch(e) { + return false; + } + }; + + tests['sessionstorage'] = function() { + try { + sessionStorage.setItem(mod, mod); + sessionStorage.removeItem(mod); + return true; + } catch(e) { + return false; + } + }; + + + tests['webworkers'] = function() { + return !!window.Worker; + }; + + + tests['applicationcache'] = function() { + return !!window.applicationCache; + }; + + + tests['svg'] = function() { + return !!document.createElementNS && !!document.createElementNS(ns.svg, 'svg').createSVGRect; + }; + + tests['inlinesvg'] = function() { + var div = document.createElement('div'); + div.innerHTML = ''; + return (div.firstChild && div.firstChild.namespaceURI) == ns.svg; + }; + + tests['smil'] = function() { + return !!document.createElementNS && /SVGAnimate/.test(toString.call(document.createElementNS(ns.svg, 'animate'))); + }; + + + tests['svgclippaths'] = function() { + return !!document.createElementNS && /SVGClipPath/.test(toString.call(document.createElementNS(ns.svg, 'clipPath'))); + }; + + function webforms() { + Modernizr['input'] = (function( props ) { + for ( var i = 0, len = props.length; i < len; i++ ) { + attrs[ props[i] ] = !!(props[i] in inputElem); + } + if (attrs.list){ + attrs.list = !!(document.createElement('datalist') && window.HTMLDataListElement); + } + return attrs; + })('autocomplete autofocus list placeholder max min multiple pattern required step'.split(' ')); + Modernizr['inputtypes'] = (function(props) { + + for ( var i = 0, bool, inputElemType, defaultView, len = props.length; i < len; i++ ) { + + inputElem.setAttribute('type', inputElemType = props[i]); + bool = inputElem.type !== 'text'; + + if ( bool ) { + + inputElem.value = smile; + inputElem.style.cssText = 'position:absolute;visibility:hidden;'; + + if ( /^range$/.test(inputElemType) && inputElem.style.WebkitAppearance !== undefined ) { + + docElement.appendChild(inputElem); + defaultView = document.defaultView; + + bool = defaultView.getComputedStyle && + defaultView.getComputedStyle(inputElem, null).WebkitAppearance !== 'textfield' && + (inputElem.offsetHeight !== 0); + + docElement.removeChild(inputElem); + + } else if ( /^(search|tel)$/.test(inputElemType) ){ + } else if ( /^(url|email)$/.test(inputElemType) ) { + bool = inputElem.checkValidity && inputElem.checkValidity() === false; + + } else { + bool = inputElem.value != smile; + } + } + + inputs[ props[i] ] = !!bool; + } + return inputs; + })('search tel url email datetime date month week time datetime-local number range color'.split(' ')); + } + for ( var feature in tests ) { + if ( hasOwnProp(tests, feature) ) { + featureName = feature.toLowerCase(); + Modernizr[featureName] = tests[feature](); + + classes.push((Modernizr[featureName] ? '' : 'no-') + featureName); + } + } + + Modernizr.input || webforms(); + + + Modernizr.addTest = function ( feature, test ) { + if ( typeof feature == 'object' ) { + for ( var key in feature ) { + if ( hasOwnProp( feature, key ) ) { + Modernizr.addTest( key, feature[ key ] ); + } + } + } else { + + feature = feature.toLowerCase(); + + if ( Modernizr[feature] !== undefined ) { + return Modernizr; + } + + test = typeof test == 'function' ? test() : test; + + if (typeof enableClasses !== "undefined" && enableClasses) { + docElement.className += ' ' + (test ? '' : 'no-') + feature; + } + Modernizr[feature] = test; + + } + + return Modernizr; + }; + + + setCss(''); + modElem = inputElem = null; + + ;(function(window, document) { + var version = '3.7.0'; + + var options = window.html5 || {}; + + var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i; + + var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i; + + var supportsHtml5Styles; + + var expando = '_html5shiv'; + + var expanID = 0; + + var expandoData = {}; + + var supportsUnknownElements; + + (function() { + try { + var a = document.createElement('a'); + a.innerHTML = ''; + supportsHtml5Styles = ('hidden' in a); + + supportsUnknownElements = a.childNodes.length == 1 || (function() { + (document.createElement)('a'); + var frag = document.createDocumentFragment(); + return ( + typeof frag.cloneNode == 'undefined' || + typeof frag.createDocumentFragment == 'undefined' || + typeof frag.createElement == 'undefined' + ); + }()); + } catch(e) { + supportsHtml5Styles = true; + supportsUnknownElements = true; + } + + }()); + + function addStyleSheet(ownerDocument, cssText) { + var p = ownerDocument.createElement('p'), + parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement; + + p.innerHTML = 'x'; + return parent.insertBefore(p.lastChild, parent.firstChild); + } + + function getElements() { + var elements = html5.elements; + return typeof elements == 'string' ? elements.split(' ') : elements; + } + + function getExpandoData(ownerDocument) { + var data = expandoData[ownerDocument[expando]]; + if (!data) { + data = {}; + expanID++; + ownerDocument[expando] = expanID; + expandoData[expanID] = data; + } + return data; + } + + function createElement(nodeName, ownerDocument, data){ + if (!ownerDocument) { + ownerDocument = document; + } + if(supportsUnknownElements){ + return ownerDocument.createElement(nodeName); + } + if (!data) { + data = getExpandoData(ownerDocument); + } + var node; + + if (data.cache[nodeName]) { + node = data.cache[nodeName].cloneNode(); + } else if (saveClones.test(nodeName)) { + node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode(); + } else { + node = data.createElem(nodeName); + } + + return node.canHaveChildren && !reSkip.test(nodeName) && !node.tagUrn ? data.frag.appendChild(node) : node; + } + + function createDocumentFragment(ownerDocument, data){ + if (!ownerDocument) { + ownerDocument = document; + } + if(supportsUnknownElements){ + return ownerDocument.createDocumentFragment(); + } + data = data || getExpandoData(ownerDocument); + var clone = data.frag.cloneNode(), + i = 0, + elems = getElements(), + l = elems.length; + for(;i if big - cssClass = ' big' + cssClass += ' big' if placement placement = "data-placement=\"#{placement}\"" if @image is 'none' - width = 300 - height = 226 - size = if big then 50 else 40 - - rng = new Math.seedrandom(@id) - x = rng() * (width - size) - y = rng() * (height - size) - - "#{ @initials() }" + return @uniqueAvatar(big, placement, cssClass) else "" + uniqueAvatar: (big = false, placement = '', cssClass = '') -> + if big + cssClass += ' big' + + width = 300 + height = 226 + size = if big then 50 else 40 + + rng = new Math.seedrandom(@id) + x = rng() * (width - size) + y = rng() * (height - size) + + "#{ @initials() }" + @_fillUp: (data) -> # set socal media links diff --git a/app/assets/javascripts/app/views/generic/attachment.jst.eco b/app/assets/javascripts/app/views/generic/attachment.jst.eco index 4b7970b86..579496696 100644 --- a/app/assets/javascripts/app/views/generic/attachment.jst.eco +++ b/app/assets/javascripts/app/views/generic/attachment.jst.eco @@ -1,9 +1,9 @@
- + Dateien wählen.. - +
diff --git a/app/assets/javascripts/app/views/modal.jst.eco b/app/assets/javascripts/app/views/modal.jst.eco index 25d9088ce..ca88a0c35 100644 --- a/app/assets/javascripts/app/views/modal.jst.eco +++ b/app/assets/javascripts/app/views/modal.jst.eco @@ -14,13 +14,19 @@

<%= @message %>

<% end %> <% if @detail: %> -
<%= @detail %>

+
<%= @detail %>
+ <% end %> + <% if @content: %> + <%- @content %> <% end %>