diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 4f9826a12..f08c7eaa1 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -32,6 +32,7 @@ //= require ardc_vocab_widget_v2 //= require select2 //= require ahoy +//= require sortable //= require_tree ./templates //= require_tree . //= require_self @@ -214,6 +215,8 @@ document.addEventListener("turbolinks:load", function() { Tracking.init(); + Collections.init(); + $('.tess-expandable').each(function () { var limit = this.dataset.heightLimit || 300; diff --git a/app/assets/javascripts/autocompleters.js b/app/assets/javascripts/autocompleters.js index 8a808fec2..43e23c86f 100644 --- a/app/assets/javascripts/autocompleters.js +++ b/app/assets/javascripts/autocompleters.js @@ -50,65 +50,75 @@ var Autocompleters = { init: function () { $("[data-role='autocompleter-group']").each(function () { - var element = this; - var existingValues = JSON.parse($(element).find('[data-role="autocompleter-existing"]').html()) || []; - var listElement = $(element).find('[data-role="autocompleter-list"]'); - var inputElement = $(element).find('[data-role="autocompleter-input"]'); - var url = $(element).data("url"); - var prefix = $(element).data("prefix"); - var labelField = $(element).data("labelField") || "title"; - var idField = $(element).data("idField") || "id"; - var singleton = $(element).data("singleton") || false; - var groupBy = $(element).data("groupBy") || false; - var templateName = $(element).data("template") || - (singleton ? "autocompleter/singleton_resource" : "autocompleter/resource"); - var transformFunction = Autocompleters.transformFunctions[$(element).data("transformFunction") || "default"]; + Autocompleters.initGroup(this); + }); + }, - // Render the existing associations on page load - if (!listElement.children("li").length) { - for (var i = 0; i < existingValues.length; i++) { - listElement.append(HandlebarsTemplates[templateName](existingValues[i])); - } + initGroup: function (element, opts) { + var existingValues = JSON.parse($(element).find('[data-role="autocompleter-existing"]').html()) || []; + var listElement = $(element).find('[data-role="autocompleter-list"]'); + var inputElement = $(element).find('[data-role="autocompleter-input"]'); + var defaults = { + url: $(element).data("url"), + prefix: $(element).data("prefix"), + labelField: $(element).data("labelField") || "title", + idField: $(element).data("idField") || "id", + singleton: $(element).data("singleton") || false, + groupBy: $(element).data("groupBy") || false, + templateName: $(element).data("template"), + transformFunction: Autocompleters.transformFunctions[$(element).data("transformFunction") || "default"] + } + opts = Object.assign({}, defaults, opts); - if (singleton && existingValues.length) { - inputElement.hide(); - } + opts.templateName = opts.templateName || (opts.singleton ? "autocompleter/singleton_resource" : + "autocompleter/resource"); + + // Render the existing associations on page load + if (!listElement.children("li").length) { + for (var i = 0; i < existingValues.length; i++) { + listElement.append(HandlebarsTemplates[opts.templateName](existingValues[i])); } - inputElement.autocomplete({ - serviceUrl: url, - dataType: "json", - deferRequestBy: 300, // Wait 300ms before submitting to stop search being flooded - paramName: "q", - groupBy: groupBy, - formatResult: Autocompleters.formatResultWithHint, - transformResult: function(response) { - return transformFunction(response, { labelField: labelField, idField: idField }); - }, - onSelect: function (suggestion) { - // Don't add duplicates - var id = suggestion.data.id; - if (!$("[data-id='" + id + "']", listElement).length) { - var obj = { item: suggestion.data.item }; - if (prefix) { - obj.prefix = prefix; - } + if (opts.singleton && existingValues.length) { + inputElement.hide(); + } + } - listElement.append(HandlebarsTemplates[templateName](obj)); - if (singleton) { - inputElement.hide(); - } + inputElement.autocomplete({ + serviceUrl: opts.url, + dataType: "json", + deferRequestBy: 300, // Wait 300ms before submitting to stop search being flooded + paramName: "q", + groupBy: opts.groupBy, + formatResult: Autocompleters.formatResultWithHint, + transformResult: function(response) { + return opts.transformFunction(response, opts); + }, + onSelect: function (suggestion) { + // Don't add duplicates + var id = suggestion.data.id; + if (!$("[data-id='" + id + "']", listElement).length) { + var obj = { item: suggestion.data.item }; + if (opts.prefix) { + obj.prefix = opts.prefix; } - $(this).val('').focus(); - }, - onSearchStart: function (query) { - inputElement.addClass("loading"); - }, - onSearchComplete: function () { - inputElement.removeClass("loading"); + listElement.append(HandlebarsTemplates[opts.templateName](obj)); + if (opts.singleton) { + inputElement.hide(); + } } - }); + + $(this).val('').focus(); + const event = new CustomEvent('autocompleters:added', { bubbles: true, detail: { object: obj } }); + listElement[0].dispatchEvent(event); + }, + onSearchStart: function (query) { + inputElement.addClass("loading"); + }, + onSearchComplete: function () { + inputElement.removeClass("loading"); + } }); } } \ No newline at end of file diff --git a/app/assets/javascripts/collections.js b/app/assets/javascripts/collections.js new file mode 100644 index 000000000..a03e22ed6 --- /dev/null +++ b/app/assets/javascripts/collections.js @@ -0,0 +1,77 @@ +var Collections = { + init: function () { + $("[data-role='collection-items-group']").each(function () { + // Set up drag/drop + var list = $('.collection-items', $(this))[0]; + const collectionItems = new Sortable.default(list, { + draggable: 'li.collection-item', + handle: '.collection-item-handle' + }); + + collectionItems.on('drag:stopped', function (e) { + // Re-compute orders after dropping. + Collections.recalculateOrder(e.data.sourceContainer); + }); + + // Set up autocompleter + var origTransform = Autocompleters.transformFunctions[$(this).data("transformFunction") || "default"]; + Autocompleters.initGroup(this, { + resourceType: $(this).data("resourceType"), + transformFunction: function (response, config) { + var result = origTransform(response, config); + result.suggestions.forEach(function (sugg) { + sugg.data.item.resource_type = config.resourceType; + sugg.data.item.resource_id = sugg.data.item.id; + sugg.data.item.id = null; + sugg.data.id = sugg.data.item.resource_type + '-' + sugg.data.item.resource_id + }); + return result; + } + }); + + + $(this).on('autocompleters:added', function () { + // Re-compute orders after new item added. + Collections.recalculateOrder(this); + }); + + $(this).on('click', '[data-role="delete-collection-item"]', function (e) { + // If the collection item yet saved, just delete from the DOM, + // otherwise, apply visible styling to show it will be deleted, which can be + // undone if the button is clicked again + var item = $(this).closest('.collection-item'); + var checkbox = $('input', $(this)); + if (!checkbox.length) { // No checkbox is rendered if item is not persisted yet. + var list = $(this).closest('ul')[0]; + // Re-compute orders after item removed. + item.remove(); + Collections.recalculateOrder(list); + } else { + if (checkbox.is(':checked')) { + checkbox.prop('checked', false); + $('input[type=text]', item).prop('disabled', false); + item.removeClass('pending-delete'); + } else { + checkbox.prop('checked', true); + $('input[type=text]', item).prop('disabled', true); + item.addClass('pending-delete'); + } + } + + return false; + }); + + // Calculate initial order - order from database may have gaps if items were deleted. + Collections.recalculateOrder(list); + }); + }, + + recalculateOrder: function (container) { + var order = 1; + container.querySelectorAll('li.collection-item').forEach(function (li) { + li.querySelector('[data-role="item-order"]').value = order; + li.querySelector('.item-order-label').innerText = order; + order++; + }); + } +} diff --git a/app/assets/javascripts/templates/autocompleter/collection_item.hbs b/app/assets/javascripts/templates/autocompleter/collection_item.hbs new file mode 100644 index 000000000..6846daab7 --- /dev/null +++ b/app/assets/javascripts/templates/autocompleter/collection_item.hbs @@ -0,0 +1,24 @@ +
  • +
    + + {{ item.order }} + +
    + +
    + + + + {{ item.title }} + +
    + +
    + + + {{#if item.id}} + + {{/if}} + +
    +
  • diff --git a/app/assets/stylesheets/collection.scss b/app/assets/stylesheets/collection.scss index ddf1dbb07..9d625d951 100644 --- a/app/assets/stylesheets/collection.scss +++ b/app/assets/stylesheets/collection.scss @@ -12,3 +12,55 @@ cursor: pointer; } } + +.collection-items { + list-style: none; + padding-left: 0; + + .collection-item { + display: flex; + align-items: center; + margin-bottom: 10px; + padding: 10px; + border-radius: $border-radius-base; + + .collection-item-title { + flex-grow: 1; + flex-shrink: 1; + } + + .collection-item-handle { + color: $text-muted; + padding: 0 15px; + user-select: none; + flex-shrink: 0; + align-self: stretch; + display: flex; + align-items: center; + gap: 5px; + } + + &.pending-delete { + background-color: $alert-danger-bg; + text-decoration: line-through; + } + } +} + +.collection-item-order-badge { + position: absolute; + left: -0.5em; + top: -0.5em; + border-radius: 50%; + width: 2em; + line-height: 2em; + height: 2em; + text-align: center; + background: $badge-bg; + color: $badge-color; +} + +.collection-item-comment { + margin: 10px 0 0 0; + font-style: italic; +} \ No newline at end of file diff --git a/app/assets/stylesheets/masonry.scss b/app/assets/stylesheets/masonry.scss index d709cf502..f789ad5d3 100644 --- a/app/assets/stylesheets/masonry.scss +++ b/app/assets/stylesheets/masonry.scss @@ -98,7 +98,7 @@ margin-bottom: 0; } - &.with-left-icon { + .with-left-icon { display: flex; .icon-container { diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index be60ef6bc..3897f3ad1 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -128,7 +128,8 @@ def set_collection # Never trust parameters from the scary internet, only allow the white list through. def collection_params params.require(:collection).permit(:title, :description, :image, :image_url, :public, { keywords: [] }, - { material_ids: [] }, { event_ids: [] }) + { material_ids: [] }, { event_ids: [] }, + { items_attributes: [:id, :resource_type, :resource_id, :order, :comment, :_destroy] }) end # Filter collection items based on a type diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb index 0b2f815ea..48e08eb4d 100644 --- a/app/helpers/collections_helper.rb +++ b/app/helpers/collections_helper.rb @@ -16,4 +16,12 @@ def item_fields(item_class) [] end end + + def item_order_badge(collection_item) + content_tag(:div, collection_item.order, class: 'collection-item-order-badge') + end + + def item_comment(collection_item) + content_tag(:blockquote, collection_item.comment, class: 'collection-item-comment') + end end diff --git a/app/models/collection.rb b/app/models/collection.rb index 461809e7c..59b4cd5a4 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -7,6 +7,10 @@ class Collection < ApplicationRecord include Collaboratable has_many :items, -> { order(:order) }, class_name: 'CollectionItem', inverse_of: :collection, dependent: :destroy + has_many :material_items, -> { where(resource_type: 'Material').order(:order) }, class_name: 'CollectionItem', + inverse_of: :collection + has_many :event_items, -> { where(resource_type: 'Event').order(:order) }, class_name: 'CollectionItem', + inverse_of: :collection has_many :events, through: :items, source: :resource, source_type: 'Event', inverse_of: :collections has_many :materials, through: :items, source: :resource, source_type: 'Material', inverse_of: :collections @@ -17,6 +21,9 @@ class Collection < ApplicationRecord # e.g. "James Bond " => "James Bond" auto_strip_attributes :title, :description, :image_url, squish: false + accepts_nested_attributes_for :items, allow_destroy: true + + after_validation :normalize_order after_commit :index_items, if: :title_previously_changed? validates :title, presence: true @@ -95,4 +102,13 @@ def index_items items.each(&:reindex_resource) end + + # Make sure order for each type goes from 1 to n with no gaps. + def normalize_order + indexes = Hash.new(0) + items.sort_by(&:order).each do |item| + next if item.marked_for_destruction? + item.order = indexes[item.resource_type] += 1 + end + end end diff --git a/app/views/collection_items/_collection_item.html.erb b/app/views/collection_items/_collection_item.html.erb new file mode 100644 index 000000000..d3203946b --- /dev/null +++ b/app/views/collection_items/_collection_item.html.erb @@ -0,0 +1,2 @@ +<% resource = collection_item.resource %> +<%= render partial: resource, locals: { collection_item: collection_item } %> diff --git a/app/views/collections/_collection_items_form.erb b/app/views/collections/_collection_items_form.erb new file mode 100644 index 000000000..3fc289175 --- /dev/null +++ b/app/views/collections/_collection_items_form.erb @@ -0,0 +1,42 @@ +<% + form_field_name = 'collection[items_attributes]' + data = { role: 'collection-items-group', + url: url, + prefix: form_field_name, + 'label-field' => 'title', + 'id-field' => 'id', + 'resource-type' => field_name.singularize.classify, + template: 'autocompleter/collection_item' } + transform_function ||= nil + group_by ||= nil + data['transform-function'] = transform_function if transform_function + data['group-by'] = group_by if group_by + + json = items.map do |item| + { item: { id: item.id, order: item.order, comment: item.comment, + resource_id: item.resource_id, resource_type: item.resource_type, + title: item.resource.title, url: polymorphic_path(item.resource) }, + prefix: form_field_name } + end.to_json + + placeholder_text = "Add a new #{f.object.class.human_attribute_name(field_name).singularize.downcase}" +%> + +
    + <%= f.label field_name %> + +

    + Re-order items by clicking and dragging the icon on the left-hand side. +

    + + <%= content_tag(:div, data: data) do %> + <%= content_tag :script, json.html_safe, type: 'application/json', data: { role: 'autocompleter-existing' } %> + + + + + <% end %> +
    diff --git a/app/views/collections/_form.html.erb b/app/views/collections/_form.html.erb index 41f31436b..eb9448bd5 100644 --- a/app/views/collections/_form.html.erb +++ b/app/views/collections/_form.html.erb @@ -17,13 +17,14 @@ <%= f.multi_input :keywords %> - <%= f.autocompleter :materials %> - - <%= f.autocompleter :events, - url: events_path(include_expired: true, sort: 'late'), - group_by: 'group', - transform_function: 'events' - %> + <%= render partial: 'collections/collection_items_form', + locals: { f: f, field_name: 'materials', url: materials_path, + items: @collection.items.where(resource_type: 'Material') } %> + + <%= render partial: 'collections/collection_items_form', + locals: { f: f, field_name: 'events', url: events_path(include_expired: true, sort: 'late'), + items: @collection.items.where(resource_type: 'Event'), + group_by: 'group', transform_function: 'events' } %>
    <%= f.submit ( f.object.new_record? ? "Create" : "Update") + " collection", :class => 'btn btn-primary' %> diff --git a/app/views/collections/show.html.erb b/app/views/collections/show.html.erb index 34e128400..a2d3b7add 100644 --- a/app/views/collections/show.html.erb +++ b/app/views/collections/show.html.erb @@ -22,10 +22,10 @@
    - <% materials = @collection.materials %> - <% materials_count = @collection.materials.count %> - <% events = @collection.events %> - <% events_count = @collection.events.count %> + <% materials = @collection.material_items %> + <% materials_count = materials.count %> + <% events = @collection.event_items %> + <% events_count = events.count %> <% activator = tab_activator %>
    -
    +
    <% if TeSS::Config.feature['materials'] %> <%= render partial: 'common/associated_resources', locals: { model: Material, diff --git a/app/views/events/_event.html.erb b/app/views/events/_event.html.erb index 14a0a18d6..ecc144bf1 100644 --- a/app/views/events/_event.html.erb +++ b/app/views/events/_event.html.erb @@ -1,56 +1,61 @@
  • <%= link_to event, class: 'link-overlay with-left-icon' do %> - + <%= item_order_badge(collection_item) if defined? collection_item %> +
    + -
    -
    -
    - <% if current_user&.is_admin? %> - <%= missing_icon(event) %> - <%= scrape_status_icon(event) %> - <%= suggestion_icon(event) %> - <% end %> +
    +
    +
    + <% if current_user&.is_admin? %> + <%= missing_icon(event) %> + <%= scrape_status_icon(event) %> + <%= suggestion_icon(event) %> + <% end %> - <%= event_status_icon(event) %> -
    - <% if event.event_types.any? %> - <% event.event_types.each do |t| %> -
    <%= EventTypeDictionary.instance.lookup_value(t, 'title') %>
    + <%= event_status_icon(event) %> +
    + <% if event.event_types.any? %> + <% event.event_types.each do |t| %> +
    <%= EventTypeDictionary.instance.lookup_value(t, 'title') %>
    + <% end %> <% end %> - <% end %> -

    - <%= event.title %> -

    -
    +

    + <%= event.title %> +

    +
    - <% if event.has_node? -%> - <%= elixir_node_icon %> - <% end -%> + <% if event.has_node? -%> + <%= elixir_node_icon %> + <% end -%> -

    <%= neatly_printed_date_range(event.start, event.end) %>

    +

    <%= neatly_printed_date_range(event.start, event.end) %>

    - <% if event.online? %> + <% if event.online? %>

    Online

    - <% else %> - <% location = [event.city, event.country].reject { |field_value| field_value.blank? }.join(", ") %> - <% if location.present? %> -

    <%= location %>

    + <% else %> + <% location = [event.city, event.country].reject { |field_value| field_value.blank? }.join(", ") %> + <% if location.present? %> +

    <%= location %>

    + <% end %> +

    + Face-to-face +

    <% end %> -

    - Face-to-face -

    - <% end %> +
    + + <%= item_comment(collection_item) if defined? collection_item %> <% end %>
  • diff --git a/app/views/materials/_material.html.erb b/app/views/materials/_material.html.erb index f657666b7..d0f05bdf7 100644 --- a/app/views/materials/_material.html.erb +++ b/app/views/materials/_material.html.erb @@ -1,5 +1,7 @@
  • <%= link_to material, class: 'link-overlay' do %> + <%= item_order_badge(collection_item) if defined? collection_item %> +
    <% if current_user&.is_admin? %> @@ -28,5 +30,7 @@ <%= keywords_and_topics(material) %>
    + + <%= item_comment(collection_item) if defined? collection_item %> <% end %>
  • diff --git a/test/controllers/collections_controller_test.rb b/test/controllers/collections_controller_test.rb index 046632e8b..523b5edc1 100644 --- a/test/controllers/collections_controller_test.rb +++ b/test/controllers/collections_controller_test.rb @@ -189,6 +189,111 @@ class CollectionsControllerTest < ActionController::TestCase assert_redirected_to collection_path(assigns(:collection)) end + test "should add items to collection" do + sign_in users(:regular_user) + collection = collections(:with_resources) + assert_difference('CollectionItem.count', 3) do + assert_difference('collection.events.count', 1) do + assert_difference('collection.materials.count', 2) do + patch :update, params: { id: collection.id, collection: { items_attributes: { + '0' => { resource_type: 'Material', resource_id: materials(:biojs).id, order: 2, comment: 'hello' }, + '1' => { resource_type: 'Event', resource_id: events(:one).id, order: 1, comment: 'hello!' }, + '2' => { resource_type: 'Material', resource_id: materials(:interpro).id, order: 1, comment: 'hello!!' } + } } } + + mats = assigns(:collection).material_items + assert_equal 2, mats.length + assert_equal 1, mats[0].order + assert_equal 'hello!!', mats[0].comment + assert_equal materials(:interpro), mats[0].resource + assert_equal 2, mats[1].order + assert_equal 'hello', mats[1].comment + assert_equal materials(:biojs), mats[1].resource + + events = assigns(:collection).event_items + assert_equal 1, events.length + assert_equal 1, events[0].order + assert_equal 'hello!', events[0].comment + assert_equal events(:one), events[0].resource + end + end + end + end + + test "should remove items from collection" do + sign_in users(:regular_user) + collection = collections(:with_resources) + ci1 = collection.items.create!(resource: materials(:biojs), order: 2, comment: 'hello') + ci2 = collection.items.create!(resource: materials(:interpro), order: 1, comment: 'hello!!') + ci3 = collection.items.create!(resource: events(:one), order: 1, comment: 'hello!') + + + assert_difference('CollectionItem.count', -1) do + assert_difference('collection.materials.count', -1) do + assert_no_difference('collection.events.count') do + patch :update, params: { id: collection.id, collection: { items_attributes: { + '0' => { id: ci1.id, resource_type: 'Material', resource_id: materials(:biojs).id, order: 2, comment: 'hello' }, + '1' => { id: ci3.id, resource_type: 'Event', resource_id: events(:one).id, order: 1, comment: 'hello!' }, + '2' => { id: ci2.id, resource_type: 'Material', resource_id: materials(:interpro).id, order: 1, comment: 'hello!!', _destroy: '1' } + } } } + + mats = assigns(:collection).material_items + assert_equal ci1.id, mats[0].id + assert_equal 1, mats.length + assert_equal 1, mats[0].order + assert_equal 'hello', mats[0].comment + assert_equal materials(:biojs), mats[0].resource + + events = assigns(:collection).event_items + assert_equal 1, events.length + assert_equal ci3.id, events[0].id + assert_equal 1, events[0].order + assert_equal 'hello!', events[0].comment + assert_equal events(:one), events[0].resource + end + end + end + end + + test "should modify items in collection" do + sign_in users(:regular_user) + collection = collections(:with_resources) + ci1 = collection.items.create!(resource: materials(:biojs), order: 2, comment: 'hello') + ci2 = collection.items.create!(resource: materials(:interpro), order: 1, comment: 'hello!!') + ci3 = collection.items.create!(resource: events(:one), order: 1, comment: 'hello!') + + assert_no_difference('CollectionItem.count') do + assert_no_difference('collection.materials.count') do + assert_no_difference('collection.events.count') do + patch :update, params: { id: collection.id, collection: { items_attributes: { + '0' => { id: ci1.id, resource_type: 'Material', resource_id: materials(:biojs).id, order: 1, comment: 'hello world' }, + '1' => { id: ci3.id, resource_type: 'Event', resource_id: events(:one).id, order: 1, comment: 'hello world!' }, + '2' => { id: ci2.id, resource_type: 'Material', resource_id: materials(:interpro).id, order: 2, comment: 'hello world!!' } + } } } + + mats = assigns(:collection).material_items + assert_equal 2, mats.length + assert_equal ci1.id, mats[0].id + assert_equal 1, mats[0].order + assert_equal 'hello world', mats[0].comment + assert_equal materials(:biojs), mats[0].resource + assert_equal ci2.id, mats[1].id + assert_equal 2, mats[1].order + assert_equal 'hello world!!', mats[1].comment + assert_equal materials(:interpro), mats[1].resource + + events = assigns(:collection).event_items + assert_equal 1, events.length + assert_equal ci3.id, events[0].id + assert_equal 1, events[0].order + assert_equal 'hello world!', events[0].comment + assert_equal events(:one), events[0].resource + end + end + end + end + + #UPDATE_CURATE TEST test 'should add and remove elements' do sign_in @collection.user @@ -354,6 +459,16 @@ class CollectionsControllerTest < ActionController::TestCase end #API Actions + test "should add materials to collection" do + sign_in users(:regular_user) + collection = collections(:with_resources) + assert_difference('CollectionItem.count', 2) do + assert_difference('collection.materials.count', 2) do + patch :update, params: { collection: { material_ids: [materials(:biojs), materials(:interpro)] }, id: collection.id } + end + end + end + test "should remove materials from collection" do sign_in users(:regular_user) collection = collections(:with_resources) @@ -495,4 +610,75 @@ class CollectionsControllerTest < ActionController::TestCase end assert_response :forbidden end + + test 'should render collection items in order' do + materials = [materials(:good_material), materials(:biojs), materials(:interpro)] + events = [events(:two), events(:one)] + @collection.items.create!(resource: materials[0], order: 2, comment: 'A good material') + @collection.items.create!(resource: materials[1], order: 1, comment: 'Start here') + @collection.items.create!(resource: materials[2], order: 3, comment: 'End here') + @collection.items.create!(resource: events[0], order: 2, comment: 'End here') + @collection.items.create!(resource: events[1], order: 1, comment: 'Start here') + + get :show, params: { id: @collection } + + assert_response :success + + assert_select '#materials ul li:nth-child(1) .link-overlay' do + assert_select 'h4', text: 'BioJS' + assert_select '.collection-item-comment', text: 'Start here' + assert_select '.collection-item-order-badge', text: '1' + end + + assert_select '#materials ul li:nth-child(2) .link-overlay' do + assert_select 'h4', text: 'Training Material Example' + assert_select '.collection-item-comment', text: 'A good material' + assert_select '.collection-item-order-badge', text: '2' + end + + assert_select '#materials ul li:nth-child(3) .link-overlay' do + assert_select 'h4', text: 'InterPro' + assert_select '.collection-item-comment', text: 'End here' + assert_select '.collection-item-order-badge', text: '3' + end + + assert_select '#events ul li:nth-child(1) .link-overlay' do + assert_select 'h4', text: 'event one' + assert_select '.collection-item-comment', text: 'Start here' + assert_select '.collection-item-order-badge', text: '1' + end + + assert_select '#events ul li:nth-child(2) .link-overlay' do + assert_select 'h4', text: 'event two' + assert_select '.collection-item-comment', text: 'End here' + assert_select '.collection-item-order-badge', text: '2' + end + end + + test 'should render collection items in order as json-api' do + materials = [materials(:good_material), materials(:biojs), materials(:interpro)] + events = [events(:two), events(:one)] + @collection.items.create!(resource: materials[0], order: 2, comment: 'A good material') + @collection.items.create!(resource: materials[1], order: 1, comment: 'Start here') + @collection.items.create!(resource: materials[2], order: 3, comment: 'End here') + @collection.items.create!(resource: events[0], order: 2, comment: 'End here') + @collection.items.create!(resource: events[1], order: 1, comment: 'Start here') + + get :show, params: { id: @collection, format: :json_api } + + assert_response :success + assert assigns(:collection) + assert_valid_json_api_response + + body = nil + assert_nothing_raised do + body = JSON.parse(response.body) + end + + response_materials = body.dig('data', 'relationships', 'materials', 'data') + assert_equal [materials[1].id, materials[0].id, materials[2].id], response_materials.map { |m| m['id'].to_i } + + response_events = body.dig('data', 'relationships', 'events', 'data') + assert_equal [events[1].id, events[0].id], response_events.map { |e| e['id'].to_i } + end end diff --git a/test/models/collection_test.rb b/test/models/collection_test.rb index 364819d88..40252b65e 100644 --- a/test/models/collection_test.rb +++ b/test/models/collection_test.rb @@ -166,4 +166,18 @@ def solr_index assert_equal 'Collection Title', collection.title assert_equal 'http://image.host/another_image.png', collection.image_url end + + test 'should normalize order of items' do + collection = collections(:secret_collection) + collection.items.create!(resource: materials(:biojs), order: 2) + collection.items.create!(resource: materials(:interpro), order: 3) + collection.items.create!(resource: events(:one), order: 42) + collection.items.create!(resource: events(:two), order: 13) + collection.save! + + assert_equal [1, 2], collection.material_items.pluck(:order) + assert_equal [materials(:biojs), materials(:interpro)], collection.material_items.map(&:resource) + assert_equal [1, 2], collection.event_items.pluck(:order) + assert_equal [events(:two), events(:one)], collection.event_items.map(&:resource) + end end diff --git a/vendor/assets/javascripts/sortable.js b/vendor/assets/javascripts/sortable.js new file mode 100644 index 000000000..51a82d7b3 --- /dev/null +++ b/vendor/assets/javascripts/sortable.js @@ -0,0 +1,5495 @@ +// From: https://github.com/Shopify/draggable +// MIT License +// +// Copyright (c) 2018 Shopify +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define("Sortable", [], factory); + else if(typeof exports === 'object') + exports["Sortable"] = factory(); + else + root["Sortable"] = factory(); +})(window, function() { + return /******/ (function(modules) { // webpackBootstrap + /******/ // The module cache + /******/ var installedModules = {}; + /******/ + /******/ // The require function + /******/ function __webpack_require__(moduleId) { + /******/ + /******/ // Check if module is in cache + /******/ if(installedModules[moduleId]) { + /******/ return installedModules[moduleId].exports; + /******/ } + /******/ // Create a new module (and put it into the cache) + /******/ var module = installedModules[moduleId] = { + /******/ i: moduleId, + /******/ l: false, + /******/ exports: {} + /******/ }; + /******/ + /******/ // Execute the module function + /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + /******/ + /******/ // Flag the module as loaded + /******/ module.l = true; + /******/ + /******/ // Return the exports of the module + /******/ return module.exports; + /******/ } + /******/ + /******/ + /******/ // expose the modules object (__webpack_modules__) + /******/ __webpack_require__.m = modules; + /******/ + /******/ // expose the module cache + /******/ __webpack_require__.c = installedModules; + /******/ + /******/ // define getter function for harmony exports + /******/ __webpack_require__.d = function(exports, name, getter) { + /******/ if(!__webpack_require__.o(exports, name)) { + /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); + /******/ } + /******/ }; + /******/ + /******/ // define __esModule on exports + /******/ __webpack_require__.r = function(exports) { + /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { + /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + /******/ } + /******/ Object.defineProperty(exports, '__esModule', { value: true }); + /******/ }; + /******/ + /******/ // create a fake namespace object + /******/ // mode & 1: value is a module id, require it + /******/ // mode & 2: merge all properties of value into the ns + /******/ // mode & 4: return value when already ns object + /******/ // mode & 8|1: behave like require + /******/ __webpack_require__.t = function(value, mode) { + /******/ if(mode & 1) value = __webpack_require__(value); + /******/ if(mode & 8) return value; + /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; + /******/ var ns = Object.create(null); + /******/ __webpack_require__.r(ns); + /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); + /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); + /******/ return ns; + /******/ }; + /******/ + /******/ // getDefaultExport function for compatibility with non-harmony modules + /******/ __webpack_require__.n = function(module) { + /******/ var getter = module && module.__esModule ? + /******/ function getDefault() { return module['default']; } : + /******/ function getModuleExports() { return module; }; + /******/ __webpack_require__.d(getter, 'a', getter); + /******/ return getter; + /******/ }; + /******/ + /******/ // Object.prototype.hasOwnProperty.call + /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; + /******/ + /******/ // __webpack_public_path__ + /******/ __webpack_require__.p = ""; + /******/ + /******/ + /******/ // Load entry module and return exports + /******/ return __webpack_require__(__webpack_require__.s = 48); + /******/ }) + /************************************************************************/ + /******/ ([ + /* 0 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _SensorEvent = __webpack_require__(19); + + Object.keys(_SensorEvent).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _SensorEvent[key]; + } + }); + }); + + /***/ }), + /* 1 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _Sensor = __webpack_require__(22); + + var _Sensor2 = _interopRequireDefault(_Sensor); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _Sensor2.default; + + /***/ }), + /* 2 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _closest = __webpack_require__(30); + + Object.defineProperty(exports, 'closest', { + enumerable: true, + get: function () { + return _interopRequireDefault(_closest).default; + } + }); + + var _requestNextAnimationFrame = __webpack_require__(28); + + Object.defineProperty(exports, 'requestNextAnimationFrame', { + enumerable: true, + get: function () { + return _interopRequireDefault(_requestNextAnimationFrame).default; + } + }); + + var _distance = __webpack_require__(26); + + Object.defineProperty(exports, 'distance', { + enumerable: true, + get: function () { + return _interopRequireDefault(_distance).default; + } + }); + + var _touchCoords = __webpack_require__(24); + + Object.defineProperty(exports, 'touchCoords', { + enumerable: true, + get: function () { + return _interopRequireDefault(_touchCoords).default; + } + }); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + /***/ }), + /* 3 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _AbstractEvent = __webpack_require__(46); + + var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _AbstractEvent2.default; + + /***/ }), + /* 4 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _AbstractPlugin = __webpack_require__(39); + + var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _AbstractPlugin2.default; + + /***/ }), + /* 5 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _Sensor = __webpack_require__(1); + + Object.defineProperty(exports, 'Sensor', { + enumerable: true, + get: function () { + return _interopRequireDefault(_Sensor).default; + } + }); + + var _MouseSensor = __webpack_require__(21); + + Object.defineProperty(exports, 'MouseSensor', { + enumerable: true, + get: function () { + return _interopRequireDefault(_MouseSensor).default; + } + }); + + var _TouchSensor = __webpack_require__(18); + + Object.defineProperty(exports, 'TouchSensor', { + enumerable: true, + get: function () { + return _interopRequireDefault(_TouchSensor).default; + } + }); + + var _DragSensor = __webpack_require__(16); + + Object.defineProperty(exports, 'DragSensor', { + enumerable: true, + get: function () { + return _interopRequireDefault(_DragSensor).default; + } + }); + + var _ForceTouchSensor = __webpack_require__(14); + + Object.defineProperty(exports, 'ForceTouchSensor', { + enumerable: true, + get: function () { + return _interopRequireDefault(_ForceTouchSensor).default; + } + }); + + var _SensorEvent = __webpack_require__(0); + + Object.keys(_SensorEvent).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _SensorEvent[key]; + } + }); + }); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + /***/ }), + /* 6 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _Announcement = __webpack_require__(41); + + Object.defineProperty(exports, 'Announcement', { + enumerable: true, + get: function () { + return _interopRequireDefault(_Announcement).default; + } + }); + Object.defineProperty(exports, 'defaultAnnouncementOptions', { + enumerable: true, + get: function () { + return _Announcement.defaultOptions; + } + }); + + var _Focusable = __webpack_require__(38); + + Object.defineProperty(exports, 'Focusable', { + enumerable: true, + get: function () { + return _interopRequireDefault(_Focusable).default; + } + }); + + var _Mirror = __webpack_require__(36); + + Object.defineProperty(exports, 'Mirror', { + enumerable: true, + get: function () { + return _interopRequireDefault(_Mirror).default; + } + }); + Object.defineProperty(exports, 'defaultMirrorOptions', { + enumerable: true, + get: function () { + return _Mirror.defaultOptions; + } + }); + + var _Scrollable = __webpack_require__(32); + + Object.defineProperty(exports, 'Scrollable', { + enumerable: true, + get: function () { + return _interopRequireDefault(_Scrollable).default; + } + }); + Object.defineProperty(exports, 'defaultScrollableOptions', { + enumerable: true, + get: function () { + return _Scrollable.defaultOptions; + } + }); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + /***/ }), + /* 7 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _DraggableEvent = __webpack_require__(42); + + Object.keys(_DraggableEvent).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _DraggableEvent[key]; + } + }); + }); + + /***/ }), + /* 8 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _DragEvent = __webpack_require__(43); + + Object.keys(_DragEvent).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _DragEvent[key]; + } + }); + }); + + /***/ }), + /* 9 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _SortableEvent = __webpack_require__(47); + + Object.keys(_SortableEvent).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _SortableEvent[key]; + } + }); + }); + + /***/ }), + /* 10 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + /** + * The Emitter is a simple emitter class that provides you with `on()`, `off()` and `trigger()` methods + * @class Emitter + * @module Emitter + */ + class Emitter { + constructor() { + this.callbacks = {}; + } + + /** + * Registers callbacks by event name + * @param {String} type + * @param {...Function} callbacks + */ + on(type, ...callbacks) { + if (!this.callbacks[type]) { + this.callbacks[type] = []; + } + + this.callbacks[type].push(...callbacks); + + return this; + } + + /** + * Unregisters callbacks by event name + * @param {String} type + * @param {Function} callback + */ + off(type, callback) { + if (!this.callbacks[type]) { + return null; + } + + const copy = this.callbacks[type].slice(0); + + for (let i = 0; i < copy.length; i++) { + if (callback === copy[i]) { + this.callbacks[type].splice(i, 1); + } + } + + return this; + } + + /** + * Triggers event callbacks by event object + * @param {AbstractEvent} event + */ + trigger(event) { + if (!this.callbacks[event.type]) { + return null; + } + + const callbacks = [...this.callbacks[event.type]]; + const caughtErrors = []; + + for (let i = callbacks.length - 1; i >= 0; i--) { + const callback = callbacks[i]; + + try { + callback(event); + } catch (error) { + caughtErrors.push(error); + } + } + + if (caughtErrors.length) { + /* eslint-disable no-console */ + console.error(`Draggable caught errors while triggering '${event.type}'`, caughtErrors); + /* eslint-disable no-console */ + } + + return this; + } + } + exports.default = Emitter; + + /***/ }), + /* 11 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _Emitter = __webpack_require__(10); + + var _Emitter2 = _interopRequireDefault(_Emitter); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _Emitter2.default; + + /***/ }), + /* 12 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.defaultOptions = undefined; + + var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + + var _utils = __webpack_require__(2); + + var _Plugins = __webpack_require__(6); + + var _Emitter = __webpack_require__(11); + + var _Emitter2 = _interopRequireDefault(_Emitter); + + var _Sensors = __webpack_require__(5); + + var _DraggableEvent = __webpack_require__(7); + + var _DragEvent = __webpack_require__(8); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + const onDragStart = Symbol('onDragStart'); + const onDragMove = Symbol('onDragMove'); + const onDragStop = Symbol('onDragStop'); + const onDragPressure = Symbol('onDragPressure'); + + /** + * @const {Object} defaultAnnouncements + * @const {Function} defaultAnnouncements['drag:start'] + * @const {Function} defaultAnnouncements['drag:stop'] + */ + const defaultAnnouncements = { + 'drag:start': event => `Picked up ${event.source.textContent.trim() || event.source.id || 'draggable element'}`, + 'drag:stop': event => `Released ${event.source.textContent.trim() || event.source.id || 'draggable element'}` + }; + + const defaultClasses = { + 'container:dragging': 'draggable-container--is-dragging', + 'source:dragging': 'draggable-source--is-dragging', + 'source:placed': 'draggable-source--placed', + 'container:placed': 'draggable-container--placed', + 'body:dragging': 'draggable--is-dragging', + 'draggable:over': 'draggable--over', + 'container:over': 'draggable-container--over', + 'source:original': 'draggable--original', + mirror: 'draggable-mirror' + }; + + const defaultOptions = exports.defaultOptions = { + draggable: '.draggable-source', + handle: null, + delay: {}, + distance: 0, + placedTimeout: 800, + plugins: [], + sensors: [], + exclude: { + plugins: [], + sensors: [] + } + }; + + /** + * This is the core draggable library that does the heavy lifting + * @class Draggable + * @module Draggable + */ + class Draggable { + + /** + * Draggable constructor. + * @constructs Draggable + * @param {HTMLElement[]|NodeList|HTMLElement} containers - Draggable containers + * @param {Object} options - Options for draggable + */ + + /** + * Default plugins draggable uses + * @static + * @property {Object} Plugins + * @property {Announcement} Plugins.Announcement + * @property {Focusable} Plugins.Focusable + * @property {Mirror} Plugins.Mirror + * @property {Scrollable} Plugins.Scrollable + * @type {Object} + */ + constructor(containers = [document.body], options = {}) { + /** + * Draggable containers + * @property containers + * @type {HTMLElement[]} + */ + if (containers instanceof NodeList || containers instanceof Array) { + this.containers = [...containers]; + } else if (containers instanceof HTMLElement) { + this.containers = [containers]; + } else { + throw new Error('Draggable containers are expected to be of type `NodeList`, `HTMLElement[]` or `HTMLElement`'); + } + + this.options = _extends({}, defaultOptions, options, { + classes: _extends({}, defaultClasses, options.classes || {}), + announcements: _extends({}, defaultAnnouncements, options.announcements || {}), + exclude: { + plugins: options.exclude && options.exclude.plugins || [], + sensors: options.exclude && options.exclude.sensors || [] + } + }); + + /** + * Draggables event emitter + * @property emitter + * @type {Emitter} + */ + this.emitter = new _Emitter2.default(); + + /** + * Current drag state + * @property dragging + * @type {Boolean} + */ + this.dragging = false; + + /** + * Active plugins + * @property plugins + * @type {Plugin[]} + */ + this.plugins = []; + + /** + * Active sensors + * @property sensors + * @type {Sensor[]} + */ + this.sensors = []; + + this[onDragStart] = this[onDragStart].bind(this); + this[onDragMove] = this[onDragMove].bind(this); + this[onDragStop] = this[onDragStop].bind(this); + this[onDragPressure] = this[onDragPressure].bind(this); + + document.addEventListener('drag:start', this[onDragStart], true); + document.addEventListener('drag:move', this[onDragMove], true); + document.addEventListener('drag:stop', this[onDragStop], true); + document.addEventListener('drag:pressure', this[onDragPressure], true); + + const defaultPlugins = Object.values(Draggable.Plugins).filter(Plugin => !this.options.exclude.plugins.includes(Plugin)); + const defaultSensors = Object.values(Draggable.Sensors).filter(sensor => !this.options.exclude.sensors.includes(sensor)); + + this.addPlugin(...[...defaultPlugins, ...this.options.plugins]); + this.addSensor(...[...defaultSensors, ...this.options.sensors]); + + const draggableInitializedEvent = new _DraggableEvent.DraggableInitializedEvent({ + draggable: this + }); + + this.on('mirror:created', ({ mirror }) => this.mirror = mirror); + this.on('mirror:destroy', () => this.mirror = null); + + this.trigger(draggableInitializedEvent); + } + + /** + * Destroys Draggable instance. This removes all internal event listeners and + * deactivates sensors and plugins + */ + + + /** + * Default sensors draggable uses + * @static + * @property {Object} Sensors + * @property {MouseSensor} Sensors.MouseSensor + * @property {TouchSensor} Sensors.TouchSensor + * @type {Object} + */ + destroy() { + document.removeEventListener('drag:start', this[onDragStart], true); + document.removeEventListener('drag:move', this[onDragMove], true); + document.removeEventListener('drag:stop', this[onDragStop], true); + document.removeEventListener('drag:pressure', this[onDragPressure], true); + + const draggableDestroyEvent = new _DraggableEvent.DraggableDestroyEvent({ + draggable: this + }); + + this.trigger(draggableDestroyEvent); + + this.removePlugin(...this.plugins.map(plugin => plugin.constructor)); + this.removeSensor(...this.sensors.map(sensor => sensor.constructor)); + } + + /** + * Adds plugin to this draggable instance. This will end up calling the attach method of the plugin + * @param {...typeof Plugin} plugins - Plugins that you want attached to draggable + * @return {Draggable} + * @example draggable.addPlugin(CustomA11yPlugin, CustomMirrorPlugin) + */ + addPlugin(...plugins) { + const activePlugins = plugins.map(Plugin => new Plugin(this)); + + activePlugins.forEach(plugin => plugin.attach()); + this.plugins = [...this.plugins, ...activePlugins]; + + return this; + } + + /** + * Removes plugins that are already attached to this draggable instance. This will end up calling + * the detach method of the plugin + * @param {...typeof Plugin} plugins - Plugins that you want detached from draggable + * @return {Draggable} + * @example draggable.removePlugin(MirrorPlugin, CustomMirrorPlugin) + */ + removePlugin(...plugins) { + const removedPlugins = this.plugins.filter(plugin => plugins.includes(plugin.constructor)); + + removedPlugins.forEach(plugin => plugin.detach()); + this.plugins = this.plugins.filter(plugin => !plugins.includes(plugin.constructor)); + + return this; + } + + /** + * Adds sensors to this draggable instance. This will end up calling the attach method of the sensor + * @param {...typeof Sensor} sensors - Sensors that you want attached to draggable + * @return {Draggable} + * @example draggable.addSensor(ForceTouchSensor, CustomSensor) + */ + addSensor(...sensors) { + const activeSensors = sensors.map(Sensor => new Sensor(this.containers, this.options)); + + activeSensors.forEach(sensor => sensor.attach()); + this.sensors = [...this.sensors, ...activeSensors]; + + return this; + } + + /** + * Removes sensors that are already attached to this draggable instance. This will end up calling + * the detach method of the sensor + * @param {...typeof Sensor} sensors - Sensors that you want attached to draggable + * @return {Draggable} + * @example draggable.removeSensor(TouchSensor, DragSensor) + */ + removeSensor(...sensors) { + const removedSensors = this.sensors.filter(sensor => sensors.includes(sensor.constructor)); + + removedSensors.forEach(sensor => sensor.detach()); + this.sensors = this.sensors.filter(sensor => !sensors.includes(sensor.constructor)); + + return this; + } + + /** + * Adds container to this draggable instance + * @param {...HTMLElement} containers - Containers you want to add to draggable + * @return {Draggable} + * @example draggable.addContainer(document.body) + */ + addContainer(...containers) { + this.containers = [...this.containers, ...containers]; + this.sensors.forEach(sensor => sensor.addContainer(...containers)); + return this; + } + + /** + * Removes container from this draggable instance + * @param {...HTMLElement} containers - Containers you want to remove from draggable + * @return {Draggable} + * @example draggable.removeContainer(document.body) + */ + removeContainer(...containers) { + this.containers = this.containers.filter(container => !containers.includes(container)); + this.sensors.forEach(sensor => sensor.removeContainer(...containers)); + return this; + } + + /** + * Adds listener for draggable events + * @param {String} type - Event name + * @param {...Function} callbacks - Event callbacks + * @return {Draggable} + * @example draggable.on('drag:start', (dragEvent) => dragEvent.cancel()); + */ + on(type, ...callbacks) { + this.emitter.on(type, ...callbacks); + return this; + } + + /** + * Removes listener from draggable + * @param {String} type - Event name + * @param {Function} callback - Event callback + * @return {Draggable} + * @example draggable.off('drag:start', handlerFunction); + */ + off(type, callback) { + this.emitter.off(type, callback); + return this; + } + + /** + * Triggers draggable event + * @param {AbstractEvent} event - Event instance + * @return {Draggable} + * @example draggable.trigger(event); + */ + trigger(event) { + this.emitter.trigger(event); + return this; + } + + /** + * Returns class name for class identifier + * @param {String} name - Name of class identifier + * @return {String|null} + */ + getClassNameFor(name) { + return this.getClassNamesFor(name)[0]; + } + + /** + * Returns class names for class identifier + * @return {String[]} + */ + getClassNamesFor(name) { + const classNames = this.options.classes[name]; + + if (classNames instanceof Array) { + return classNames; + } else if (typeof classNames === 'string' || classNames instanceof String) { + return [classNames]; + } else { + return []; + } + } + + /** + * Returns true if this draggable instance is currently dragging + * @return {Boolean} + */ + isDragging() { + return Boolean(this.dragging); + } + + /** + * Returns all draggable elements + * @return {HTMLElement[]} + */ + getDraggableElements() { + return this.containers.reduce((current, container) => { + return [...current, ...this.getDraggableElementsForContainer(container)]; + }, []); + } + + /** + * Returns draggable elements for a given container, excluding the mirror and + * original source element if present + * @param {HTMLElement} container + * @return {HTMLElement[]} + */ + getDraggableElementsForContainer(container) { + const allDraggableElements = container.querySelectorAll(this.options.draggable); + + return [...allDraggableElements].filter(childElement => { + return childElement !== this.originalSource && childElement !== this.mirror; + }); + } + + /** + * Drag start handler + * @private + * @param {Event} event - DOM Drag event + */ + [onDragStart](event) { + const sensorEvent = getSensorEvent(event); + const { target, container } = sensorEvent; + + if (!this.containers.includes(container)) { + return; + } + + if (this.options.handle && target && !(0, _utils.closest)(target, this.options.handle)) { + sensorEvent.cancel(); + return; + } + + // Find draggable source element + this.originalSource = (0, _utils.closest)(target, this.options.draggable); + this.sourceContainer = container; + + if (!this.originalSource) { + sensorEvent.cancel(); + return; + } + + if (this.lastPlacedSource && this.lastPlacedContainer) { + clearTimeout(this.placedTimeoutID); + this.lastPlacedSource.classList.remove(...this.getClassNamesFor('source:placed')); + this.lastPlacedContainer.classList.remove(...this.getClassNamesFor('container:placed')); + } + + this.source = this.originalSource.cloneNode(true); + this.originalSource.parentNode.insertBefore(this.source, this.originalSource); + this.originalSource.style.display = 'none'; + + const dragEvent = new _DragEvent.DragStartEvent({ + source: this.source, + originalSource: this.originalSource, + sourceContainer: container, + sensorEvent + }); + + this.trigger(dragEvent); + + this.dragging = !dragEvent.canceled(); + + if (dragEvent.canceled()) { + this.source.parentNode.removeChild(this.source); + this.originalSource.style.display = null; + return; + } + + this.originalSource.classList.add(...this.getClassNamesFor('source:original')); + this.source.classList.add(...this.getClassNamesFor('source:dragging')); + this.sourceContainer.classList.add(...this.getClassNamesFor('container:dragging')); + document.body.classList.add(...this.getClassNamesFor('body:dragging')); + applyUserSelect(document.body, 'none'); + + requestAnimationFrame(() => { + const oldSensorEvent = getSensorEvent(event); + const newSensorEvent = oldSensorEvent.clone({ target: this.source }); + + this[onDragMove](_extends({}, event, { + detail: newSensorEvent + })); + }); + } + + /** + * Drag move handler + * @private + * @param {Event} event - DOM Drag event + */ + [onDragMove](event) { + if (!this.dragging) { + return; + } + + const sensorEvent = getSensorEvent(event); + const { container } = sensorEvent; + let target = sensorEvent.target; + + const dragMoveEvent = new _DragEvent.DragMoveEvent({ + source: this.source, + originalSource: this.originalSource, + sourceContainer: container, + sensorEvent + }); + + this.trigger(dragMoveEvent); + + if (dragMoveEvent.canceled()) { + sensorEvent.cancel(); + } + + target = (0, _utils.closest)(target, this.options.draggable); + const withinCorrectContainer = (0, _utils.closest)(sensorEvent.target, this.containers); + const overContainer = sensorEvent.overContainer || withinCorrectContainer; + const isLeavingContainer = this.currentOverContainer && overContainer !== this.currentOverContainer; + const isLeavingDraggable = this.currentOver && target !== this.currentOver; + const isOverContainer = overContainer && this.currentOverContainer !== overContainer; + const isOverDraggable = withinCorrectContainer && target && this.currentOver !== target; + + if (isLeavingDraggable) { + const dragOutEvent = new _DragEvent.DragOutEvent({ + source: this.source, + originalSource: this.originalSource, + sourceContainer: container, + sensorEvent, + over: this.currentOver, + overContainer: this.currentOverContainer + }); + + this.currentOver.classList.remove(...this.getClassNamesFor('draggable:over')); + this.currentOver = null; + + this.trigger(dragOutEvent); + } + + if (isLeavingContainer) { + const dragOutContainerEvent = new _DragEvent.DragOutContainerEvent({ + source: this.source, + originalSource: this.originalSource, + sourceContainer: container, + sensorEvent, + overContainer: this.currentOverContainer + }); + + this.currentOverContainer.classList.remove(...this.getClassNamesFor('container:over')); + this.currentOverContainer = null; + + this.trigger(dragOutContainerEvent); + } + + if (isOverContainer) { + overContainer.classList.add(...this.getClassNamesFor('container:over')); + + const dragOverContainerEvent = new _DragEvent.DragOverContainerEvent({ + source: this.source, + originalSource: this.originalSource, + sourceContainer: container, + sensorEvent, + overContainer + }); + + this.currentOverContainer = overContainer; + + this.trigger(dragOverContainerEvent); + } + + if (isOverDraggable) { + target.classList.add(...this.getClassNamesFor('draggable:over')); + + const dragOverEvent = new _DragEvent.DragOverEvent({ + source: this.source, + originalSource: this.originalSource, + sourceContainer: container, + sensorEvent, + overContainer, + over: target + }); + + this.currentOver = target; + + this.trigger(dragOverEvent); + } + } + + /** + * Drag stop handler + * @private + * @param {Event} event - DOM Drag event + */ + [onDragStop](event) { + if (!this.dragging) { + return; + } + + this.dragging = false; + + const dragStopEvent = new _DragEvent.DragStopEvent({ + source: this.source, + originalSource: this.originalSource, + sensorEvent: event.sensorEvent, + sourceContainer: this.sourceContainer + }); + + this.trigger(dragStopEvent); + + this.source.parentNode.insertBefore(this.originalSource, this.source); + this.source.parentNode.removeChild(this.source); + this.originalSource.style.display = ''; + + this.source.classList.remove(...this.getClassNamesFor('source:dragging')); + this.originalSource.classList.remove(...this.getClassNamesFor('source:original')); + this.originalSource.classList.add(...this.getClassNamesFor('source:placed')); + this.sourceContainer.classList.add(...this.getClassNamesFor('container:placed')); + this.sourceContainer.classList.remove(...this.getClassNamesFor('container:dragging')); + document.body.classList.remove(...this.getClassNamesFor('body:dragging')); + applyUserSelect(document.body, ''); + + if (this.currentOver) { + this.currentOver.classList.remove(...this.getClassNamesFor('draggable:over')); + } + + if (this.currentOverContainer) { + this.currentOverContainer.classList.remove(...this.getClassNamesFor('container:over')); + } + + this.lastPlacedSource = this.originalSource; + this.lastPlacedContainer = this.sourceContainer; + + this.placedTimeoutID = setTimeout(() => { + if (this.lastPlacedSource) { + this.lastPlacedSource.classList.remove(...this.getClassNamesFor('source:placed')); + } + + if (this.lastPlacedContainer) { + this.lastPlacedContainer.classList.remove(...this.getClassNamesFor('container:placed')); + } + + this.lastPlacedSource = null; + this.lastPlacedContainer = null; + }, this.options.placedTimeout); + + const dragStoppedEvent = new _DragEvent.DragStoppedEvent({ + source: this.source, + originalSource: this.originalSource, + sensorEvent: event.sensorEvent, + sourceContainer: this.sourceContainer + }); + + this.trigger(dragStoppedEvent); + + this.source = null; + this.originalSource = null; + this.currentOverContainer = null; + this.currentOver = null; + this.sourceContainer = null; + } + + /** + * Drag pressure handler + * @private + * @param {Event} event - DOM Drag event + */ + [onDragPressure](event) { + if (!this.dragging) { + return; + } + + const sensorEvent = getSensorEvent(event); + const source = this.source || (0, _utils.closest)(sensorEvent.originalEvent.target, this.options.draggable); + + const dragPressureEvent = new _DragEvent.DragPressureEvent({ + sensorEvent, + source, + pressure: sensorEvent.pressure + }); + + this.trigger(dragPressureEvent); + } + } + + exports.default = Draggable; + Draggable.Plugins = { Announcement: _Plugins.Announcement, Focusable: _Plugins.Focusable, Mirror: _Plugins.Mirror, Scrollable: _Plugins.Scrollable }; + Draggable.Sensors = { MouseSensor: _Sensors.MouseSensor, TouchSensor: _Sensors.TouchSensor }; + function getSensorEvent(event) { + return event.detail; + } + + function applyUserSelect(element, value) { + element.style.webkitUserSelect = value; + element.style.mozUserSelect = value; + element.style.msUserSelect = value; + element.style.oUserSelect = value; + element.style.userSelect = value; + } + + /***/ }), + /* 13 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _Sensor = __webpack_require__(1); + + var _Sensor2 = _interopRequireDefault(_Sensor); + + var _SensorEvent = __webpack_require__(0); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + const onMouseForceWillBegin = Symbol('onMouseForceWillBegin'); + const onMouseForceDown = Symbol('onMouseForceDown'); + const onMouseDown = Symbol('onMouseDown'); + const onMouseForceChange = Symbol('onMouseForceChange'); + const onMouseMove = Symbol('onMouseMove'); + const onMouseUp = Symbol('onMouseUp'); + const onMouseForceGlobalChange = Symbol('onMouseForceGlobalChange'); + + /** + * This sensor picks up native force touch events and dictates drag operations + * @class ForceTouchSensor + * @module ForceTouchSensor + * @extends Sensor + */ + class ForceTouchSensor extends _Sensor2.default { + /** + * ForceTouchSensor constructor. + * @constructs ForceTouchSensor + * @param {HTMLElement[]|NodeList|HTMLElement} containers - Containers + * @param {Object} options - Options + */ + constructor(containers = [], options = {}) { + super(containers, options); + + /** + * Draggable element needs to be remembered to unset the draggable attribute after drag operation has completed + * @property mightDrag + * @type {Boolean} + */ + this.mightDrag = false; + + this[onMouseForceWillBegin] = this[onMouseForceWillBegin].bind(this); + this[onMouseForceDown] = this[onMouseForceDown].bind(this); + this[onMouseDown] = this[onMouseDown].bind(this); + this[onMouseForceChange] = this[onMouseForceChange].bind(this); + this[onMouseMove] = this[onMouseMove].bind(this); + this[onMouseUp] = this[onMouseUp].bind(this); + } + + /** + * Attaches sensors event listeners to the DOM + */ + attach() { + for (const container of this.containers) { + container.addEventListener('webkitmouseforcewillbegin', this[onMouseForceWillBegin], false); + container.addEventListener('webkitmouseforcedown', this[onMouseForceDown], false); + container.addEventListener('mousedown', this[onMouseDown], true); + container.addEventListener('webkitmouseforcechanged', this[onMouseForceChange], false); + } + + document.addEventListener('mousemove', this[onMouseMove]); + document.addEventListener('mouseup', this[onMouseUp]); + } + + /** + * Detaches sensors event listeners to the DOM + */ + detach() { + for (const container of this.containers) { + container.removeEventListener('webkitmouseforcewillbegin', this[onMouseForceWillBegin], false); + container.removeEventListener('webkitmouseforcedown', this[onMouseForceDown], false); + container.removeEventListener('mousedown', this[onMouseDown], true); + container.removeEventListener('webkitmouseforcechanged', this[onMouseForceChange], false); + } + + document.removeEventListener('mousemove', this[onMouseMove]); + document.removeEventListener('mouseup', this[onMouseUp]); + } + + /** + * Mouse force will begin handler + * @private + * @param {Event} event - Mouse force will begin event + */ + [onMouseForceWillBegin](event) { + event.preventDefault(); + this.mightDrag = true; + } + + /** + * Mouse force down handler + * @private + * @param {Event} event - Mouse force down event + */ + [onMouseForceDown](event) { + if (this.dragging) { + return; + } + + const target = document.elementFromPoint(event.clientX, event.clientY); + const container = event.currentTarget; + + const dragStartEvent = new _SensorEvent.DragStartSensorEvent({ + clientX: event.clientX, + clientY: event.clientY, + target, + container, + originalEvent: event + }); + + this.trigger(container, dragStartEvent); + + this.currentContainer = container; + this.dragging = !dragStartEvent.canceled(); + this.mightDrag = false; + } + + /** + * Mouse up handler + * @private + * @param {Event} event - Mouse up event + */ + [onMouseUp](event) { + if (!this.dragging) { + return; + } + + const dragStopEvent = new _SensorEvent.DragStopSensorEvent({ + clientX: event.clientX, + clientY: event.clientY, + target: null, + container: this.currentContainer, + originalEvent: event + }); + + this.trigger(this.currentContainer, dragStopEvent); + + this.currentContainer = null; + this.dragging = false; + this.mightDrag = false; + } + + /** + * Mouse down handler + * @private + * @param {Event} event - Mouse down event + */ + [onMouseDown](event) { + if (!this.mightDrag) { + return; + } + + // Need workaround for real click + // Cancel potential drag events + event.stopPropagation(); + event.stopImmediatePropagation(); + event.preventDefault(); + } + + /** + * Mouse move handler + * @private + * @param {Event} event - Mouse force will begin event + */ + [onMouseMove](event) { + if (!this.dragging) { + return; + } + + const target = document.elementFromPoint(event.clientX, event.clientY); + + const dragMoveEvent = new _SensorEvent.DragMoveSensorEvent({ + clientX: event.clientX, + clientY: event.clientY, + target, + container: this.currentContainer, + originalEvent: event + }); + + this.trigger(this.currentContainer, dragMoveEvent); + } + + /** + * Mouse force change handler + * @private + * @param {Event} event - Mouse force change event + */ + [onMouseForceChange](event) { + if (this.dragging) { + return; + } + + const target = event.target; + const container = event.currentTarget; + + const dragPressureEvent = new _SensorEvent.DragPressureSensorEvent({ + pressure: event.webkitForce, + clientX: event.clientX, + clientY: event.clientY, + target, + container, + originalEvent: event + }); + + this.trigger(container, dragPressureEvent); + } + + /** + * Mouse force global change handler + * @private + * @param {Event} event - Mouse force global change event + */ + [onMouseForceGlobalChange](event) { + if (!this.dragging) { + return; + } + + const target = event.target; + + const dragPressureEvent = new _SensorEvent.DragPressureSensorEvent({ + pressure: event.webkitForce, + clientX: event.clientX, + clientY: event.clientY, + target, + container: this.currentContainer, + originalEvent: event + }); + + this.trigger(this.currentContainer, dragPressureEvent); + } + } + exports.default = ForceTouchSensor; + + /***/ }), + /* 14 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _ForceTouchSensor = __webpack_require__(13); + + var _ForceTouchSensor2 = _interopRequireDefault(_ForceTouchSensor); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _ForceTouchSensor2.default; + + /***/ }), + /* 15 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _utils = __webpack_require__(2); + + var _Sensor = __webpack_require__(1); + + var _Sensor2 = _interopRequireDefault(_Sensor); + + var _SensorEvent = __webpack_require__(0); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + const onMouseDown = Symbol('onMouseDown'); + const onMouseUp = Symbol('onMouseUp'); + const onDragStart = Symbol('onDragStart'); + const onDragOver = Symbol('onDragOver'); + const onDragEnd = Symbol('onDragEnd'); + const onDrop = Symbol('onDrop'); + const reset = Symbol('reset'); + + /** + * This sensor picks up native browser drag events and dictates drag operations + * @class DragSensor + * @module DragSensor + * @extends Sensor + */ + class DragSensor extends _Sensor2.default { + /** + * DragSensor constructor. + * @constructs DragSensor + * @param {HTMLElement[]|NodeList|HTMLElement} containers - Containers + * @param {Object} options - Options + */ + constructor(containers = [], options = {}) { + super(containers, options); + + /** + * Mouse down timer which will end up setting the draggable attribute, unless canceled + * @property mouseDownTimeout + * @type {Number} + */ + this.mouseDownTimeout = null; + + /** + * Draggable element needs to be remembered to unset the draggable attribute after drag operation has completed + * @property draggableElement + * @type {HTMLElement} + */ + this.draggableElement = null; + + /** + * Native draggable element could be links or images, their draggable state will be disabled during drag operation + * @property nativeDraggableElement + * @type {HTMLElement} + */ + this.nativeDraggableElement = null; + + this[onMouseDown] = this[onMouseDown].bind(this); + this[onMouseUp] = this[onMouseUp].bind(this); + this[onDragStart] = this[onDragStart].bind(this); + this[onDragOver] = this[onDragOver].bind(this); + this[onDragEnd] = this[onDragEnd].bind(this); + this[onDrop] = this[onDrop].bind(this); + } + + /** + * Attaches sensors event listeners to the DOM + */ + attach() { + document.addEventListener('mousedown', this[onMouseDown], true); + } + + /** + * Detaches sensors event listeners to the DOM + */ + detach() { + document.removeEventListener('mousedown', this[onMouseDown], true); + } + + /** + * Drag start handler + * @private + * @param {Event} event - Drag start event + */ + [onDragStart](event) { + // Need for firefox. "text" key is needed for IE + event.dataTransfer.setData('text', ''); + event.dataTransfer.effectAllowed = this.options.type; + + const target = document.elementFromPoint(event.clientX, event.clientY); + this.currentContainer = (0, _utils.closest)(event.target, this.containers); + + if (!this.currentContainer) { + return; + } + + const dragStartEvent = new _SensorEvent.DragStartSensorEvent({ + clientX: event.clientX, + clientY: event.clientY, + target, + container: this.currentContainer, + originalEvent: event + }); + + // Workaround + setTimeout(() => { + this.trigger(this.currentContainer, dragStartEvent); + + if (dragStartEvent.canceled()) { + this.dragging = false; + } else { + this.dragging = true; + } + }, 0); + } + + /** + * Drag over handler + * @private + * @param {Event} event - Drag over event + */ + [onDragOver](event) { + if (!this.dragging) { + return; + } + + const target = document.elementFromPoint(event.clientX, event.clientY); + const container = this.currentContainer; + + const dragMoveEvent = new _SensorEvent.DragMoveSensorEvent({ + clientX: event.clientX, + clientY: event.clientY, + target, + container, + originalEvent: event + }); + + this.trigger(container, dragMoveEvent); + + if (!dragMoveEvent.canceled()) { + event.preventDefault(); + event.dataTransfer.dropEffect = this.options.type; + } + } + + /** + * Drag end handler + * @private + * @param {Event} event - Drag end event + */ + [onDragEnd](event) { + if (!this.dragging) { + return; + } + + document.removeEventListener('mouseup', this[onMouseUp], true); + + const target = document.elementFromPoint(event.clientX, event.clientY); + const container = this.currentContainer; + + const dragStopEvent = new _SensorEvent.DragStopSensorEvent({ + clientX: event.clientX, + clientY: event.clientY, + target, + container, + originalEvent: event + }); + + this.trigger(container, dragStopEvent); + + this.dragging = false; + this.startEvent = null; + + this[reset](); + } + + /** + * Drop handler + * @private + * @param {Event} event - Drop event + */ + [onDrop](event) { + // eslint-disable-line class-methods-use-this + event.preventDefault(); + } + + /** + * Mouse down handler + * @private + * @param {Event} event - Mouse down event + */ + [onMouseDown](event) { + // Firefox bug for inputs within draggables https://bugzilla.mozilla.org/show_bug.cgi?id=739071 + if (event.target && (event.target.form || event.target.contenteditable)) { + return; + } + + const nativeDraggableElement = (0, _utils.closest)(event.target, element => element.draggable); + + if (nativeDraggableElement) { + nativeDraggableElement.draggable = false; + this.nativeDraggableElement = nativeDraggableElement; + } + + document.addEventListener('mouseup', this[onMouseUp], true); + document.addEventListener('dragstart', this[onDragStart], false); + document.addEventListener('dragover', this[onDragOver], false); + document.addEventListener('dragend', this[onDragEnd], false); + document.addEventListener('drop', this[onDrop], false); + + const target = (0, _utils.closest)(event.target, this.options.draggable); + + if (!target) { + return; + } + + this.startEvent = event; + + this.mouseDownTimeout = setTimeout(() => { + target.draggable = true; + this.draggableElement = target; + }, this.delay.drag); + } + + /** + * Mouse up handler + * @private + * @param {Event} event - Mouse up event + */ + [onMouseUp]() { + this[reset](); + } + + /** + * Mouse up handler + * @private + * @param {Event} event - Mouse up event + */ + [reset]() { + clearTimeout(this.mouseDownTimeout); + + document.removeEventListener('mouseup', this[onMouseUp], true); + document.removeEventListener('dragstart', this[onDragStart], false); + document.removeEventListener('dragover', this[onDragOver], false); + document.removeEventListener('dragend', this[onDragEnd], false); + document.removeEventListener('drop', this[onDrop], false); + + if (this.nativeDraggableElement) { + this.nativeDraggableElement.draggable = true; + this.nativeDraggableElement = null; + } + + if (this.draggableElement) { + this.draggableElement.draggable = false; + this.draggableElement = null; + } + } + } + exports.default = DragSensor; + + /***/ }), + /* 16 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _DragSensor = __webpack_require__(15); + + var _DragSensor2 = _interopRequireDefault(_DragSensor); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _DragSensor2.default; + + /***/ }), + /* 17 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _utils = __webpack_require__(2); + + var _Sensor = __webpack_require__(1); + + var _Sensor2 = _interopRequireDefault(_Sensor); + + var _SensorEvent = __webpack_require__(0); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + const onTouchStart = Symbol('onTouchStart'); + const onTouchEnd = Symbol('onTouchEnd'); + const onTouchMove = Symbol('onTouchMove'); + const startDrag = Symbol('startDrag'); + const onDistanceChange = Symbol('onDistanceChange'); + + /** + * Prevents scrolling when set to true + * @var {Boolean} preventScrolling + */ + let preventScrolling = false; + +// WebKit requires cancelable `touchmove` events to be added as early as possible + window.addEventListener('touchmove', event => { + if (!preventScrolling) { + return; + } + + // Prevent scrolling + event.preventDefault(); + }, { passive: false }); + + /** + * This sensor picks up native browser touch events and dictates drag operations + * @class TouchSensor + * @module TouchSensor + * @extends Sensor + */ + class TouchSensor extends _Sensor2.default { + /** + * TouchSensor constructor. + * @constructs TouchSensor + * @param {HTMLElement[]|NodeList|HTMLElement} containers - Containers + * @param {Object} options - Options + */ + constructor(containers = [], options = {}) { + super(containers, options); + + /** + * Closest scrollable container so accidental scroll can cancel long touch + * @property currentScrollableParent + * @type {HTMLElement} + */ + this.currentScrollableParent = null; + + /** + * TimeoutID for managing delay + * @property tapTimeout + * @type {Number} + */ + this.tapTimeout = null; + + /** + * touchMoved indicates if touch has moved during tapTimeout + * @property touchMoved + * @type {Boolean} + */ + this.touchMoved = false; + + /** + * Save pageX coordinates for delay drag + * @property {Numbre} pageX + * @private + */ + this.pageX = null; + + /** + * Save pageY coordinates for delay drag + * @property {Numbre} pageY + * @private + */ + this.pageY = null; + + this[onTouchStart] = this[onTouchStart].bind(this); + this[onTouchEnd] = this[onTouchEnd].bind(this); + this[onTouchMove] = this[onTouchMove].bind(this); + this[startDrag] = this[startDrag].bind(this); + this[onDistanceChange] = this[onDistanceChange].bind(this); + } + + /** + * Attaches sensors event listeners to the DOM + */ + attach() { + document.addEventListener('touchstart', this[onTouchStart]); + } + + /** + * Detaches sensors event listeners to the DOM + */ + detach() { + document.removeEventListener('touchstart', this[onTouchStart]); + } + + /** + * Touch start handler + * @private + * @param {Event} event - Touch start event + */ + [onTouchStart](event) { + const container = (0, _utils.closest)(event.target, this.containers); + + if (!container) { + return; + } + const { distance = 0 } = this.options; + const { delay } = this; + const { pageX, pageY } = (0, _utils.touchCoords)(event); + + Object.assign(this, { pageX, pageY }); + this.onTouchStartAt = Date.now(); + this.startEvent = event; + this.currentContainer = container; + + document.addEventListener('touchend', this[onTouchEnd]); + document.addEventListener('touchcancel', this[onTouchEnd]); + document.addEventListener('touchmove', this[onDistanceChange]); + container.addEventListener('contextmenu', onContextMenu); + + if (distance) { + preventScrolling = true; + } + + this.tapTimeout = window.setTimeout(() => { + this[onDistanceChange]({ touches: [{ pageX: this.pageX, pageY: this.pageY }] }); + }, delay.touch); + } + + /** + * Start the drag + * @private + */ + [startDrag]() { + const startEvent = this.startEvent; + const container = this.currentContainer; + const touch = (0, _utils.touchCoords)(startEvent); + + const dragStartEvent = new _SensorEvent.DragStartSensorEvent({ + clientX: touch.pageX, + clientY: touch.pageY, + target: startEvent.target, + container, + originalEvent: startEvent + }); + + this.trigger(this.currentContainer, dragStartEvent); + + this.dragging = !dragStartEvent.canceled(); + + if (this.dragging) { + document.addEventListener('touchmove', this[onTouchMove]); + } + preventScrolling = this.dragging; + } + + /** + * Touch move handler prior to drag start. + * @private + * @param {Event} event - Touch move event + */ + [onDistanceChange](event) { + const { distance } = this.options; + const { startEvent, delay } = this; + const start = (0, _utils.touchCoords)(startEvent); + const current = (0, _utils.touchCoords)(event); + const timeElapsed = Date.now() - this.onTouchStartAt; + const distanceTravelled = (0, _utils.distance)(start.pageX, start.pageY, current.pageX, current.pageY); + + Object.assign(this, current); + + clearTimeout(this.tapTimeout); + + if (timeElapsed < delay.touch) { + // moved during delay + document.removeEventListener('touchmove', this[onDistanceChange]); + } else if (distanceTravelled >= distance) { + document.removeEventListener('touchmove', this[onDistanceChange]); + this[startDrag](); + } + } + + /** + * Mouse move handler while dragging + * @private + * @param {Event} event - Touch move event + */ + [onTouchMove](event) { + if (!this.dragging) { + return; + } + const { pageX, pageY } = (0, _utils.touchCoords)(event); + const target = document.elementFromPoint(pageX - window.scrollX, pageY - window.scrollY); + + const dragMoveEvent = new _SensorEvent.DragMoveSensorEvent({ + clientX: pageX, + clientY: pageY, + target, + container: this.currentContainer, + originalEvent: event + }); + + this.trigger(this.currentContainer, dragMoveEvent); + } + + /** + * Touch end handler + * @private + * @param {Event} event - Touch end event + */ + [onTouchEnd](event) { + clearTimeout(this.tapTimeout); + preventScrolling = false; + + document.removeEventListener('touchend', this[onTouchEnd]); + document.removeEventListener('touchcancel', this[onTouchEnd]); + document.removeEventListener('touchmove', this[onDistanceChange]); + + if (this.currentContainer) { + this.currentContainer.removeEventListener('contextmenu', onContextMenu); + } + + if (!this.dragging) { + return; + } + + document.removeEventListener('touchmove', this[onTouchMove]); + + const { pageX, pageY } = (0, _utils.touchCoords)(event); + const target = document.elementFromPoint(pageX - window.scrollX, pageY - window.scrollY); + + event.preventDefault(); + + const dragStopEvent = new _SensorEvent.DragStopSensorEvent({ + clientX: pageX, + clientY: pageY, + target, + container: this.currentContainer, + originalEvent: event + }); + + this.trigger(this.currentContainer, dragStopEvent); + + this.currentContainer = null; + this.dragging = false; + this.startEvent = null; + } + } + + exports.default = TouchSensor; + function onContextMenu(event) { + event.preventDefault(); + event.stopPropagation(); + } + + /***/ }), + /* 18 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _TouchSensor = __webpack_require__(17); + + var _TouchSensor2 = _interopRequireDefault(_TouchSensor); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _TouchSensor2.default; + + /***/ }), + /* 19 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.DragPressureSensorEvent = exports.DragStopSensorEvent = exports.DragMoveSensorEvent = exports.DragStartSensorEvent = exports.SensorEvent = undefined; + + var _AbstractEvent = __webpack_require__(3); + + var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + /** + * Base sensor event + * @class SensorEvent + * @module SensorEvent + * @extends AbstractEvent + */ + class SensorEvent extends _AbstractEvent2.default { + /** + * Original browser event that triggered a sensor + * @property originalEvent + * @type {Event} + * @readonly + */ + get originalEvent() { + return this.data.originalEvent; + } + + /** + * Normalized clientX for both touch and mouse events + * @property clientX + * @type {Number} + * @readonly + */ + get clientX() { + return this.data.clientX; + } + + /** + * Normalized clientY for both touch and mouse events + * @property clientY + * @type {Number} + * @readonly + */ + get clientY() { + return this.data.clientY; + } + + /** + * Normalized target for both touch and mouse events + * Returns the element that is behind cursor or touch pointer + * @property target + * @type {HTMLElement} + * @readonly + */ + get target() { + return this.data.target; + } + + /** + * Container that initiated the sensor + * @property container + * @type {HTMLElement} + * @readonly + */ + get container() { + return this.data.container; + } + + /** + * Trackpad pressure + * @property pressure + * @type {Number} + * @readonly + */ + get pressure() { + return this.data.pressure; + } + } + + exports.SensorEvent = SensorEvent; /** + * Drag start sensor event + * @class DragStartSensorEvent + * @module DragStartSensorEvent + * @extends SensorEvent + */ + + class DragStartSensorEvent extends SensorEvent {} + + exports.DragStartSensorEvent = DragStartSensorEvent; /** + * Drag move sensor event + * @class DragMoveSensorEvent + * @module DragMoveSensorEvent + * @extends SensorEvent + */ + + DragStartSensorEvent.type = 'drag:start'; + class DragMoveSensorEvent extends SensorEvent {} + + exports.DragMoveSensorEvent = DragMoveSensorEvent; /** + * Drag stop sensor event + * @class DragStopSensorEvent + * @module DragStopSensorEvent + * @extends SensorEvent + */ + + DragMoveSensorEvent.type = 'drag:move'; + class DragStopSensorEvent extends SensorEvent {} + + exports.DragStopSensorEvent = DragStopSensorEvent; /** + * Drag pressure sensor event + * @class DragPressureSensorEvent + * @module DragPressureSensorEvent + * @extends SensorEvent + */ + + DragStopSensorEvent.type = 'drag:stop'; + class DragPressureSensorEvent extends SensorEvent {} + exports.DragPressureSensorEvent = DragPressureSensorEvent; + DragPressureSensorEvent.type = 'drag:pressure'; + + /***/ }), + /* 20 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _utils = __webpack_require__(2); + + var _Sensor = __webpack_require__(1); + + var _Sensor2 = _interopRequireDefault(_Sensor); + + var _SensorEvent = __webpack_require__(0); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + const onContextMenuWhileDragging = Symbol('onContextMenuWhileDragging'); + const onMouseDown = Symbol('onMouseDown'); + const onMouseMove = Symbol('onMouseMove'); + const onMouseUp = Symbol('onMouseUp'); + const startDrag = Symbol('startDrag'); + const onDistanceChange = Symbol('onDistanceChange'); + + /** + * This sensor picks up native browser mouse events and dictates drag operations + * @class MouseSensor + * @module MouseSensor + * @extends Sensor + */ + class MouseSensor extends _Sensor2.default { + /** + * MouseSensor constructor. + * @constructs MouseSensor + * @param {HTMLElement[]|NodeList|HTMLElement} containers - Containers + * @param {Object} options - Options + */ + constructor(containers = [], options = {}) { + super(containers, options); + + /** + * Mouse down timer which will end up triggering the drag start operation + * @property mouseDownTimeout + * @type {Number} + */ + this.mouseDownTimeout = null; + + /** + * Save pageX coordinates for delay drag + * @property {Numbre} pageX + * @private + */ + this.pageX = null; + + /** + * Save pageY coordinates for delay drag + * @property {Numbre} pageY + * @private + */ + this.pageY = null; + + this[onContextMenuWhileDragging] = this[onContextMenuWhileDragging].bind(this); + this[onMouseDown] = this[onMouseDown].bind(this); + this[onMouseMove] = this[onMouseMove].bind(this); + this[onMouseUp] = this[onMouseUp].bind(this); + this[startDrag] = this[startDrag].bind(this); + this[onDistanceChange] = this[onDistanceChange].bind(this); + } + + /** + * Attaches sensors event listeners to the DOM + */ + attach() { + document.addEventListener('mousedown', this[onMouseDown], true); + } + + /** + * Detaches sensors event listeners to the DOM + */ + detach() { + document.removeEventListener('mousedown', this[onMouseDown], true); + } + + /** + * Mouse down handler + * @private + * @param {Event} event - Mouse down event + */ + [onMouseDown](event) { + if (event.button !== 0 || event.ctrlKey || event.metaKey) { + return; + } + const container = (0, _utils.closest)(event.target, this.containers); + + if (!container) { + return; + } + + const { delay } = this; + const { pageX, pageY } = event; + + Object.assign(this, { pageX, pageY }); + this.onMouseDownAt = Date.now(); + this.startEvent = event; + + this.currentContainer = container; + document.addEventListener('mouseup', this[onMouseUp]); + document.addEventListener('dragstart', preventNativeDragStart); + document.addEventListener('mousemove', this[onDistanceChange]); + + this.mouseDownTimeout = window.setTimeout(() => { + this[onDistanceChange]({ pageX: this.pageX, pageY: this.pageY }); + }, delay.mouse); + } + + /** + * Start the drag + * @private + */ + [startDrag]() { + const startEvent = this.startEvent; + const container = this.currentContainer; + + const dragStartEvent = new _SensorEvent.DragStartSensorEvent({ + clientX: startEvent.clientX, + clientY: startEvent.clientY, + target: startEvent.target, + container, + originalEvent: startEvent + }); + + this.trigger(this.currentContainer, dragStartEvent); + + this.dragging = !dragStartEvent.canceled(); + + if (this.dragging) { + document.addEventListener('contextmenu', this[onContextMenuWhileDragging], true); + document.addEventListener('mousemove', this[onMouseMove]); + } + } + + /** + * Detect change in distance, starting drag when both + * delay and distance requirements are met + * @private + * @param {Event} event - Mouse move event + */ + [onDistanceChange](event) { + const { pageX, pageY } = event; + const { distance } = this.options; + const { startEvent, delay } = this; + + Object.assign(this, { pageX, pageY }); + + if (!this.currentContainer) { + return; + } + + const timeElapsed = Date.now() - this.onMouseDownAt; + const distanceTravelled = (0, _utils.distance)(startEvent.pageX, startEvent.pageY, pageX, pageY) || 0; + + clearTimeout(this.mouseDownTimeout); + + if (timeElapsed < delay.mouse) { + // moved during delay + document.removeEventListener('mousemove', this[onDistanceChange]); + } else if (distanceTravelled >= distance) { + document.removeEventListener('mousemove', this[onDistanceChange]); + this[startDrag](); + } + } + + /** + * Mouse move handler + * @private + * @param {Event} event - Mouse move event + */ + [onMouseMove](event) { + if (!this.dragging) { + return; + } + + const target = document.elementFromPoint(event.clientX, event.clientY); + + const dragMoveEvent = new _SensorEvent.DragMoveSensorEvent({ + clientX: event.clientX, + clientY: event.clientY, + target, + container: this.currentContainer, + originalEvent: event + }); + + this.trigger(this.currentContainer, dragMoveEvent); + } + + /** + * Mouse up handler + * @private + * @param {Event} event - Mouse up event + */ + [onMouseUp](event) { + clearTimeout(this.mouseDownTimeout); + + if (event.button !== 0) { + return; + } + + document.removeEventListener('mouseup', this[onMouseUp]); + document.removeEventListener('dragstart', preventNativeDragStart); + document.removeEventListener('mousemove', this[onDistanceChange]); + + if (!this.dragging) { + return; + } + + const target = document.elementFromPoint(event.clientX, event.clientY); + + const dragStopEvent = new _SensorEvent.DragStopSensorEvent({ + clientX: event.clientX, + clientY: event.clientY, + target, + container: this.currentContainer, + originalEvent: event + }); + + this.trigger(this.currentContainer, dragStopEvent); + + document.removeEventListener('contextmenu', this[onContextMenuWhileDragging], true); + document.removeEventListener('mousemove', this[onMouseMove]); + + this.currentContainer = null; + this.dragging = false; + this.startEvent = null; + } + + /** + * Context menu handler + * @private + * @param {Event} event - Context menu event + */ + [onContextMenuWhileDragging](event) { + event.preventDefault(); + } + } + + exports.default = MouseSensor; + function preventNativeDragStart(event) { + event.preventDefault(); + } + + /***/ }), + /* 21 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _MouseSensor = __webpack_require__(20); + + var _MouseSensor2 = _interopRequireDefault(_MouseSensor); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _MouseSensor2.default; + + /***/ }), + /* 22 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + + const defaultDealy = { + mouse: 0, + drag: 0, + touch: 100 + }; + + /** + * Base sensor class. Extend from this class to create a new or custom sensor + * @class Sensor + * @module Sensor + */ + class Sensor { + /** + * Sensor constructor. + * @constructs Sensor + * @param {HTMLElement[]|NodeList|HTMLElement} containers - Containers + * @param {Object} options - Options + */ + constructor(containers = [], options = {}) { + /** + * Current containers + * @property containers + * @type {HTMLElement[]} + */ + this.containers = [...containers]; + + /** + * Current options + * @property options + * @type {Object} + */ + this.options = _extends({}, options); + + /** + * Current drag state + * @property dragging + * @type {Boolean} + */ + this.dragging = false; + + /** + * Current container + * @property currentContainer + * @type {HTMLElement} + */ + this.currentContainer = null; + + /** + * The event of the initial sensor down + * @property startEvent + * @type {Event} + */ + this.startEvent = null; + + /** + * The delay of each sensor + * @property delay + * @type {Object} + */ + this.delay = calcDelay(options.delay); + } + + /** + * Attaches sensors event listeners to the DOM + * @return {Sensor} + */ + attach() { + return this; + } + + /** + * Detaches sensors event listeners to the DOM + * @return {Sensor} + */ + detach() { + return this; + } + + /** + * Adds container to this sensor instance + * @param {...HTMLElement} containers - Containers you want to add to this sensor + * @example draggable.addContainer(document.body) + */ + addContainer(...containers) { + this.containers = [...this.containers, ...containers]; + } + + /** + * Removes container from this sensor instance + * @param {...HTMLElement} containers - Containers you want to remove from this sensor + * @example draggable.removeContainer(document.body) + */ + removeContainer(...containers) { + this.containers = this.containers.filter(container => !containers.includes(container)); + } + + /** + * Triggers event on target element + * @param {HTMLElement} element - Element to trigger event on + * @param {SensorEvent} sensorEvent - Sensor event to trigger + */ + trigger(element, sensorEvent) { + const event = document.createEvent('Event'); + event.detail = sensorEvent; + event.initEvent(sensorEvent.type, true, true); + element.dispatchEvent(event); + this.lastEvent = sensorEvent; + + return sensorEvent; + } + } + + exports.default = Sensor; /** + * Calculate the delay of each sensor through the delay in the options + * @param {undefined|Number|Object} optionsDelay - the delay in the options + * @return {Object} + */ + + function calcDelay(optionsDelay) { + const delay = {}; + + if (optionsDelay === undefined) { + return _extends({}, defaultDealy); + } + + if (typeof optionsDelay === 'number') { + for (const key in defaultDealy) { + if (defaultDealy.hasOwnProperty(key)) { + delay[key] = optionsDelay; + } + } + return delay; + } + + for (const key in defaultDealy) { + if (defaultDealy.hasOwnProperty(key)) { + if (optionsDelay[key] === undefined) { + delay[key] = defaultDealy[key]; + } else { + delay[key] = optionsDelay[key]; + } + } + } + + return delay; + } + + /***/ }), + /* 23 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.default = touchCoords; + /** + * Returns the first touch event found in touches or changedTouches of a touch events. + * @param {TouchEvent} event a touch event + * @return {Touch} a touch object + */ + function touchCoords(event = {}) { + const { touches, changedTouches } = event; + return touches && touches[0] || changedTouches && changedTouches[0]; + } + + /***/ }), + /* 24 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _touchCoords = __webpack_require__(23); + + var _touchCoords2 = _interopRequireDefault(_touchCoords); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _touchCoords2.default; + + /***/ }), + /* 25 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.default = distance; + /** + * Returns the distance between two points + * @param {Number} x1 The X position of the first point + * @param {Number} y1 The Y position of the first point + * @param {Number} x2 The X position of the second point + * @param {Number} y2 The Y position of the second point + * @return {Number} + */ + function distance(x1, y1, x2, y2) { + return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); + } + + /***/ }), + /* 26 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _distance = __webpack_require__(25); + + var _distance2 = _interopRequireDefault(_distance); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _distance2.default; + + /***/ }), + /* 27 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.default = requestNextAnimationFrame; + function requestNextAnimationFrame(callback) { + return requestAnimationFrame(() => { + requestAnimationFrame(callback); + }); + } + + /***/ }), + /* 28 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _requestNextAnimationFrame = __webpack_require__(27); + + var _requestNextAnimationFrame2 = _interopRequireDefault(_requestNextAnimationFrame); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _requestNextAnimationFrame2.default; + + /***/ }), + /* 29 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.default = closest; + const matchFunction = Element.prototype.matches || Element.prototype.webkitMatchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector; + + /** + * Get the closest parent element of a given element that matches the given + * selector string or matching function + * + * @param {Element} element The child element to find a parent of + * @param {String|Function} selector The string or function to use to match + * the parent element + * @return {Element|null} + */ + function closest(element, value) { + if (!element) { + return null; + } + + const selector = value; + const callback = value; + const nodeList = value; + const singleElement = value; + + const isSelector = Boolean(typeof value === 'string'); + const isFunction = Boolean(typeof value === 'function'); + const isNodeList = Boolean(value instanceof NodeList || value instanceof Array); + const isElement = Boolean(value instanceof HTMLElement); + + function conditionFn(currentElement) { + if (!currentElement) { + return currentElement; + } else if (isSelector) { + return matchFunction.call(currentElement, selector); + } else if (isNodeList) { + return [...nodeList].includes(currentElement); + } else if (isElement) { + return singleElement === currentElement; + } else if (isFunction) { + return callback(currentElement); + } else { + return null; + } + } + + let current = element; + + do { + current = current.correspondingUseElement || current.correspondingElement || current; + + if (conditionFn(current)) { + return current; + } + + current = current.parentNode; + } while (current && current !== document.body && current !== document); + + return null; + } + + /***/ }), + /* 30 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _closest = __webpack_require__(29); + + var _closest2 = _interopRequireDefault(_closest); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _closest2.default; + + /***/ }), + /* 31 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.defaultOptions = exports.scroll = exports.onDragStop = exports.onDragMove = exports.onDragStart = undefined; + + var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + + var _AbstractPlugin = __webpack_require__(4); + + var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin); + + var _utils = __webpack_require__(2); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + const onDragStart = exports.onDragStart = Symbol('onDragStart'); + const onDragMove = exports.onDragMove = Symbol('onDragMove'); + const onDragStop = exports.onDragStop = Symbol('onDragStop'); + const scroll = exports.scroll = Symbol('scroll'); + + /** + * Scrollable default options + * @property {Object} defaultOptions + * @property {Number} defaultOptions.speed + * @property {Number} defaultOptions.sensitivity + * @property {HTMLElement[]} defaultOptions.scrollableElements + * @type {Object} + */ + const defaultOptions = exports.defaultOptions = { + speed: 6, + sensitivity: 50, + scrollableElements: [] + }; + + /** + * Scrollable plugin which scrolls the closest scrollable parent + * @class Scrollable + * @module Scrollable + * @extends AbstractPlugin + */ + class Scrollable extends _AbstractPlugin2.default { + /** + * Scrollable constructor. + * @constructs Scrollable + * @param {Draggable} draggable - Draggable instance + */ + constructor(draggable) { + super(draggable); + + /** + * Scrollable options + * @property {Object} options + * @property {Number} options.speed + * @property {Number} options.sensitivity + * @property {HTMLElement[]} options.scrollableElements + * @type {Object} + */ + this.options = _extends({}, defaultOptions, this.getOptions()); + + /** + * Keeps current mouse position + * @property {Object} currentMousePosition + * @property {Number} currentMousePosition.clientX + * @property {Number} currentMousePosition.clientY + * @type {Object|null} + */ + this.currentMousePosition = null; + + /** + * Scroll animation frame + * @property scrollAnimationFrame + * @type {Number|null} + */ + this.scrollAnimationFrame = null; + + /** + * Closest scrollable element + * @property scrollableElement + * @type {HTMLElement|null} + */ + this.scrollableElement = null; + + /** + * Animation frame looking for the closest scrollable element + * @property findScrollableElementFrame + * @type {Number|null} + */ + this.findScrollableElementFrame = null; + + this[onDragStart] = this[onDragStart].bind(this); + this[onDragMove] = this[onDragMove].bind(this); + this[onDragStop] = this[onDragStop].bind(this); + this[scroll] = this[scroll].bind(this); + } + + /** + * Attaches plugins event listeners + */ + attach() { + this.draggable.on('drag:start', this[onDragStart]).on('drag:move', this[onDragMove]).on('drag:stop', this[onDragStop]); + } + + /** + * Detaches plugins event listeners + */ + detach() { + this.draggable.off('drag:start', this[onDragStart]).off('drag:move', this[onDragMove]).off('drag:stop', this[onDragStop]); + } + + /** + * Returns options passed through draggable + * @return {Object} + */ + getOptions() { + return this.draggable.options.scrollable || {}; + } + + /** + * Returns closest scrollable elements by element + * @param {HTMLElement} target + * @return {HTMLElement} + */ + getScrollableElement(target) { + if (this.hasDefinedScrollableElements()) { + return (0, _utils.closest)(target, this.options.scrollableElements) || document.documentElement; + } else { + return closestScrollableElement(target); + } + } + + /** + * Returns true if at least one scrollable element have been defined via options + * @param {HTMLElement} target + * @return {Boolean} + */ + hasDefinedScrollableElements() { + return Boolean(this.options.scrollableElements.length !== 0); + } + + /** + * Drag start handler. Finds closest scrollable parent in separate frame + * @param {DragStartEvent} dragEvent + * @private + */ + [onDragStart](dragEvent) { + this.findScrollableElementFrame = requestAnimationFrame(() => { + this.scrollableElement = this.getScrollableElement(dragEvent.source); + }); + } + + /** + * Drag move handler. Remembers mouse position and initiates scrolling + * @param {DragMoveEvent} dragEvent + * @private + */ + [onDragMove](dragEvent) { + this.findScrollableElementFrame = requestAnimationFrame(() => { + this.scrollableElement = this.getScrollableElement(dragEvent.sensorEvent.target); + }); + + if (!this.scrollableElement) { + return; + } + + const sensorEvent = dragEvent.sensorEvent; + const scrollOffset = { x: 0, y: 0 }; + + if ('ontouchstart' in window) { + scrollOffset.y = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; + scrollOffset.x = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0; + } + + this.currentMousePosition = { + clientX: sensorEvent.clientX - scrollOffset.x, + clientY: sensorEvent.clientY - scrollOffset.y + }; + + this.scrollAnimationFrame = requestAnimationFrame(this[scroll]); + } + + /** + * Drag stop handler. Cancels scroll animations and resets state + * @private + */ + [onDragStop]() { + cancelAnimationFrame(this.scrollAnimationFrame); + cancelAnimationFrame(this.findScrollableElementFrame); + + this.scrollableElement = null; + this.scrollAnimationFrame = null; + this.findScrollableElementFrame = null; + this.currentMousePosition = null; + } + + /** + * Scroll function that does the heavylifting + * @private + */ + [scroll]() { + if (!this.scrollableElement || !this.currentMousePosition) { + return; + } + + cancelAnimationFrame(this.scrollAnimationFrame); + + const { speed, sensitivity } = this.options; + + const rect = this.scrollableElement.getBoundingClientRect(); + const bottomCutOff = rect.bottom > window.innerHeight; + const topCutOff = rect.top < 0; + const cutOff = topCutOff || bottomCutOff; + + const documentScrollingElement = getDocumentScrollingElement(); + const scrollableElement = this.scrollableElement; + const clientX = this.currentMousePosition.clientX; + const clientY = this.currentMousePosition.clientY; + + if (scrollableElement !== document.body && scrollableElement !== document.documentElement && !cutOff) { + const { offsetHeight, offsetWidth } = scrollableElement; + + if (rect.top + offsetHeight - clientY < sensitivity) { + scrollableElement.scrollTop += speed; + } else if (clientY - rect.top < sensitivity) { + scrollableElement.scrollTop -= speed; + } + + if (rect.left + offsetWidth - clientX < sensitivity) { + scrollableElement.scrollLeft += speed; + } else if (clientX - rect.left < sensitivity) { + scrollableElement.scrollLeft -= speed; + } + } else { + const { innerHeight, innerWidth } = window; + + if (clientY < sensitivity) { + documentScrollingElement.scrollTop -= speed; + } else if (innerHeight - clientY < sensitivity) { + documentScrollingElement.scrollTop += speed; + } + + if (clientX < sensitivity) { + documentScrollingElement.scrollLeft -= speed; + } else if (innerWidth - clientX < sensitivity) { + documentScrollingElement.scrollLeft += speed; + } + } + + this.scrollAnimationFrame = requestAnimationFrame(this[scroll]); + } + } + + exports.default = Scrollable; /** + * Returns true if the passed element has overflow + * @param {HTMLElement} element + * @return {Boolean} + * @private + */ + + function hasOverflow(element) { + const overflowRegex = /(auto|scroll)/; + const computedStyles = getComputedStyle(element, null); + + const overflow = computedStyles.getPropertyValue('overflow') + computedStyles.getPropertyValue('overflow-y') + computedStyles.getPropertyValue('overflow-x'); + + return overflowRegex.test(overflow); + } + + /** + * Returns true if the passed element is statically positioned + * @param {HTMLElement} element + * @return {Boolean} + * @private + */ + function isStaticallyPositioned(element) { + const position = getComputedStyle(element).getPropertyValue('position'); + return position === 'static'; + } + + /** + * Finds closest scrollable element + * @param {HTMLElement} element + * @return {HTMLElement} + * @private + */ + function closestScrollableElement(element) { + if (!element) { + return getDocumentScrollingElement(); + } + + const position = getComputedStyle(element).getPropertyValue('position'); + const excludeStaticParents = position === 'absolute'; + + const scrollableElement = (0, _utils.closest)(element, parent => { + if (excludeStaticParents && isStaticallyPositioned(parent)) { + return false; + } + return hasOverflow(parent); + }); + + if (position === 'fixed' || !scrollableElement) { + return getDocumentScrollingElement(); + } else { + return scrollableElement; + } + } + + /** + * Returns element that scrolls document + * @return {HTMLElement} + * @private + */ + function getDocumentScrollingElement() { + return document.scrollingElement || document.documentElement; + } + + /***/ }), + /* 32 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.defaultOptions = undefined; + + var _Scrollable = __webpack_require__(31); + + var _Scrollable2 = _interopRequireDefault(_Scrollable); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _Scrollable2.default; + exports.defaultOptions = _Scrollable.defaultOptions; + + /***/ }), + /* 33 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.MirrorDestroyEvent = exports.MirrorMoveEvent = exports.MirrorAttachedEvent = exports.MirrorCreatedEvent = exports.MirrorCreateEvent = exports.MirrorEvent = undefined; + + var _AbstractEvent = __webpack_require__(3); + + var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + /** + * Base mirror event + * @class MirrorEvent + * @module MirrorEvent + * @extends AbstractEvent + */ + class MirrorEvent extends _AbstractEvent2.default { + /** + * Draggables source element + * @property source + * @type {HTMLElement} + * @readonly + */ + get source() { + return this.data.source; + } + + /** + * Draggables original source element + * @property originalSource + * @type {HTMLElement} + * @readonly + */ + get originalSource() { + return this.data.originalSource; + } + + /** + * Draggables source container element + * @property sourceContainer + * @type {HTMLElement} + * @readonly + */ + get sourceContainer() { + return this.data.sourceContainer; + } + + /** + * Sensor event + * @property sensorEvent + * @type {SensorEvent} + * @readonly + */ + get sensorEvent() { + return this.data.sensorEvent; + } + + /** + * Drag event + * @property dragEvent + * @type {DragEvent} + * @readonly + */ + get dragEvent() { + return this.data.dragEvent; + } + + /** + * Original event that triggered sensor event + * @property originalEvent + * @type {Event} + * @readonly + */ + get originalEvent() { + if (this.sensorEvent) { + return this.sensorEvent.originalEvent; + } + + return null; + } + } + + exports.MirrorEvent = MirrorEvent; /** + * Mirror create event + * @class MirrorCreateEvent + * @module MirrorCreateEvent + * @extends MirrorEvent + */ + + class MirrorCreateEvent extends MirrorEvent {} + + exports.MirrorCreateEvent = MirrorCreateEvent; /** + * Mirror created event + * @class MirrorCreatedEvent + * @module MirrorCreatedEvent + * @extends MirrorEvent + */ + + MirrorCreateEvent.type = 'mirror:create'; + class MirrorCreatedEvent extends MirrorEvent { + + /** + * Draggables mirror element + * @property mirror + * @type {HTMLElement} + * @readonly + */ + get mirror() { + return this.data.mirror; + } + } + + exports.MirrorCreatedEvent = MirrorCreatedEvent; /** + * Mirror attached event + * @class MirrorAttachedEvent + * @module MirrorAttachedEvent + * @extends MirrorEvent + */ + + MirrorCreatedEvent.type = 'mirror:created'; + class MirrorAttachedEvent extends MirrorEvent { + + /** + * Draggables mirror element + * @property mirror + * @type {HTMLElement} + * @readonly + */ + get mirror() { + return this.data.mirror; + } + } + + exports.MirrorAttachedEvent = MirrorAttachedEvent; /** + * Mirror move event + * @class MirrorMoveEvent + * @module MirrorMoveEvent + * @extends MirrorEvent + */ + + MirrorAttachedEvent.type = 'mirror:attached'; + class MirrorMoveEvent extends MirrorEvent { + + /** + * Draggables mirror element + * @property mirror + * @type {HTMLElement} + * @readonly + */ + get mirror() { + return this.data.mirror; + } + + /** + * Sensor has exceeded mirror's threshold on x axis + * @type {Boolean} + * @readonly + */ + get passedThreshX() { + return this.data.passedThreshX; + } + + /** + * Sensor has exceeded mirror's threshold on y axis + * @type {Boolean} + * @readonly + */ + get passedThreshY() { + return this.data.passedThreshY; + } + } + + exports.MirrorMoveEvent = MirrorMoveEvent; /** + * Mirror destroy event + * @class MirrorDestroyEvent + * @module MirrorDestroyEvent + * @extends MirrorEvent + */ + + MirrorMoveEvent.type = 'mirror:move'; + MirrorMoveEvent.cancelable = true; + class MirrorDestroyEvent extends MirrorEvent { + + /** + * Draggables mirror element + * @property mirror + * @type {HTMLElement} + * @readonly + */ + get mirror() { + return this.data.mirror; + } + } + exports.MirrorDestroyEvent = MirrorDestroyEvent; + MirrorDestroyEvent.type = 'mirror:destroy'; + MirrorDestroyEvent.cancelable = true; + + /***/ }), + /* 34 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _MirrorEvent = __webpack_require__(33); + + Object.keys(_MirrorEvent).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _MirrorEvent[key]; + } + }); + }); + + /***/ }), + /* 35 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.defaultOptions = exports.getAppendableContainer = exports.onScroll = exports.onMirrorMove = exports.onMirrorCreated = exports.onDragStop = exports.onDragMove = exports.onDragStart = undefined; + + var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + + var _AbstractPlugin = __webpack_require__(4); + + var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin); + + var _MirrorEvent = __webpack_require__(34); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } + + const onDragStart = exports.onDragStart = Symbol('onDragStart'); + const onDragMove = exports.onDragMove = Symbol('onDragMove'); + const onDragStop = exports.onDragStop = Symbol('onDragStop'); + const onMirrorCreated = exports.onMirrorCreated = Symbol('onMirrorCreated'); + const onMirrorMove = exports.onMirrorMove = Symbol('onMirrorMove'); + const onScroll = exports.onScroll = Symbol('onScroll'); + const getAppendableContainer = exports.getAppendableContainer = Symbol('getAppendableContainer'); + + /** + * Mirror default options + * @property {Object} defaultOptions + * @property {Boolean} defaultOptions.constrainDimensions + * @property {Boolean} defaultOptions.xAxis + * @property {Boolean} defaultOptions.yAxis + * @property {null} defaultOptions.cursorOffsetX + * @property {null} defaultOptions.cursorOffsetY + * @type {Object} + */ + const defaultOptions = exports.defaultOptions = { + constrainDimensions: false, + xAxis: true, + yAxis: true, + cursorOffsetX: null, + cursorOffsetY: null, + thresholdX: null, + thresholdY: null + }; + + /** + * Mirror plugin which controls the mirror positioning while dragging + * @class Mirror + * @module Mirror + * @extends AbstractPlugin + */ + class Mirror extends _AbstractPlugin2.default { + /** + * Mirror constructor. + * @constructs Mirror + * @param {Draggable} draggable - Draggable instance + */ + constructor(draggable) { + super(draggable); + + /** + * Mirror options + * @property {Object} options + * @property {Boolean} options.constrainDimensions + * @property {Boolean} options.xAxis + * @property {Boolean} options.yAxis + * @property {Number|null} options.cursorOffsetX + * @property {Number|null} options.cursorOffsetY + * @property {String|HTMLElement|Function} options.appendTo + * @type {Object} + */ + this.options = _extends({}, defaultOptions, this.getOptions()); + + /** + * Scroll offset for touch devices because the mirror is positioned fixed + * @property {Object} scrollOffset + * @property {Number} scrollOffset.x + * @property {Number} scrollOffset.y + */ + this.scrollOffset = { x: 0, y: 0 }; + + /** + * Initial scroll offset for touch devices because the mirror is positioned fixed + * @property {Object} scrollOffset + * @property {Number} scrollOffset.x + * @property {Number} scrollOffset.y + */ + this.initialScrollOffset = { + x: window.scrollX, + y: window.scrollY + }; + + this[onDragStart] = this[onDragStart].bind(this); + this[onDragMove] = this[onDragMove].bind(this); + this[onDragStop] = this[onDragStop].bind(this); + this[onMirrorCreated] = this[onMirrorCreated].bind(this); + this[onMirrorMove] = this[onMirrorMove].bind(this); + this[onScroll] = this[onScroll].bind(this); + } + + /** + * Attaches plugins event listeners + */ + attach() { + this.draggable.on('drag:start', this[onDragStart]).on('drag:move', this[onDragMove]).on('drag:stop', this[onDragStop]).on('mirror:created', this[onMirrorCreated]).on('mirror:move', this[onMirrorMove]); + } + + /** + * Detaches plugins event listeners + */ + detach() { + this.draggable.off('drag:start', this[onDragStart]).off('drag:move', this[onDragMove]).off('drag:stop', this[onDragStop]).off('mirror:created', this[onMirrorCreated]).off('mirror:move', this[onMirrorMove]); + } + + /** + * Returns options passed through draggable + * @return {Object} + */ + getOptions() { + return this.draggable.options.mirror || {}; + } + + [onDragStart](dragEvent) { + if (dragEvent.canceled()) { + return; + } + + if ('ontouchstart' in window) { + document.addEventListener('scroll', this[onScroll], true); + } + + this.initialScrollOffset = { + x: window.scrollX, + y: window.scrollY + }; + + const { source, originalSource, sourceContainer, sensorEvent } = dragEvent; + + // Last sensor position of mirror move + this.lastMirrorMovedClient = { + x: sensorEvent.clientX, + y: sensorEvent.clientY + }; + + const mirrorCreateEvent = new _MirrorEvent.MirrorCreateEvent({ + source, + originalSource, + sourceContainer, + sensorEvent, + dragEvent + }); + + this.draggable.trigger(mirrorCreateEvent); + + if (isNativeDragEvent(sensorEvent) || mirrorCreateEvent.canceled()) { + return; + } + + const appendableContainer = this[getAppendableContainer](source) || sourceContainer; + this.mirror = source.cloneNode(true); + + const mirrorCreatedEvent = new _MirrorEvent.MirrorCreatedEvent({ + source, + originalSource, + sourceContainer, + sensorEvent, + dragEvent, + mirror: this.mirror + }); + + const mirrorAttachedEvent = new _MirrorEvent.MirrorAttachedEvent({ + source, + originalSource, + sourceContainer, + sensorEvent, + dragEvent, + mirror: this.mirror + }); + + this.draggable.trigger(mirrorCreatedEvent); + appendableContainer.appendChild(this.mirror); + this.draggable.trigger(mirrorAttachedEvent); + } + + [onDragMove](dragEvent) { + if (!this.mirror || dragEvent.canceled()) { + return; + } + + const { source, originalSource, sourceContainer, sensorEvent } = dragEvent; + + let passedThreshX = true; + let passedThreshY = true; + + if (this.options.thresholdX || this.options.thresholdY) { + const { x: lastX, y: lastY } = this.lastMirrorMovedClient; + + if (Math.abs(lastX - sensorEvent.clientX) < this.options.thresholdX) { + passedThreshX = false; + } else { + this.lastMirrorMovedClient.x = sensorEvent.clientX; + } + + if (Math.abs(lastY - sensorEvent.clientY) < this.options.thresholdY) { + passedThreshY = false; + } else { + this.lastMirrorMovedClient.y = sensorEvent.clientY; + } + + if (!passedThreshX && !passedThreshY) { + return; + } + } + + const mirrorMoveEvent = new _MirrorEvent.MirrorMoveEvent({ + source, + originalSource, + sourceContainer, + sensorEvent, + dragEvent, + mirror: this.mirror, + passedThreshX, + passedThreshY + }); + + this.draggable.trigger(mirrorMoveEvent); + } + + [onDragStop](dragEvent) { + if ('ontouchstart' in window) { + document.removeEventListener('scroll', this[onScroll], true); + } + + this.initialScrollOffset = { x: 0, y: 0 }; + this.scrollOffset = { x: 0, y: 0 }; + + if (!this.mirror) { + return; + } + + const { source, sourceContainer, sensorEvent } = dragEvent; + + const mirrorDestroyEvent = new _MirrorEvent.MirrorDestroyEvent({ + source, + mirror: this.mirror, + sourceContainer, + sensorEvent, + dragEvent + }); + + this.draggable.trigger(mirrorDestroyEvent); + + if (!mirrorDestroyEvent.canceled()) { + this.mirror.parentNode.removeChild(this.mirror); + } + } + + [onScroll]() { + this.scrollOffset = { + x: window.scrollX - this.initialScrollOffset.x, + y: window.scrollY - this.initialScrollOffset.y + }; + } + + /** + * Mirror created handler + * @param {MirrorCreatedEvent} mirrorEvent + * @return {Promise} + * @private + */ + [onMirrorCreated]({ mirror, source, sensorEvent }) { + const mirrorClasses = this.draggable.getClassNamesFor('mirror'); + + const setState = (_ref) => { + let { mirrorOffset, initialX, initialY } = _ref, + args = _objectWithoutProperties(_ref, ['mirrorOffset', 'initialX', 'initialY']); + + this.mirrorOffset = mirrorOffset; + this.initialX = initialX; + this.initialY = initialY; + this.lastMovedX = initialX; + this.lastMovedY = initialY; + return _extends({ mirrorOffset, initialX, initialY }, args); + }; + + mirror.style.display = 'none'; + + const initialState = { + mirror, + source, + sensorEvent, + mirrorClasses, + scrollOffset: this.scrollOffset, + options: this.options, + passedThreshX: true, + passedThreshY: true + }; + + return Promise.resolve(initialState) + // Fix reflow here + .then(computeMirrorDimensions).then(calculateMirrorOffset).then(resetMirror).then(addMirrorClasses).then(positionMirror({ initial: true })).then(removeMirrorID).then(setState); + } + + /** + * Mirror move handler + * @param {MirrorMoveEvent} mirrorEvent + * @return {Promise|null} + * @private + */ + [onMirrorMove](mirrorEvent) { + if (mirrorEvent.canceled()) { + return null; + } + + const setState = (_ref2) => { + let { lastMovedX, lastMovedY } = _ref2, + args = _objectWithoutProperties(_ref2, ['lastMovedX', 'lastMovedY']); + + this.lastMovedX = lastMovedX; + this.lastMovedY = lastMovedY; + + return _extends({ lastMovedX, lastMovedY }, args); + }; + + const initialState = { + mirror: mirrorEvent.mirror, + sensorEvent: mirrorEvent.sensorEvent, + mirrorOffset: this.mirrorOffset, + options: this.options, + initialX: this.initialX, + initialY: this.initialY, + scrollOffset: this.scrollOffset, + passedThreshX: mirrorEvent.passedThreshX, + passedThreshY: mirrorEvent.passedThreshY, + lastMovedX: this.lastMovedX, + lastMovedY: this.lastMovedY + }; + + return Promise.resolve(initialState).then(positionMirror({ raf: true })).then(setState); + } + + /** + * Returns appendable container for mirror based on the appendTo option + * @private + * @param {Object} options + * @param {HTMLElement} options.source - Current source + * @return {HTMLElement} + */ + [getAppendableContainer](source) { + const appendTo = this.options.appendTo; + + if (typeof appendTo === 'string') { + return document.querySelector(appendTo); + } else if (appendTo instanceof HTMLElement) { + return appendTo; + } else if (typeof appendTo === 'function') { + return appendTo(source); + } else { + return source.parentNode; + } + } + } + + exports.default = Mirror; /** + * Computes mirror dimensions based on the source element + * Adds sourceRect to state + * @param {Object} state + * @param {HTMLElement} state.source + * @return {Promise} + * @private + */ + + function computeMirrorDimensions(_ref3) { + let { source } = _ref3, + args = _objectWithoutProperties(_ref3, ['source']); + + return withPromise(resolve => { + const sourceRect = source.getBoundingClientRect(); + resolve(_extends({ source, sourceRect }, args)); + }); + } + + /** + * Calculates mirror offset + * Adds mirrorOffset to state + * @param {Object} state + * @param {SensorEvent} state.sensorEvent + * @param {DOMRect} state.sourceRect + * @return {Promise} + * @private + */ + function calculateMirrorOffset(_ref4) { + let { sensorEvent, sourceRect, options } = _ref4, + args = _objectWithoutProperties(_ref4, ['sensorEvent', 'sourceRect', 'options']); + + return withPromise(resolve => { + const top = options.cursorOffsetY === null ? sensorEvent.clientY - sourceRect.top : options.cursorOffsetY; + const left = options.cursorOffsetX === null ? sensorEvent.clientX - sourceRect.left : options.cursorOffsetX; + + const mirrorOffset = { top, left }; + + resolve(_extends({ sensorEvent, sourceRect, mirrorOffset, options }, args)); + }); + } + + /** + * Applys mirror styles + * @param {Object} state + * @param {HTMLElement} state.mirror + * @param {HTMLElement} state.source + * @param {Object} state.options + * @return {Promise} + * @private + */ + function resetMirror(_ref5) { + let { mirror, source, options } = _ref5, + args = _objectWithoutProperties(_ref5, ['mirror', 'source', 'options']); + + return withPromise(resolve => { + let offsetHeight; + let offsetWidth; + + if (options.constrainDimensions) { + const computedSourceStyles = getComputedStyle(source); + offsetHeight = computedSourceStyles.getPropertyValue('height'); + offsetWidth = computedSourceStyles.getPropertyValue('width'); + } + + mirror.style.display = null; + mirror.style.position = 'fixed'; + mirror.style.pointerEvents = 'none'; + mirror.style.top = 0; + mirror.style.left = 0; + mirror.style.margin = 0; + + if (options.constrainDimensions) { + mirror.style.height = offsetHeight; + mirror.style.width = offsetWidth; + } + + resolve(_extends({ mirror, source, options }, args)); + }); + } + + /** + * Applys mirror class on mirror element + * @param {Object} state + * @param {HTMLElement} state.mirror + * @param {String[]} state.mirrorClasses + * @return {Promise} + * @private + */ + function addMirrorClasses(_ref6) { + let { mirror, mirrorClasses } = _ref6, + args = _objectWithoutProperties(_ref6, ['mirror', 'mirrorClasses']); + + return withPromise(resolve => { + mirror.classList.add(...mirrorClasses); + resolve(_extends({ mirror, mirrorClasses }, args)); + }); + } + + /** + * Removes source ID from cloned mirror element + * @param {Object} state + * @param {HTMLElement} state.mirror + * @return {Promise} + * @private + */ + function removeMirrorID(_ref7) { + let { mirror } = _ref7, + args = _objectWithoutProperties(_ref7, ['mirror']); + + return withPromise(resolve => { + mirror.removeAttribute('id'); + delete mirror.id; + resolve(_extends({ mirror }, args)); + }); + } + + /** + * Positions mirror with translate3d + * @param {Object} state + * @param {HTMLElement} state.mirror + * @param {SensorEvent} state.sensorEvent + * @param {Object} state.mirrorOffset + * @param {Number} state.initialY + * @param {Number} state.initialX + * @param {Object} state.options + * @return {Promise} + * @private + */ + function positionMirror({ withFrame = false, initial = false } = {}) { + return (_ref8) => { + let { + mirror, + sensorEvent, + mirrorOffset, + initialY, + initialX, + scrollOffset, + options, + passedThreshX, + passedThreshY, + lastMovedX, + lastMovedY + } = _ref8, + args = _objectWithoutProperties(_ref8, ['mirror', 'sensorEvent', 'mirrorOffset', 'initialY', 'initialX', 'scrollOffset', 'options', 'passedThreshX', 'passedThreshY', 'lastMovedX', 'lastMovedY']); + + return withPromise(resolve => { + const result = _extends({ + mirror, + sensorEvent, + mirrorOffset, + options + }, args); + + if (mirrorOffset) { + const x = passedThreshX ? Math.round((sensorEvent.clientX - mirrorOffset.left - scrollOffset.x) / (options.thresholdX || 1)) * (options.thresholdX || 1) : Math.round(lastMovedX); + const y = passedThreshY ? Math.round((sensorEvent.clientY - mirrorOffset.top - scrollOffset.y) / (options.thresholdY || 1)) * (options.thresholdY || 1) : Math.round(lastMovedY); + + if (options.xAxis && options.yAxis || initial) { + mirror.style.transform = `translate3d(${x}px, ${y}px, 0)`; + } else if (options.xAxis && !options.yAxis) { + mirror.style.transform = `translate3d(${x}px, ${initialY}px, 0)`; + } else if (options.yAxis && !options.xAxis) { + mirror.style.transform = `translate3d(${initialX}px, ${y}px, 0)`; + } + + if (initial) { + result.initialX = x; + result.initialY = y; + } + + result.lastMovedX = x; + result.lastMovedY = y; + } + + resolve(result); + }, { frame: withFrame }); + }; + } + + /** + * Wraps functions in promise with potential animation frame option + * @param {Function} callback + * @param {Object} options + * @param {Boolean} options.raf + * @return {Promise} + * @private + */ + function withPromise(callback, { raf = false } = {}) { + return new Promise((resolve, reject) => { + if (raf) { + requestAnimationFrame(() => { + callback(resolve, reject); + }); + } else { + callback(resolve, reject); + } + }); + } + + /** + * Returns true if the sensor event was triggered by a native browser drag event + * @param {SensorEvent} sensorEvent + */ + function isNativeDragEvent(sensorEvent) { + return (/^drag/.test(sensorEvent.originalEvent.type) + ); + } + + /***/ }), + /* 36 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.defaultOptions = undefined; + + var _Mirror = __webpack_require__(35); + + var _Mirror2 = _interopRequireDefault(_Mirror); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _Mirror2.default; + exports.defaultOptions = _Mirror.defaultOptions; + + /***/ }), + /* 37 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + + var _AbstractPlugin = __webpack_require__(4); + + var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + const onInitialize = Symbol('onInitialize'); + const onDestroy = Symbol('onDestroy'); + + /** + * Focusable default options + * @property {Object} defaultOptions + * @type {Object} + */ + const defaultOptions = {}; + + /** + * Focusable plugin + * @class Focusable + * @module Focusable + * @extends AbstractPlugin + */ + class Focusable extends _AbstractPlugin2.default { + /** + * Focusable constructor. + * @constructs Focusable + * @param {Draggable} draggable - Draggable instance + */ + constructor(draggable) { + super(draggable); + + /** + * Focusable options + * @property {Object} options + * @type {Object} + */ + this.options = _extends({}, defaultOptions, this.getOptions()); + + this[onInitialize] = this[onInitialize].bind(this); + this[onDestroy] = this[onDestroy].bind(this); + } + + /** + * Attaches listeners to draggable + */ + attach() { + this.draggable.on('draggable:initialize', this[onInitialize]).on('draggable:destroy', this[onDestroy]); + } + + /** + * Detaches listeners from draggable + */ + detach() { + this.draggable.off('draggable:initialize', this[onInitialize]).off('draggable:destroy', this[onDestroy]); + + // Remove modified elements when detach + this[onDestroy](); + } + + /** + * Returns options passed through draggable + * @return {Object} + */ + getOptions() { + return this.draggable.options.focusable || {}; + } + + /** + * Returns draggable containers and elements + * @return {HTMLElement[]} + */ + getElements() { + return [...this.draggable.containers, ...this.draggable.getDraggableElements()]; + } + + /** + * Intialize handler + * @private + */ + [onInitialize]() { + // Can wait until the next best frame is available + requestAnimationFrame(() => { + this.getElements().forEach(element => decorateElement(element)); + }); + } + + /** + * Destroy handler + * @private + */ + [onDestroy]() { + // Can wait until the next best frame is available + requestAnimationFrame(() => { + this.getElements().forEach(element => stripElement(element)); + }); + } + } + + exports.default = Focusable; /** + * Keeps track of all the elements that are missing tabindex attributes + * so they can be reset when draggable gets destroyed + * @const {HTMLElement[]} elementsWithMissingTabIndex + */ + + const elementsWithMissingTabIndex = []; + + /** + * Decorates element with tabindex attributes + * @param {HTMLElement} element + * @return {Object} + * @private + */ + function decorateElement(element) { + const hasMissingTabIndex = Boolean(!element.getAttribute('tabindex') && element.tabIndex === -1); + + if (hasMissingTabIndex) { + elementsWithMissingTabIndex.push(element); + element.tabIndex = 0; + } + } + + /** + * Removes elements tabindex attributes + * @param {HTMLElement} element + * @private + */ + function stripElement(element) { + const tabIndexElementPosition = elementsWithMissingTabIndex.indexOf(element); + + if (tabIndexElementPosition !== -1) { + element.tabIndex = -1; + elementsWithMissingTabIndex.splice(tabIndexElementPosition, 1); + } + } + + /***/ }), + /* 38 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _Focusable = __webpack_require__(37); + + var _Focusable2 = _interopRequireDefault(_Focusable); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _Focusable2.default; + + /***/ }), + /* 39 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + /** + * All draggable plugins inherit from this class. + * @abstract + * @class AbstractPlugin + * @module AbstractPlugin + */ + class AbstractPlugin { + /** + * AbstractPlugin constructor. + * @constructs AbstractPlugin + * @param {Draggable} draggable - Draggable instance + */ + constructor(draggable) { + /** + * Draggable instance + * @property draggable + * @type {Draggable} + */ + this.draggable = draggable; + } + + /** + * Override to add listeners + * @abstract + */ + attach() { + throw new Error('Not Implemented'); + } + + /** + * Override to remove listeners + * @abstract + */ + detach() { + throw new Error('Not Implemented'); + } + } + exports.default = AbstractPlugin; + + /***/ }), + /* 40 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.defaultOptions = undefined; + + var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + + var _AbstractPlugin = __webpack_require__(4); + + var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + const onInitialize = Symbol('onInitialize'); + const onDestroy = Symbol('onDestroy'); + const announceEvent = Symbol('announceEvent'); + const announceMessage = Symbol('announceMessage'); + + const ARIA_RELEVANT = 'aria-relevant'; + const ARIA_ATOMIC = 'aria-atomic'; + const ARIA_LIVE = 'aria-live'; + const ROLE = 'role'; + + /** + * Announcement default options + * @property {Object} defaultOptions + * @property {Number} defaultOptions.expire + * @type {Object} + */ + const defaultOptions = exports.defaultOptions = { + expire: 7000 + }; + + /** + * Announcement plugin + * @class Announcement + * @module Announcement + * @extends AbstractPlugin + */ + class Announcement extends _AbstractPlugin2.default { + /** + * Announcement constructor. + * @constructs Announcement + * @param {Draggable} draggable - Draggable instance + */ + constructor(draggable) { + super(draggable); + + /** + * Plugin options + * @property options + * @type {Object} + */ + this.options = _extends({}, defaultOptions, this.getOptions()); + + /** + * Original draggable trigger method. Hack until we have onAll or on('all') + * @property originalTriggerMethod + * @type {Function} + */ + this.originalTriggerMethod = this.draggable.trigger; + + this[onInitialize] = this[onInitialize].bind(this); + this[onDestroy] = this[onDestroy].bind(this); + } + + /** + * Attaches listeners to draggable + */ + attach() { + this.draggable.on('draggable:initialize', this[onInitialize]); + } + + /** + * Detaches listeners from draggable + */ + detach() { + this.draggable.off('draggable:destroy', this[onDestroy]); + } + + /** + * Returns passed in options + */ + getOptions() { + return this.draggable.options.announcements || {}; + } + + /** + * Announces event + * @private + * @param {AbstractEvent} event + */ + [announceEvent](event) { + const message = this.options[event.type]; + + if (message && typeof message === 'string') { + this[announceMessage](message); + } + + if (message && typeof message === 'function') { + this[announceMessage](message(event)); + } + } + + /** + * Announces message to screen reader + * @private + * @param {String} message + */ + [announceMessage](message) { + announce(message, { expire: this.options.expire }); + } + + /** + * Initialize hander + * @private + */ + [onInitialize]() { + // Hack until there is an api for listening for all events + this.draggable.trigger = event => { + try { + this[announceEvent](event); + } finally { + // Ensure that original trigger is called + this.originalTriggerMethod.call(this.draggable, event); + } + }; + } + + /** + * Destroy hander + * @private + */ + [onDestroy]() { + this.draggable.trigger = this.originalTriggerMethod; + } + } + + exports.default = Announcement; /** + * @const {HTMLElement} liveRegion + */ + + const liveRegion = createRegion(); + + /** + * Announces message via live region + * @param {String} message + * @param {Object} options + * @param {Number} options.expire + */ + function announce(message, { expire }) { + const element = document.createElement('div'); + + element.textContent = message; + liveRegion.appendChild(element); + + return setTimeout(() => { + liveRegion.removeChild(element); + }, expire); + } + + /** + * Creates region element + * @return {HTMLElement} + */ + function createRegion() { + const element = document.createElement('div'); + + element.setAttribute('id', 'draggable-live-region'); + element.setAttribute(ARIA_RELEVANT, 'additions'); + element.setAttribute(ARIA_ATOMIC, 'true'); + element.setAttribute(ARIA_LIVE, 'assertive'); + element.setAttribute(ROLE, 'log'); + + element.style.position = 'fixed'; + element.style.width = '1px'; + element.style.height = '1px'; + element.style.top = '-1px'; + element.style.overflow = 'hidden'; + + return element; + } + +// Append live region element as early as possible + document.addEventListener('DOMContentLoaded', () => { + document.body.appendChild(liveRegion); + }); + + /***/ }), + /* 41 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.defaultOptions = undefined; + + var _Announcement = __webpack_require__(40); + + var _Announcement2 = _interopRequireDefault(_Announcement); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _Announcement2.default; + exports.defaultOptions = _Announcement.defaultOptions; + + /***/ }), + /* 42 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.DraggableDestroyEvent = exports.DraggableInitializedEvent = exports.DraggableEvent = undefined; + + var _AbstractEvent = __webpack_require__(3); + + var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + /** + * Base draggable event + * @class DraggableEvent + * @module DraggableEvent + * @extends AbstractEvent + */ + class DraggableEvent extends _AbstractEvent2.default { + + /** + * Draggable instance + * @property draggable + * @type {Draggable} + * @readonly + */ + get draggable() { + return this.data.draggable; + } + } + + exports.DraggableEvent = DraggableEvent; /** + * Draggable initialized event + * @class DraggableInitializedEvent + * @module DraggableInitializedEvent + * @extends DraggableEvent + */ + + DraggableEvent.type = 'draggable'; + class DraggableInitializedEvent extends DraggableEvent {} + + exports.DraggableInitializedEvent = DraggableInitializedEvent; /** + * Draggable destory event + * @class DraggableInitializedEvent + * @module DraggableDestroyEvent + * @extends DraggableDestroyEvent + */ + + DraggableInitializedEvent.type = 'draggable:initialize'; + class DraggableDestroyEvent extends DraggableEvent {} + exports.DraggableDestroyEvent = DraggableDestroyEvent; + DraggableDestroyEvent.type = 'draggable:destroy'; + + /***/ }), + /* 43 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.DragStoppedEvent = exports.DragStopEvent = exports.DragPressureEvent = exports.DragOutContainerEvent = exports.DragOverContainerEvent = exports.DragOutEvent = exports.DragOverEvent = exports.DragMoveEvent = exports.DragStartEvent = exports.DragEvent = undefined; + + var _AbstractEvent = __webpack_require__(3); + + var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + /** + * Base drag event + * @class DragEvent + * @module DragEvent + * @extends AbstractEvent + */ + class DragEvent extends _AbstractEvent2.default { + + /** + * Draggables source element + * @property source + * @type {HTMLElement} + * @readonly + */ + get source() { + return this.data.source; + } + + /** + * Draggables original source element + * @property originalSource + * @type {HTMLElement} + * @readonly + */ + get originalSource() { + return this.data.originalSource; + } + + /** + * Draggables mirror element + * @property mirror + * @type {HTMLElement} + * @readonly + */ + get mirror() { + return this.data.mirror; + } + + /** + * Draggables source container element + * @property sourceContainer + * @type {HTMLElement} + * @readonly + */ + get sourceContainer() { + return this.data.sourceContainer; + } + + /** + * Sensor event + * @property sensorEvent + * @type {SensorEvent} + * @readonly + */ + get sensorEvent() { + return this.data.sensorEvent; + } + + /** + * Original event that triggered sensor event + * @property originalEvent + * @type {Event} + * @readonly + */ + get originalEvent() { + if (this.sensorEvent) { + return this.sensorEvent.originalEvent; + } + + return null; + } + } + + exports.DragEvent = DragEvent; /** + * Drag start event + * @class DragStartEvent + * @module DragStartEvent + * @extends DragEvent + */ + + DragEvent.type = 'drag'; + class DragStartEvent extends DragEvent {} + + exports.DragStartEvent = DragStartEvent; /** + * Drag move event + * @class DragMoveEvent + * @module DragMoveEvent + * @extends DragEvent + */ + + DragStartEvent.type = 'drag:start'; + DragStartEvent.cancelable = true; + class DragMoveEvent extends DragEvent {} + + exports.DragMoveEvent = DragMoveEvent; /** + * Drag over event + * @class DragOverEvent + * @module DragOverEvent + * @extends DragEvent + */ + + DragMoveEvent.type = 'drag:move'; + class DragOverEvent extends DragEvent { + + /** + * Draggable container you are over + * @property overContainer + * @type {HTMLElement} + * @readonly + */ + get overContainer() { + return this.data.overContainer; + } + + /** + * Draggable element you are over + * @property over + * @type {HTMLElement} + * @readonly + */ + get over() { + return this.data.over; + } + } + + exports.DragOverEvent = DragOverEvent; /** + * Drag out event + * @class DragOutEvent + * @module DragOutEvent + * @extends DragEvent + */ + + DragOverEvent.type = 'drag:over'; + DragOverEvent.cancelable = true; + class DragOutEvent extends DragEvent { + + /** + * Draggable container you are over + * @property overContainer + * @type {HTMLElement} + * @readonly + */ + get overContainer() { + return this.data.overContainer; + } + + /** + * Draggable element you left + * @property over + * @type {HTMLElement} + * @readonly + */ + get over() { + return this.data.over; + } + } + + exports.DragOutEvent = DragOutEvent; /** + * Drag over container event + * @class DragOverContainerEvent + * @module DragOverContainerEvent + * @extends DragEvent + */ + + DragOutEvent.type = 'drag:out'; + class DragOverContainerEvent extends DragEvent { + + /** + * Draggable container you are over + * @property overContainer + * @type {HTMLElement} + * @readonly + */ + get overContainer() { + return this.data.overContainer; + } + } + + exports.DragOverContainerEvent = DragOverContainerEvent; /** + * Drag out container event + * @class DragOutContainerEvent + * @module DragOutContainerEvent + * @extends DragEvent + */ + + DragOverContainerEvent.type = 'drag:over:container'; + class DragOutContainerEvent extends DragEvent { + + /** + * Draggable container you left + * @property overContainer + * @type {HTMLElement} + * @readonly + */ + get overContainer() { + return this.data.overContainer; + } + } + + exports.DragOutContainerEvent = DragOutContainerEvent; /** + * Drag pressure event + * @class DragPressureEvent + * @module DragPressureEvent + * @extends DragEvent + */ + + DragOutContainerEvent.type = 'drag:out:container'; + class DragPressureEvent extends DragEvent { + + /** + * Pressure applied on draggable element + * @property pressure + * @type {Number} + * @readonly + */ + get pressure() { + return this.data.pressure; + } + } + + exports.DragPressureEvent = DragPressureEvent; /** + * Drag stop event + * @class DragStopEvent + * @module DragStopEvent + * @extends DragEvent + */ + + DragPressureEvent.type = 'drag:pressure'; + class DragStopEvent extends DragEvent {} + + exports.DragStopEvent = DragStopEvent; /** + * Drag stopped event + * @class DragStoppedEvent + * @module DragStoppedEvent + * @extends DragEvent + */ + + DragStopEvent.type = 'drag:stop'; + class DragStoppedEvent extends DragEvent {} + exports.DragStoppedEvent = DragStoppedEvent; + DragStoppedEvent.type = 'drag:stopped'; + + /***/ }), + /* 44 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _DragEvent = __webpack_require__(8); + + Object.keys(_DragEvent).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _DragEvent[key]; + } + }); + }); + + var _DraggableEvent = __webpack_require__(7); + + Object.keys(_DraggableEvent).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _DraggableEvent[key]; + } + }); + }); + + var _Plugins = __webpack_require__(6); + + Object.keys(_Plugins).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _Plugins[key]; + } + }); + }); + + var _Sensors = __webpack_require__(5); + + Object.keys(_Sensors).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _Sensors[key]; + } + }); + }); + + var _Draggable = __webpack_require__(12); + + var _Draggable2 = _interopRequireDefault(_Draggable); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _Draggable2.default; + + /***/ }), + /* 45 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + + var _Draggable = __webpack_require__(44); + + var _Draggable2 = _interopRequireDefault(_Draggable); + + var _SortableEvent = __webpack_require__(9); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + const onDragStart = Symbol('onDragStart'); + const onDragOverContainer = Symbol('onDragOverContainer'); + const onDragOver = Symbol('onDragOver'); + const onDragStop = Symbol('onDragStop'); + + /** + * Returns announcement message when a Draggable element has been sorted with another Draggable element + * or moved into a new container + * @param {SortableSortedEvent} sortableEvent + * @return {String} + */ + function onSortableSortedDefaultAnnouncement({ dragEvent }) { + const sourceText = dragEvent.source.textContent.trim() || dragEvent.source.id || 'sortable element'; + + if (dragEvent.over) { + const overText = dragEvent.over.textContent.trim() || dragEvent.over.id || 'sortable element'; + const isFollowing = dragEvent.source.compareDocumentPosition(dragEvent.over) & Node.DOCUMENT_POSITION_FOLLOWING; + + if (isFollowing) { + return `Placed ${sourceText} after ${overText}`; + } else { + return `Placed ${sourceText} before ${overText}`; + } + } else { + // need to figure out how to compute container name + return `Placed ${sourceText} into a different container`; + } + } + + /** + * @const {Object} defaultAnnouncements + * @const {Function} defaultAnnouncements['sortable:sorted'] + */ + const defaultAnnouncements = { + 'sortable:sorted': onSortableSortedDefaultAnnouncement + }; + + /** + * Sortable is built on top of Draggable and allows sorting of draggable elements. Sortable will keep + * track of the original index and emits the new index as you drag over draggable elements. + * @class Sortable + * @module Sortable + * @extends Draggable + */ + class Sortable extends _Draggable2.default { + /** + * Sortable constructor. + * @constructs Sortable + * @param {HTMLElement[]|NodeList|HTMLElement} containers - Sortable containers + * @param {Object} options - Options for Sortable + */ + constructor(containers = [], options = {}) { + super(containers, _extends({}, options, { + announcements: _extends({}, defaultAnnouncements, options.announcements || {}) + })); + + /** + * start index of source on drag start + * @property startIndex + * @type {Number} + */ + this.startIndex = null; + + /** + * start container on drag start + * @property startContainer + * @type {HTMLElement} + * @default null + */ + this.startContainer = null; + + this[onDragStart] = this[onDragStart].bind(this); + this[onDragOverContainer] = this[onDragOverContainer].bind(this); + this[onDragOver] = this[onDragOver].bind(this); + this[onDragStop] = this[onDragStop].bind(this); + + this.on('drag:start', this[onDragStart]).on('drag:over:container', this[onDragOverContainer]).on('drag:over', this[onDragOver]).on('drag:stop', this[onDragStop]); + } + + /** + * Destroys Sortable instance. + */ + destroy() { + super.destroy(); + + this.off('drag:start', this[onDragStart]).off('drag:over:container', this[onDragOverContainer]).off('drag:over', this[onDragOver]).off('drag:stop', this[onDragStop]); + } + + /** + * Returns true index of element within its container during drag operation, i.e. excluding mirror and original source + * @param {HTMLElement} element - An element + * @return {Number} + */ + index(element) { + return this.getSortableElementsForContainer(element.parentNode).indexOf(element); + } + + /** + * Returns sortable elements for a given container, excluding the mirror and + * original source element if present + * @param {HTMLElement} container + * @return {HTMLElement[]} + */ + getSortableElementsForContainer(container) { + const allSortableElements = container.querySelectorAll(this.options.draggable); + + return [...allSortableElements].filter(childElement => { + return childElement !== this.originalSource && childElement !== this.mirror && childElement.parentNode === container; + }); + } + + /** + * Drag start handler + * @private + * @param {DragStartEvent} event - Drag start event + */ + [onDragStart](event) { + this.startContainer = event.source.parentNode; + this.startIndex = this.index(event.source); + + const sortableStartEvent = new _SortableEvent.SortableStartEvent({ + dragEvent: event, + startIndex: this.startIndex, + startContainer: this.startContainer + }); + + this.trigger(sortableStartEvent); + + if (sortableStartEvent.canceled()) { + event.cancel(); + } + } + + /** + * Drag over container handler + * @private + * @param {DragOverContainerEvent} event - Drag over container event + */ + [onDragOverContainer](event) { + if (event.canceled()) { + return; + } + + const { source, over, overContainer } = event; + const oldIndex = this.index(source); + + const sortableSortEvent = new _SortableEvent.SortableSortEvent({ + dragEvent: event, + currentIndex: oldIndex, + source, + over + }); + + this.trigger(sortableSortEvent); + + if (sortableSortEvent.canceled()) { + return; + } + + const children = this.getSortableElementsForContainer(overContainer); + const moves = move({ source, over, overContainer, children }); + + if (!moves) { + return; + } + + const { oldContainer, newContainer } = moves; + const newIndex = this.index(event.source); + + const sortableSortedEvent = new _SortableEvent.SortableSortedEvent({ + dragEvent: event, + oldIndex, + newIndex, + oldContainer, + newContainer + }); + + this.trigger(sortableSortedEvent); + } + + /** + * Drag over handler + * @private + * @param {DragOverEvent} event - Drag over event + */ + [onDragOver](event) { + if (event.over === event.originalSource || event.over === event.source) { + return; + } + + const { source, over, overContainer } = event; + const oldIndex = this.index(source); + + const sortableSortEvent = new _SortableEvent.SortableSortEvent({ + dragEvent: event, + currentIndex: oldIndex, + source, + over + }); + + this.trigger(sortableSortEvent); + + if (sortableSortEvent.canceled()) { + return; + } + + const children = this.getDraggableElementsForContainer(overContainer); + const moves = move({ source, over, overContainer, children }); + + if (!moves) { + return; + } + + const { oldContainer, newContainer } = moves; + const newIndex = this.index(source); + + const sortableSortedEvent = new _SortableEvent.SortableSortedEvent({ + dragEvent: event, + oldIndex, + newIndex, + oldContainer, + newContainer + }); + + this.trigger(sortableSortedEvent); + } + + /** + * Drag stop handler + * @private + * @param {DragStopEvent} event - Drag stop event + */ + [onDragStop](event) { + const sortableStopEvent = new _SortableEvent.SortableStopEvent({ + dragEvent: event, + oldIndex: this.startIndex, + newIndex: this.index(event.source), + oldContainer: this.startContainer, + newContainer: event.source.parentNode + }); + + this.trigger(sortableStopEvent); + + this.startIndex = null; + this.startContainer = null; + } + } + + exports.default = Sortable; + function index(element) { + return Array.prototype.indexOf.call(element.parentNode.children, element); + } + + function move({ source, over, overContainer, children }) { + const emptyOverContainer = !children.length; + const differentContainer = source.parentNode !== overContainer; + const sameContainer = over && source.parentNode === over.parentNode; + + if (emptyOverContainer) { + return moveInsideEmptyContainer(source, overContainer); + } else if (sameContainer) { + return moveWithinContainer(source, over); + } else if (differentContainer) { + return moveOutsideContainer(source, over, overContainer); + } else { + return null; + } + } + + function moveInsideEmptyContainer(source, overContainer) { + const oldContainer = source.parentNode; + + overContainer.appendChild(source); + + return { oldContainer, newContainer: overContainer }; + } + + function moveWithinContainer(source, over) { + const oldIndex = index(source); + const newIndex = index(over); + + if (oldIndex < newIndex) { + source.parentNode.insertBefore(source, over.nextElementSibling); + } else { + source.parentNode.insertBefore(source, over); + } + + return { oldContainer: source.parentNode, newContainer: source.parentNode }; + } + + function moveOutsideContainer(source, over, overContainer) { + const oldContainer = source.parentNode; + + if (over) { + over.parentNode.insertBefore(source, over); + } else { + // need to figure out proper position + overContainer.appendChild(source); + } + + return { oldContainer, newContainer: source.parentNode }; + } + + /***/ }), + /* 46 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + + const canceled = Symbol('canceled'); + + /** + * All events fired by draggable inherit this class. You can call `cancel()` to + * cancel a specific event or you can check if an event has been canceled by + * calling `canceled()`. + * @abstract + * @class AbstractEvent + * @module AbstractEvent + */ + class AbstractEvent { + + /** + * AbstractEvent constructor. + * @constructs AbstractEvent + * @param {object} data - Event data + */ + + /** + * Event type + * @static + * @abstract + * @property type + * @type {String} + */ + constructor(data) { + this[canceled] = false; + this.data = data; + } + + /** + * Read-only type + * @abstract + * @return {String} + */ + + + /** + * Event cancelable + * @static + * @abstract + * @property cancelable + * @type {Boolean} + */ + get type() { + return this.constructor.type; + } + + /** + * Read-only cancelable + * @abstract + * @return {Boolean} + */ + get cancelable() { + return this.constructor.cancelable; + } + + /** + * Cancels the event instance + * @abstract + */ + cancel() { + this[canceled] = true; + } + + /** + * Check if event has been canceled + * @abstract + * @return {Boolean} + */ + canceled() { + return Boolean(this[canceled]); + } + + /** + * Returns new event instance with existing event data. + * This method allows for overriding of event data. + * @param {Object} data + * @return {AbstractEvent} + */ + clone(data) { + return new this.constructor(_extends({}, this.data, data)); + } + } + exports.default = AbstractEvent; + AbstractEvent.type = 'event'; + AbstractEvent.cancelable = false; + + /***/ }), + /* 47 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.SortableStopEvent = exports.SortableSortedEvent = exports.SortableSortEvent = exports.SortableStartEvent = exports.SortableEvent = undefined; + + var _AbstractEvent = __webpack_require__(3); + + var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + /** + * Base sortable event + * @class SortableEvent + * @module SortableEvent + * @extends AbstractEvent + */ + class SortableEvent extends _AbstractEvent2.default { + + /** + * Original drag event that triggered this sortable event + * @property dragEvent + * @type {DragEvent} + * @readonly + */ + get dragEvent() { + return this.data.dragEvent; + } + } + + exports.SortableEvent = SortableEvent; /** + * Sortable start event + * @class SortableStartEvent + * @module SortableStartEvent + * @extends SortableEvent + */ + + SortableEvent.type = 'sortable'; + class SortableStartEvent extends SortableEvent { + + /** + * Start index of source on sortable start + * @property startIndex + * @type {Number} + * @readonly + */ + get startIndex() { + return this.data.startIndex; + } + + /** + * Start container on sortable start + * @property startContainer + * @type {HTMLElement} + * @readonly + */ + get startContainer() { + return this.data.startContainer; + } + } + + exports.SortableStartEvent = SortableStartEvent; /** + * Sortable sort event + * @class SortableSortEvent + * @module SortableSortEvent + * @extends SortableEvent + */ + + SortableStartEvent.type = 'sortable:start'; + SortableStartEvent.cancelable = true; + class SortableSortEvent extends SortableEvent { + + /** + * Index of current draggable element + * @property currentIndex + * @type {Number} + * @readonly + */ + get currentIndex() { + return this.data.currentIndex; + } + + /** + * Draggable element you are hovering over + * @property over + * @type {HTMLElement} + * @readonly + */ + get over() { + return this.data.over; + } + + /** + * Draggable container element you are hovering over + * @property overContainer + * @type {HTMLElement} + * @readonly + */ + get overContainer() { + return this.data.dragEvent.overContainer; + } + } + + exports.SortableSortEvent = SortableSortEvent; /** + * Sortable sorted event + * @class SortableSortedEvent + * @module SortableSortedEvent + * @extends SortableEvent + */ + + SortableSortEvent.type = 'sortable:sort'; + SortableSortEvent.cancelable = true; + class SortableSortedEvent extends SortableEvent { + + /** + * Index of last sorted event + * @property oldIndex + * @type {Number} + * @readonly + */ + get oldIndex() { + return this.data.oldIndex; + } + + /** + * New index of this sorted event + * @property newIndex + * @type {Number} + * @readonly + */ + get newIndex() { + return this.data.newIndex; + } + + /** + * Old container of draggable element + * @property oldContainer + * @type {HTMLElement} + * @readonly + */ + get oldContainer() { + return this.data.oldContainer; + } + + /** + * New container of draggable element + * @property newContainer + * @type {HTMLElement} + * @readonly + */ + get newContainer() { + return this.data.newContainer; + } + } + + exports.SortableSortedEvent = SortableSortedEvent; /** + * Sortable stop event + * @class SortableStopEvent + * @module SortableStopEvent + * @extends SortableEvent + */ + + SortableSortedEvent.type = 'sortable:sorted'; + class SortableStopEvent extends SortableEvent { + + /** + * Original index on sortable start + * @property oldIndex + * @type {Number} + * @readonly + */ + get oldIndex() { + return this.data.oldIndex; + } + + /** + * New index of draggable element + * @property newIndex + * @type {Number} + * @readonly + */ + get newIndex() { + return this.data.newIndex; + } + + /** + * Original container of draggable element + * @property oldContainer + * @type {HTMLElement} + * @readonly + */ + get oldContainer() { + return this.data.oldContainer; + } + + /** + * New container of draggable element + * @property newContainer + * @type {HTMLElement} + * @readonly + */ + get newContainer() { + return this.data.newContainer; + } + } + exports.SortableStopEvent = SortableStopEvent; + SortableStopEvent.type = 'sortable:stop'; + + /***/ }), + /* 48 */ + /***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _SortableEvent = __webpack_require__(9); + + Object.keys(_SortableEvent).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _SortableEvent[key]; + } + }); + }); + + var _Sortable = __webpack_require__(45); + + var _Sortable2 = _interopRequireDefault(_Sortable); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _Sortable2.default; + + /***/ }) + /******/ ]); +}); \ No newline at end of file