Skip to content

Commit

Permalink
[PBNTR-692] Draggable in Rails (#3863)
Browse files Browse the repository at this point in the history
[Runway Story](https://runway.powerhrg.com/backlog_items/PBNTR-692)

- ✅ Create the subcomponent structure for draggable.option and
draggable.container
- ✅ Attaches reorganized list as array of string IDs to container
- ✅ Use enhanced element for drag logic

---------

Co-authored-by: Jasper <[email protected]>
Co-authored-by: Jasper Furniss <[email protected]>
  • Loading branch information
3 people authored Nov 13, 2024
1 parent 1356072 commit ce8404d
Show file tree
Hide file tree
Showing 15 changed files with 289 additions and 1 deletion.
2 changes: 1 addition & 1 deletion playbook-website/config/menu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ kits:
platforms: *1
status: stable
- name: draggable
platforms: *2
platforms: *1
description:
status: stable
- category: message_text_patterns
Expand Down
3 changes: 3 additions & 0 deletions playbook/app/entrypoints/playbook-rails.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ PbStarRating.start()
import PbRadio from 'kits/pb_radio'
PbRadio.start()

import PbDraggable from 'kits/pb_draggable'
PbDraggable.start()

import 'flatpickr'

// React-Rendered Rails Kits =====
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<% initial_items = [
{
id: "1",
url: "https://unsplash.it/500/400/?image=633",
},
{
id: "2",
url: "https://unsplash.it/500/400/?image=634",
},
{
id: "3",
url: "https://unsplash.it/500/400/?image=637",
},
] %>

<%= pb_rails("draggable", props: {initial_items: initial_items}) do %>
<%= pb_rails("draggable/draggable_container") do %>
<%= pb_rails("flex") do %>
<% initial_items.each do |item| %>
<%= pb_rails("draggable/draggable_item", props:{drag_id: item[:id]}) do %>
<%= pb_rails("image", props: { alt: item[:id], size: "md", url: item[:url], margin: "xs" }) %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
The `draggable` kit gives you a full subcomponent structure that allows it to be used with almost any kit.

`initial_items` is a REQUIRED prop, which is the array of objects that contains data for the the draggable items.

`draggable/draggable_container` = This specifies the container within which items can be dropped.

`draggable/draggable_item` = This specifies the items that can be dragged and dropped. `drag_id` is a REQUIRED prop for draggable_item and must match the id on the items within `initial_items`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<% initial_items = [
{ id: "21", name: "Joe Black" },
{ id: "22", name: "Nancy White" },
{ id: "23", name: "Bill Green" },
] %>

<%= pb_rails("draggable", props: {initial_items: initial_items}) do %>
<%= pb_rails("draggable/draggable_container") do %>
<% initial_items.each do |item| %>
<%= pb_rails("draggable/draggable_item", props:{drag_id: item[:id]}) do %>
<%= pb_rails("card", props: {highlight: {position: "side", color:"primary"}, margin_bottom: "xs", padding: "xs"}) do %>
<%= pb_rails("flex", props:{align_items: "stretch", flex_direction:"column"}) do %>
<%= pb_rails("flex", props:{gap: "xs"}) do %>
<%= pb_rails("title", props: { text: item[:name], tag: "h4", size: 4 }) %>
<%= pb_rails("badge", props: {text:"35-12345" ,variant: "primary"}) %>
<% end %>
<%= pb_rails("caption", props: { size: "xs", text: "8:00A • Township Name • 90210" }) %>
<%= pb_rails("flex", props:{gap: "xxs", spacing:"between"}) do %>
<%= pb_rails("flex", props:{gap: "xxs"}) do %>
<%= pb_rails("caption", props: { size: "xs" , color: "error" }) do %>
<%= pb_rails("icon", props: { icon: "house-circle-exclamation", fixed_width: true }) %>
<% end %>
<%= pb_rails("caption", props: { size: "xs" , color: "success" }) do %>
<%= pb_rails("icon", props: { icon: "file-circle-check", fixed_width: true }) %>
<% end %>
<% end %>
<%= pb_rails("flex") do %>
<%= pb_rails("badge", props: {text:"Schedule QA" ,variant: "warning", rounded: true}) %>
<%= pb_rails("badge", props: {text:"Flex" ,variant: "primary", rounded: true}) %>
<%= pb_rails("badge", props: {text:"R99" ,variant: "primary", rounded: true}) %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<% initial_items = [
{ id: "31", name: "Philadelphia" },
{ id: "32", name: "New Jersey" },
{ id: "33", name: "Maryland" },
{ id: "34", name: "Connecticut" },

] %>

<%= pb_rails("draggable", props: {initial_items: initial_items}) do %>
<%= pb_rails("draggable/draggable_container") do %>
<%= pb_rails("list", props: {ordered: false}) do %>
<% initial_items.each do |item| %>
<%= pb_rails("draggable/draggable_item", props:{drag_id: item[:id]}) do %>
<%= pb_rails("list/item") do %><%= item[:name] %><% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
6 changes: 6 additions & 0 deletions playbook/app/pb_kits/playbook/pb_draggable/docs/example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,10 @@ examples:
- draggable_with_cards: Draggable with Cards
- draggable_multiple_containers: Dragging Across Multiple Containers

rails:
- draggable_default_rails: Default
- draggable_with_list_rails: Draggable with List Kit
- draggable_with_cards_rails: Draggable with Cards



3 changes: 3 additions & 0 deletions playbook/app/pb_kits/playbook/pb_draggable/draggable.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%= pb_content_tag do %>
<%= content.presence %>
<% end %>
18 changes: 18 additions & 0 deletions playbook/app/pb_kits/playbook/pb_draggable/draggable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Playbook
module PbDraggable
class Draggable < ::Playbook::KitBase
prop :initial_items, type: Playbook::Props::Array,
default: []

def data
Hash(prop(:data)).merge(pb_draggable: true)
end

def classname
generate_classname("pb_draggable")
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%= pb_content_tag do %>
<%= content.presence %>
<% end %>
15 changes: 15 additions & 0 deletions playbook/app/pb_kits/playbook/pb_draggable/draggable_container.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Playbook
module PbDraggable
class DraggableContainer < ::Playbook::KitBase
def data
Hash(prop(:data)).merge(pb_draggable_container: true)
end

def classname
generate_classname("pb_draggable_container")
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<%= pb_content_tag(:div, {
id: "item_#{object.drag_id}",
draggable: true
}) do %>
<%= content.presence %>
<% end %>

18 changes: 18 additions & 0 deletions playbook/app/pb_kits/playbook/pb_draggable/draggable_item.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Playbook
module PbDraggable
class DraggableItem < ::Playbook::KitBase
prop :drag_id, type: Playbook::Props::String,
default: ""

def data
Hash(prop(:data)).merge(pb_draggable_item: true)
end

def classname
generate_classname("pb_draggable_item")
end
end
end
end
125 changes: 125 additions & 0 deletions playbook/app/pb_kits/playbook/pb_draggable/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import PbEnhancedElement from "../pb_enhanced_element";

const DRAGGABLE_SELECTOR = "[data-pb-draggable]";
const DRAGGABLE_CONTAINER = ".pb_draggable_container";

export default class PbDraggable extends PbEnhancedElement {
static get selector() {
return DRAGGABLE_SELECTOR;
}

connect() {
this.draggedItem = null;
this.draggedItemId = null;
document.addEventListener("DOMContentLoaded", () => this.bindEventListeners());
}

bindEventListeners() {
// Needed to prevent images within draggable items from being independently draggable
// Needed if using Image kit in draggable items
this.element.querySelectorAll(".pb_draggable_item img").forEach(img => {
img.setAttribute("draggable", "false");
});

this.element.querySelectorAll(".pb_draggable_item").forEach(item => {
item.addEventListener("dragstart", this.handleDragStart.bind(this));
item.addEventListener("dragend", this.handleDragEnd.bind(this));
item.addEventListener("dragenter", this.handleDragEnter.bind(this));
});

const container = this.element.querySelector(DRAGGABLE_CONTAINER);
if (container) {
container.addEventListener("dragover", this.handleDragOver.bind(this));
container.addEventListener("drop", this.handleDrop.bind(this));
}
}

handleDragStart(event) {
// Needed to prevent images within draggable items from being independently draggable
// Needed if using Image kit in draggable items
if (event.target.tagName.toLowerCase() === 'img') {
event.preventDefault();
return;
}

this.draggedItem = event.target;
this.draggedItemId = event.target.id;
event.target.classList.add("is_dragging");

if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', this.draggedItemId);
}

setTimeout(() => {
event.target.style.opacity = '0.5';
}, 0);
}

handleDragEnter(event) {
if (!this.draggedItem || event.target === this.draggedItem) return;

const targetItem = event.target.closest('.pb_draggable_item');
if (!targetItem) return;

const container = targetItem.parentNode;
const items = Array.from(container.children);
const draggedIndex = items.indexOf(this.draggedItem);
const targetIndex = items.indexOf(targetItem);

if (draggedIndex > targetIndex) {
container.insertBefore(this.draggedItem, targetItem);
} else {
container.insertBefore(this.draggedItem, targetItem.nextSibling);
}
}

handleDragOver(event) {
event.preventDefault();
const container = event.target.closest(DRAGGABLE_CONTAINER);

if (container) {
container.classList.add("active_container");
}
}

handleDrop(event) {
event.preventDefault();
const container = event.target.closest(DRAGGABLE_CONTAINER);
if (!container || !this.draggedItem) return;

container.classList.remove("active_container");
this.draggedItem.style.opacity = '1';

// Updated order of items as an array of item IDs
const reorderedItems = Array.from(container.children)
.filter(item => item.classList.contains("pb_draggable_item"))
.map(item => item.id.replace("item_", ""));

// Store reordered items in a data attribute on the container
container.setAttribute("data-reordered-items", JSON.stringify(reorderedItems));

const customEvent = new CustomEvent('pb-draggable-reorder', {
detail: {
reorderedItems,
containerId: container.id,
}
});
this.element.dispatchEvent(customEvent);

this.draggedItem = null;
this.draggedItemId = null;
}


handleDragEnd(event) {
event.target.classList.remove("is_dragging");
event.target.style.opacity = '1';
this.draggedItem = null;
this.draggedItemId = null;

this.element.querySelectorAll(DRAGGABLE_CONTAINER).forEach(container => {
container.classList.remove("active_container");
});
}
}

0 comments on commit ce8404d

Please sign in to comment.