Skip to content

Instantly share code, notes, and snippets.

@tbcooney
Forked from ftes/_seating_plan.html.haml
Created April 20, 2020 14:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tbcooney/fcc545c690a112a47b4dd55aed1ccde1 to your computer and use it in GitHub Desktop.
Save tbcooney/fcc545c690a112a47b4dd55aed1ccde1 to your computer and use it in GitHub Desktop.
Drag and drop in CSS grid with rails 6, stimulus and rails-ujs
-# https://johnbeatty.co/2018/03/09/stimulus-js-tutorial-how-do-i-drag-and-drop-items-in-a-list/
.grid--draggable{ 'data-controller': 'seating-plan',
'data-seating-plan-endpoint': endpoint,
'data-action': 'dragstart->seating-plan#onDragStart dragover->seating-plan#onDragOver dragenter->seating-plan#onDragEnter drop->seating-plan#onDrop dragend->seating-plan#onDragEnd' }
- seating_plan.each do |seat|
- if seat[:is_empty]
.grid__item.grid__item--empty{ 'data-row': seat[:row],
'data-col': seat[:col],
style: "grid-row: #{seat[:row]}; grid-column: #{seat[:col]};",
class: ('grid__item--border' if seat[:is_border]) }
- else
.grid__item{ draggable: 'true',
'data-item-id': seat[:student].id,
'data-row': seat[:row],
'data-col': seat[:col],
style: "grid-row: #{seat[:row]}; grid-column: #{seat[:col]};" }
.card
.card-content
%p.title.has-text-centered= seat[:student].name
%footer.card-footer
%p.card-footer-item.has-text-weight-bold
-# TODO Add sum
4
%p.card-footer-item.has-text-grey
-# TODO Add sum
10
# models/school_class.rb
class SchoolClass < ApplicationRecord
strip_attributes
has_many :students
def seating_plan
row_min, row_max = students.map { |s| s.seat_row }.minmax
col_min, col_max = students.map { |s| s.seat_col }.minmax
row_offset = row_min - 2
col_offset = col_min - 2
# Can't use double splat operator { **tmp, s.seat_hash => s } with non-symbol (object) keys
students_index = students.reduce({}) { |tmp, s| tmp.merge({seat_hash(s.seat_row, s.seat_col) => s}) }
seat_coordinates = (row_min - 1..row_max + 1).to_a.product((col_min - 1..col_max + 1).to_a)
return seat_coordinates.map do |row, col|
{
row: row - row_offset,
col: col - col_offset,
is_empty: !students_index.has_key?(seat_hash(row, col)),
student: students_index[seat_hash(row, col)],
is_border: row < row_min || row > row_max || col < col_min || col > col_max,
}
end
end
def seating_plan=(seats)
# TODO exception handling
transaction do
seats.each(&(method :update_seat))
end
end
private
def seat_hash(row, col)
{row: row, col: col}.freeze
end
def update_seat(student_id:, row:, col:)
student = students.find(student_id)
# Bypass validation (when swapping seats, one seat is briefly occupied twice)
student.attributes = { seat_row: row, seat_col: col }
student.save! validate: false
end
end
class SchoolClassesController < ApplicationController
before_action :set_school_class, only: [:seating_plan]
def seating_plan
seating_plan = seating_plan_params[:students].map { |s| s.to_h.symbolize_keys }
puts "Plan: #{seating_plan.inspect}"
@school_class.seating_plan = seating_plan
respond_to do |format|
format.html { redirect_back fallback_location: school_classes_path, notice: t('.notice') }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_school_class
@school_class = SchoolClass.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def seating_plan_params
params.require(:school_class).permit(students: [:student_id, :row, :col])
end
end
@import 'bulma/bulma';
.grid--draggable {
display: grid;
grid-gap: 10px;
grid-auto-rows: 1fr;
grid-auto-columns: 1fr;
padding: 20px 0;
[draggable] {
cursor: move; /* fallback if grab is not supported */
cursor: grab;
}
.grid__item--border {
visibility: hidden;
}
&.grid--dragging {
.grid__item--border {
visibility: visible
}
.grid__item--empty {
background-color: $grey-lightest;
}
}
}
import {Controller} from 'stimulus'
import Rails from '@rails/ujs'
// https://johnbeatty.co/2018/03/09/stimulus-js-tutorial-how-do-i-drag-and-drop-items-in-a-list/
export default class extends Controller {
onDragStart(event) {
this.element.classList.add('grid--dragging')
const data = JSON.stringify({
row: event.target.getAttribute('data-row'),
col: event.target.getAttribute('data-col'),
})
event.dataTransfer.setData('application/drag-key', data)
event.dataTransfer.effectAllowed = 'move'
console.log('start')
}
onDragOver(event) {
event.preventDefault()
console.log('over')
return true
}
onDragEnter(event) {
event.preventDefault()
console.log('enter')
}
getPosition(el) {
return [el.getAttribute('data-row'), el.getAttribute('data-col')]
}
getPositions() {
const items = [...this.element.querySelectorAll('.grid__item[draggable]')]
return items.map(el => ({
student_id: el.getAttribute('data-item-id'),
row: this.getPosition(el)[0],
col: this.getPosition(el)[1],
}))
}
setPosition(el, [row, col]) {
el.setAttribute('data-row', row)
el.setAttribute('data-col', col)
el.style['grid-row'] = row
el.style['grid-column'] = col
}
swap(el1, el2) {
const el1Pos = this.getPosition(el1)
const el2Pos = this.getPosition(el2)
this.setPosition(el1, el2Pos)
this.setPosition(el2, el1Pos)
}
onDrop(event) {
const { row, col } = JSON.parse(event.dataTransfer.getData("application/drag-key"))
const targetEl = event.target.closest('[data-row][data-col]')
const sourceEl = this.element.querySelector(`[data-row='${row}'][data-col='${col}']`)
this.swap(sourceEl, targetEl)
event.preventDefault()
console.log('drop')
}
submit(data) {
Rails.ajax({
url: this.data.get('endpoint'),
type: 'put',
beforeSend(xhr, options) {
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8')
// Workaround: add options.data late to avoid Content-Type header to already being set in stone
// https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee#L53
options.data = JSON.stringify(data)
return true
},
});
}
onDragEnd(event) {
this.element.classList.remove('grid--dragging')
this.submit({
school_class: {
students: this.getPositions()
}
})
console.log('end')
}
}
@tbcooney
Copy link
Author

Set time aside and try to extended this for an interactive seat picker.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment