Extract Rsx_Droppable into its own file

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-01-15 07:53:49 +00:00
parent e389093a0a
commit 1683df867b
5 changed files with 272 additions and 179 deletions

View File

@@ -15,7 +15,6 @@ class Rsx_Behaviors {
static _on_framework_core_init() { static _on_framework_core_init() {
Rsx_Behaviors._init_ignore_invalid_anchor_links(); Rsx_Behaviors._init_ignore_invalid_anchor_links();
Rsx_Behaviors._trim_copied_text(); Rsx_Behaviors._trim_copied_text();
Rsx_Behaviors._init_file_drop_handler();
} }
/** /**
@@ -107,163 +106,4 @@ class Rsx_Behaviors {
}); });
} }
/**
* Global file drop handler
*
* Intercepts all file drag/drop operations and routes them to components
* marked with the `rsx-droppable` class. Components receive a `file-drop`
* event with the dropped files.
*
* CSS Classes:
* - `rsx-droppable` - Marks element as a valid drop target
* - `rsx-drop-active` - Added to all droppables during file drag
* - `rsx-drop-target` - Added to the specific element that will receive drop
*
* Behavior:
* - Single visible droppable: auto-becomes target
* - Multiple visible droppables: must hover over specific element
* - No-drop cursor when not over valid target
* - Widgets handle their own file type filtering
*/
static _init_file_drop_handler() {
let drag_counter = 0;
let current_target = null;
// Get all visible droppable elements
const get_visible_droppables = () => {
return $('.rsx-droppable').filter(function() {
const $el = $(this);
// Must be visible and not hidden by CSS
return $el.is(':visible') && $el.css('visibility') !== 'hidden';
});
};
// Check if event contains files
const has_files = (e) => {
if (e.originalEvent && e.originalEvent.dataTransfer) {
const types = e.originalEvent.dataTransfer.types;
return types && (types.includes('Files') || types.indexOf('Files') >= 0);
}
return false;
};
// Update which element is the current target
const update_target = ($new_target) => {
if (current_target) {
$(current_target).removeClass('rsx-drop-target');
}
current_target = $new_target ? $new_target[0] : null;
if (current_target) {
$(current_target).addClass('rsx-drop-target');
}
};
// Clear all drag state
const clear_drag_state = () => {
drag_counter = 0;
$('.rsx-drop-active').removeClass('rsx-drop-active');
update_target(null);
};
// dragenter - file enters the window
$(document).on('dragenter', function(e) {
if (!has_files(e)) return;
drag_counter++;
if (drag_counter === 1) {
// First entry - activate all droppables
const $droppables = get_visible_droppables();
$droppables.addClass('rsx-drop-active');
// If only one droppable, auto-target it
if ($droppables.length === 1) {
update_target($droppables.first());
}
}
});
// dragleave - file leaves an element
$(document).on('dragleave', function(e) {
if (!has_files(e)) return;
drag_counter--;
if (drag_counter <= 0) {
clear_drag_state();
}
});
// dragover - file is over an element (fires continuously)
$(document).on('dragover', function(e) {
if (!has_files(e)) return;
e.preventDefault(); // Required to allow drop
const $droppables = get_visible_droppables();
if ($droppables.length === 0) {
// No drop targets - show no-drop cursor
e.originalEvent.dataTransfer.dropEffect = 'none';
return;
}
if ($droppables.length === 1) {
// Single target - already set, allow copy
e.originalEvent.dataTransfer.dropEffect = 'copy';
return;
}
// Multiple targets - find if we're over one
const $hovered = $(e.target).closest('.rsx-droppable');
if ($hovered.length && $hovered.hasClass('rsx-drop-active')) {
// Over a valid droppable
update_target($hovered);
e.originalEvent.dataTransfer.dropEffect = 'copy';
} else {
// Not over any droppable - show no-drop cursor
update_target(null);
e.originalEvent.dataTransfer.dropEffect = 'none';
}
});
// drop - file is dropped
$(document).on('drop', function(e) {
if (!has_files(e)) return;
e.preventDefault();
e.stopPropagation();
const files = e.originalEvent.dataTransfer.files;
if (current_target && files.length > 0) {
// Trigger file-drop event on the component
const $target = $(current_target);
const component = $target.component();
if (component) {
component.trigger('file-drop', {
files: files,
dataTransfer: e.originalEvent.dataTransfer,
originalEvent: e.originalEvent
});
} else {
// No component - trigger jQuery event on element directly
$target.trigger('file-drop', {
files: files,
dataTransfer: e.originalEvent.dataTransfer,
originalEvent: e.originalEvent
});
}
}
clear_drag_state();
});
// Handle drag end (e.g., user presses Escape)
$(document).on('dragend', function(e) {
clear_drag_state();
});
}
} }

View File

@@ -0,0 +1,167 @@
/**
* Rsx_Droppable - Global File Drop Handler
*
* Intercepts all file drag/drop operations and routes them to components
* marked with the `rsx-droppable` class. Components receive a `file-drop`
* event with the dropped files.
*
* CSS Classes:
* - `rsx-droppable` - Marks element as a valid drop target
* - `rsx-drop-active` - Added to all droppables during file drag
* - `rsx-drop-target` - Added to the specific element that will receive drop
*
* Behavior:
* - Single visible droppable: auto-becomes target
* - Multiple visible droppables: must hover over specific element
* - No-drop cursor when not over valid target
* - Widgets handle their own file type filtering
*
* @internal Framework use only - not part of public API
*/
class Rsx_Droppable {
static _on_framework_core_init() {
Rsx_Droppable._init();
}
static _init() {
let drag_counter = 0;
let current_target = null;
// Get all visible droppable elements
const get_visible_droppables = () => {
return $('.rsx-droppable').filter(function() {
const $el = $(this);
// Must be visible and not hidden by CSS
return $el.is(':visible') && $el.css('visibility') !== 'hidden';
});
};
// Check if event contains files
const has_files = (e) => {
if (e.originalEvent && e.originalEvent.dataTransfer) {
const types = e.originalEvent.dataTransfer.types;
return types && (types.includes('Files') || types.indexOf('Files') >= 0);
}
return false;
};
// Update which element is the current target
const update_target = ($new_target) => {
if (current_target) {
$(current_target).removeClass('rsx-drop-target');
}
current_target = $new_target ? $new_target[0] : null;
if (current_target) {
$(current_target).addClass('rsx-drop-target');
}
};
// Clear all drag state
const clear_drag_state = () => {
drag_counter = 0;
$('.rsx-drop-active').removeClass('rsx-drop-active');
update_target(null);
};
// dragenter - file enters the window
$(document).on('dragenter', function(e) {
if (!has_files(e)) return;
drag_counter++;
if (drag_counter === 1) {
// First entry - activate all droppables
const $droppables = get_visible_droppables();
$droppables.addClass('rsx-drop-active');
// If only one droppable, auto-target it
if ($droppables.length === 1) {
update_target($droppables.first());
}
}
});
// dragleave - file leaves an element
$(document).on('dragleave', function(e) {
if (!has_files(e)) return;
drag_counter--;
if (drag_counter <= 0) {
clear_drag_state();
}
});
// dragover - file is over an element (fires continuously)
$(document).on('dragover', function(e) {
if (!has_files(e)) return;
e.preventDefault(); // Required to allow drop
const $droppables = get_visible_droppables();
if ($droppables.length === 0) {
// No drop targets - show no-drop cursor
e.originalEvent.dataTransfer.dropEffect = 'none';
return;
}
if ($droppables.length === 1) {
// Single target - already set, allow copy
e.originalEvent.dataTransfer.dropEffect = 'copy';
return;
}
// Multiple targets - find if we're over one
const $hovered = $(e.target).closest('.rsx-droppable');
if ($hovered.length && $hovered.hasClass('rsx-drop-active')) {
// Over a valid droppable
update_target($hovered);
e.originalEvent.dataTransfer.dropEffect = 'copy';
} else {
// Not over any droppable - show no-drop cursor
update_target(null);
e.originalEvent.dataTransfer.dropEffect = 'none';
}
});
// drop - file is dropped
$(document).on('drop', function(e) {
if (!has_files(e)) return;
e.preventDefault();
e.stopPropagation();
const files = e.originalEvent.dataTransfer.files;
if (current_target && files.length > 0) {
// Trigger file-drop event on the component
const $target = $(current_target);
const component = $target.component();
if (component) {
component.trigger('file-drop', {
files: files,
dataTransfer: e.originalEvent.dataTransfer,
originalEvent: e.originalEvent
});
} else {
// No component - trigger jQuery event on element directly
$target.trigger('file-drop', {
files: files,
dataTransfer: e.originalEvent.dataTransfer,
originalEvent: e.originalEvent
});
}
}
clear_drag_state();
});
// Handle drag end (e.g., user presses Escape)
$(document).on('dragend', function(e) {
clear_drag_state();
});
}
}

View File

@@ -1,24 +1,24 @@
NAME NAME
File Drop Handler - Global drag-and-drop file interception system Droppable - Global drag-and-drop file interception system
SYNOPSIS SYNOPSIS
Add class="rsx-droppable" to any element or component to receive Add class="rsx-droppable" to any element or component to receive
dropped files via the file-drop event. dropped files via the file-drop event.
DESCRIPTION DESCRIPTION
The RSpade framework provides a global file drop handler that intercepts Droppable is the RSX framework's global file drop system. It intercepts
all file drag-and-drop operations at the document level. This system all file drag-and-drop operations at the document level and routes
routes dropped files to designated drop targets using CSS class-based dropped files to designated drop targets using CSS class-based
registration. registration.
Unlike traditional HTML5 drag-and-drop which requires explicit event Unlike traditional HTML5 drag-and-drop which requires explicit event
handlers on each element, this system provides: handlers on each element, Droppable provides:
- Automatic visual feedback during file drags - Automatic visual feedback during file drags
- Smart target selection (single vs multiple targets) - Smart target selection (single vs multiple targets)
- Cursor feedback indicating valid/invalid drop zones - Cursor feedback indicating valid/invalid drop zones
- Component event integration - Component event integration
The framework initializes this handler automatically during bootstrap. Droppable initializes automatically during framework bootstrap.
No manual initialization is required. No manual initialization is required.
CSS CLASSES CSS CLASSES
@@ -111,7 +111,7 @@ FILE-DROP EVENT
FILE VALIDATION FILE VALIDATION
Each widget is responsible for validating dropped files. The framework Each widget is responsible for validating dropped files. Droppable
provides files without filtering. Common validation patterns: provides files without filtering. Common validation patterns:
By MIME Type: By MIME Type:
@@ -161,7 +161,7 @@ FILE VALIDATION
CURSOR FEEDBACK CURSOR FEEDBACK
The framework automatically sets dropEffect to control cursor appearance: Droppable automatically sets dropEffect to control cursor appearance:
- "copy" cursor: Shown when hovering over a valid drop target - "copy" cursor: Shown when hovering over a valid drop target
- "none" cursor: Shown when no valid target exists or when hovering - "none" cursor: Shown when no valid target exists or when hovering
@@ -172,12 +172,12 @@ CURSOR FEEDBACK
IMPLEMENTATION NOTES IMPLEMENTATION NOTES
Drag Counter: Drag Counter:
The framework uses a drag counter to track when files enter and Droppable uses a drag counter to track when files enter and leave
leave the window. This handles the common issue where dragenter the window. This handles the common issue where dragenter and
and dragleave fire for child elements. dragleave fire for child elements.
Event Prevention: Event Prevention:
The handler prevents default browser behavior for all file drags, Droppable prevents default browser behavior for all file drags,
ensuring files are never accidentally downloaded or opened. ensuring files are never accidentally downloaded or opened.
Cleanup: Cleanup:
@@ -276,7 +276,7 @@ STYLING RECOMMENDATIONS
background: rgba(0, 123, 255, 0.15); background: rgba(0, 123, 255, 0.15);
} }
RSX VS HTML5 DRAG-DROP DROPPABLE VS HTML5 DRAG-DROP
Standard HTML5: Standard HTML5:
- Must add dragenter, dragover, drop handlers to each element - Must add dragenter, dragover, drop handlers to each element
@@ -284,9 +284,9 @@ RSX VS HTML5 DRAG-DROP
- Must track drag state per element - Must track drag state per element
- No automatic multi-target coordination - No automatic multi-target coordination
RSX: Droppable:
- Add rsx-droppable class, handle file-drop event - Add rsx-droppable class, handle file-drop event
- Framework handles all drag events - Droppable handles all drag events
- Automatic state management and cleanup - Automatic state management and cleanup
- Smart single vs multi-target behavior - Smart single vs multi-target behavior
@@ -315,4 +315,4 @@ SEE ALSO
VERSION VERSION
RSpade Framework 1.0 RSpade Framework 1.0
Last Updated: 2025-01-14 Last Updated: 2026-01-15

View File

@@ -1330,6 +1330,7 @@ FUTURE DEVELOPMENT
✓ Session-based attachment security with can_user_assign_this_file() ✓ Session-based attachment security with can_user_assign_this_file()
✓ Attachment API (attach_to, add_to, detach) ✓ Attachment API (attach_to, add_to, detach)
✓ Model helper methods (get_attachment, get_attachments) ✓ Model helper methods (get_attachment, get_attachments)
✓ Droppable - Global drag-and-drop file interception system
Planned Enhancements: Planned Enhancements:
@@ -1367,7 +1368,7 @@ FUTURE DEVELOPMENT
- Automatic cleanup of unused thumbnails - Automatic cleanup of unused thumbnails
JQHTML Upload Widgets: JQHTML Upload Widgets:
- Drag-and-drop file uploader component Droppable - Global drag-and-drop file interception (see droppable.txt)
- Multi-file upload queue with progress - Multi-file upload queue with progress
- Image cropper/editor widget - Image cropper/editor widget
- Camera capture widget - Camera capture widget
@@ -1426,10 +1427,11 @@ SECURITY
they own or have permission to view. they own or have permission to view.
SEE ALSO SEE ALSO
droppable.txt - Global drag-and-drop file interception system
model.txt - Model system documentation model.txt - Model system documentation
storage_directories.txt - Storage directory conventions storage_directories.txt - Storage directory conventions
migrations.txt - Database migration system migrations.txt - Database migration system
VERSION VERSION
RSpade Framework 1.0 RSpade Framework 1.0
Last Updated: 2025-11-04 Last Updated: 2026-01-15

View File

@@ -385,6 +385,89 @@ DELETING FILES
->where('fileable_id', $project_id) ->where('fileable_id', $project_id)
->delete(); ->delete();
DRAG-AND-DROP UPLOADS WITH DROPPABLE
Use Droppable to enable drag-and-drop file uploads in JQHTML components.
See droppable.txt for full documentation.
Basic Integration:
Template (My_Uploader.jqhtml):
<Define:My_Uploader tag="div" class="rsx-droppable My_Uploader">
<div class="drop-hint">Drop files here or click to upload</div>
<input type="file" $sid="file_input" style="display: none" />
<ul $sid="file_list"></ul>
</Define:My_Uploader>
JavaScript (My_Uploader.js):
class My_Uploader extends Jqhtml_Component {
on_render() {
// Handle dropped files via Droppable
this.on('file-drop', (_, data) => {
this._upload_files(data.files);
});
// Handle click-to-upload
this.$.on('click', () => this.$sid('file_input').click());
this.$sid('file_input').on('change', (e) => {
this._upload_files(e.target.files);
});
}
async _upload_files(files) {
for (let file of files) {
// Validate
if (file.size > 10 * 1024 * 1024) {
Flash.error(`${file.name} exceeds 10 MB limit`);
continue;
}
// Upload
const formData = new FormData();
formData.append('file', file);
const response = await $.ajax({
url: '/_upload',
type: 'POST',
data: formData,
processData: false,
contentType: false
});
if (response.success) {
this._add_to_list(response.attachment);
}
}
}
_add_to_list(attachment) {
this.$sid('file_list').append(
`<li>${attachment.file_name}</li>`
);
}
}
SCSS (My_Uploader.scss):
.My_Uploader {
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
cursor: pointer;
&.rsx-drop-active {
border-color: #007bff;
background: rgba(0, 123, 255, 0.05);
}
&.rsx-drop-target {
border-style: solid;
background: rgba(0, 123, 255, 0.15);
}
}
SECURITY CONSIDERATIONS SECURITY CONSIDERATIONS
1. Always validate files: 1. Always validate files:
@@ -426,9 +509,10 @@ SECURITY CONSIDERATIONS
SEE ALSO SEE ALSO
file_upload.txt - Complete file upload system documentation file_upload.txt - Complete file upload system documentation
droppable.txt - Global drag-and-drop file interception system
model.txt - Model system documentation model.txt - Model system documentation
routing.txt - Route and endpoint documentation routing.txt - Route and endpoint documentation
VERSION VERSION
RSpade Framework 1.0 RSpade Framework 1.0
Last Updated: 2025-11-02 Last Updated: 2026-01-15