Commit 6b27b9f9 authored by James Lopez's avatar James Lopez

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into fix/project-wiki-ending

parents c401e833 f838dbe4
......@@ -24,7 +24,12 @@ before_script:
- bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"
- RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate
stages:
- test
- notifications
spec:feature:
stage: test
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
......@@ -33,6 +38,7 @@ spec:feature:
- mysql
spec:api:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
tags:
......@@ -40,6 +46,7 @@ spec:api:
- mysql
spec:models:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
tags:
......@@ -47,6 +54,7 @@ spec:models:
- mysql
spec:lib:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
tags:
......@@ -54,6 +62,7 @@ spec:lib:
- mysql
spec:services:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
tags:
......@@ -61,6 +70,7 @@ spec:services:
- mysql
spec:benchmark:
stage: test
script:
- RAILS_ENV=test bundle exec rake spec:benchmark
tags:
......@@ -69,6 +79,7 @@ spec:benchmark:
allow_failure: true
spec:other:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
tags:
......@@ -76,6 +87,7 @@ spec:other:
- mysql
spinach:project:half:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half
tags:
......@@ -83,6 +95,7 @@ spinach:project:half:
- mysql
spinach:project:rest:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest
tags:
......@@ -90,6 +103,7 @@ spinach:project:rest:
- mysql
spinach:other:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other
tags:
......@@ -97,6 +111,7 @@ spinach:other:
- mysql
teaspoon:
stage: test
script:
- RAILS_ENV=test bundle exec teaspoon
tags:
......@@ -104,6 +119,7 @@ teaspoon:
- mysql
rubocop:
stage: test
script:
- bundle exec rubocop
tags:
......@@ -111,6 +127,7 @@ rubocop:
- mysql
brakeman:
stage: test
script:
- bundle exec rake brakeman
tags:
......@@ -118,6 +135,7 @@ brakeman:
- mysql
flog:
stage: test
script:
- bundle exec rake flog
tags:
......@@ -125,6 +143,7 @@ flog:
- mysql
flay:
stage: test
script:
- bundle exec rake flay
tags:
......@@ -132,6 +151,7 @@ flay:
- mysql
bundler:audit:
stage: test
script:
- "bundle exec bundle-audit update"
- "bundle exec bundle-audit check"
......@@ -143,6 +163,7 @@ bundler:audit:
# Ruby 2.2 jobs
spec:feature:ruby22:
stage: test
image: ruby:2.2
only:
- master
......@@ -158,6 +179,7 @@ spec:feature:ruby22:
- mysql
spec:api:ruby22:
stage: test
image: ruby:2.2
only:
- master
......@@ -172,6 +194,7 @@ spec:api:ruby22:
- mysql
spec:models:ruby22:
stage: test
image: ruby:2.2
only:
- master
......@@ -186,6 +209,7 @@ spec:models:ruby22:
- mysql
spec:lib:ruby22:
stage: test
image: ruby:2.2
only:
- master
......@@ -200,6 +224,7 @@ spec:lib:ruby22:
- mysql
spec:services:ruby22:
stage: test
image: ruby:2.2
only:
- master
......@@ -214,6 +239,7 @@ spec:services:ruby22:
- mysql
spec:benchmark:ruby22:
stage: test
image: ruby:2.2
only:
- master
......@@ -229,6 +255,7 @@ spec:benchmark:ruby22:
allow_failure: true
spec:other:ruby22:
stage: test
image: ruby:2.2
only:
- master
......@@ -243,6 +270,7 @@ spec:other:ruby22:
- mysql
spinach:project:half:ruby22:
stage: test
image: ruby:2.2
only:
- master
......@@ -257,6 +285,7 @@ spinach:project:half:ruby22:
- mysql
spinach:project:rest:ruby22:
stage: test
image: ruby:2.2
only:
- master
......@@ -271,6 +300,7 @@ spinach:project:rest:ruby22:
- mysql
spinach:other:ruby22:
stage: test
image: ruby:2.2
only:
- master
......@@ -284,3 +314,14 @@ spinach:other:ruby22:
- ruby
- mysql
notify:slack:
stage: notifications
script:
- ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Check <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>"
when: on_failure
only:
- master@gitlab-org/gitlab-ce
- tags@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- tags@gitlab-org/gitlab-ee
\ No newline at end of file
......@@ -3,6 +3,13 @@ Please view this file on the master branch, on stable branches it's out of date.
v 8.6.0 (unreleased)
- Improve the formatting for the user page bio (Connor Shea)
- Fix issue when pushing to projects ending in .wiki
- Fix avatar stretching by providing a cropping feature (Johann Pardanaud)
- Strip leading and trailing spaces in URL validator (evuez)
- Update documentation to reflect Guest role not being enforced on internal projects
v 8.5.2
- Fix sidebar overlapping content when screen width was below 1200px
- Fix error 500 when commenting on a commit
v 8.5.1
- Fix group projects styles
......@@ -21,6 +28,7 @@ v 8.5.1
- Changed padding & background color for highlighted notes
- Re-add the newrelic_rpm gem which was removed without any deprecation or warning (Stan Hu)
- Update sentry-raven gem to 0.15.6
- Add build coverage in project's builds page (Steffen Köhler)
v 8.5.0
- Fix duplicate "me" in tooltip of the "thumbsup" awards Emoji (Stan Hu)
......@@ -101,6 +109,9 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos
v 8.4.5
- No CE-specific changes
v 8.4.4
- Update omniauth-saml gem to 1.4.2
- Prevent long-running backup tasks from timing out the database connection
......
......@@ -77,6 +77,9 @@ gem "haml-rails", '~> 0.9.0'
# Files attachments
gem "carrierwave", '~> 0.9.0'
# Image editing
gem "mini_magick", '~> 4.4.0'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
......
......@@ -468,6 +468,7 @@ GEM
method_source (0.8.2)
mime-types (1.25.1)
mimemagic (0.3.0)
mini_magick (4.4.0)
mini_portile2 (2.0.0)
minitest (5.7.0)
mousetrap-rails (1.4.6)
......@@ -955,13 +956,14 @@ DEPENDENCIES
loofah (~> 2.0.3)
mail_room (~> 0.6.1)
method_source (~> 0.8)
mini_magick (~> 4.4.0)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.3.16)
nested_form (~> 0.3.2)
net-ssh (~> 3.0.1)
nokogiri (~> 1.6.7, >= 1.6.7.2)
newrelic_rpm (~> 3.14)
nokogiri (~> 1.6.7, >= 1.6.7.2)
nprogress-rails (~> 0.1.6.7)
oauth2 (~> 1.0.0)
octokit (~> 3.8.0)
......
......@@ -44,6 +44,7 @@
#= require jquery.nicescroll
#= require_tree .
#= require fuzzaldrin-plus
#= require cropper.js
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
......@@ -210,7 +211,7 @@ $ ->
$this = $(this)
$this.attr 'value', $this.val()
return
$(document)
.off 'keyup', 'input[type="search"]'
.on 'keyup', 'input[type="search"]' , (e) ->
......@@ -253,7 +254,7 @@ $ ->
$('.page-with-sidebar')
.removeClass('right-sidebar-collapsed')
.addClass('right-sidebar-expanded')
$.cookie("collapsed_gutter",
$.cookie("collapsed_gutter",
$('.right-sidebar')
.hasClass('right-sidebar-collapsed'), { path: '/' })
......
......@@ -16,11 +16,11 @@ class @Autosave
try
text = window.localStorage.getItem @key
catch
catch e
return
@field.val text if text?.length > 0
@field.trigger "input"
@field.trigger "input"
save: ->
return unless window.localStorage?
......@@ -35,5 +35,5 @@ class @Autosave
reset: ->
return unless window.localStorage?
try
try
window.localStorage.removeItem @key
......@@ -70,6 +70,7 @@ class @MergeRequestTabs
@loadCommits($target.attr('href'))
else if action == 'diffs'
@loadDiff($target.attr('href'))
@shrinkView()
else if action == 'builds'
@loadBuilds($target.attr('href'))
......@@ -185,3 +186,14 @@ class @MergeRequestTabs
expandViewContainer: ->
$('.container-fluid').removeClass('container-limited')
shrinkView: ->
$gutterIcon = $('.gutter-toggle i')
# Wait until listeners are set
setTimeout( ->
# Only when sidebar is collapsed
if $gutterIcon.is('.fa-angle-double-right')
$gutterIcon.closest('a').trigger('click')
, 0)
......@@ -16,11 +16,50 @@ class @Profile
$('.update-notifications').on 'ajax:complete', ->
$(this).find('.btn-save').enable()
$('.js-choose-user-avatar-button').bind "click", ->
form = $(this).closest("form")
form.find(".js-user-avatar-input").click()
# Avatar management
$avatarInput = $('.js-user-avatar-input')
$filename = $('.js-avatar-filename')
$modalCrop = $('.modal-profile-crop')
$modalCropImg = $('.modal-profile-crop-image')
$('.js-choose-user-avatar-button').on "click", ->
$form = $(this).closest("form")
$form.find(".js-user-avatar-input").click()
$modalCrop.on 'shown.bs.modal', ->
setTimeout ( -> # The cropper must be asynchronously initialized
$modalCropImg.cropper
aspectRatio: 1
modal: false
scalable: false
rotatable: false
zoomable: false
crop: (event) ->
['x', 'y'].forEach (key) ->
$("#user_avatar_crop_#{key}").val(Math.floor(event[key]))
$("#user_avatar_crop_size").val(Math.floor(event.width))
), 0
$modalCrop.on 'hidden.bs.modal', ->
$modalCropImg.attr('src', '').cropper('destroy')
$avatarInput.val('')
$filename.text($filename.data('label'))
$('.js-user-avatar-input').bind "change", ->
$('.js-upload-user-avatar').on 'click', ->
$('.edit_user').submit()
$avatarInput.on "change", ->
form = $(this).closest("form")
filename = $(this).val().replace(/^.*[\\\/]/, '')
form.find(".js-avatar-filename").text(filename)
$filename.data('label', $filename.text()).text(filename)
reader = new FileReader
reader.onload = (event) ->
$modalCrop.modal('show')
$modalCropImg.attr('src', event.target.result)
fileData = reader.readAsDataURL(this.files[0])
......@@ -8,4 +8,10 @@ $(document).on("click", '.toggle-nav-collapse', (e) ->
$('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
$('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
setTimeout ( ->
niceScrollBars = $('.nicescroll').niceScroll();
niceScrollBars.updateScrollBar();
), 300
)
......@@ -9,6 +9,7 @@
*= require_self
*= require dropzone/basic
*= require cal-heatmap
*= require cropper.css
*/
/*
......
......@@ -142,9 +142,13 @@ header {
}
@media (max-width: $screen-md-max) {
.header-collapsed, .header-expanded {
@include collapsed-header;
.header-collapsed {
margin-left: $sidebar_collapsed_width;
}
.header-expanded {
margin-left: $sidebar_width;
}
}
@media(min-width: $screen-md-max) {
......
......@@ -110,7 +110,20 @@ ul.content-list {
> li {
border-color: $table-border-color;
color: $gl-gray;
color: $list-text-color;
font-size: $list-font-size;
.title {
color: $list-title-color;
font-weight: 600;
}
.description {
p {
@include str-truncated;
margin-bottom: 0;
}
}
.avatar {
margin-right: 15px;
......@@ -127,13 +140,6 @@ ul.content-list {
}
}
.panel > .content-list {
li {
margin: 0;
padding: $gl-padding;
}
}
ul.controls {
padding-top: 1px;
float: right;
......
......@@ -41,6 +41,12 @@
transition: $transition;
}
@mixin transform($transform) {
-webkit-transform: $transform;
-ms-transform: $transform;
transform: $transform;
}
/**
* Prefilled mixins
* Mixins with fixed values
......
......@@ -12,6 +12,10 @@
height: 100%;
transition-duration: .3s;
}
&.right-sidebar-expanded {
padding-right: $gutter_width;
}
}
.sidebar-wrapper {
......@@ -45,19 +49,6 @@
overflow: hidden;
transition-duration: .3s;
.home {
z-index: 1;
position: absolute;
left: 0px;
}
#logo {
z-index: 2;
position: absolute;
width: 58px;
cursor: pointer;
}
a {
float: left;
height: $header-height;
......@@ -83,7 +74,7 @@
width: 158px;
float: left;
margin: 0;
margin-left: 50px;
margin-left: 14px;
font-size: 19px;
line-height: 41px;
font-weight: normal;
......@@ -194,6 +185,10 @@
@mixin expanded-sidebar {
padding-left: $sidebar_width;
&.right-sidebar-collapsed {
padding-right: $sidebar_collapsed_width;
}
.sidebar-wrapper {
width: $sidebar_width;
......@@ -213,17 +208,13 @@
}
}
@mixin expanded-gutter {
padding-right: $gutter_width;
}
@mixin collapsed-gutter {
padding-right: $sidebar_collapsed_width;
}
@mixin collapsed-sidebar {
padding-left: $sidebar_collapsed_width;
&.right-sidebar-collapsed {
padding-right: $sidebar_collapsed_width;
}
.sidebar-wrapper {
width: $sidebar_collapsed_width;
......@@ -287,47 +278,10 @@
background: #f2f6f7;
}
// page is small enough
@media (max-width: $screen-md-max) {
.page-sidebar-collapsed {
@include collapsed-sidebar;
}
.page-sidebar-expanded {
@include collapsed-sidebar;
}
.page-gutter {
&.right-sidebar-collapsed {
@include collapsed-gutter;
}
&.right-sidebar-expanded {
@include expanded-gutter;
}
}
.collapse-nav {
display: none;
}
.page-sidebar-collapsed {
@include collapsed-sidebar;
}
// page is large enough
@media(min-width: $screen-md-max) {
.page-gutter {
&.right-sidebar-collapsed {
@include collapsed-gutter;
}
&.right-sidebar-expanded {
@include expanded-gutter;
}
}
.page-sidebar-collapsed {
@include collapsed-sidebar;
}
.page-sidebar-expanded {
@include expanded-sidebar;
}
}
\ No newline at end of file
.page-sidebar-expanded {
@include expanded-sidebar;
}
......@@ -32,6 +32,8 @@ $gl-avatar-size: 40px;
$secondary-text: #7f8fa4;
$error-exclamation-point: #E62958;
$border-radius-default: 3px;
$list-title-color: #333333;
$list-text-color: #555555;
/*
* Color schema
......
......@@ -203,14 +203,7 @@
overflow: hidden;
}
.issuable-count,
.issuable-nav,
.assignee > *,
.milestone > *,
.labels > *,
.participants > *,
.light > *,
.project-reference > * {
.hide-collapsed {
display: none;
}
......
......@@ -4,13 +4,7 @@
position: relative;
.issue-title {
margin-bottom: 5px;
font-size: $list-font-size;
font-weight: 600;
}
.issue-info {
color: $gl-gray;
margin-bottom: 2px;
}
.issue-check {
......
......@@ -148,15 +148,8 @@
position: relative;
.merge-request-title {
margin-bottom: 5px;
font-size: $list-font-size;
font-weight: 600;
}
.merge-request-info {
color: $gl-gray;
margin-bottom: 2px;
}
}
.merge-request-labels {
......
......@@ -78,3 +78,39 @@
max-width: 750px;
margin: auto;
}
.modal-profile-crop {
.modal-dialog {
width: 500px;
}
.modal-body {
p {
display: table;
margin: auto;
overflow: hidden;
}
img {
display: block;
max-width: 400px;
max-height: 400px;
}
.cropper-bg {
background: none;
}
.cropper-crop-box {
box-sizing: content-box;
border: 999px solid transparentize(#ccc, 0.5);
@include transform(translate(-999px, -999px));
}
}
}
@media (max-width: 520px) {
.modal-profile-crop .modal-dialog {
width: auto;
}
}
......@@ -397,15 +397,10 @@ pre.light-well {
.project-full-name {
@include str-truncated;
font-weight: 600;
color: #4c4e54;
}
.project-controls {
float: right;
color: $gl-gray;
.controls {
line-height: 40px;
color: #7f8fa4;
a:hover {
text-decoration: none;
......@@ -415,16 +410,6 @@ pre.light-well {
margin-left: 10px;
}
}
.project-description {
color: #7f8fa4;
p {
@include str-truncated;
margin-bottom: 0;
color: #7f8fa4;
}
}
}
.bottom {
......
......@@ -2,30 +2,6 @@
padding: 2px;
}
.snippet-row {
.snippet-title {
font-size: 15px;
font-weight: bold;
line-height: 20px;
margin-bottom: 2px;
.monospace {
font-weight: normal;
}
}
.snippet-info {
color: #888;
font-size: 13px;
line-height: 24px;
a {
color: #888;
}
}
}
.snippet-holder {
margin-bottom: -$gl-padding;
......
......@@ -3,4 +3,15 @@
margin: 35px 0 20px;
font-weight: bold;
}
.example {
&:before {
content: "Example";
color: #BBB;
}
padding: 15px;
border: 1px dashed #ddd;
margin-bottom: 15px;
}
}
......@@ -65,6 +65,9 @@ class ProfilesController < Profiles::ApplicationController
def user_params
params.require(:user).permit(
:avatar_crop_x,
:avatar_crop_y,
:avatar_crop_size,
:avatar,
:bio,
:email,
......
......@@ -6,6 +6,10 @@ module AuthHelper
Gitlab.config.ldap.enabled
end
def omniauth_enabled?
Gitlab.config.omniauth.enabled
end
def provider_has_icon?(name)
PROVIDERS_WITH_ICONS.include?(name.to_s)
end
......
......@@ -801,10 +801,7 @@ class Project < ActiveRecord::Base
end
def change_head(branch)
# Cached divergent commit counts are based on repository head
repository.expire_branch_cache
repository.expire_root_ref_cache
repository.before_change_head
gitlab_shell.update_repository_head(self.path_with_namespace, branch)
reload_default_branch
end
......
......@@ -245,15 +245,6 @@ class Repository
expire_emptiness_caches if empty?
end
# Expires _all_ caches, including those that would normally only be expired
# under specific conditions.
def expire_all_caches!
expire_cache
expire_root_ref_cache
expire_emptiness_caches
expire_has_visible_content_cache
end
def expire_branch_cache(branch_name = nil)
# When we push to the root branch we have to flush the cache for all other
# branches as their statistics are based on the commits relative to the
......@@ -307,6 +298,46 @@ class Repository
cache.expire(:branch_names)
end
# Runs code just before a repository is deleted.
def before_delete
expire_cache if exists?
expire_root_ref_cache
expire_emptiness_caches
end
# Runs code just before the HEAD of a repository is changed.
def before_change_head
# Cached divergent commit counts are based on repository head
expire_branch_cache
expire_root_ref_cache
end
# Runs code before creating a new tag.
def before_create_tag
expire_cache
end
# Runs code after a repository has been forked/imported.
def after_import
expire_emptiness_caches
end
# Runs code after a new commit has been pushed.
def after_push_commit(branch_name)
expire_cache(branch_name)
end
# Runs code after a new branch has been created.
def after_create_branch
expire_has_visible_content_cache
end
# Runs code after an existing branch has been removed.
def after_remove_branch
expire_has_visible_content_cache
end
def method_missing(m, *args, &block)
if m == :lookup && !block_given?
lookup_cache[m] ||= {}
......
......@@ -98,6 +98,9 @@ class User < ActiveRecord::Base
# Virtual attribute for authenticating by either username or email
attr_accessor :login
# Virtual attributes to define avatar cropping
attr_accessor :avatar_crop_x, :avatar_crop_y, :avatar_crop_size
#
# Relations
#
......@@ -163,6 +166,11 @@ class User < ActiveRecord::Base
validate :owns_public_email, if: ->(user) { user.public_email_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
validates :avatar_crop_x, :avatar_crop_y, :avatar_crop_size,
numericality: { only_integer: true },
presence: true,
if: ->(user) { user.avatar? }
before_validation :generate_password, on: :create
before_validation :restricted_signup_domains, on: :create
before_validation :sanitize_attrs
......
......@@ -16,13 +16,13 @@ class GitPushService < BaseService
# 5. Executes the project's services
#
def execute
@project.repository.expire_cache(branch_name)
@project.repository.after_push_commit(branch_name)
if push_remove_branch?
@project.repository.expire_has_visible_content_cache
@project.repository.after_remove_branch
@push_commits = []
elsif push_to_new_branch?
@project.repository.expire_has_visible_content_cache
@project.repository.after_create_branch
# Re-find the pushed commits.
if is_default_branch?
......
......@@ -2,7 +2,7 @@ class GitTagPushService
attr_accessor :project, :user, :push_data
def execute(project, user, oldrev, newrev, ref)
project.repository.expire_cache
project.repository.before_create_tag
@project, @user = project, user
@push_data = build_push_data(oldrev, newrev, ref)
......
......@@ -13,6 +13,5 @@ module Notes
note
end
end
end
......@@ -76,11 +76,9 @@ module Projects
end
def flush_caches(project, wiki_path)
project.repository.expire_all_caches! if project.repository.exists?
project.repository.before_delete
wiki_repo = Repository.new(wiki_path, project)
wiki_repo.expire_all_caches! if wiki_repo.exists?
Repository.new(wiki_path, project).before_delete
end
end
end
......@@ -130,8 +130,8 @@ class TodoService
end
def handle_note(note, author)
# Skip system notes, like status changes and cross-references
return if note.system
# Skip system notes, notes on commit, and notes on project snippet
return if note.system? || ['Commit', 'Snippet'].include?(note.noteable_type)
project = note.project
target = note.noteable
......
......@@ -2,11 +2,22 @@
class AvatarUploader < CarrierWave::Uploader::Base
include UploaderHelper
include CarrierWave::MiniMagick
storage :file
after :store, :reset_events_cache
process :cropper
def cropper
return unless model.respond_to?(:avatar_crop_size) && model.valid?
manipulate! do |img|
img.crop "#{model.avatar_crop_size}x#{model.avatar_crop_size}+#{model.avatar_crop_x}+#{model.avatar_crop_y}"
end
end
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
......
......@@ -29,8 +29,11 @@ class UrlValidator < ActiveModel::EachValidator
end
def valid_url?(value)
return false if value.nil?
options = default_options.merge(self.options)
value.strip!
value =~ /\A#{URI.regexp(options[:protocols])}\z/
end
end
......@@ -4,7 +4,7 @@
= render 'devise/shared/signin_box'
-# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box
- if Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?
- if omniauth_enabled? && devise_mapping.omniauthable?
.clearfix.prepend-top-20
= render 'devise/shared/omniauth_box'
......@@ -14,6 +14,6 @@
= render 'devise/shared/signup_box'
-# Show a message if none of the mechanisms above are enabled
- if !signin_enabled? && !ldap_enabled? && !(Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?)
- if !signin_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
%div
No authentication methods configured.
......@@ -31,64 +31,91 @@
%h2#blocks Blocks
%h4
.lead
Content block separated with botton border
%code .content-block
.example
.content-block
%h4 Normal block inside content
= lorem
.content-block
%h4 Second block
= lorem
.lead
Gray content block with side padding using
%code .gray-content-block
.gray-content-block.middle-block
%h4 Normal block inside content
= lorem
.example
.gray-content-block
%h4 Normal block inside content
= lorem
.gray-content-block.second-block
%h4 Second block
= lorem
.gray-content-block.second-block
%h4 Second block
= lorem
%h4
.lead
Cover block for profile page with avatar, name and description
%code .cover-block
%br
.cover-block
.avatar-holder
= image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: ''
.cover-title
John Smith
.cover-desc
= lorem
.example
.cover-block
.avatar-holder
= image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: ''
.cover-title
John Smith
.cover-desc
= lorem
.cover-controls
= link_to '#', class: 'btn btn-gray' do
= icon('pencil')
&nbsp;
= link_to '#', class: 'btn btn-gray' do
= icon('rss')
.cover-controls
= link_to '#', class: 'btn btn-gray' do
= icon('pencil')
&nbsp;
= link_to '#', class: 'btn btn-gray' do
= icon('rss')
%h2#lists Lists
%h4
.lead
Simple list using
%code .content-list
%ul.content-list
%li
One item
%li
One item
%li
One item
%h4
%code .well-list
%ul.well-list
%li
One item
%li
One item
%li
One item
.example
%ul.content-list
%li
One item
%li
One item
%li
One item
%h4
%code .panel .well-list
.lead
List with avatar, title and description using
%code .content-list
.example
%ul.content-list
%li
= image_tag 'no_avatar.png', class: 'avatar s40'
.title Title
.description Description
%li
= image_tag 'no_avatar.png', class: 'avatar s40'
.title Title
.description Description
%li
= image_tag 'no_avatar.png', class: 'avatar s40'
.title Title
.description Description
.panel.panel-default
.panel-heading Your list
.lead
List with hover effect
%code .well-list
.example
%ul.well-list
%li
One item
......@@ -97,17 +124,18 @@
%li
One item
%h4
%code .bordered-list
%ul.bordered-list
%li
One item
%li
One item
%li
One item
.lead
List inside panel
.example
.panel.panel-default
.panel-heading Your list
%ul.well-list
%li
One item
%li
One item
%li
One item
%h2#tables Tables
......@@ -138,9 +166,9 @@
%h2#navs Navigation
%h4
.lead
Holder for top page navigation. Includes navigation, search field, sorting and button
%code .top-area
%p Holder for top page navigation. Includes navigation, search field, sorting and button
.example
.top-area
......@@ -161,9 +189,9 @@
= link_to 'New issue', '#', class: 'btn btn-new'
%h4
.lead
Only nav links without button and search
%code .nav-links
%p Only nav links without button and search
.example
%ul.nav-links
%li.active
......@@ -228,43 +256,47 @@
%h2#forms Forms
%h4
.lead
Horizontal form when label rendered inline with input
%code form.horizontal-form
%form.form-horizontal
.form-group
%label.col-sm-2.control-label{:for => "inputEmail3"} Email
.col-sm-10
%input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/
.form-group
%label.col-sm-2.control-label{:for => "inputPassword3"} Password
.col-sm-10
%input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
%label
%input{:type => "checkbox"}/
Remember me
.form-group
.col-sm-offset-2.col-sm-10
%button.btn.btn-default{:type => "submit"} Sign in
%h4
.example
%form.form-horizontal
.form-group
%label.col-sm-2.control-label{:for => "inputEmail3"} Email
.col-sm-10
%input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/
.form-group
%label.col-sm-2.control-label{:for => "inputPassword3"} Password
.col-sm-10
%input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
%label
%input{:type => "checkbox"}/
Remember me
.form-group
.col-sm-offset-2.col-sm-10
%button.btn.btn-default{:type => "submit"} Sign in
.lead
Form when label rendered above input
%code form
%form
.form-group
%label{:for => "exampleInputEmail1"} Email address
%input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/
.form-group
%label{:for => "exampleInputPassword1"} Password
%input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/
.checkbox
%label
%input{:type => "checkbox"}/
Remember me
%button.btn.btn-default{:type => "submit"} Sign in
.example
%form
.form-group
%label{:for => "exampleInputEmail1"} Email address
%input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/
.form-group
%label{:for => "exampleInputPassword1"} Password
%input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/
.checkbox
%label
%input{:type => "checkbox"}/
Remember me
%button.btn.btn-default{:type => "submit"} Sign in
%h2#file File
%h4
......
......@@ -90,6 +90,9 @@
&nbsp;
%span.file_name.js-avatar-filename File name...
= f.file_field :avatar, class: "js-user-avatar-input hidden"
= f.hidden_field :avatar_crop_x
= f.hidden_field :avatar_crop_y
= f.hidden_field :avatar_crop_size
.light The maximum file size allowed is 200KB.
- if @user.avatar?
%hr
......@@ -99,3 +102,19 @@
.form-actions
= f.submit 'Save changes', class: "btn btn-success"
= link_to "Cancel", user_path(current_user), class: "btn btn-cancel"
.modal.modal-profile-crop
.modal-dialog
.modal-content
.modal-header
%button.close{type: 'button', data: {dismiss: 'modal'}}
%span
&times;
%h4.modal-title
Crop your new profile picture
.modal-body
%p
%img.modal-profile-crop-image
.modal-footer
%button.btn.btn-primary.js-upload-user-avatar{:type => "button"}
Set new profile picture
......@@ -51,9 +51,11 @@
%th Name
%th Duration
%th Finished at
- if @project.build_coverage_enabled?
%th Coverage
%th
- @builds.each do |build|
= render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, allow_retry: true
= render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, coverage: @project.build_coverage_enabled?, allow_retry: true
= paginate @builds, theme: 'gitlab'
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
= link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue'
= link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue'
= link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes
= render 'projects/notes/notes_with_form'
......@@ -5,7 +5,7 @@
.issue-title
%span.issue-title-text
= link_to_gfm issue.title, issue_path(issue), class: "row_title"
= link_to_gfm issue.title, issue_path(issue), class: "title"
%ul.controls.light
- if issue.closed?
%li
......
......@@ -8,12 +8,12 @@
.detail-page-header
.pull-right
- if can?(current_user, :create_issue, @project)
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New Issue', id: 'new_issue_link' do
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do
= icon('plus')
New Issue
New issue
- if can?(current_user, :update_issue, @issue)
= link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue'
= link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue'
= link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do
= icon('pencil-square-o')
......
%li{ class: mr_css_classes(merge_request) }
.merge-request-title
%span.merge-request-title-text
= link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title"
= link_to_gfm merge_request.title, merge_request_path(merge_request), class: "title"
%ul.controls.light
- if merge_request.merged?
%li
......
......@@ -8,7 +8,7 @@
.pull-right
= link_to 'New issue', new_namespace_project_issue_path(project.namespace, project)
%ul.well-list.issues-list
%ul.content-list.issues-list
- group[1].each do |issue|
= render 'projects/issues/issue', issue: issue
= paginate @issues, theme: "gitlab"
......
......@@ -8,7 +8,7 @@
.pull-right
= link_to 'New merge request', new_namespace_project_merge_request_path(project.namespace, project)
%ul.well-list.mr-list
%ul.content-list.mr-list
- group[1].each do |merge_request|
= render 'projects/merge_requests/merge_request', merge_request: merge_request
= paginate @merge_requests, theme: "gitlab"
......
......@@ -22,13 +22,13 @@
= number_with_delimiter(group.users.count)
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
= link_to group, class: 'group-name' do
%span.item-title= group.name
= link_to group, class: 'group-name title' do
= group.name
- if group_member
as
%span #{group_member.human_access}
- if group.description.present?
.light
.description
= markdown(group.description, pipeline: :description)
......@@ -3,7 +3,8 @@
= icon('users')
%span
= participants.count
.title
.title.hide-collapsed
= pluralize participants.count, "participant"
- participants.each do |participant|
= link_to_member(@project, participant, name: false, size: 24)
%span.hide-collapsed
= link_to_member(@project, participant, name: false, size: 24)
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
.issuable-sidebar
.block
%span.issuable-count.pull-left
%span.issuable-count.hide-collapsed.pull-left
= issuable.iid
of
= issuables_count(issuable)
%span.pull-right
%a.gutter-toggle{href: '#'}
= sidebar_gutter_toggle_icon
.issuable-nav.pull-right.btn-group{role: 'group', "aria-label" => '...'}
.issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'}
- if prev_issuable = prev_issuable_for(issuable)
= link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn'
- else
......@@ -27,13 +27,13 @@
= link_to_member_avatar(issuable.assignee, size: 24)
- else
= icon('user')
.title
.title.hide-collapsed
%label
Assignee
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.pull-right
= link_to 'Edit', '#', class: 'edit-link'
.value
.value.hide-collapsed
- if issuable.assignee
%strong= link_to_member(@project, issuable.assignee, size: 24)
- if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
......@@ -42,7 +42,7 @@
- else
.light None
.selectbox
.selectbox.hide-collapsed
= users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true)
.block.milestone
......@@ -53,13 +53,13 @@
= issuable.milestone.title
- else
No
.title
.title.hide-collapsed
%label
Milestone
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.pull-right
= link_to 'Edit', '#', class: 'edit-link'
.value
.value.hide-collapsed
- if issuable.milestone
%span.back-to-milestone
= link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
......@@ -68,7 +68,7 @@
= issuable.milestone.title
- else
.light None
.selectbox
.selectbox.hide-collapsed
= f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }})
= hidden_field_tag :issuable_context
= f.submit class: 'btn hide'
......@@ -79,18 +79,18 @@
= icon('tags')
%span
= issuable.labels.count
.title
.title.hide-collapsed
%label Labels
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.pull-right
= link_to 'Edit', '#', class: 'edit-link'
.value.issuable-show-labels
.value.issuable-show-labels.hide-collapsed
- if issuable.labels.any?
- issuable.labels.each do |label|
= link_to_label(label, type: issuable.to_ability_name)
- else
.light None
.selectbox
.selectbox.hide-collapsed
= f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
{ selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" }
......@@ -101,12 +101,12 @@
.block.light
.sidebar-collapsed-icon
= icon('rss')
.title
.title.hide-collapsed
%label.light Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
%button.btn.btn-block.btn-gray.subscribe-button{:type => 'button'}
%button.btn.btn-block.btn-gray.subscribe-button.hide-collapsed{:type => 'button'}
%span= subscribed ? 'Unsubscribe' : 'Subscribe'
.subscription-status{data: {status: subscribtion_status}}
.subscription-status.hide-collapsed{data: {status: subscribtion_status}}
.unsubscribed{class: ( 'hidden' if subscribed )}
You're not receiving notifications from this thread.
.subscribed{class: ( 'hidden' unless subscribed )}
......@@ -116,8 +116,7 @@
.block.project-reference
.sidebar-collapsed-icon
= clipboard_button(clipboard_text: project_ref)
.title
.cross-project-reference
.cross-project-reference.hide-collapsed
%span
Reference:
%cite{title: project_ref}
......
......@@ -7,7 +7,7 @@
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
%ul.projects-list
%ul.projects-list.content-list
- if projects.any?
- projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) ? 'hide' : nil
......
......@@ -19,7 +19,7 @@
= image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
- else
= project_icon(project, alt: '', class: 'avatar project-avatar s40')
%span.project-full-name
%span.project-full-name.title
%span.namespace-name
- if project.namespace && !skip_namespace
= project.namespace.human_name
......@@ -27,7 +27,7 @@
%span.project-name.filter-title
= project.name
.project-controls
.controls
- if ci_commit
%span
= render_ci_status(ci_commit)
......@@ -43,9 +43,9 @@
title: "#{visibility_level_label(project.visibility_level)} - #{project_visibility_level_description(project.visibility_level)}"}
= visibility_level_icon(project.visibility_level, fw: false)
- if show_last_commit_as_description
.project-description
.description
= link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit),
class: "commit-row-message"
- elsif project.description.present?
.project-description
.description
= markdown(project.description, pipeline: :description)
%li.snippet-row
= image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: ''
.snippet-title
= link_to reliable_snippet_path(snippet) do
= link_to reliable_snippet_path(snippet), class: 'title' do
= truncate(snippet.title, length: 60)
- if snippet.private?
%span.label.label-gray
%i.fa.fa-lock
= icon('lock')
private
%span.monospace.pull-right
= snippet.file_name
......@@ -15,6 +17,5 @@
.snippet-info
= link_to user_snippets_path(snippet.author) do
= image_tag avatar_icon(snippet.author_email), class: "avatar s24", alt: ''
= snippet.author_name
authored #{time_ago_with_tooltip(snippet.created_at)}
%ul.bordered-list
%ul.content-list
= render partial: 'shared/snippets/snippet', collection: @snippets
- if @snippets.empty?
%li
......
......@@ -27,7 +27,7 @@ class RepositoryForkWorker
return
end
project.repository.expire_emptiness_caches
project.repository.after_import
project.import_finish
end
end
......@@ -18,7 +18,7 @@ class RepositoryImportWorker
return
end
project.repository.expire_emptiness_caches
project.repository.after_import
project.import_finish
end
end
......@@ -42,7 +42,7 @@ Gitlab-shell communicates with Sidekiq via the “communication board” (Redis)
## System Layout
When referring to ~git in the pictures it means the home directory of the git user which is typically /home/git.
When referring to `~git` in the pictures it means the home directory of the git user which is typically /home/git.
GitLab is primarily installed within the `/home/git` user home directory as `git` user. Within the home directory is where the gitlabhq server software resides as well as the repositories (though the repository location is configurable).
......
......@@ -18,7 +18,7 @@ You accept and agree to the following terms and conditions for Your present and
6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [[]named here]".
7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [insert_name_here]".
8. You agree to notify GitLab B.V. of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
......
......@@ -6,7 +6,7 @@ If a user is both in a project group and in the project itself, the highest perm
If a user is a GitLab administrator they receive all permissions.
On public projects the Guest role is not enforced.
On public and internal projects the Guest role is not enforced.
All users will be able to create issues, leave comments, and pull or download the project code.
To add or import a user, you can follow the [project users and members
......@@ -26,6 +26,7 @@ documentation](../workflow/add-user/add-user.md).
| Create code snippets | | ✓ | ✓ | ✓ | ✓ |
| Manage issue tracker | | ✓ | ✓ | ✓ | ✓ |
| Manage labels | | ✓ | ✓ | ✓ | ✓ |
| See a commit status | | ✓ | ✓ | ✓ | ✓ |
| Manage merge requests | | | ✓ | ✓ | ✓ |
| Create new merge request | | | ✓ | ✓ | ✓ |
| Create new branches | | | ✓ | ✓ | ✓ |
......@@ -35,6 +36,7 @@ documentation](../workflow/add-user/add-user.md).
| Add tags | | | ✓ | ✓ | ✓ |
| Write a wiki | | | ✓ | ✓ | ✓ |
| Cancel and retry builds | | | ✓ | ✓ | ✓ |
| Create or update commit status | | | ✓ | ✓ | ✓ |
| Create new milestones | | | | ✓ | ✓ |
| Add new team members | | | | ✓ | ✓ |
| Push to protected branches | | | | ✓ | ✓ |
......
......@@ -69,6 +69,7 @@ branches and tags.
```bash
git remote add origin git@gitlab.com:<group>/<project>.git
git push --all origin
git push --tags origin
```
## Contribute to this guide
......
Feature: Login form
Scenario: I see Crowd form
Given Crowd integration enabled
When I visit sign in page
Then I should see Crowd login form
Scenario: I see Crowd form when sign-in is disabled
Given Crowd integration enabled
And Sign-in is disabled
When I visit sign in page
Then I should see Crowd login form
......@@ -3,6 +3,7 @@ Feature: Project Builds Summary
Given I sign in as a user
And I own a project
And project has CI enabled
And project has coverage enabled
And project has a recent build
Scenario: I browse build details page
......@@ -12,6 +13,7 @@ Feature: Project Builds Summary
Scenario: I browse project builds page
When I visit project builds page
Then I see coverage
Then I see button to CI Lint
Scenario: I erase a build
......
class Spinach::Features::LoginForm < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedSnippet
include SharedUser
include SharedSearch
step 'Sign-in is disabled' do
allow_any_instance_of(ApplicationHelper).to receive(:signin_enabled?).and_return(false)
end
step 'Crowd integration enabled' do
expect(Gitlab::OAuth::Provider).to receive(:providers).and_return([:crowd])
expect(Gitlab.config.omniauth).to receive(:enabled).and_return(true)
allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_return(root_path)
end
step 'I should see Crowd login form' do
expect(page).to have_selector '#tab-crowd form'
end
step 'I visit sign in page' do
visit new_user_session_path
end
end
......@@ -27,9 +27,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step 'I change my avatar' do
attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
click_button "Save changes"
@user.reload
attach_avatar
end
step 'I should see new avatar' do
......@@ -42,9 +40,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step 'I have an avatar' do
attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
click_button "Save changes"
@user.reload
attach_avatar
end
step 'I remove my avatar' do
......@@ -233,4 +229,16 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
step "I see that application is removed" do
expect(page.find(".oauth-applications")).not_to have_content "test_changed"
end
def attach_avatar
attach_file :user_avatar, Rails.root.join(*%w(spec fixtures banana_sample.gif))
page.find('#user_avatar_crop_x', visible: false).set('0')
page.find('#user_avatar_crop_y', visible: false).set('0')
page.find('#user_avatar_crop_size', visible: false).set('256')
click_button "Save changes"
@user.reload
end
end
......@@ -4,6 +4,12 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps
include SharedBuilds
include RepoHelpers
step 'I see coverage' do
page.within('td.coverage') do
expect(page).to have_content "99.9%"
end
end
step 'I see button to CI Lint' do
page.within('.nav-controls') do
ci_lint_tool_link = page.find_link('CI Lint')
......
......@@ -5,9 +5,13 @@ module SharedBuilds
@project.enable_ci
end
step 'project has coverage enabled' do
@project.update_attribute(:build_coverage_regex, /Coverage (\d+)%/)
end
step 'project has a recent build' do
@ci_commit = create(:ci_commit, project: @project, sha: @project.commit.sha)
@build = create(:ci_build, commit: @ci_commit)
@build = create(:ci_build_with_coverage, commit: @ci_commit)
end
step 'recent build is successful' do
......
......@@ -6,7 +6,7 @@ timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout)
Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768])
end
Capybara.default_wait_time = timeout
......
#!/bin/bash
# Sends Slack notification ERROR_MSG to CHANNEL
# An env. variable CI_SLACK_WEBHOOK_URL needs to be set.
CHANNEL=$1
ERROR_MSG=$2
if [ -z "$CHANNEL" ] || [ -z "$ERROR_MSG" ] || [ -z "$CI_SLACK_WEBHOOK_URL" ]; then
echo "Missing argument(s) - Use: $0 channel message"
echo "and set CI_SLACK_WEBHOOK_URL environment variable."
else
curl -X POST --data-urlencode 'payload={"channel": "'"$CHANNEL"'", "username": "gitlab-ci", "text": "'"$ERROR_MSG"'", "icon_emoji": ":gitlab:"}' "$CI_SLACK_WEBHOOK_URL"
fi
\ No newline at end of file
require 'spec_helper'
describe NamespacesController do
let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
let!(:user) { create(:user, :with_avatar) }
describe "GET show" do
context "when the namespace belongs to a user" do
......
require 'spec_helper'
describe Profiles::AvatarsController do
let(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png")) }
let(:user) { create(:user, :with_avatar) }
before do
sign_in(user)
......
require 'spec_helper'
describe UploadsController do
let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
let!(:user) { create(:user, :with_avatar) }
describe "GET show" do
context "when viewing a user avatar" do
......
......@@ -36,6 +36,13 @@ FactoryGirl.define do
end
end
trait :with_avatar do
avatar { fixture_file_upload(Rails.root.join(*%w(spec fixtures dk.png)), 'image/png') }
avatar_crop_x 0
avatar_crop_y 0
avatar_crop_size 256
end
factory :omniauth_user do
ignore do
extern_uid '123456'
......
......@@ -53,6 +53,10 @@ FactoryGirl.define do
tag true
end
factory :ci_build_with_coverage do
coverage 99.9
end
trait :trace do
after(:create) do |build, evaluator|
build.trace = 'BUILD TRACE'
......@@ -68,7 +72,7 @@ FactoryGirl.define do
build.artifacts_metadata =
fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'),
'application/x-gzip')
build.save!
end
end
......
......@@ -13,7 +13,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(Milestone::None.title)
expect(page).to have_css('.title', count: 1)
expect(page).to have_css('.issue .title', count: 1)
end
scenario 'filters by a specific Milestone', js: true do
......@@ -23,7 +23,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(milestone.title)
expect(page).to have_css('.title', count: 1)
expect(page).to have_css('.issue .title', count: 1)
end
def visit_issues(project)
......
......@@ -77,7 +77,7 @@ describe ApplicationHelper do
let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
it 'should return an url for the avatar' do
user = create(:user, avatar: File.open(avatar_file_path))
user = create(:user, :with_avatar, avatar: File.open(avatar_file_path))
expect(helper.avatar_icon(user.email).to_s).
to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
......@@ -88,7 +88,7 @@ describe ApplicationHelper do
# Must be stubbed after the stub above, and separately
stub_config_setting(url: Settings.send(:build_gitlab_url))
user = create(:user, avatar: File.open(avatar_file_path))
user = create(:user, :with_avatar, avatar: File.open(avatar_file_path))
expect(helper.avatar_icon(user.email).to_s).
to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif")
......@@ -102,7 +102,7 @@ describe ApplicationHelper do
describe 'using a User' do
it 'should return an URL for the avatar' do
user = create(:user, avatar: File.open(avatar_file_path))
user = create(:user, :with_avatar, avatar: File.open(avatar_file_path))
expect(helper.avatar_icon(user).to_s).
to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
......
......@@ -19,6 +19,10 @@
require 'spec_helper'
describe ProjectHook, models: true do
describe "Associations" do
it { is_expected.to belong_to :project }
end
describe '.push_hooks' do
it 'should return hooks for push events only' do
hook = create(:project_hook, push_events: true)
......
......@@ -18,20 +18,14 @@
require 'spec_helper'
describe ProjectHook, models: true do
describe "Associations" do
it { is_expected.to belong_to :project }
end
describe "Mass assignment" do
end
describe WebHook, models: true do
describe "Validations" do
it { is_expected.to validate_presence_of(:url) }
context "url format" do
describe 'url' do
it { is_expected.to allow_value("http://example.com").for(:url) }
it { is_expected.to allow_value("https://excample.com").for(:url) }
it { is_expected.to allow_value("https://example.com").for(:url) }
it { is_expected.to allow_value(" https://example.com ").for(:url) }
it { is_expected.to allow_value("http://test.com/api").for(:url) }
it { is_expected.to allow_value("http://test.com/api?key=abc").for(:url) }
it { is_expected.to allow_value("http://test.com/api?key=abc&type=def").for(:url) }
......@@ -39,6 +33,12 @@ describe ProjectHook, models: true do
it { is_expected.not_to allow_value("example.com").for(:url) }
it { is_expected.not_to allow_value("ftp://example.com").for(:url) }
it { is_expected.not_to allow_value("herp-and-derp").for(:url) }
it 'strips :url before saving it' do
hook = create(:project_hook, url: ' https://example.com ')
expect(hook.url).to eq('https://example.com')
end
end
end
......
......@@ -362,14 +362,14 @@ describe Repository, models: true do
repository.expire_cache('master')
end
it 'expires the emptiness cache for an empty repository' do
it 'expires the emptiness caches for an empty repository' do
expect(repository).to receive(:empty?).and_return(true)
expect(repository).to receive(:expire_emptiness_caches)
repository.expire_cache
end
it 'does not expire the emptiness cache for a non-empty repository' do
it 'does not expire the emptiness caches for a non-empty repository' do
expect(repository).to receive(:empty?).and_return(false)
expect(repository).to_not receive(:expire_emptiness_caches)
......@@ -464,4 +464,108 @@ describe Repository, models: true do
expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present
end
end
describe '#before_delete' do
describe 'when a repository does not exist' do
before do
allow(repository).to receive(:exists?).and_return(false)
end
it 'does not flush caches that depend on repository data' do
expect(repository).to_not receive(:expire_cache)
repository.before_delete
end
it 'flushes the root ref cache' do
expect(repository).to receive(:expire_root_ref_cache)
repository.before_delete
end
it 'flushes the emptiness caches' do
expect(repository).to receive(:expire_emptiness_caches)
repository.before_delete
end
end
describe 'when a repository exists' do
before do
allow(repository).to receive(:exists?).and_return(true)
end
it 'flushes the caches that depend on repository data' do
expect(repository).to receive(:expire_cache)
repository.before_delete
end
it 'flushes the root ref cache' do
expect(repository).to receive(:expire_root_ref_cache)
repository.before_delete
end
it 'flushes the emptiness caches' do
expect(repository).to receive(:expire_emptiness_caches)
repository.before_delete
end
end
end
describe '#before_change_head' do
it 'flushes the branch cache' do
expect(repository).to receive(:expire_branch_cache)
repository.before_change_head
end
it 'flushes the root ref cache' do
expect(repository).to receive(:expire_root_ref_cache)
repository.before_change_head
end
end
describe '#before_create_tag' do
it 'flushes the cache' do
expect(repository).to receive(:expire_cache)
repository.before_create_tag
end
end
describe '#after_import' do
it 'flushes the emptiness cachess' do
expect(repository).to receive(:expire_emptiness_caches)
repository.after_import
end
end
describe '#after_push_commit' do
it 'flushes the cache' do
expect(repository).to receive(:expire_cache).with('master')
repository.after_push_commit('master')
end
end
describe '#after_create_branch' do
it 'flushes the visible content cache' do
expect(repository).to receive(:expire_has_visible_content_cache)
repository.after_create_branch
end
end
describe '#after_remove_branch' do
it 'flushes the visible content cache' do
expect(repository).to receive(:expire_has_visible_content_cache)
repository.after_remove_branch
end
end
end
......@@ -174,6 +174,18 @@ describe User, models: true do
end
end
end
describe 'avatar' do
it 'only validates when avatar is present' do
user = build(:user, :with_avatar)
user.avatar_crop_x = nil
user.avatar_crop_y = nil
user.avatar_crop_size = nil
expect(user).not_to be_valid
end
end
end
describe "Respond to" do
......
......@@ -110,6 +110,8 @@ describe TodoService, services: true do
let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) }
let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) }
let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) }
let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') }
let(:system_note) { create(:system_note, project: project, noteable: issue) }
......@@ -145,6 +147,14 @@ describe TodoService, services: true do
should_not_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_not_create_todo(user: stranger, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
end
it 'does not create todo when leaving a note on commit' do
should_not_create_any_todo { service.new_note(note_on_commit, john_doe) }
end
it 'does not create todo when leaving a note on snippet' do
should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) }
end
end
end
......
......@@ -7,7 +7,7 @@ timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout)
Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768])
end
Capybara.default_wait_time = timeout
......
require 'rails_helper'
describe 'devise/shared/_signin_box' do
describe 'Crowd form' do
before do
stub_devise
assign(:ldap_servers, [])
end
it 'is shown when Crowd is enabled' do
enable_crowd
render
expect(rendered).to have_selector('#tab-crowd form')
end
it 'is not shown when Crowd is disabled' do
render
expect(rendered).not_to have_selector('#tab-crowd')
end
end
def stub_devise
allow(view).to receive(:devise_mapping).and_return(Devise.mappings[:user])
allow(view).to receive(:resource).and_return(spy)
allow(view).to receive(:resource_name).and_return(:user)
end
def enable_crowd
allow(view).to receive(:form_based_providers).and_return([:crowd])
allow(view).to receive(:crowd_enabled?).and_return(true)
allow(view).to receive(:user_omniauth_authorize_path).with('crowd').
and_return('/crowd')
end
end
/*!
* Cropper v2.2.5
* https://github.com/fengyuanchen/cropper
*
* Copyright (c) 2014-2016 Fengyuan Chen and contributors
* Released under the MIT license
*
* Date: 2016-01-18T05:42:50.800Z
*/
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as anonymous module.
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node / CommonJS
factory(require('jquery'));
} else {
// Browser globals.
factory(jQuery);
}
})(function ($) {
'use strict';
// Globals
var $window = $(window);
var $document = $(document);
var location = window.location;
var ArrayBuffer = window.ArrayBuffer;
var Uint8Array = window.Uint8Array;
var DataView = window.DataView;
var btoa = window.btoa;
// Constants
var NAMESPACE = 'cropper';
// Classes
var CLASS_MODAL = 'cropper-modal';
var CLASS_HIDE = 'cropper-hide';
var CLASS_HIDDEN = 'cropper-hidden';
var CLASS_INVISIBLE = 'cropper-invisible';
var CLASS_MOVE = 'cropper-move';
var CLASS_CROP = 'cropper-crop';
var CLASS_DISABLED = 'cropper-disabled';
var CLASS_BG = 'cropper-bg';
// Events
var EVENT_MOUSE_DOWN = 'mousedown touchstart pointerdown MSPointerDown';
var EVENT_MOUSE_MOVE = 'mousemove touchmove pointermove MSPointerMove';
var EVENT_MOUSE_UP = 'mouseup touchend touchcancel pointerup pointercancel MSPointerUp MSPointerCancel';
var EVENT_WHEEL = 'wheel mousewheel DOMMouseScroll';
var EVENT_DBLCLICK = 'dblclick';
var EVENT_LOAD = 'load.' + NAMESPACE;
var EVENT_ERROR = 'error.' + NAMESPACE;
var EVENT_RESIZE = 'resize.' + NAMESPACE; // Bind to window with namespace
var EVENT_BUILD = 'build.' + NAMESPACE;
var EVENT_BUILT = 'built.' + NAMESPACE;
var EVENT_CROP_START = 'cropstart.' + NAMESPACE;
var EVENT_CROP_MOVE = 'cropmove.' + NAMESPACE;
var EVENT_CROP_END = 'cropend.' + NAMESPACE;
var EVENT_CROP = 'crop.' + NAMESPACE;
var EVENT_ZOOM = 'zoom.' + NAMESPACE;
// RegExps
var REGEXP_ACTIONS = /e|w|s|n|se|sw|ne|nw|all|crop|move|zoom/;
var REGEXP_DATA_URL = /^data\:/;
var REGEXP_DATA_URL_HEAD = /^data\:([^\;]+)\;base64,/;
var REGEXP_DATA_URL_JPEG = /^data\:image\/jpeg.*;base64,/;
// Data keys
var DATA_PREVIEW = 'preview';
var DATA_ACTION = 'action';
// Actions
var ACTION_EAST = 'e';
var ACTION_WEST = 'w';
var ACTION_SOUTH = 's';
var ACTION_NORTH = 'n';
var ACTION_SOUTH_EAST = 'se';
var ACTION_SOUTH_WEST = 'sw';
var ACTION_NORTH_EAST = 'ne';
var ACTION_NORTH_WEST = 'nw';
var ACTION_ALL = 'all';
var ACTION_CROP = 'crop';
var ACTION_MOVE = 'move';
var ACTION_ZOOM = 'zoom';
var ACTION_NONE = 'none';
// Supports
var SUPPORT_CANVAS = $.isFunction($('<canvas>')[0].getContext);
// Maths
var num = Number;
var min = Math.min;
var max = Math.max;
var abs = Math.abs;
var sin = Math.sin;
var cos = Math.cos;
var sqrt = Math.sqrt;
var round = Math.round;
var floor = Math.floor;
// Utilities
var fromCharCode = String.fromCharCode;
function isNumber(n) {
return typeof n === 'number' && !isNaN(n);
}
function isUndefined(n) {
return typeof n === 'undefined';
}
function toArray(obj, offset) {
var args = [];
// This is necessary for IE8
if (isNumber(offset)) {
args.push(offset);
}
return args.slice.apply(obj, args);
}
// Custom proxy to avoid jQuery's guid
function proxy(fn, context) {
var args = toArray(arguments, 2);
return function () {
return fn.apply(context, args.concat(toArray(arguments)));
};
}
function isCrossOriginURL(url) {
var parts = url.match(/^(https?:)\/\/([^\:\/\?#]+):?(\d*)/i);
return parts && (
parts[1] !== location.protocol ||
parts[2] !== location.hostname ||
parts[3] !== location.port
);
}
function addTimestamp(url) {
var timestamp = 'timestamp=' + (new Date()).getTime();
return (url + (url.indexOf('?') === -1 ? '?' : '&') + timestamp);
}
function getCrossOrigin(crossOrigin) {
return crossOrigin ? ' crossOrigin="' + crossOrigin + '"' : '';
}
function getImageSize(image, callback) {
var newImage;
// Modern browsers
if (image.naturalWidth) {
return callback(image.naturalWidth, image.naturalHeight);
}
// IE8: Don't use `new Image()` here (#319)
newImage = document.createElement('img');
newImage.onload = function () {
callback(this.width, this.height);
};
newImage.src = image.src;
}
function getTransform(options) {
var transforms = [];
var rotate = options.rotate;
var scaleX = options.scaleX;
var scaleY = options.scaleY;
if (isNumber(rotate)) {
transforms.push('rotate(' + rotate + 'deg)');
}
if (isNumber(scaleX) && isNumber(scaleY)) {
transforms.push('scale(' + scaleX + ',' + scaleY + ')');
}
return transforms.length ? transforms.join(' ') : 'none';
}
function getRotatedSizes(data, isReversed) {
var deg = abs(data.degree) % 180;
var arc = (deg > 90 ? (180 - deg) : deg) * Math.PI / 180;
var sinArc = sin(arc);
var cosArc = cos(arc);
var width = data.width;
var height = data.height;
var aspectRatio = data.aspectRatio;
var newWidth;
var newHeight;
if (!isReversed) {
newWidth = width * cosArc + height * sinArc;
newHeight = width * sinArc + height * cosArc;
} else {
newWidth = width / (cosArc + sinArc / aspectRatio);
newHeight = newWidth / aspectRatio;
}
return {
width: newWidth,
height: newHeight
};
}
function getSourceCanvas(image, data) {
var canvas = $('<canvas>')[0];
var context = canvas.getContext('2d');
var x = 0;
var y = 0;
var width = data.naturalWidth;
var height = data.naturalHeight;
var rotate = data.rotate;
var scaleX = data.scaleX;
var scaleY = data.scaleY;
var scalable = isNumber(scaleX) && isNumber(scaleY) && (scaleX !== 1 || scaleY !== 1);
var rotatable = isNumber(rotate) && rotate !== 0;
var advanced = rotatable || scalable;
var canvasWidth = width;
var canvasHeight = height;
var translateX;
var translateY;
var rotated;
if (scalable) {
translateX = width / 2;
translateY = height / 2;
}
if (rotatable) {
rotated = getRotatedSizes({
width: width,
height: height,
degree: rotate
});
canvasWidth = rotated.width;
canvasHeight = rotated.height;
translateX = rotated.width / 2;
translateY = rotated.height / 2;
}
canvas.width = canvasWidth;
canvas.height = canvasHeight;
if (advanced) {
x = -width / 2;
y = -height / 2;
context.save();
context.translate(translateX, translateY);
}
if (rotatable) {
context.rotate(rotate * Math.PI / 180);
}
// Should call `scale` after rotated
if (scalable) {
context.scale(scaleX, scaleY);
}
context.drawImage(image, floor(x), floor(y), floor(width), floor(height));
if (advanced) {
context.restore();
}
return canvas;
}
function getTouchesCenter(touches) {
var length = touches.length;
var pageX = 0;
var pageY = 0;
if (length) {
$.each(touches, function (i, touch) {
pageX += touch.pageX;
pageY += touch.pageY;
});
pageX /= length;
pageY /= length;
}
return {
pageX: pageX,
pageY: pageY
};
}
function getStringFromCharCode(dataView, start, length) {
var str = '';
var i;
for (i = start, length += start; i < length; i++) {
str += fromCharCode(dataView.getUint8(i));
}
return str;
}
function getOrientation(arrayBuffer) {
var dataView = new DataView(arrayBuffer);
var length = dataView.byteLength;
var orientation;
var exifIDCode;
var tiffOffset;
var firstIFDOffset;
var littleEndian;
var endianness;
var app1Start;
var ifdStart;
var offset;
var i;
// Only handle JPEG image (start by 0xFFD8)
if (dataView.getUint8(0) === 0xFF && dataView.getUint8(1) === 0xD8) {
offset = 2;
while (offset < length) {
if (dataView.getUint8(offset) === 0xFF && dataView.getUint8(offset + 1) === 0xE1) {
app1Start = offset;
break;
}
offset++;
}
}
if (app1Start) {
exifIDCode = app1Start + 4;
tiffOffset = app1Start + 10;
if (getStringFromCharCode(dataView, exifIDCode, 4) === 'Exif') {
endianness = dataView.getUint16(tiffOffset);
littleEndian = endianness === 0x4949;
if (littleEndian || endianness === 0x4D4D /* bigEndian */) {
if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002A) {
firstIFDOffset = dataView.getUint32(tiffOffset + 4, littleEndian);
if (firstIFDOffset >= 0x00000008) {
ifdStart = tiffOffset + firstIFDOffset;
}
}
}
}
}
if (ifdStart) {
length = dataView.getUint16(ifdStart, littleEndian);
for (i = 0; i < length; i++) {
offset = ifdStart + i * 12 + 2;
if (dataView.getUint16(offset, littleEndian) === 0x0112 /* Orientation */) {
// 8 is the offset of the current tag's value
offset += 8;
// Get the original orientation value
orientation = dataView.getUint16(offset, littleEndian);
// Override the orientation with the default value: 1
dataView.setUint16(offset, 1, littleEndian);
break;
}
}
}
return orientation;
}
function dataURLToArrayBuffer(dataURL) {
var base64 = dataURL.replace(REGEXP_DATA_URL_HEAD, '');
var binary = atob(base64);
var length = binary.length;
var arrayBuffer = new ArrayBuffer(length);
var dataView = new Uint8Array(arrayBuffer);
var i;
for (i = 0; i < length; i++) {
dataView[i] = binary.charCodeAt(i);
}
return arrayBuffer;
}
// Only available for JPEG image
function arrayBufferToDataURL(arrayBuffer) {
var dataView = new Uint8Array(arrayBuffer);
var length = dataView.length;
var base64 = '';
var i;
for (i = 0; i < length; i++) {
base64 += fromCharCode(dataView[i]);
}
return 'data:image/jpeg;base64,' + btoa(base64);
}
function Cropper(element, options) {
this.$element = $(element);
this.options = $.extend({}, Cropper.DEFAULTS, $.isPlainObject(options) && options);
this.isLoaded = false;
this.isBuilt = false;
this.isCompleted = false;
this.isRotated = false;
this.isCropped = false;
this.isDisabled = false;
this.isReplaced = false;
this.isLimited = false;
this.wheeling = false;
this.isImg = false;
this.originalUrl = '';
this.canvas = null;
this.cropBox = null;
this.init();
}
Cropper.prototype = {
constructor: Cropper,
init: function () {
var $this = this.$element;
var url;
if ($this.is('img')) {
this.isImg = true;
// Should use `$.fn.attr` here. e.g.: "img/picture.jpg"
this.originalUrl = url = $this.attr('src');
// Stop when it's a blank image
if (!url) {
return;
}
// Should use `$.fn.prop` here. e.g.: "http://example.com/img/picture.jpg"
url = $this.prop('src');
} else if ($this.is('canvas') && SUPPORT_CANVAS) {
url = $this[0].toDataURL();
}
this.load(url);
},
// A shortcut for triggering custom events
trigger: function (type, data) {
var e = $.Event(type, data);
this.$element.trigger(e);
return e;
},
load: function (url) {
var options = this.options;
var $this = this.$element;
var read;
var xhr;
if (!url) {
return;
}
// Trigger build event first
$this.one(EVENT_BUILD, options.build);
if (this.trigger(EVENT_BUILD).isDefaultPrevented()) {
return;
}
this.url = url;
this.image = {};
if (!options.checkOrientation || !ArrayBuffer) {
return this.clone();
}
read = $.proxy(this.read, this);
// XMLHttpRequest disallows to open a Data URL in some browsers like IE11 and Safari
if (REGEXP_DATA_URL.test(url)) {
return REGEXP_DATA_URL_JPEG.test(url) ?
read(dataURLToArrayBuffer(url)) :
this.clone();
}
xhr = new XMLHttpRequest();
xhr.onerror = xhr.onabort = $.proxy(function () {
this.clone();
}, this);
xhr.onload = function () {
read(this.response);
};
xhr.open('get', url);
xhr.responseType = 'arraybuffer';
xhr.send();
},
read: function (arrayBuffer) {
var options = this.options;
var orientation = getOrientation(arrayBuffer);
var image = this.image;
var rotate;
var scaleX;
var scaleY;
if (orientation > 1) {
this.url = arrayBufferToDataURL(arrayBuffer);
switch (orientation) {
// flip horizontal
case 2:
scaleX = -1;
break;
// rotate left 180°
case 3:
rotate = -180;
break;
// flip vertical
case 4:
scaleY = -1;
break;
// flip vertical + rotate right 90°
case 5:
rotate = 90;
scaleY = -1;
break;
// rotate right 90°
case 6:
rotate = 90;
break;
// flip horizontal + rotate right 90°
case 7:
rotate = 90;
scaleX = -1;
break;
// rotate left 90°
case 8:
rotate = -90;
break;
}
}
if (options.rotatable) {
image.rotate = rotate;
}
if (options.scalable) {
image.scaleX = scaleX;
image.scaleY = scaleY;
}
this.clone();
},
clone: function () {
var options = this.options;
var $this = this.$element;
var url = this.url;
var crossOrigin = '';
var crossOriginUrl;
var $clone;
if (options.checkCrossOrigin && isCrossOriginURL(url)) {
crossOrigin = $this.prop('crossOrigin');
if (crossOrigin) {
crossOriginUrl = url;
} else {
crossOrigin = 'anonymous';
// Bust cache (#148) when there is not a "crossOrigin" property
crossOriginUrl = addTimestamp(url);
}
}
this.crossOrigin = crossOrigin;
this.crossOriginUrl = crossOriginUrl;
this.$clone = $clone = $('<img' + getCrossOrigin(crossOrigin) + ' src="' + (crossOriginUrl || url) + '">');
if (this.isImg) {
if ($this[0].complete) {
this.start();
} else {
$this.one(EVENT_LOAD, $.proxy(this.start, this));
}
} else {
$clone.
one(EVENT_LOAD, $.proxy(this.start, this)).
one(EVENT_ERROR, $.proxy(this.stop, this)).
addClass(CLASS_HIDE).
insertAfter($this);
}
},
start: function () {
var $image = this.$element;
var $clone = this.$clone;
if (!this.isImg) {
$clone.off(EVENT_ERROR, this.stop);
$image = $clone;
}
getImageSize($image[0], $.proxy(function (naturalWidth, naturalHeight) {
$.extend(this.image, {
naturalWidth: naturalWidth,
naturalHeight: naturalHeight,
aspectRatio: naturalWidth / naturalHeight
});
this.isLoaded = true;
this.build();
}, this));
},
stop: function () {
this.$clone.remove();
this.$clone = null;
},
build: function () {
var options = this.options;
var $this = this.$element;
var $clone = this.$clone;
var $cropper;
var $cropBox;
var $face;
if (!this.isLoaded) {
return;
}
// Unbuild first when replace
if (this.isBuilt) {
this.unbuild();
}
// Create cropper elements
this.$container = $this.parent();
this.$cropper = $cropper = $(Cropper.TEMPLATE);
this.$canvas = $cropper.find('.cropper-canvas').append($clone);
this.$dragBox = $cropper.find('.cropper-drag-box');
this.$cropBox = $cropBox = $cropper.find('.cropper-crop-box');
this.$viewBox = $cropper.find('.cropper-view-box');
this.$face = $face = $cropBox.find('.cropper-face');
// Hide the original image
$this.addClass(CLASS_HIDDEN).after($cropper);
// Show the clone image if is hidden
if (!this.isImg) {
$clone.removeClass(CLASS_HIDE);
}
this.initPreview();
this.bind();
options.aspectRatio = max(0, options.aspectRatio) || NaN;
options.viewMode = max(0, min(3, round(options.viewMode))) || 0;
if (options.autoCrop) {
this.isCropped = true;
if (options.modal) {
this.$dragBox.addClass(CLASS_MODAL);
}
} else {
$cropBox.addClass(CLASS_HIDDEN);
}
if (!options.guides) {
$cropBox.find('.cropper-dashed').addClass(CLASS_HIDDEN);
}
if (!options.center) {
$cropBox.find('.cropper-center').addClass(CLASS_HIDDEN);
}
if (options.cropBoxMovable) {
$face.addClass(CLASS_MOVE).data(DATA_ACTION, ACTION_ALL);
}
if (!options.highlight) {
$face.addClass(CLASS_INVISIBLE);
}
if (options.background) {
$cropper.addClass(CLASS_BG);
}
if (!options.cropBoxResizable) {
$cropBox.find('.cropper-line, .cropper-point').addClass(CLASS_HIDDEN);
}
this.setDragMode(options.dragMode);
this.render();
this.isBuilt = true;
this.setData(options.data);
$this.one(EVENT_BUILT, options.built);
// Trigger the built event asynchronously to keep `data('cropper')` is defined
setTimeout($.proxy(function () {
this.trigger(EVENT_BUILT);
this.isCompleted = true;
}, this), 0);
},
unbuild: function () {
if (!this.isBuilt) {
return;
}
this.isBuilt = false;
this.isCompleted = false;
this.initialImage = null;
// Clear `initialCanvas` is necessary when replace
this.initialCanvas = null;
this.initialCropBox = null;
this.container = null;
this.canvas = null;
// Clear `cropBox` is necessary when replace
this.cropBox = null;
this.unbind();
this.resetPreview();
this.$preview = null;
this.$viewBox = null;
this.$cropBox = null;
this.$dragBox = null;
this.$canvas = null;
this.$container = null;
this.$cropper.remove();
this.$cropper = null;
},
render: function () {
this.initContainer();
this.initCanvas();
this.initCropBox();
this.renderCanvas();
if (this.isCropped) {
this.renderCropBox();
}
},
initContainer: function () {
var options = this.options;
var $this = this.$element;
var $container = this.$container;
var $cropper = this.$cropper;
$cropper.addClass(CLASS_HIDDEN);
$this.removeClass(CLASS_HIDDEN);
$cropper.css((this.container = {
width: max($container.width(), num(options.minContainerWidth) || 200),
height: max($container.height(), num(options.minContainerHeight) || 100)
}));
$this.addClass(CLASS_HIDDEN);
$cropper.removeClass(CLASS_HIDDEN);
},
// Canvas (image wrapper)
initCanvas: function () {
var viewMode = this.options.viewMode;
var container = this.container;
var containerWidth = container.width;
var containerHeight = container.height;
var image = this.image;
var imageNaturalWidth = image.naturalWidth;
var imageNaturalHeight = image.naturalHeight;
var is90Degree = abs(image.rotate) === 90;
var naturalWidth = is90Degree ? imageNaturalHeight : imageNaturalWidth;
var naturalHeight = is90Degree ? imageNaturalWidth : imageNaturalHeight;
var aspectRatio = naturalWidth / naturalHeight;
var canvasWidth = containerWidth;
var canvasHeight = containerHeight;
var canvas;
if (containerHeight * aspectRatio > containerWidth) {
if (viewMode === 3) {
canvasWidth = containerHeight * aspectRatio;
} else {
canvasHeight = containerWidth / aspectRatio;
}
} else {
if (viewMode === 3) {
canvasHeight = containerWidth / aspectRatio;
} else {
canvasWidth = containerHeight * aspectRatio;
}
}
canvas = {
naturalWidth: naturalWidth,
naturalHeight: naturalHeight,
aspectRatio: aspectRatio,
width: canvasWidth,
height: canvasHeight
};
canvas.oldLeft = canvas.left = (containerWidth - canvasWidth) / 2;
canvas.oldTop = canvas.top = (containerHeight - canvasHeight) / 2;
this.canvas = canvas;
this.isLimited = (viewMode === 1 || viewMode === 2);
this.limitCanvas(true, true);
this.initialImage = $.extend({}, image);
this.initialCanvas = $.extend({}, canvas);
},
limitCanvas: function (isSizeLimited, isPositionLimited) {
var options = this.options;
var viewMode = options.viewMode;
var container = this.container;
var containerWidth = container.width;
var containerHeight = container.height;
var canvas = this.canvas;
var aspectRatio = canvas.aspectRatio;
var cropBox = this.cropBox;
var isCropped = this.isCropped && cropBox;
var minCanvasWidth;
var minCanvasHeight;
var newCanvasLeft;
var newCanvasTop;
if (isSizeLimited) {
minCanvasWidth = num(options.minCanvasWidth) || 0;
minCanvasHeight = num(options.minCanvasHeight) || 0;
if (viewMode) {
if (viewMode > 1) {
minCanvasWidth = max(minCanvasWidth, containerWidth);
minCanvasHeight = max(minCanvasHeight, containerHeight);
if (viewMode === 3) {
if (minCanvasHeight * aspectRatio > minCanvasWidth) {
minCanvasWidth = minCanvasHeight * aspectRatio;
} else {
minCanvasHeight = minCanvasWidth / aspectRatio;
}
}
} else {
if (minCanvasWidth) {
minCanvasWidth = max(minCanvasWidth, isCropped ? cropBox.width : 0);
} else if (minCanvasHeight) {
minCanvasHeight = max(minCanvasHeight, isCropped ? cropBox.height : 0);
} else if (isCropped) {
minCanvasWidth = cropBox.width;
minCanvasHeight = cropBox.height;
if (minCanvasHeight * aspectRatio > minCanvasWidth) {
minCanvasWidth = minCanvasHeight * aspectRatio;
} else {
minCanvasHeight = minCanvasWidth / aspectRatio;
}
}
}
}
if (minCanvasWidth && minCanvasHeight) {
if (minCanvasHeight * aspectRatio > minCanvasWidth) {
minCanvasHeight = minCanvasWidth / aspectRatio;
} else {
minCanvasWidth = minCanvasHeight * aspectRatio;
}
} else if (minCanvasWidth) {
minCanvasHeight = minCanvasWidth / aspectRatio;
} else if (minCanvasHeight) {
minCanvasWidth = minCanvasHeight * aspectRatio;
}
canvas.minWidth = minCanvasWidth;
canvas.minHeight = minCanvasHeight;
canvas.maxWidth = Infinity;
canvas.maxHeight = Infinity;
}
if (isPositionLimited) {
if (viewMode) {
newCanvasLeft = containerWidth - canvas.width;
newCanvasTop = containerHeight - canvas.height;
canvas.minLeft = min(0, newCanvasLeft);
canvas.minTop = min(0, newCanvasTop);
canvas.maxLeft = max(0, newCanvasLeft);
canvas.maxTop = max(0, newCanvasTop);
if (isCropped && this.isLimited) {
canvas.minLeft = min(
cropBox.left,
cropBox.left + cropBox.width - canvas.width
);
canvas.minTop = min(
cropBox.top,
cropBox.top + cropBox.height - canvas.height
);
canvas.maxLeft = cropBox.left;
canvas.maxTop = cropBox.top;
if (viewMode === 2) {
if (canvas.width >= containerWidth) {
canvas.minLeft = min(0, newCanvasLeft);
canvas.maxLeft = max(0, newCanvasLeft);
}
if (canvas.height >= containerHeight) {
canvas.minTop = min(0, newCanvasTop);
canvas.maxTop = max(0, newCanvasTop);
}
}
}
} else {
canvas.minLeft = -canvas.width;
canvas.minTop = -canvas.height;
canvas.maxLeft = containerWidth;
canvas.maxTop = containerHeight;
}
}
},
renderCanvas: function (isChanged) {
var canvas = this.canvas;
var image = this.image;
var rotate = image.rotate;
var naturalWidth = image.naturalWidth;
var naturalHeight = image.naturalHeight;
var aspectRatio;
var rotated;
if (this.isRotated) {
this.isRotated = false;
// Computes rotated sizes with image sizes
rotated = getRotatedSizes({
width: image.width,
height: image.height,
degree: rotate
});
aspectRatio = rotated.width / rotated.height;
if (aspectRatio !== canvas.aspectRatio) {
canvas.left -= (rotated.width - canvas.width) / 2;
canvas.top -= (rotated.height - canvas.height) / 2;
canvas.width = rotated.width;
canvas.height = rotated.height;
canvas.aspectRatio = aspectRatio;
canvas.naturalWidth = naturalWidth;
canvas.naturalHeight = naturalHeight;
// Computes rotated sizes with natural image sizes
if (rotate % 180) {
rotated = getRotatedSizes({
width: naturalWidth,
height: naturalHeight,
degree: rotate
});
canvas.naturalWidth = rotated.width;
canvas.naturalHeight = rotated.height;
}
this.limitCanvas(true, false);
}
}
if (canvas.width > canvas.maxWidth || canvas.width < canvas.minWidth) {
canvas.left = canvas.oldLeft;
}
if (canvas.height > canvas.maxHeight || canvas.height < canvas.minHeight) {
canvas.top = canvas.oldTop;
}
canvas.width = min(max(canvas.width, canvas.minWidth), canvas.maxWidth);
canvas.height = min(max(canvas.height, canvas.minHeight), canvas.maxHeight);
this.limitCanvas(false, true);
canvas.oldLeft = canvas.left = min(max(canvas.left, canvas.minLeft), canvas.maxLeft);
canvas.oldTop = canvas.top = min(max(canvas.top, canvas.minTop), canvas.maxTop);
this.$canvas.css({
width: canvas.width,
height: canvas.height,
left: canvas.left,
top: canvas.top
});
this.renderImage();
if (this.isCropped && this.isLimited) {
this.limitCropBox(true, true);
}
if (isChanged) {
this.output();
}
},
renderImage: function (isChanged) {
var canvas = this.canvas;
var image = this.image;
var reversed;
if (image.rotate) {
reversed = getRotatedSizes({
width: canvas.width,
height: canvas.height,
degree: image.rotate,
aspectRatio: image.aspectRatio
}, true);
}
$.extend(image, reversed ? {
width: reversed.width,
height: reversed.height,
left: (canvas.width - reversed.width) / 2,
top: (canvas.height - reversed.height) / 2
} : {
width: canvas.width,
height: canvas.height,
left: 0,
top: 0
});
this.$clone.css({
width: image.width,
height: image.height,
marginLeft: image.left,
marginTop: image.top,
transform: getTransform(image)
});
if (isChanged) {
this.output();
}
},
initCropBox: function () {
var options = this.options;
var canvas = this.canvas;
var aspectRatio = options.aspectRatio;
var autoCropArea = num(options.autoCropArea) || 0.8;
var cropBox = {
width: canvas.width,
height: canvas.height
};
if (aspectRatio) {
if (canvas.height * aspectRatio > canvas.width) {
cropBox.height = cropBox.width / aspectRatio;
} else {
cropBox.width = cropBox.height * aspectRatio;
}
}
this.cropBox = cropBox;
this.limitCropBox(true, true);
// Initialize auto crop area
cropBox.width = min(max(cropBox.width, cropBox.minWidth), cropBox.maxWidth);
cropBox.height = min(max(cropBox.height, cropBox.minHeight), cropBox.maxHeight);
// The width of auto crop area must large than "minWidth", and the height too. (#164)
cropBox.width = max(cropBox.minWidth, cropBox.width * autoCropArea);
cropBox.height = max(cropBox.minHeight, cropBox.height * autoCropArea);
cropBox.oldLeft = cropBox.left = canvas.left + (canvas.width - cropBox.width) / 2;
cropBox.oldTop = cropBox.top = canvas.top + (canvas.height - cropBox.height) / 2;
this.initialCropBox = $.extend({}, cropBox);
},
limitCropBox: function (isSizeLimited, isPositionLimited) {
var options = this.options;
var aspectRatio = options.aspectRatio;
var container = this.container;
var containerWidth = container.width;
var containerHeight = container.height;
var canvas = this.canvas;
var cropBox = this.cropBox;
var isLimited = this.isLimited;
var minCropBoxWidth;
var minCropBoxHeight;
var maxCropBoxWidth;
var maxCropBoxHeight;
if (isSizeLimited) {
minCropBoxWidth = num(options.minCropBoxWidth) || 0;
minCropBoxHeight = num(options.minCropBoxHeight) || 0;
// The min/maxCropBoxWidth/Height must be less than containerWidth/Height
minCropBoxWidth = min(minCropBoxWidth, containerWidth);
minCropBoxHeight = min(minCropBoxHeight, containerHeight);
maxCropBoxWidth = min(containerWidth, isLimited ? canvas.width : containerWidth);
maxCropBoxHeight = min(containerHeight, isLimited ? canvas.height : containerHeight);
if (aspectRatio) {
if (minCropBoxWidth && minCropBoxHeight) {
if (minCropBoxHeight * aspectRatio > minCropBoxWidth) {
minCropBoxHeight = minCropBoxWidth / aspectRatio;
} else {
minCropBoxWidth = minCropBoxHeight * aspectRatio;
}
} else if (minCropBoxWidth) {
minCropBoxHeight = minCropBoxWidth / aspectRatio;
} else if (minCropBoxHeight) {
minCropBoxWidth = minCropBoxHeight * aspectRatio;
}
if (maxCropBoxHeight * aspectRatio > maxCropBoxWidth) {
maxCropBoxHeight = maxCropBoxWidth / aspectRatio;
} else {
maxCropBoxWidth = maxCropBoxHeight * aspectRatio;
}
}
// The minWidth/Height must be less than maxWidth/Height
cropBox.minWidth = min(minCropBoxWidth, maxCropBoxWidth);
cropBox.minHeight = min(minCropBoxHeight, maxCropBoxHeight);
cropBox.maxWidth = maxCropBoxWidth;
cropBox.maxHeight = maxCropBoxHeight;
}
if (isPositionLimited) {
if (isLimited) {
cropBox.minLeft = max(0, canvas.left);
cropBox.minTop = max(0, canvas.top);
cropBox.maxLeft = min(containerWidth, canvas.left + canvas.width) - cropBox.width;
cropBox.maxTop = min(containerHeight, canvas.top + canvas.height) - cropBox.height;
} else {
cropBox.minLeft = 0;
cropBox.minTop = 0;
cropBox.maxLeft = containerWidth - cropBox.width;
cropBox.maxTop = containerHeight - cropBox.height;
}
}
},
renderCropBox: function () {
var options = this.options;
var container = this.container;
var containerWidth = container.width;
var containerHeight = container.height;
var cropBox = this.cropBox;
if (cropBox.width > cropBox.maxWidth || cropBox.width < cropBox.minWidth) {
cropBox.left = cropBox.oldLeft;
}
if (cropBox.height > cropBox.maxHeight || cropBox.height < cropBox.minHeight) {
cropBox.top = cropBox.oldTop;
}
cropBox.width = min(max(cropBox.width, cropBox.minWidth), cropBox.maxWidth);
cropBox.height = min(max(cropBox.height, cropBox.minHeight), cropBox.maxHeight);
this.limitCropBox(false, true);
cropBox.oldLeft = cropBox.left = min(max(cropBox.left, cropBox.minLeft), cropBox.maxLeft);
cropBox.oldTop = cropBox.top = min(max(cropBox.top, cropBox.minTop), cropBox.maxTop);
if (options.movable && options.cropBoxMovable) {
// Turn to move the canvas when the crop box is equal to the container
this.$face.data(DATA_ACTION, (cropBox.width === containerWidth && cropBox.height === containerHeight) ? ACTION_MOVE : ACTION_ALL);
}
this.$cropBox.css({
width: cropBox.width,
height: cropBox.height,
left: cropBox.left,
top: cropBox.top
});
if (this.isCropped && this.isLimited) {
this.limitCanvas(true, true);
}
if (!this.isDisabled) {
this.output();
}
},
output: function () {
this.preview();
if (this.isCompleted) {
this.trigger(EVENT_CROP, this.getData());
} else if (!this.isBuilt) {
// Only trigger one crop event before complete
this.$element.one(EVENT_BUILT, $.proxy(function () {
this.trigger(EVENT_CROP, this.getData());
}, this));
}
},
initPreview: function () {
var crossOrigin = getCrossOrigin(this.crossOrigin);
var url = crossOrigin ? this.crossOriginUrl : this.url;
this.$preview = $(this.options.preview);
this.$viewBox.html('<img' + crossOrigin + ' src="' + url + '">');
this.$preview.each(function () {
var $this = $(this);
// Save the original size for recover
$this.data(DATA_PREVIEW, {
width: $this.width(),
height: $this.height(),
html: $this.html()
});
/**
* Override img element styles
* Add `display:block` to avoid margin top issue
* (Occur only when margin-top <= -height)
*/
$this.html(
'<img' + crossOrigin + ' src="' + url + '" style="' +
'display:block;width:100%;height:auto;' +
'min-width:0!important;min-height:0!important;' +
'max-width:none!important;max-height:none!important;' +
'image-orientation:0deg!important;">'
);
});
},
resetPreview: function () {
this.$preview.each(function () {
var $this = $(this);
var data = $this.data(DATA_PREVIEW);
$this.css({
width: data.width,
height: data.height
}).html(data.html).removeData(DATA_PREVIEW);
});
},
preview: function () {
var image = this.image;
var canvas = this.canvas;
var cropBox = this.cropBox;
var cropBoxWidth = cropBox.width;
var cropBoxHeight = cropBox.height;
var width = image.width;
var height = image.height;
var left = cropBox.left - canvas.left - image.left;
var top = cropBox.top - canvas.top - image.top;
if (!this.isCropped || this.isDisabled) {
return;
}
this.$viewBox.find('img').css({
width: width,
height: height,
marginLeft: -left,
marginTop: -top,
transform: getTransform(image)
});
this.$preview.each(function () {
var $this = $(this);
var data = $this.data(DATA_PREVIEW);
var originalWidth = data.width;
var originalHeight = data.height;
var newWidth = originalWidth;
var newHeight = originalHeight;
var ratio = 1;
if (cropBoxWidth) {
ratio = originalWidth / cropBoxWidth;
newHeight = cropBoxHeight * ratio;
}
if (cropBoxHeight && newHeight > originalHeight) {
ratio = originalHeight / cropBoxHeight;
newWidth = cropBoxWidth * ratio;
newHeight = originalHeight;
}
$this.css({
width: newWidth,
height: newHeight
}).find('img').css({
width: width * ratio,
height: height * ratio,
marginLeft: -left * ratio,
marginTop: -top * ratio,
transform: getTransform(image)
});
});
},
bind: function () {
var options = this.options;
var $this = this.$element;
var $cropper = this.$cropper;
if ($.isFunction(options.cropstart)) {
$this.on(EVENT_CROP_START, options.cropstart);
}
if ($.isFunction(options.cropmove)) {
$this.on(EVENT_CROP_MOVE, options.cropmove);
}
if ($.isFunction(options.cropend)) {
$this.on(EVENT_CROP_END, options.cropend);
}
if ($.isFunction(options.crop)) {
$this.on(EVENT_CROP, options.crop);
}
if ($.isFunction(options.zoom)) {
$this.on(EVENT_ZOOM, options.zoom);
}
$cropper.on(EVENT_MOUSE_DOWN, $.proxy(this.cropStart, this));
if (options.zoomable && options.zoomOnWheel) {
$cropper.on(EVENT_WHEEL, $.proxy(this.wheel, this));
}
if (options.toggleDragModeOnDblclick) {
$cropper.on(EVENT_DBLCLICK, $.proxy(this.dblclick, this));
}
$document.
on(EVENT_MOUSE_MOVE, (this._cropMove = proxy(this.cropMove, this))).
on(EVENT_MOUSE_UP, (this._cropEnd = proxy(this.cropEnd, this)));
if (options.responsive) {
$window.on(EVENT_RESIZE, (this._resize = proxy(this.resize, this)));
}
},
unbind: function () {
var options = this.options;
var $this = this.$element;
var $cropper = this.$cropper;
if ($.isFunction(options.cropstart)) {
$this.off(EVENT_CROP_START, options.cropstart);
}
if ($.isFunction(options.cropmove)) {
$this.off(EVENT_CROP_MOVE, options.cropmove);
}
if ($.isFunction(options.cropend)) {
$this.off(EVENT_CROP_END, options.cropend);
}
if ($.isFunction(options.crop)) {
$this.off(EVENT_CROP, options.crop);
}
if ($.isFunction(options.zoom)) {
$this.off(EVENT_ZOOM, options.zoom);
}
$cropper.off(EVENT_MOUSE_DOWN, this.cropStart);
if (options.zoomable && options.zoomOnWheel) {
$cropper.off(EVENT_WHEEL, this.wheel);
}
if (options.toggleDragModeOnDblclick) {
$cropper.off(EVENT_DBLCLICK, this.dblclick);
}
$document.
off(EVENT_MOUSE_MOVE, this._cropMove).
off(EVENT_MOUSE_UP, this._cropEnd);
if (options.responsive) {
$window.off(EVENT_RESIZE, this._resize);
}
},
resize: function () {
var restore = this.options.restore;
var $container = this.$container;
var container = this.container;
var canvasData;
var cropBoxData;
var ratio;
// Check `container` is necessary for IE8
if (this.isDisabled || !container) {
return;
}
ratio = $container.width() / container.width;
// Resize when width changed or height changed
if (ratio !== 1 || $container.height() !== container.height) {
if (restore) {
canvasData = this.getCanvasData();
cropBoxData = this.getCropBoxData();
}
this.render();
if (restore) {
this.setCanvasData($.each(canvasData, function (i, n) {
canvasData[i] = n * ratio;
}));
this.setCropBoxData($.each(cropBoxData, function (i, n) {
cropBoxData[i] = n * ratio;
}));
}
}
},
dblclick: function () {
if (this.isDisabled) {
return;
}
if (this.$dragBox.hasClass(CLASS_CROP)) {
this.setDragMode(ACTION_MOVE);
} else {
this.setDragMode(ACTION_CROP);
}
},
wheel: function (event) {
var e = event.originalEvent || event;
var ratio = num(this.options.wheelZoomRatio) || 0.1;
var delta = 1;
if (this.isDisabled) {
return;
}
event.preventDefault();
// Limit wheel speed to prevent zoom too fast
if (this.wheeling) {
return;
}
this.wheeling = true;
setTimeout($.proxy(function () {
this.wheeling = false;
}, this), 50);
if (e.deltaY) {
delta = e.deltaY > 0 ? 1 : -1;
} else if (e.wheelDelta) {
delta = -e.wheelDelta / 120;
} else if (e.detail) {
delta = e.detail > 0 ? 1 : -1;
}
this.zoom(-delta * ratio, event);
},
cropStart: function (event) {
var options = this.options;
var originalEvent = event.originalEvent;
var touches = originalEvent && originalEvent.touches;
var e = event;
var touchesLength;
var action;
if (this.isDisabled) {
return;
}
if (touches) {
touchesLength = touches.length;
if (touchesLength > 1) {
if (options.zoomable && options.zoomOnTouch && touchesLength === 2) {
e = touches[1];
this.startX2 = e.pageX;
this.startY2 = e.pageY;
action = ACTION_ZOOM;
} else {
return;
}
}
e = touches[0];
}
action = action || $(e.target).data(DATA_ACTION);
if (REGEXP_ACTIONS.test(action)) {
if (this.trigger(EVENT_CROP_START, {
originalEvent: originalEvent,
action: action
}).isDefaultPrevented()) {
return;
}
event.preventDefault();
this.action = action;
this.cropping = false;
// IE8 has `event.pageX/Y`, but not `event.originalEvent.pageX/Y`
// IE10 has `event.originalEvent.pageX/Y`, but not `event.pageX/Y`
this.startX = e.pageX || originalEvent && originalEvent.pageX;
this.startY = e.pageY || originalEvent && originalEvent.pageY;
if (action === ACTION_CROP) {
this.cropping = true;
this.$dragBox.addClass(CLASS_MODAL);
}
}
},
cropMove: function (event) {
var options = this.options;
var originalEvent = event.originalEvent;
var touches = originalEvent && originalEvent.touches;
var e = event;
var action = this.action;
var touchesLength;
if (this.isDisabled) {
return;
}
if (touches) {
touchesLength = touches.length;
if (touchesLength > 1) {
if (options.zoomable && options.zoomOnTouch && touchesLength === 2) {
e = touches[1];
this.endX2 = e.pageX;
this.endY2 = e.pageY;
} else {
return;
}
}
e = touches[0];
}
if (action) {
if (this.trigger(EVENT_CROP_MOVE, {
originalEvent: originalEvent,
action: action
}).isDefaultPrevented()) {
return;
}
event.preventDefault();
this.endX = e.pageX || originalEvent && originalEvent.pageX;
this.endY = e.pageY || originalEvent && originalEvent.pageY;
this.change(e.shiftKey, action === ACTION_ZOOM ? event : null);
}
},
cropEnd: function (event) {
var originalEvent = event.originalEvent;
var action = this.action;
if (this.isDisabled) {
return;
}
if (action) {
event.preventDefault();
if (this.cropping) {
this.cropping = false;
this.$dragBox.toggleClass(CLASS_MODAL, this.isCropped && this.options.modal);
}
this.action = '';
this.trigger(EVENT_CROP_END, {
originalEvent: originalEvent,
action: action
});
}
},
change: function (shiftKey, event) {
var options = this.options;
var aspectRatio = options.aspectRatio;
var action = this.action;
var container = this.container;
var canvas = this.canvas;
var cropBox = this.cropBox;
var width = cropBox.width;
var height = cropBox.height;
var left = cropBox.left;
var top = cropBox.top;
var right = left + width;
var bottom = top + height;
var minLeft = 0;
var minTop = 0;
var maxWidth = container.width;
var maxHeight = container.height;
var renderable = true;
var offset;
var range;
// Locking aspect ratio in "free mode" by holding shift key (#259)
if (!aspectRatio && shiftKey) {
aspectRatio = width && height ? width / height : 1;
}
if (this.limited) {
minLeft = cropBox.minLeft;
minTop = cropBox.minTop;
maxWidth = minLeft + min(container.width, canvas.width);
maxHeight = minTop + min(container.height, canvas.height);
}
range = {
x: this.endX - this.startX,
y: this.endY - this.startY
};
if (aspectRatio) {
range.X = range.y * aspectRatio;
range.Y = range.x / aspectRatio;
}
switch (action) {
// Move crop box
case ACTION_ALL:
left += range.x;
top += range.y;
break;
// Resize crop box
case ACTION_EAST:
if (range.x >= 0 && (right >= maxWidth || aspectRatio &&
(top <= minTop || bottom >= maxHeight))) {
renderable = false;
break;
}
width += range.x;
if (aspectRatio) {
height = width / aspectRatio;
top -= range.Y / 2;
}
if (width < 0) {
action = ACTION_WEST;
width = 0;
}
break;
case ACTION_NORTH:
if (range.y <= 0 && (top <= minTop || aspectRatio &&
(left <= minLeft || right >= maxWidth))) {
renderable = false;
break;
}
height -= range.y;
top += range.y;
if (aspectRatio) {
width = height * aspectRatio;
left += range.X / 2;
}
if (height < 0) {
action = ACTION_SOUTH;
height = 0;
}
break;
case ACTION_WEST:
if (range.x <= 0 && (left <= minLeft || aspectRatio &&
(top <= minTop || bottom >= maxHeight))) {
renderable = false;
break;
}
width -= range.x;
left += range.x;
if (aspectRatio) {
height = width / aspectRatio;
top += range.Y / 2;
}
if (width < 0) {
action = ACTION_EAST;
width = 0;
}
break;
case ACTION_SOUTH:
if (range.y >= 0 && (bottom >= maxHeight || aspectRatio &&
(left <= minLeft || right >= maxWidth))) {
renderable = false;
break;
}
height += range.y;
if (aspectRatio) {
width = height * aspectRatio;
left -= range.X / 2;
}
if (height < 0) {
action = ACTION_NORTH;
height = 0;
}
break;
case ACTION_NORTH_EAST:
if (aspectRatio) {
if (range.y <= 0 && (top <= minTop || right >= maxWidth)) {
renderable = false;
break;
}
height -= range.y;
top += range.y;
width = height * aspectRatio;
} else {
if (range.x >= 0) {
if (right < maxWidth) {
width += range.x;
} else if (range.y <= 0 && top <= minTop) {
renderable = false;
}
} else {
width += range.x;
}
if (range.y <= 0) {
if (top > minTop) {
height -= range.y;
top += range.y;
}
} else {
height -= range.y;
top += range.y;
}
}
if (width < 0 && height < 0) {
action = ACTION_SOUTH_WEST;
height = 0;
width = 0;
} else if (width < 0) {
action = ACTION_NORTH_WEST;
width = 0;
} else if (height < 0) {
action = ACTION_SOUTH_EAST;
height = 0;
}
break;
case ACTION_NORTH_WEST:
if (aspectRatio) {
if (range.y <= 0 && (top <= minTop || left <= minLeft)) {
renderable = false;
break;
}
height -= range.y;
top += range.y;
width = height * aspectRatio;
left += range.X;
} else {
if (range.x <= 0) {
if (left > minLeft) {
width -= range.x;
left += range.x;
} else if (range.y <= 0 && top <= minTop) {
renderable = false;
}
} else {
width -= range.x;
left += range.x;
}
if (range.y <= 0) {
if (top > minTop) {
height -= range.y;
top += range.y;
}
} else {
height -= range.y;
top += range.y;
}
}
if (width < 0 && height < 0) {
action = ACTION_SOUTH_EAST;
height = 0;
width = 0;
} else if (width < 0) {
action = ACTION_NORTH_EAST;
width = 0;
} else if (height < 0) {
action = ACTION_SOUTH_WEST;
height = 0;
}
break;
case ACTION_SOUTH_WEST:
if (aspectRatio) {
if (range.x <= 0 && (left <= minLeft || bottom >= maxHeight)) {
renderable = false;
break;
}
width -= range.x;
left += range.x;
height = width / aspectRatio;
} else {
if (range.x <= 0) {
if (left > minLeft) {
width -= range.x;
left += range.x;
} else if (range.y >= 0 && bottom >= maxHeight) {
renderable = false;
}
} else {
width -= range.x;
left += range.x;
}
if (range.y >= 0) {
if (bottom < maxHeight) {
height += range.y;
}
} else {
height += range.y;
}
}
if (width < 0 && height < 0) {
action = ACTION_NORTH_EAST;
height = 0;
width = 0;
} else if (width < 0) {
action = ACTION_SOUTH_EAST;
width = 0;
} else if (height < 0) {
action = ACTION_NORTH_WEST;
height = 0;
}
break;
case ACTION_SOUTH_EAST:
if (aspectRatio) {
if (range.x >= 0 && (right >= maxWidth || bottom >= maxHeight)) {
renderable = false;
break;
}
width += range.x;
height = width / aspectRatio;
} else {
if (range.x >= 0) {
if (right < maxWidth) {
width += range.x;
} else if (range.y >= 0 && bottom >= maxHeight) {
renderable = false;
}
} else {
width += range.x;
}
if (range.y >= 0) {
if (bottom < maxHeight) {
height += range.y;
}
} else {
height += range.y;
}
}
if (width < 0 && height < 0) {
action = ACTION_NORTH_WEST;
height = 0;
width = 0;
} else if (width < 0) {
action = ACTION_SOUTH_WEST;
width = 0;
} else if (height < 0) {
action = ACTION_NORTH_EAST;
height = 0;
}
break;
// Move canvas
case ACTION_MOVE:
this.move(range.x, range.y);
renderable = false;
break;
// Zoom canvas
case ACTION_ZOOM:
this.zoom((function (x1, y1, x2, y2) {
var z1 = sqrt(x1 * x1 + y1 * y1);
var z2 = sqrt(x2 * x2 + y2 * y2);
return (z2 - z1) / z1;
})(
abs(this.startX - this.startX2),
abs(this.startY - this.startY2),
abs(this.endX - this.endX2),
abs(this.endY - this.endY2)
), event);
this.startX2 = this.endX2;
this.startY2 = this.endY2;
renderable = false;
break;
// Create crop box
case ACTION_CROP:
if (!range.x || !range.y) {
renderable = false;
break;
}
offset = this.$cropper.offset();
left = this.startX - offset.left;
top = this.startY - offset.top;
width = cropBox.minWidth;
height = cropBox.minHeight;
if (range.x > 0) {
action = range.y > 0 ? ACTION_SOUTH_EAST : ACTION_NORTH_EAST;
} else if (range.x < 0) {
left -= width;
action = range.y > 0 ? ACTION_SOUTH_WEST : ACTION_NORTH_WEST;
}
if (range.y < 0) {
top -= height;
}
// Show the crop box if is hidden
if (!this.isCropped) {
this.$cropBox.removeClass(CLASS_HIDDEN);
this.isCropped = true;
if (this.limited) {
this.limitCropBox(true, true);
}
}
break;
// No default
}
if (renderable) {
cropBox.width = width;
cropBox.height = height;
cropBox.left = left;
cropBox.top = top;
this.action = action;
this.renderCropBox();
}
// Override
this.startX = this.endX;
this.startY = this.endY;
},
// Show the crop box manually
crop: function () {
if (!this.isBuilt || this.isDisabled) {
return;
}
if (!this.isCropped) {
this.isCropped = true;
this.limitCropBox(true, true);
if (this.options.modal) {
this.$dragBox.addClass(CLASS_MODAL);
}
this.$cropBox.removeClass(CLASS_HIDDEN);
}
this.setCropBoxData(this.initialCropBox);
},
// Reset the image and crop box to their initial states
reset: function () {
if (!this.isBuilt || this.isDisabled) {
return;
}
this.image = $.extend({}, this.initialImage);
this.canvas = $.extend({}, this.initialCanvas);
this.cropBox = $.extend({}, this.initialCropBox);
this.renderCanvas();
if (this.isCropped) {
this.renderCropBox();
}
},
// Clear the crop box
clear: function () {
if (!this.isCropped || this.isDisabled) {
return;
}
$.extend(this.cropBox, {
left: 0,
top: 0,
width: 0,
height: 0
});
this.isCropped = false;
this.renderCropBox();
this.limitCanvas(true, true);
// Render canvas after crop box rendered
this.renderCanvas();
this.$dragBox.removeClass(CLASS_MODAL);
this.$cropBox.addClass(CLASS_HIDDEN);
},
/**
* Replace the image's src and rebuild the cropper
*
* @param {String} url
*/
replace: function (url) {
if (!this.isDisabled && url) {
if (this.isImg) {
this.isReplaced = true;
this.$element.attr('src', url);
}
// Clear previous data
this.options.data = null;
this.load(url);
}
},
// Enable (unfreeze) the cropper
enable: function () {
if (this.isBuilt) {
this.isDisabled = false;
this.$cropper.removeClass(CLASS_DISABLED);
}
},
// Disable (freeze) the cropper
disable: function () {
if (this.isBuilt) {
this.isDisabled = true;
this.$cropper.addClass(CLASS_DISABLED);
}
},
// Destroy the cropper and remove the instance from the image
destroy: function () {
var $this = this.$element;
if (this.isLoaded) {
if (this.isImg && this.isReplaced) {
$this.attr('src', this.originalUrl);
}
this.unbuild();
$this.removeClass(CLASS_HIDDEN);
} else {
if (this.isImg) {
$this.off(EVENT_LOAD, this.start);
} else if (this.$clone) {
this.$clone.remove();
}
}
$this.removeData(NAMESPACE);
},
/**
* Move the canvas with relative offsets
*
* @param {Number} offsetX
* @param {Number} offsetY (optional)
*/
move: function (offsetX, offsetY) {
var canvas = this.canvas;
this.moveTo(
isUndefined(offsetX) ? offsetX : canvas.left + num(offsetX),
isUndefined(offsetY) ? offsetY : canvas.top + num(offsetY)
);
},
/**
* Move the canvas to an absolute point
*
* @param {Number} x
* @param {Number} y (optional)
*/
moveTo: function (x, y) {
var canvas = this.canvas;
var isChanged = false;
// If "y" is not present, its default value is "x"
if (isUndefined(y)) {
y = x;
}
x = num(x);
y = num(y);
if (this.isBuilt && !this.isDisabled && this.options.movable) {
if (isNumber(x)) {
canvas.left = x;
isChanged = true;
}
if (isNumber(y)) {
canvas.top = y;
isChanged = true;
}
if (isChanged) {
this.renderCanvas(true);
}
}
},
/**
* Zoom the canvas with a relative ratio
*
* @param {Number} ratio
* @param {jQuery Event} _event (private)
*/
zoom: function (ratio, _event) {
var canvas = this.canvas;
ratio = num(ratio);
if (ratio < 0) {
ratio = 1 / (1 - ratio);
} else {
ratio = 1 + ratio;
}
this.zoomTo(canvas.width * ratio / canvas.naturalWidth, _event);
},
/**
* Zoom the canvas to an absolute ratio
*
* @param {Number} ratio
* @param {jQuery Event} _event (private)
*/
zoomTo: function (ratio, _event) {
var options = this.options;
var canvas = this.canvas;
var width = canvas.width;
var height = canvas.height;
var naturalWidth = canvas.naturalWidth;
var naturalHeight = canvas.naturalHeight;
var originalEvent;
var newWidth;
var newHeight;
var offset;
var center;
ratio = num(ratio);
if (ratio >= 0 && this.isBuilt && !this.isDisabled && options.zoomable) {
newWidth = naturalWidth * ratio;
newHeight = naturalHeight * ratio;
if (_event) {
originalEvent = _event.originalEvent;
}
if (this.trigger(EVENT_ZOOM, {
originalEvent: originalEvent,
oldRatio: width / naturalWidth,
ratio: newWidth / naturalWidth
}).isDefaultPrevented()) {
return;
}
if (originalEvent) {
offset = this.$cropper.offset();
center = originalEvent.touches ? getTouchesCenter(originalEvent.touches) : {
pageX: _event.pageX || originalEvent.pageX || 0,
pageY: _event.pageY || originalEvent.pageY || 0
};
// Zoom from the triggering point of the event
canvas.left -= (newWidth - width) * (
((center.pageX - offset.left) - canvas.left) / width
);
canvas.top -= (newHeight - height) * (
((center.pageY - offset.top) - canvas.top) / height
);
} else {
// Zoom from the center of the canvas
canvas.left -= (newWidth - width) / 2;
canvas.top -= (newHeight - height) / 2;
}
canvas.width = newWidth;
canvas.height = newHeight;
this.renderCanvas(true);
}
},
/**
* Rotate the canvas with a relative degree
*
* @param {Number} degree
*/
rotate: function (degree) {
this.rotateTo((this.image.rotate || 0) + num(degree));
},
/**
* Rotate the canvas to an absolute degree
* https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function#rotate()
*
* @param {Number} degree
*/
rotateTo: function (degree) {
degree = num(degree);
if (isNumber(degree) && this.isBuilt && !this.isDisabled && this.options.rotatable) {
this.image.rotate = degree % 360;
this.isRotated = true;
this.renderCanvas(true);
}
},
/**
* Scale the image
* https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function#scale()
*
* @param {Number} scaleX
* @param {Number} scaleY (optional)
*/
scale: function (scaleX, scaleY) {
var image = this.image;
var isChanged = false;
// If "scaleY" is not present, its default value is "scaleX"
if (isUndefined(scaleY)) {
scaleY = scaleX;
}
scaleX = num(scaleX);
scaleY = num(scaleY);
if (this.isBuilt && !this.isDisabled && this.options.scalable) {
if (isNumber(scaleX)) {
image.scaleX = scaleX;
isChanged = true;
}
if (isNumber(scaleY)) {
image.scaleY = scaleY;
isChanged = true;
}
if (isChanged) {
this.renderImage(true);
}
}
},
/**
* Scale the abscissa of the image
*
* @param {Number} scaleX
*/
scaleX: function (scaleX) {
var scaleY = this.image.scaleY;
this.scale(scaleX, isNumber(scaleY) ? scaleY : 1);
},
/**
* Scale the ordinate of the image
*
* @param {Number} scaleY
*/
scaleY: function (scaleY) {
var scaleX = this.image.scaleX;
this.scale(isNumber(scaleX) ? scaleX : 1, scaleY);
},
/**
* Get the cropped area position and size data (base on the original image)
*
* @param {Boolean} isRounded (optional)
* @return {Object} data
*/
getData: function (isRounded) {
var options = this.options;
var image = this.image;
var canvas = this.canvas;
var cropBox = this.cropBox;
var ratio;
var data;
if (this.isBuilt && this.isCropped) {
data = {
x: cropBox.left - canvas.left,
y: cropBox.top - canvas.top,
width: cropBox.width,
height: cropBox.height
};
ratio = image.width / image.naturalWidth;
$.each(data, function (i, n) {
n = n / ratio;
data[i] = isRounded ? round(n) : n;
});
} else {
data = {
x: 0,
y: 0,
width: 0,
height: 0
};
}
if (options.rotatable) {
data.rotate = image.rotate || 0;
}
if (options.scalable) {
data.scaleX = image.scaleX || 1;
data.scaleY = image.scaleY || 1;
}
return data;
},
/**
* Set the cropped area position and size with new data
*
* @param {Object} data
*/
setData: function (data) {
var options = this.options;
var image = this.image;
var canvas = this.canvas;
var cropBoxData = {};
var isRotated;
var isScaled;
var ratio;
if ($.isFunction(data)) {
data = data.call(this.element);
}
if (this.isBuilt && !this.isDisabled && $.isPlainObject(data)) {
if (options.rotatable) {
if (isNumber(data.rotate) && data.rotate !== image.rotate) {
image.rotate = data.rotate;
this.isRotated = isRotated = true;
}
}
if (options.scalable) {
if (isNumber(data.scaleX) && data.scaleX !== image.scaleX) {
image.scaleX = data.scaleX;
isScaled = true;
}
if (isNumber(data.scaleY) && data.scaleY !== image.scaleY) {
image.scaleY = data.scaleY;
isScaled = true;
}
}
if (isRotated) {
this.renderCanvas();
} else if (isScaled) {
this.renderImage();
}
ratio = image.width / image.naturalWidth;
if (isNumber(data.x)) {
cropBoxData.left = data.x * ratio + canvas.left;
}
if (isNumber(data.y)) {
cropBoxData.top = data.y * ratio + canvas.top;
}
if (isNumber(data.width)) {
cropBoxData.width = data.width * ratio;
}
if (isNumber(data.height)) {
cropBoxData.height = data.height * ratio;
}
this.setCropBoxData(cropBoxData);
}
},
/**
* Get the container size data
*
* @return {Object} data
*/
getContainerData: function () {
return this.isBuilt ? this.container : {};
},
/**
* Get the image position and size data
*
* @return {Object} data
*/
getImageData: function () {
return this.isLoaded ? this.image : {};
},
/**
* Get the canvas position and size data
*
* @return {Object} data
*/
getCanvasData: function () {
var canvas = this.canvas;
var data = {};
if (this.isBuilt) {
$.each([
'left',
'top',
'width',
'height',
'naturalWidth',
'naturalHeight'
], function (i, n) {
data[n] = canvas[n];
});
}
return data;
},
/**
* Set the canvas position and size with new data
*
* @param {Object} data
*/
setCanvasData: function (data) {
var canvas = this.canvas;
var aspectRatio = canvas.aspectRatio;
if ($.isFunction(data)) {
data = data.call(this.$element);
}
if (this.isBuilt && !this.isDisabled && $.isPlainObject(data)) {
if (isNumber(data.left)) {
canvas.left = data.left;
}
if (isNumber(data.top)) {
canvas.top = data.top;
}
if (isNumber(data.width)) {
canvas.width = data.width;
canvas.height = data.width / aspectRatio;
} else if (isNumber(data.height)) {
canvas.height = data.height;
canvas.width = data.height * aspectRatio;
}
this.renderCanvas(true);
}
},
/**
* Get the crop box position and size data
*
* @return {Object} data
*/
getCropBoxData: function () {
var cropBox = this.cropBox;
var data;
if (this.isBuilt && this.isCropped) {
data = {
left: cropBox.left,
top: cropBox.top,
width: cropBox.width,
height: cropBox.height
};
}
return data || {};
},
/**
* Set the crop box position and size with new data
*
* @param {Object} data
*/
setCropBoxData: function (data) {
var cropBox = this.cropBox;
var aspectRatio = this.options.aspectRatio;
var isWidthChanged;
var isHeightChanged;
if ($.isFunction(data)) {
data = data.call(this.$element);
}
if (this.isBuilt && this.isCropped && !this.isDisabled && $.isPlainObject(data)) {
if (isNumber(data.left)) {
cropBox.left = data.left;
}
if (isNumber(data.top)) {
cropBox.top = data.top;
}
if (isNumber(data.width)) {
isWidthChanged = true;
cropBox.width = data.width;
}
if (isNumber(data.height)) {
isHeightChanged = true;
cropBox.height = data.height;
}
if (aspectRatio) {
if (isWidthChanged) {
cropBox.height = cropBox.width / aspectRatio;
} else if (isHeightChanged) {
cropBox.width = cropBox.height * aspectRatio;
}
}
this.renderCropBox();
}
},
/**
* Get a canvas drawn the cropped image
*
* @param {Object} options (optional)
* @return {HTMLCanvasElement} canvas
*/
getCroppedCanvas: function (options) {
var originalWidth;
var originalHeight;
var canvasWidth;
var canvasHeight;
var scaledWidth;
var scaledHeight;
var scaledRatio;
var aspectRatio;
var canvas;
var context;
var data;
if (!this.isBuilt || !this.isCropped || !SUPPORT_CANVAS) {
return;
}
if (!$.isPlainObject(options)) {
options = {};
}
data = this.getData();
originalWidth = data.width;
originalHeight = data.height;
aspectRatio = originalWidth / originalHeight;
if ($.isPlainObject(options)) {
scaledWidth = options.width;
scaledHeight = options.height;
if (scaledWidth) {
scaledHeight = scaledWidth / aspectRatio;
scaledRatio = scaledWidth / originalWidth;
} else if (scaledHeight) {
scaledWidth = scaledHeight * aspectRatio;
scaledRatio = scaledHeight / originalHeight;
}
}
// The canvas element will use `Math.floor` on a float number, so floor first
canvasWidth = floor(scaledWidth || originalWidth);
canvasHeight = floor(scaledHeight || originalHeight);
canvas = $('<canvas>')[0];
canvas.width = canvasWidth;
canvas.height = canvasHeight;
context = canvas.getContext('2d');
if (options.fillColor) {
context.fillStyle = options.fillColor;
context.fillRect(0, 0, canvasWidth, canvasHeight);
}
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.drawImage
context.drawImage.apply(context, (function () {
var source = getSourceCanvas(this.$clone[0], this.image);
var sourceWidth = source.width;
var sourceHeight = source.height;
var args = [source];
// Source canvas
var srcX = data.x;
var srcY = data.y;
var srcWidth;
var srcHeight;
// Destination canvas
var dstX;
var dstY;
var dstWidth;
var dstHeight;
if (srcX <= -originalWidth || srcX > sourceWidth) {
srcX = srcWidth = dstX = dstWidth = 0;
} else if (srcX <= 0) {
dstX = -srcX;
srcX = 0;
srcWidth = dstWidth = min(sourceWidth, originalWidth + srcX);
} else if (srcX <= sourceWidth) {
dstX = 0;
srcWidth = dstWidth = min(originalWidth, sourceWidth - srcX);
}
if (srcWidth <= 0 || srcY <= -originalHeight || srcY > sourceHeight) {
srcY = srcHeight = dstY = dstHeight = 0;
} else if (srcY <= 0) {
dstY = -srcY;
srcY = 0;
srcHeight = dstHeight = min(sourceHeight, originalHeight + srcY);
} else if (srcY <= sourceHeight) {
dstY = 0;
srcHeight = dstHeight = min(originalHeight, sourceHeight - srcY);
}
// All the numerical parameters should be integer for `drawImage` (#476)
args.push(floor(srcX), floor(srcY), floor(srcWidth), floor(srcHeight));
// Scale destination sizes
if (scaledRatio) {
dstX *= scaledRatio;
dstY *= scaledRatio;
dstWidth *= scaledRatio;
dstHeight *= scaledRatio;
}
// Avoid "IndexSizeError" in IE and Firefox
if (dstWidth > 0 && dstHeight > 0) {
args.push(floor(dstX), floor(dstY), floor(dstWidth), floor(dstHeight));
}
return args;
}).call(this));
return canvas;
},
/**
* Change the aspect ratio of the crop box
*
* @param {Number} aspectRatio
*/
setAspectRatio: function (aspectRatio) {
var options = this.options;
if (!this.isDisabled && !isUndefined(aspectRatio)) {
// 0 -> NaN
options.aspectRatio = max(0, aspectRatio) || NaN;
if (this.isBuilt) {
this.initCropBox();
if (this.isCropped) {
this.renderCropBox();
}
}
}
},
/**
* Change the drag mode
*
* @param {String} mode (optional)
*/
setDragMode: function (mode) {
var options = this.options;
var croppable;
var movable;
if (this.isLoaded && !this.isDisabled) {
croppable = mode === ACTION_CROP;
movable = options.movable && mode === ACTION_MOVE;
mode = (croppable || movable) ? mode : ACTION_NONE;
this.$dragBox.
data(DATA_ACTION, mode).
toggleClass(CLASS_CROP, croppable).
toggleClass(CLASS_MOVE, movable);
if (!options.cropBoxMovable) {
// Sync drag mode to crop box when it is not movable(#300)
this.$face.
data(DATA_ACTION, mode).
toggleClass(CLASS_CROP, croppable).
toggleClass(CLASS_MOVE, movable);
}
}
}
};
Cropper.DEFAULTS = {
// Define the view mode of the cropper
viewMode: 0, // 0, 1, 2, 3
// Define the dragging mode of the cropper
dragMode: 'crop', // 'crop', 'move' or 'none'
// Define the aspect ratio of the crop box
aspectRatio: NaN,
// An object with the previous cropping result data
data: null,
// A jQuery selector for adding extra containers to preview
preview: '',
// Re-render the cropper when resize the window
responsive: true,
// Restore the cropped area after resize the window
restore: true,
// Check if the current image is a cross-origin image
checkCrossOrigin: true,
// Check the current image's Exif Orientation information
checkOrientation: true,
// Show the black modal
modal: true,
// Show the dashed lines for guiding
guides: true,
// Show the center indicator for guiding
center: true,
// Show the white modal to highlight the crop box
highlight: true,
// Show the grid background
background: true,
// Enable to crop the image automatically when initialize
autoCrop: true,
// Define the percentage of automatic cropping area when initializes
autoCropArea: 0.8,
// Enable to move the image
movable: true,
// Enable to rotate the image
rotatable: true,
// Enable to scale the image
scalable: true,
// Enable to zoom the image
zoomable: true,
// Enable to zoom the image by dragging touch
zoomOnTouch: true,
// Enable to zoom the image by wheeling mouse
zoomOnWheel: true,
// Define zoom ratio when zoom the image by wheeling mouse
wheelZoomRatio: 0.1,
// Enable to move the crop box
cropBoxMovable: true,
// Enable to resize the crop box
cropBoxResizable: true,
// Toggle drag mode between "crop" and "move" when click twice on the cropper
toggleDragModeOnDblclick: true,
// Size limitation
minCanvasWidth: 0,
minCanvasHeight: 0,
minCropBoxWidth: 0,
minCropBoxHeight: 0,
minContainerWidth: 200,
minContainerHeight: 100,
// Shortcuts of events
build: null,
built: null,
cropstart: null,
cropmove: null,
cropend: null,
crop: null,
zoom: null
};
Cropper.setDefaults = function (options) {
$.extend(Cropper.DEFAULTS, options);
};
Cropper.TEMPLATE = (
'<div class="cropper-container">' +
'<div class="cropper-wrap-box">' +
'<div class="cropper-canvas"></div>' +
'</div>' +
'<div class="cropper-drag-box"></div>' +
'<div class="cropper-crop-box">' +
'<span class="cropper-view-box"></span>' +
'<span class="cropper-dashed dashed-h"></span>' +
'<span class="cropper-dashed dashed-v"></span>' +
'<span class="cropper-center"></span>' +
'<span class="cropper-face"></span>' +
'<span class="cropper-line line-e" data-action="e"></span>' +
'<span class="cropper-line line-n" data-action="n"></span>' +
'<span class="cropper-line line-w" data-action="w"></span>' +
'<span class="cropper-line line-s" data-action="s"></span>' +
'<span class="cropper-point point-e" data-action="e"></span>' +
'<span class="cropper-point point-n" data-action="n"></span>' +
'<span class="cropper-point point-w" data-action="w"></span>' +
'<span class="cropper-point point-s" data-action="s"></span>' +
'<span class="cropper-point point-ne" data-action="ne"></span>' +
'<span class="cropper-point point-nw" data-action="nw"></span>' +
'<span class="cropper-point point-sw" data-action="sw"></span>' +
'<span class="cropper-point point-se" data-action="se"></span>' +
'</div>' +
'</div>'
);
// Save the other cropper
Cropper.other = $.fn.cropper;
// Register as jQuery plugin
$.fn.cropper = function (option) {
var args = toArray(arguments, 1);
var result;
this.each(function () {
var $this = $(this);
var data = $this.data(NAMESPACE);
var options;
var fn;
if (!data) {
if (/destroy/.test(option)) {
return;
}
options = $.extend({}, $this.data(), $.isPlainObject(option) && option);
$this.data(NAMESPACE, (data = new Cropper(this, options)));
}
if (typeof option === 'string' && $.isFunction(fn = data[option])) {
result = fn.apply(data, args);
}
});
return isUndefined(result) ? this : result;
};
$.fn.cropper.Constructor = Cropper;
$.fn.cropper.setDefaults = Cropper.setDefaults;
// No conflict
$.fn.cropper.noConflict = function () {
$.fn.cropper = Cropper.other;
return this;
};
});
/*!
* Cropper v2.2.5
* https://github.com/fengyuanchen/cropper
*
* Copyright (c) 2014-2016 Fengyuan Chen and contributors
* Released under the MIT license
*
* Date: 2016-01-18T05:42:29.639Z
*/
.cropper-container {
font-size: 0;
line-height: 0;
position: relative;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
direction: ltr !important;
-ms-touch-action: none;
touch-action: none;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
}
.cropper-container img {
display: block;
width: 100%;
min-width: 0 !important;
max-width: none !important;
height: 100%;
min-height: 0 !important;
max-height: none !important;
image-orientation: 0deg !important;
}
.cropper-wrap-box,
.cropper-canvas,
.cropper-drag-box,
.cropper-crop-box,
.cropper-modal {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.cropper-wrap-box {
overflow: hidden;
}
.cropper-drag-box {
opacity: 0;
background-color: #fff;
filter: alpha(opacity=0);
}
.cropper-modal {
opacity: .5;
background-color: #000;
filter: alpha(opacity=50);
}
.cropper-view-box {
display: block;
overflow: hidden;
width: 100%;
height: 100%;
outline: 1px solid #39f;
outline-color: rgba(51, 153, 255, .75);
}
.cropper-dashed {
position: absolute;
display: block;
opacity: .5;
border: 0 dashed #eee;
filter: alpha(opacity=50);
}
.cropper-dashed.dashed-h {
top: 33.33333%;
left: 0;
width: 100%;
height: 33.33333%;
border-top-width: 1px;
border-bottom-width: 1px;
}
.cropper-dashed.dashed-v {
top: 0;
left: 33.33333%;
width: 33.33333%;
height: 100%;
border-right-width: 1px;
border-left-width: 1px;
}
.cropper-center {
position: absolute;
top: 50%;
left: 50%;
display: block;
width: 0;
height: 0;
opacity: .75;
filter: alpha(opacity=75);
}
.cropper-center:before,
.cropper-center:after {
position: absolute;
display: block;
content: ' ';
background-color: #eee;
}
.cropper-center:before {
top: 0;
left: -3px;
width: 7px;
height: 1px;
}
.cropper-center:after {
top: -3px;
left: 0;
width: 1px;
height: 7px;
}
.cropper-face,
.cropper-line,
.cropper-point {
position: absolute;
display: block;
width: 100%;
height: 100%;
opacity: .1;
filter: alpha(opacity=10);
}
.cropper-face {
top: 0;
left: 0;
background-color: #fff;
}
.cropper-line {
background-color: #39f;
}
.cropper-line.line-e {
top: 0;
right: -3px;
width: 5px;
cursor: e-resize;
}
.cropper-line.line-n {
top: -3px;
left: 0;
height: 5px;
cursor: n-resize;
}
.cropper-line.line-w {
top: 0;
left: -3px;
width: 5px;
cursor: w-resize;
}
.cropper-line.line-s {
bottom: -3px;
left: 0;
height: 5px;
cursor: s-resize;
}
.cropper-point {
width: 5px;
height: 5px;
opacity: .75;
background-color: #39f;
filter: alpha(opacity=75);
}
.cropper-point.point-e {
top: 50%;
right: -3px;
margin-top: -3px;
cursor: e-resize;
}
.cropper-point.point-n {
top: -3px;
left: 50%;
margin-left: -3px;
cursor: n-resize;
}
.cropper-point.point-w {
top: 50%;
left: -3px;
margin-top: -3px;
cursor: w-resize;
}
.cropper-point.point-s {
bottom: -3px;
left: 50%;
margin-left: -3px;
cursor: s-resize;
}
.cropper-point.point-ne {
top: -3px;
right: -3px;
cursor: ne-resize;
}
.cropper-point.point-nw {
top: -3px;
left: -3px;
cursor: nw-resize;
}
.cropper-point.point-sw {
bottom: -3px;
left: -3px;
cursor: sw-resize;
}
.cropper-point.point-se {
right: -3px;
bottom: -3px;
width: 20px;
height: 20px;
cursor: se-resize;
opacity: 1;
filter: alpha(opacity=100);
}
.cropper-point.point-se:before {
position: absolute;
right: -50%;
bottom: -50%;
display: block;
width: 200%;
height: 200%;
content: ' ';
opacity: 0;
background-color: #39f;
filter: alpha(opacity=0);
}
@media (min-width: 768px) {
.cropper-point.point-se {
width: 15px;
height: 15px;
}
}
@media (min-width: 992px) {
.cropper-point.point-se {
width: 10px;
height: 10px;
}
}
@media (min-width: 1200px) {
.cropper-point.point-se {
width: 5px;
height: 5px;
opacity: .75;
filter: alpha(opacity=75);
}
}
.cropper-invisible {
opacity: 0;
filter: alpha(opacity=0);
}
.cropper-bg {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');
}
.cropper-hide {
position: absolute;
display: block;
width: 0;
height: 0;
}
.cropper-hidden {
display: none !important;
}
.cropper-move {
cursor: move;
}
.cropper-crop {
cursor: crosshair;
}
.cropper-disabled .cropper-drag-box,
.cropper-disabled .cropper-face,
.cropper-disabled .cropper-line,
.cropper-disabled .cropper-point {
cursor: not-allowed;
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment