effective_bootstrap 0.9.25 → 0.9.30

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ // Styles for the article editor iframe body content
2
+ // This file is included by the article editor iframe.
3
+ body { padding-top: 0px !important; }
@@ -0,0 +1,3 @@
1
+ // Styles for the article editor look and feel
2
+ // Use this to customize the textarea input, toolbar, etc
3
+ // Does not affect the article editor iframe contents
@@ -1,5 +1,6 @@
1
1
  @import 'effective_bootstrap/base';
2
2
 
3
+ @import 'effective_article_editor/input';
3
4
  @import 'effective_checks/input';
4
5
  @import 'effective_datetime/input';
5
6
  @import 'effective_file/input';
@@ -0,0 +1 @@
1
+ @import 'effective_article_editor/content';
@@ -1,42 +1,17 @@
1
- body.dragging,
2
- body.dragging * {
3
- cursor: move !important;
4
- }
5
-
6
1
  .form-has-many {
7
- .has-many-placeholder {
8
- position: relative;
9
- height: 2rem;
10
-
11
- &:before {
12
- position: absolute;
13
- content: '';
14
- background-image: asset-data-url('icons/arrow-right-circle.svg');
15
- background-repeat: no-repeat;
16
- height: 2rem;
17
- width: 2rem;
18
- }
19
- }
20
-
21
- .has-many-fields.dragged {
22
- position: absolute;
23
- opacity: 0;
24
- z-index: 2000;
25
- .has-many-move { display: none; }
26
- }
27
-
28
- .has-many-move svg { margin-top: 6px; }
2
+ .has-many-fields.sortable-ghost { border-top: solid 3px #212529; }
29
3
  .has-many-move { display: none; }
4
+ .has-many-remove-disabled { opacity: 0; cursor: default !important; }
30
5
 
31
- .has-many-remove { margin-top: 1.5rem; }
6
+ .has-many-remove { margin-top: 1rem; }
32
7
  .has-many-move { margin-top: 1.5rem; }
33
8
  }
34
9
 
35
10
  .form-has-many.reordering {
36
- .has-many-move { display: inline-block; }
11
+ .has-many-move { display: inline-block; cursor: grab; }
37
12
  }
38
13
 
39
14
  .form-has-many.tight {
40
15
  .has-many-remove { margin-top: 0; }
41
- .has-many-move { margin-top: 0; }
16
+ .has-many-move { margin-top: 0.5rem; }
42
17
  }
@@ -22,6 +22,10 @@ module Effective
22
22
  alias_method :super_text_area, :text_area
23
23
  alias_method :super_hidden_field, :hidden_field
24
24
 
25
+ def article_editor(name, options = {}, &block)
26
+ Effective::FormInputs::ArticleEditor.new(name, options, builder: self).to_html(&block)
27
+ end
28
+
25
29
  def clear(name = 'Clear', options = {})
26
30
  (options = name; name = 'Clear') if name.kind_of?(Hash)
27
31
  Effective::FormInputs::Clear.new(name, options, builder: self).to_html
@@ -0,0 +1,77 @@
1
+ module Effective
2
+ module FormInputs
3
+ class ArticleEditor < Effective::FormInput
4
+
5
+ def build_input(&block)
6
+ @builder.super_text_area(name, options[:input])
7
+ end
8
+
9
+ def input_html_options
10
+ { class: 'effective_article_editor form-control', id: unique_id, autocomplete: 'off' }
11
+ end
12
+
13
+ def input_js_options
14
+ {
15
+ active_storage: active_storage,
16
+ css: '/assets/effective_article_editor/',
17
+ custom: {
18
+ css: ['/assets/application.css', '/assets/effective_bootstrap_article_editor.css']
19
+ },
20
+ classes: {
21
+ body: 'article-editor-body',
22
+ table: 'table'
23
+ },
24
+ grid: {
25
+ classname: 'row',
26
+ columns: 12,
27
+ gutter: '1px',
28
+ offset: {
29
+ left: '15px',
30
+ right: '15px',
31
+ },
32
+ patterns: {
33
+ '6|6': 'col-sm-6|col-sm-6',
34
+ '4|4|4': 'col-sm-4|col-sm-4|col-sm-4',
35
+ '3|3|3|3': 'col-sm-3|col-sm-3|col-sm-3|col-sm-3',
36
+ '2|2|2|2|2|2': 'col-sm-2|col-sm-2|col-sm-2|col-sm-2|col-sm-2|col-sm-2',
37
+ '3|6|3': 'col-sm-3|col-sm-6|col-sm-3',
38
+ '2|8|2': 'col-sm-2|col-sm-8|col-sm-2',
39
+ '5|7': 'col-sm-5|col-sm-7',
40
+ '7|5': 'col-sm-7|col-sm-5',
41
+ '4|8': 'col-sm-4|col-sm-8',
42
+ '8|4': 'col-sm-8|col-sm-4',
43
+ '3|9': 'col-sm-3|col-sm-9',
44
+ '9|3': 'col-sm-9|col-sm-3',
45
+ '2|10': 'col-sm-2|col-sm-10',
46
+ '10|2': 'col-sm-10|col-sm-2',
47
+ '12': 'col-sm-12'
48
+ }
49
+ },
50
+ plugins: ['blockcode', 'imageposition', 'imageresize', 'inlineformat', 'removeformat', 'reorder', 'style'],
51
+ quote: {
52
+ template: '<blockquote><p></p></blockquote>'
53
+ },
54
+ styles: {
55
+ table: {
56
+ 'bordered': { title: 'Bordered', classname: 'table-bordered' },
57
+ 'responsive': { title: 'Responsive', classname: 'table-responsive' },
58
+ 'small': { title: 'Small', classname: 'table-sm' },
59
+ 'striped': { title: 'Striped', classname: 'table-striped' },
60
+ }
61
+ }
62
+ }
63
+ end
64
+
65
+ def active_storage
66
+ return @active_storage unless @active_storage.nil?
67
+
68
+ @active_storage = if options.key?(:active_storage)
69
+ options.delete(:active_storage)
70
+ else
71
+ defined?(ActiveStorage).present?
72
+ end
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -7,8 +7,9 @@ module Effective
7
7
  object.send(name).build() if build? && collection.blank?
8
8
 
9
9
  errors = (@builder.error(name) if errors?) || BLANK
10
+ can_remove_method
10
11
 
11
- errors + content_tag(:div, options[:input]) do
12
+ errors + content_tag(:div, options[:input].except(:collection)) do
12
13
  has_many_fields_for(block) + has_many_links_for(block)
13
14
  end
14
15
  end
@@ -76,6 +77,11 @@ module Effective
76
77
  end
77
78
  end
78
79
 
80
+ def can_remove_method
81
+ return @can_remove_method unless @can_remove_method.nil?
82
+ @can_remove_method = (options[:input].delete(:can_remove_method) || false)
83
+ end
84
+
79
85
  # reorder: true
80
86
  def reorder?
81
87
  return @reorder unless @reorder.nil?
@@ -107,19 +113,23 @@ module Effective
107
113
 
108
114
  def render_resource(resource, block)
109
115
  remove = BLANK
116
+ reorder = BLANK
117
+ can_remove = (can_remove_method.blank? || !!resource.send(can_remove_method))
110
118
 
111
119
  content = @builder.fields_for(name, resource) do |form|
112
120
  fields = block.call(form)
113
121
 
114
- remove += form.super_hidden_field(:_destroy) if remove? && resource.persisted?
115
- remove += form.super_hidden_field(:position) if reorder? && !fields.include?('][position]')
122
+ remove += form.super_hidden_field(:_destroy) if remove? && can_remove && resource.persisted?
123
+ reorder += form.super_hidden_field(:position) if reorder? && !fields.include?('][position]')
116
124
 
117
125
  fields
118
126
  end
119
127
 
120
- remove += link_to_remove(resource) if (remove? || resource.new_record?)
128
+ if remove?
129
+ remove += (can_remove || resource.new_record?) ? link_to_remove(resource) : disabled_link_to_remove(resource)
130
+ end
121
131
 
122
- content_tag(:div, render_fields(content, remove), class: 'has-many-fields')
132
+ content_tag(:div, render_fields(content, (remove + reorder)), class: 'has-many-fields')
123
133
  end
124
134
 
125
135
  def render_fields(content, remove)
@@ -169,7 +179,7 @@ module Effective
169
179
  def link_to_reorder(block)
170
180
  content_tag(
171
181
  :button,
172
- icon('list') + 'Reorder',
182
+ icon('reorder') + 'Reorder',
173
183
  class: 'has-many-reorder btn btn-secondary',
174
184
  title: 'Reorder',
175
185
  data: {
@@ -181,7 +191,7 @@ module Effective
181
191
  def link_to_remove(resource)
182
192
  content_tag(
183
193
  :button,
184
- icon('trash-2') + 'Remove',
194
+ icon('trash-2'),
185
195
  class: 'has-many-remove btn btn-danger',
186
196
  title: 'Remove',
187
197
  data: {
@@ -191,8 +201,20 @@ module Effective
191
201
  )
192
202
  end
193
203
 
204
+ def disabled_link_to_remove(resource)
205
+ content_tag(
206
+ :button,
207
+ icon('trash-2'),
208
+ class: 'has-many-remove-disabled btn btn-danger',
209
+ title: 'Remove',
210
+ data: {
211
+ 'effective-form-has-many-remove-disabled': true,
212
+ }
213
+ )
214
+ end
215
+
194
216
  def has_many_move
195
- @has_many_move ||= content_tag(:span, icon('move'), class: 'has-many-move')
217
+ @has_many_move ||= content_tag(:span, icon('grip-lines'), class: 'has-many-move')
196
218
  end
197
219
 
198
220
  def build_resource
@@ -1,26 +1,4 @@
1
1
  EffectiveBootstrap.setup do |config|
2
- # Authorization Method
3
- #
4
- # This method is called by all controller actions with the appropriate action and resource
5
- # If it raises an exception or returns false, an Effective::AccessDenied Error will be raised
6
- #
7
- # Use via Proc:
8
- # Proc.new { |controller, action, resource| authorize!(action, resource) } # CanCan
9
- # Proc.new { |controller, action, resource| can?(action, resource) } # CanCan with skip_authorization_check
10
- # Proc.new { |controller, action, resource| authorize "#{action}?", resource } # Pundit
11
- # Proc.new { |controller, action, resource| current_user.is?(:admin) } # Custom logic
12
- #
13
- # Use via Boolean:
14
- # config.authorization_method = true # Always authorized
15
- # config.authorization_method = false # Always unauthorized
16
- #
17
- # Use via Method (probably in your application_controller.rb):
18
- # config.authorization_method = :my_authorization_method
19
- # def my_authorization_method(resource, action)
20
- # true
21
- # end
22
- config.authorization_method = Proc.new { |controller, action, resource| authorize!(action, resource) }
23
-
24
2
  # Replaces rails_ujs data-confirm with a custom inline implementation.
25
3
  # You will need to recompile assets (or "rm -rf tmp/") if you change this.
26
4
  config.use_custom_data_confirm = true
@@ -1,33 +1,14 @@
1
1
  require 'inline_svg'
2
+ require 'effective_resources'
2
3
  require 'effective_bootstrap/engine'
3
4
  require 'effective_bootstrap/version'
4
5
 
5
6
  module EffectiveBootstrap
6
7
 
7
- # The following are all valid config keys
8
- mattr_accessor :authorization_method
9
-
10
- mattr_accessor :use_custom_data_confirm
11
-
12
- def self.setup
13
- yield self
8
+ def self.config_keys
9
+ [:use_custom_data_confirm]
14
10
  end
15
11
 
16
- def self.authorized?(controller, action, resource)
17
- @_exceptions ||= [Effective::AccessDenied, (CanCan::AccessDenied if defined?(CanCan)), (Pundit::NotAuthorizedError if defined?(Pundit))].compact
18
-
19
- return !!authorization_method unless authorization_method.respond_to?(:call)
20
- controller = controller.controller if controller.respond_to?(:controller)
21
-
22
- begin
23
- !!(controller || self).instance_exec((controller || self), action, resource, &authorization_method)
24
- rescue *@_exceptions
25
- false
26
- end
27
- end
28
-
29
- def self.authorize!(controller, action, resource)
30
- raise Effective::AccessDenied.new('Access Denied', action, resource) unless authorized?(controller, action, resource)
31
- end
12
+ include EffectiveGem
32
13
 
33
14
  end
@@ -8,7 +8,11 @@ module EffectiveBootstrap
8
8
  end
9
9
 
10
10
  initializer 'effective_bootstrap.assets' do |app|
11
- app.config.assets.precompile += ['effective_bootstrap_manifest.js', 'icons/*']
11
+ app.config.assets.precompile += [
12
+ 'effective_bootstrap_manifest.js',
13
+ 'effective_bootstrap_article_editor.css',
14
+ 'icons/*'
15
+ ]
12
16
  end
13
17
 
14
18
  initializer 'effective_bootstrap.action_text' do |app|
@@ -1,3 +1,3 @@
1
1
  module EffectiveBootstrap
2
- VERSION = '0.9.25'.freeze
2
+ VERSION = '0.9.30'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: effective_bootstrap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.25
4
+ version: 0.9.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code and Effect
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-15 00:00:00.000000000 Z
11
+ date: 2021-06-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -26,18 +26,32 @@ dependencies:
26
26
  version: 4.0.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bootstrap
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "<"
32
+ - !ruby/object:Gem::Version
33
+ version: '5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "<"
39
+ - !ruby/object:Gem::Version
40
+ version: '5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: effective_resources
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - ">="
32
46
  - !ruby/object:Gem::Version
33
- version: 4.0.0
47
+ version: '0'
34
48
  type: :runtime
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - ">="
39
53
  - !ruby/object:Gem::Version
40
- version: 4.0.0
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: inline_svg
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -242,6 +256,7 @@ files:
242
256
  - app/assets/images/icons/globe.svg
243
257
  - app/assets/images/icons/google.svg
244
258
  - app/assets/images/icons/grid.svg
259
+ - app/assets/images/icons/grip-lines.svg
245
260
  - app/assets/images/icons/hard-drive.svg
246
261
  - app/assets/images/icons/hash.svg
247
262
  - app/assets/images/icons/headphones.svg
@@ -318,6 +333,7 @@ files:
318
333
  - app/assets/images/icons/radio.svg
319
334
  - app/assets/images/icons/refresh-ccw.svg
320
335
  - app/assets/images/icons/refresh-cw.svg
336
+ - app/assets/images/icons/reorder.svg
321
337
  - app/assets/images/icons/repeat.svg
322
338
  - app/assets/images/icons/rewind.svg
323
339
  - app/assets/images/icons/rotate-ccw.svg
@@ -404,6 +420,8 @@ files:
404
420
  - app/assets/images/icons/zap.svg
405
421
  - app/assets/images/icons/zoom-in.svg
406
422
  - app/assets/images/icons/zoom-out.svg
423
+ - app/assets/javascripts/effective_article_editor/initialize.js.coffee
424
+ - app/assets/javascripts/effective_article_editor/input.js
407
425
  - app/assets/javascripts/effective_bootstrap.js
408
426
  - app/assets/javascripts/effective_bootstrap/base.js.coffee
409
427
  - app/assets/javascripts/effective_bootstrap/confirm.js.coffee.erb
@@ -435,7 +453,8 @@ files:
435
453
  - app/assets/javascripts/effective_file/input.js
436
454
  - app/assets/javascripts/effective_has_many/initialize.js.coffee
437
455
  - app/assets/javascripts/effective_has_many/input.js
438
- - app/assets/javascripts/effective_has_many/jquery.sortable.js
456
+ - app/assets/javascripts/effective_has_many/sortable-jquery.js
457
+ - app/assets/javascripts/effective_has_many/sortable.js
439
458
  - app/assets/javascripts/effective_integer/initialize.js.coffee
440
459
  - app/assets/javascripts/effective_integer/input.js
441
460
  - app/assets/javascripts/effective_number_text/initialize.js.coffee
@@ -592,11 +611,14 @@ files:
592
611
  - app/assets/javascripts/moment/locale/zh-hk.js
593
612
  - app/assets/javascripts/moment/locale/zh-tw.js
594
613
  - app/assets/javascripts/moment/moment.js
614
+ - app/assets/stylesheets/effective_article_editor/content.scss
615
+ - app/assets/stylesheets/effective_article_editor/input.scss
595
616
  - app/assets/stylesheets/effective_bootstrap.scss
596
617
  - app/assets/stylesheets/effective_bootstrap/base.scss
597
618
  - app/assets/stylesheets/effective_bootstrap/forms.scss
598
619
  - app/assets/stylesheets/effective_bootstrap/icons.scss
599
620
  - app/assets/stylesheets/effective_bootstrap/overrides.scss
621
+ - app/assets/stylesheets/effective_bootstrap_article_editor.scss
600
622
  - app/assets/stylesheets/effective_bootstrap_editor.scss
601
623
  - app/assets/stylesheets/effective_checks/input.scss
602
624
  - app/assets/stylesheets/effective_datetime/bootstrap-datetimepicker.scss
@@ -622,6 +644,7 @@ files:
622
644
  - app/helpers/effective_icons_helper.rb
623
645
  - app/models/effective/form_builder.rb
624
646
  - app/models/effective/form_input.rb
647
+ - app/models/effective/form_inputs/article_editor.rb
625
648
  - app/models/effective/form_inputs/check_box.rb
626
649
  - app/models/effective/form_inputs/checks.rb
627
650
  - app/models/effective/form_inputs/ck_editor.rb
@@ -1,696 +0,0 @@
1
- /* ===================================================
2
- * jquery-sortable.js v0.9.13
3
- * http://johnny.github.com/jquery-sortable/
4
- * ===================================================
5
- * Copyright (c) 2012 Jonas von Andrian
6
- * All rights reserved.
7
- *
8
- * Redistribution and use in source and binary forms, with or without
9
- * modification, are permitted provided that the following conditions are met:
10
- * * Redistributions of source code must retain the above copyright
11
- * notice, this list of conditions and the following disclaimer.
12
- * * Redistributions in binary form must reproduce the above copyright
13
- * notice, this list of conditions and the following disclaimer in the
14
- * documentation and/or other materials provided with the distribution.
15
- * * The name of the author may not be used to endorse or promote products
16
- * derived from this software without specific prior written permission.
17
- *
18
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
- * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
22
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
- * ========================================================== */
29
-
30
-
31
- !function ( $, window, pluginName, undefined){
32
- var containerDefaults = {
33
- // If true, items can be dragged from this container
34
- drag: true,
35
- // If true, items can be dropped onto this container
36
- drop: true,
37
- // Exclude items from being draggable, if the
38
- // selector matches the item
39
- exclude: "",
40
- // If true, search for nested containers within an item.If you nest containers,
41
- // either the original selector with which you call the plugin must only match the top containers,
42
- // or you need to specify a group (see the bootstrap nav example)
43
- nested: true,
44
- // If true, the items are assumed to be arranged vertically
45
- vertical: true
46
- }, // end container defaults
47
- groupDefaults = {
48
- // This is executed after the placeholder has been moved.
49
- // $closestItemOrContainer contains the closest item, the placeholder
50
- // has been put at or the closest empty Container, the placeholder has
51
- // been appended to.
52
- afterMove: function ($placeholder, container, $closestItemOrContainer) {
53
- },
54
- // The exact css path between the container and its items, e.g. "> tbody"
55
- containerPath: "",
56
- // The css selector of the containers
57
- containerSelector: "ol, ul",
58
- // Distance the mouse has to travel to start dragging
59
- distance: 0,
60
- // Time in milliseconds after mousedown until dragging should start.
61
- // This option can be used to prevent unwanted drags when clicking on an element.
62
- delay: 0,
63
- // The css selector of the drag handle
64
- handle: "",
65
- // The exact css path between the item and its subcontainers.
66
- // It should only match the immediate items of a container.
67
- // No item of a subcontainer should be matched. E.g. for ol>div>li the itemPath is "> div"
68
- itemPath: "",
69
- // The css selector of the items
70
- itemSelector: "li",
71
- // The class given to "body" while an item is being dragged
72
- bodyClass: "dragging",
73
- // The class giving to an item while being dragged
74
- draggedClass: "dragged",
75
- // Check if the dragged item may be inside the container.
76
- // Use with care, since the search for a valid container entails a depth first search
77
- // and may be quite expensive.
78
- isValidTarget: function ($item, container) {
79
- return true
80
- },
81
- // Executed before onDrop if placeholder is detached.
82
- // This happens if pullPlaceholder is set to false and the drop occurs outside a container.
83
- onCancel: function ($item, container, _super, event) {
84
- },
85
- // Executed at the beginning of a mouse move event.
86
- // The Placeholder has not been moved yet.
87
- onDrag: function ($item, position, _super, event) {
88
- $item.css(position)
89
- },
90
- // Called after the drag has been started,
91
- // that is the mouse button is being held down and
92
- // the mouse is moving.
93
- // The container is the closest initialized container.
94
- // Therefore it might not be the container, that actually contains the item.
95
- onDragStart: function ($item, container, _super, event) {
96
- $item.css({
97
- height: $item.outerHeight(),
98
- width: $item.outerWidth()
99
- })
100
- $item.addClass(container.group.options.draggedClass)
101
- $("body").addClass(container.group.options.bodyClass)
102
- },
103
- // Called when the mouse button is being released
104
- onDrop: function ($item, container, _super, event) {
105
- $item.removeClass(container.group.options.draggedClass).removeAttr("style")
106
- $("body").removeClass(container.group.options.bodyClass)
107
- // START MONKEY PATCH (for submitting form when heading positions are changed on touchscreen)
108
- $item.trigger('movimento:drop:complete', $item)
109
- // END MONKEY PATCH
110
- },
111
- // Called on mousedown. If falsy value is returned, the dragging will not start.
112
- // Ignore if element clicked is input, select or textarea
113
- onMousedown: function ($item, _super, event) {
114
- if (!event.target.nodeName.match(/^(input|select|textarea)$/i)) {
115
- event.preventDefault()
116
- return true
117
- }
118
- },
119
- // The class of the placeholder (must match placeholder option markup)
120
- placeholderClass: "placeholder",
121
- // Template for the placeholder. Can be any valid jQuery input
122
- // e.g. a string, a DOM element.
123
- // The placeholder must have the class "placeholder"
124
- placeholder: '<li class="placeholder"></li>',
125
- // If true, the position of the placeholder is calculated on every mousemove.
126
- // If false, it is only calculated when the mouse is above a container.
127
- pullPlaceholder: true,
128
- // Specifies serialization of the container group.
129
- // The pair $parent/$children is either container/items or item/subcontainers.
130
- serialize: function ($parent, $children, parentIsContainer) {
131
- var result = $.extend({}, $parent.data())
132
-
133
- if(parentIsContainer)
134
- return [$children]
135
- else if ($children[0]){
136
- result.children = $children
137
- }
138
-
139
- delete result.subContainers
140
- delete result.sortable
141
-
142
- return result
143
- },
144
- // Set tolerance while dragging. Positive values decrease sensitivity,
145
- // negative values increase it.
146
- tolerance: 0
147
- }, // end group defaults
148
- containerGroups = {},
149
- groupCounter = 0,
150
- emptyBox = {
151
- left: 0,
152
- top: 0,
153
- bottom: 0,
154
- right:0
155
- },
156
- eventNames = {
157
- start: "touchstart.sortable mousedown.sortable",
158
- drop: "touchend.sortable touchcancel.sortable mouseup.sortable",
159
- drag: "touchmove.sortable mousemove.sortable",
160
- scroll: "scroll.sortable"
161
- },
162
- subContainerKey = "subContainers"
163
-
164
- /*
165
- * a is Array [left, right, top, bottom]
166
- * b is array [left, top]
167
- */
168
- function d(a,b) {
169
- var x = Math.max(0, a[0] - b[0], b[0] - a[1]),
170
- y = Math.max(0, a[2] - b[1], b[1] - a[3])
171
- return x+y;
172
- }
173
-
174
- function setDimensions(array, dimensions, tolerance, useOffset) {
175
- var i = array.length,
176
- offsetMethod = useOffset ? "offset" : "position"
177
- tolerance = tolerance || 0
178
-
179
- while(i--){
180
- var el = array[i].el ? array[i].el : $(array[i]),
181
- // use fitting method
182
- pos = el[offsetMethod]()
183
- pos.left += parseInt(el.css('margin-left'), 10)
184
- pos.top += parseInt(el.css('margin-top'),10)
185
- dimensions[i] = [
186
- pos.left - tolerance,
187
- pos.left + el.outerWidth() + tolerance,
188
- pos.top - tolerance,
189
- pos.top + el.outerHeight() + tolerance
190
- ]
191
- }
192
- }
193
-
194
- function getRelativePosition(pointer, element) {
195
- var offset = element.offset()
196
- return {
197
- left: pointer.left - offset.left,
198
- top: pointer.top - offset.top
199
- }
200
- }
201
-
202
- function sortByDistanceDesc(dimensions, pointer, lastPointer) {
203
- pointer = [pointer.left, pointer.top]
204
- lastPointer = lastPointer && [lastPointer.left, lastPointer.top]
205
-
206
- var dim,
207
- i = dimensions.length,
208
- distances = []
209
-
210
- while(i--){
211
- dim = dimensions[i]
212
- distances[i] = [i,d(dim,pointer), lastPointer && d(dim, lastPointer)]
213
- }
214
- distances = distances.sort(function (a,b) {
215
- return b[1] - a[1] || b[2] - a[2] || b[0] - a[0]
216
- })
217
-
218
- // last entry is the closest
219
- return distances
220
- }
221
-
222
- function ContainerGroup(options) {
223
- this.options = $.extend({}, groupDefaults, options)
224
- this.containers = []
225
-
226
- if(!this.options.rootGroup){
227
- this.scrollProxy = $.proxy(this.scroll, this)
228
- this.dragProxy = $.proxy(this.drag, this)
229
- this.dropProxy = $.proxy(this.drop, this)
230
- this.placeholder = $(this.options.placeholder)
231
-
232
- if(!options.isValidTarget)
233
- this.options.isValidTarget = undefined
234
- }
235
- }
236
-
237
- ContainerGroup.get = function (options) {
238
- if(!containerGroups[options.group]) {
239
- if(options.group === undefined)
240
- options.group = groupCounter ++
241
-
242
- containerGroups[options.group] = new ContainerGroup(options)
243
- }
244
-
245
- return containerGroups[options.group]
246
- }
247
-
248
- ContainerGroup.prototype = {
249
- dragInit: function (e, itemContainer) {
250
- this.$document = $(itemContainer.el[0].ownerDocument)
251
-
252
- // get item to drag
253
- var closestItem = $(e.target).closest(this.options.itemSelector);
254
- // using the length of this item, prevents the plugin from being started if there is no handle being clicked on.
255
- // this may also be helpful in instantiating multidrag.
256
- if (closestItem.length) {
257
- this.item = closestItem;
258
- this.itemContainer = itemContainer;
259
- if (this.item.is(this.options.exclude) || !this.options.onMousedown(this.item, groupDefaults.onMousedown, e)) {
260
- return;
261
- }
262
- this.setPointer(e);
263
- this.toggleListeners('on');
264
- this.setupDelayTimer();
265
- this.dragInitDone = true;
266
- }
267
- },
268
- drag: function (e) {
269
- if(!this.dragging){
270
- if(!this.distanceMet(e) || !this.delayMet)
271
- return
272
-
273
- this.options.onDragStart(this.item, this.itemContainer, groupDefaults.onDragStart, e)
274
- this.item.before(this.placeholder)
275
- this.dragging = true
276
- }
277
-
278
- this.setPointer(e)
279
- // place item under the cursor
280
- this.options.onDrag(this.item,
281
- getRelativePosition(this.pointer, this.item.offsetParent()),
282
- groupDefaults.onDrag,
283
- e)
284
-
285
- var p = this.getPointer(e),
286
- box = this.sameResultBox,
287
- t = this.options.tolerance
288
-
289
- if(!box || box.top - t > p.top || box.bottom + t < p.top || box.left - t > p.left || box.right + t < p.left)
290
- if(!this.searchValidTarget()){
291
- this.placeholder.detach()
292
- this.lastAppendedItem = undefined
293
- }
294
- },
295
- drop: function (e) {
296
- this.toggleListeners('off')
297
-
298
- this.dragInitDone = false
299
-
300
- if(this.dragging){
301
- // processing Drop, check if placeholder is detached
302
- if(this.placeholder.closest("html")[0]){
303
- this.placeholder.before(this.item).detach()
304
- } else {
305
- this.options.onCancel(this.item, this.itemContainer, groupDefaults.onCancel, e)
306
- }
307
- this.options.onDrop(this.item, this.getContainer(this.item), groupDefaults.onDrop, e)
308
-
309
- // cleanup
310
- this.clearDimensions()
311
- this.clearOffsetParent()
312
- this.lastAppendedItem = this.sameResultBox = undefined
313
- this.dragging = false
314
- }
315
- },
316
- searchValidTarget: function (pointer, lastPointer) {
317
- if(!pointer){
318
- pointer = this.relativePointer || this.pointer
319
- lastPointer = this.lastRelativePointer || this.lastPointer
320
- }
321
-
322
- var distances = sortByDistanceDesc(this.getContainerDimensions(),
323
- pointer,
324
- lastPointer),
325
- i = distances.length
326
-
327
- while(i--){
328
- var index = distances[i][0],
329
- distance = distances[i][1]
330
-
331
- if(!distance || this.options.pullPlaceholder){
332
- var container = this.containers[index]
333
- if(!container.disabled){
334
- if(!this.$getOffsetParent()){
335
- var offsetParent = container.getItemOffsetParent()
336
- pointer = getRelativePosition(pointer, offsetParent)
337
- lastPointer = getRelativePosition(lastPointer, offsetParent)
338
- }
339
- if(container.searchValidTarget(pointer, lastPointer))
340
- return true
341
- }
342
- }
343
- }
344
- if(this.sameResultBox)
345
- this.sameResultBox = undefined
346
- },
347
- movePlaceholder: function (container, item, method, sameResultBox) {
348
- var lastAppendedItem = this.lastAppendedItem
349
- if(!sameResultBox && lastAppendedItem && lastAppendedItem[0] === item[0])
350
- return;
351
-
352
- item[method](this.placeholder)
353
- this.lastAppendedItem = item
354
- this.sameResultBox = sameResultBox
355
- this.options.afterMove(this.placeholder, container, item)
356
- },
357
- getContainerDimensions: function () {
358
- if(!this.containerDimensions)
359
- setDimensions(this.containers, this.containerDimensions = [], this.options.tolerance, !this.$getOffsetParent())
360
- return this.containerDimensions
361
- },
362
- getContainer: function (element) {
363
- return element.closest(this.options.containerSelector).data(pluginName)
364
- },
365
- $getOffsetParent: function () {
366
- if(this.offsetParent === undefined){
367
- var i = this.containers.length - 1,
368
- offsetParent = this.containers[i].getItemOffsetParent()
369
-
370
- if(!this.options.rootGroup){
371
- while(i--){
372
- if(offsetParent[0] != this.containers[i].getItemOffsetParent()[0]){
373
- // If every container has the same offset parent,
374
- // use position() which is relative to this parent,
375
- // otherwise use offset()
376
- // compare #setDimensions
377
- offsetParent = false
378
- break;
379
- }
380
- }
381
- }
382
-
383
- this.offsetParent = offsetParent
384
- }
385
- return this.offsetParent
386
- },
387
- setPointer: function (e) {
388
- var pointer = this.getPointer(e)
389
-
390
- if(this.$getOffsetParent()){
391
- var relativePointer = getRelativePosition(pointer, this.$getOffsetParent())
392
- this.lastRelativePointer = this.relativePointer
393
- this.relativePointer = relativePointer
394
- }
395
-
396
- this.lastPointer = this.pointer
397
- this.pointer = pointer
398
- },
399
- distanceMet: function (e) {
400
- var currentPointer = this.getPointer(e)
401
- return (Math.max(
402
- Math.abs(this.pointer.left - currentPointer.left),
403
- Math.abs(this.pointer.top - currentPointer.top)
404
- ) >= this.options.distance)
405
- },
406
- getPointer: function(e) {
407
- var o = e.originalEvent || e.originalEvent.touches && e.originalEvent.touches[0]
408
- return {
409
- left: e.pageX || o.pageX,
410
- top: e.pageY || o.pageY
411
- }
412
- },
413
- setupDelayTimer: function () {
414
- var that = this
415
- this.delayMet = !this.options.delay
416
-
417
- // init delay timer if needed
418
- if (!this.delayMet) {
419
- clearTimeout(this._mouseDelayTimer);
420
- this._mouseDelayTimer = setTimeout(function() {
421
- that.delayMet = true
422
- }, this.options.delay)
423
- }
424
- },
425
- scroll: function (e) {
426
- this.clearDimensions()
427
- this.clearOffsetParent() // TODO is this needed?
428
- },
429
- toggleListeners: function (method) {
430
- var that = this,
431
- events = ['drag','drop','scroll']
432
-
433
- $.each(events,function (i,event) {
434
- that.$document[method](eventNames[event], that[event + 'Proxy'])
435
- })
436
- },
437
- clearOffsetParent: function () {
438
- this.offsetParent = undefined
439
- },
440
- // Recursively clear container and item dimensions
441
- clearDimensions: function () {
442
- this.traverse(function(object){
443
- object._clearDimensions()
444
- })
445
- },
446
- traverse: function(callback) {
447
- callback(this)
448
- var i = this.containers.length
449
- while(i--){
450
- this.containers[i].traverse(callback)
451
- }
452
- },
453
- _clearDimensions: function(){
454
- this.containerDimensions = undefined
455
- },
456
- _destroy: function () {
457
- containerGroups[this.options.group] = undefined
458
- }
459
- }
460
-
461
- function Container(element, options) {
462
- this.el = element
463
- this.options = $.extend( {}, containerDefaults, options)
464
-
465
- this.group = ContainerGroup.get(this.options)
466
- this.rootGroup = this.options.rootGroup || this.group
467
- this.handle = this.rootGroup.options.handle || this.rootGroup.options.itemSelector
468
-
469
- var itemPath = this.rootGroup.options.itemPath
470
- this.target = itemPath ? this.el.find(itemPath) : this.el
471
-
472
- this.target.on(eventNames.start, this.handle, $.proxy(this.dragInit, this))
473
-
474
- if(this.options.drop)
475
- this.group.containers.push(this)
476
- }
477
-
478
- Container.prototype = {
479
- dragInit: function (e) {
480
- var rootGroup = this.rootGroup
481
-
482
- if( !this.disabled &&
483
- !rootGroup.dragInitDone &&
484
- this.options.drag &&
485
- this.isValidDrag(e)) {
486
- rootGroup.dragInit(e, this)
487
- }
488
- },
489
- isValidDrag: function(e) {
490
- return e.which == 1 ||
491
- e.type == "touchstart" && e.originalEvent.touches.length == 1
492
- },
493
- searchValidTarget: function (pointer, lastPointer) {
494
- var distances = sortByDistanceDesc(this.getItemDimensions(),
495
- pointer,
496
- lastPointer),
497
- i = distances.length,
498
- rootGroup = this.rootGroup,
499
- validTarget = !rootGroup.options.isValidTarget ||
500
- rootGroup.options.isValidTarget(rootGroup.item, this)
501
-
502
- if(!i && validTarget){
503
- rootGroup.movePlaceholder(this, this.target, "append")
504
- return true
505
- } else
506
- while(i--){
507
- var index = distances[i][0],
508
- distance = distances[i][1]
509
- if(!distance && this.hasChildGroup(index)){
510
- var found = this.getContainerGroup(index).searchValidTarget(pointer, lastPointer)
511
- if(found)
512
- return true
513
- }
514
- else if(validTarget){
515
- this.movePlaceholder(index, pointer)
516
- return true
517
- }
518
- }
519
- },
520
- movePlaceholder: function (index, pointer) {
521
- var item = $(this.items[index]),
522
- dim = this.itemDimensions[index],
523
- method = "after",
524
- width = item.outerWidth(),
525
- height = item.outerHeight(),
526
- offset = item.offset(),
527
- sameResultBox = {
528
- left: offset.left,
529
- right: offset.left + width,
530
- top: offset.top,
531
- bottom: offset.top + height
532
- }
533
- if(this.options.vertical){
534
- var yCenter = (dim[2] + dim[3]) / 2,
535
- inUpperHalf = pointer.top <= yCenter
536
- if(inUpperHalf){
537
- method = "before"
538
- sameResultBox.bottom -= height / 2
539
- } else
540
- sameResultBox.top += height / 2
541
- } else {
542
- var xCenter = (dim[0] + dim[1]) / 2,
543
- inLeftHalf = pointer.left <= xCenter
544
- if(inLeftHalf){
545
- method = "before"
546
- sameResultBox.right -= width / 2
547
- } else
548
- sameResultBox.left += width / 2
549
- }
550
- if(this.hasChildGroup(index))
551
- sameResultBox = emptyBox
552
- this.rootGroup.movePlaceholder(this, item, method, sameResultBox)
553
- },
554
- getItemDimensions: function () {
555
- if(!this.itemDimensions){
556
- this.items = this.$getChildren(this.el, "item").filter(
557
- ":not(." + this.group.options.placeholderClass + ", ." + this.group.options.draggedClass + ")"
558
- ).get()
559
- setDimensions(this.items, this.itemDimensions = [], this.options.tolerance)
560
- }
561
- return this.itemDimensions
562
- },
563
- getItemOffsetParent: function () {
564
- var offsetParent,
565
- el = this.el
566
- // Since el might be empty we have to check el itself and
567
- // can not do something like el.children().first().offsetParent()
568
- if(el.css("position") === "relative" || el.css("position") === "absolute" || el.css("position") === "fixed")
569
- offsetParent = el
570
- else
571
- offsetParent = el.offsetParent()
572
- return offsetParent
573
- },
574
- hasChildGroup: function (index) {
575
- return this.options.nested && this.getContainerGroup(index)
576
- },
577
- getContainerGroup: function (index) {
578
- var childGroup = $.data(this.items[index], subContainerKey)
579
- if( childGroup === undefined){
580
- var childContainers = this.$getChildren(this.items[index], "container")
581
- childGroup = false
582
-
583
- if(childContainers[0]){
584
- var options = $.extend({}, this.options, {
585
- rootGroup: this.rootGroup,
586
- group: groupCounter ++
587
- })
588
- childGroup = childContainers[pluginName](options).data(pluginName).group
589
- }
590
- $.data(this.items[index], subContainerKey, childGroup)
591
- }
592
- return childGroup
593
- },
594
- $getChildren: function (parent, type) {
595
- var options = this.rootGroup.options,
596
- path = options[type + "Path"],
597
- selector = options[type + "Selector"]
598
-
599
- parent = $(parent)
600
- if(path)
601
- parent = parent.find(path)
602
-
603
- return parent.children(selector)
604
- },
605
- _serialize: function (parent, isContainer) {
606
- var that = this,
607
- childType = isContainer ? "item" : "container",
608
-
609
- children = this.$getChildren(parent, childType).not(this.options.exclude).map(function () {
610
- return that._serialize($(this), !isContainer)
611
- }).get()
612
-
613
- return this.rootGroup.options.serialize(parent, children, isContainer)
614
- },
615
- traverse: function(callback) {
616
- $.each(this.items || [], function(item){
617
- var group = $.data(this, subContainerKey)
618
- if(group)
619
- group.traverse(callback)
620
- });
621
-
622
- callback(this)
623
- },
624
- _clearDimensions: function () {
625
- this.itemDimensions = undefined
626
- },
627
- _destroy: function() {
628
- var that = this;
629
-
630
- this.target.off(eventNames.start, this.handle);
631
- this.el.removeData(pluginName)
632
-
633
- if(this.options.drop)
634
- this.group.containers = $.grep(this.group.containers, function(val){
635
- return val != that
636
- })
637
-
638
- $.each(this.items || [], function(){
639
- $.removeData(this, subContainerKey)
640
- })
641
- }
642
- }
643
-
644
- var API = {
645
- enable: function() {
646
- this.traverse(function(object){
647
- object.disabled = false
648
- })
649
- },
650
- disable: function (){
651
- this.traverse(function(object){
652
- object.disabled = true
653
- })
654
- },
655
- serialize: function () {
656
- return this._serialize(this.el, true)
657
- },
658
- refresh: function() {
659
- this.traverse(function(object){
660
- object._clearDimensions()
661
- })
662
- },
663
- destroy: function () {
664
- this.traverse(function(object){
665
- object._destroy();
666
- })
667
- }
668
- }
669
-
670
- $.extend(Container.prototype, API)
671
-
672
- /**
673
- * jQuery API
674
- *
675
- * Parameters are
676
- * either options on init
677
- * or a method name followed by arguments to pass to the method
678
- */
679
- $.fn[pluginName] = function(methodOrOptions) {
680
- var args = Array.prototype.slice.call(arguments, 1)
681
-
682
- return this.map(function(){
683
- var $t = $(this),
684
- object = $t.data(pluginName)
685
-
686
- if(object && API[methodOrOptions])
687
- return API[methodOrOptions].apply(object, args) || this
688
- else if(!object && (methodOrOptions === undefined ||
689
- typeof methodOrOptions === "object"))
690
- $t.data(pluginName, new Container($t, methodOrOptions))
691
-
692
- return this
693
- });
694
- };
695
-
696
- }(jQuery, window, 'sortable');