diff --git a/app/RSpade/Core/Js/Rsx_Behaviors.js b/app/RSpade/Core/Js/Rsx_Behaviors.js
index 34218fa97..a870d1f76 100755
--- a/app/RSpade/Core/Js/Rsx_Behaviors.js
+++ b/app/RSpade/Core/Js/Rsx_Behaviors.js
@@ -15,7 +15,6 @@ class Rsx_Behaviors {
static _on_framework_core_init() {
Rsx_Behaviors._init_ignore_invalid_anchor_links();
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();
- });
- }
}
diff --git a/app/RSpade/Core/Js/Rsx_Droppable.js b/app/RSpade/Core/Js/Rsx_Droppable.js
new file mode 100755
index 000000000..60c4a0801
--- /dev/null
+++ b/app/RSpade/Core/Js/Rsx_Droppable.js
@@ -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();
+ });
+ }
+}
diff --git a/app/RSpade/man/file_drop.txt b/app/RSpade/man/droppable.txt
similarity index 91%
rename from app/RSpade/man/file_drop.txt
rename to app/RSpade/man/droppable.txt
index ac34908a9..51726b141 100755
--- a/app/RSpade/man/file_drop.txt
+++ b/app/RSpade/man/droppable.txt
@@ -1,24 +1,24 @@
NAME
- File Drop Handler - Global drag-and-drop file interception system
+ Droppable - Global drag-and-drop file interception system
SYNOPSIS
Add class="rsx-droppable" to any element or component to receive
dropped files via the file-drop event.
DESCRIPTION
- The RSpade framework provides a global file drop handler that intercepts
- all file drag-and-drop operations at the document level. This system
- routes dropped files to designated drop targets using CSS class-based
+ Droppable is the RSX framework's global file drop system. It intercepts
+ all file drag-and-drop operations at the document level and routes
+ dropped files to designated drop targets using CSS class-based
registration.
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
- Smart target selection (single vs multiple targets)
- Cursor feedback indicating valid/invalid drop zones
- Component event integration
- The framework initializes this handler automatically during bootstrap.
+ Droppable initializes automatically during framework bootstrap.
No manual initialization is required.
CSS CLASSES
@@ -111,7 +111,7 @@ FILE-DROP EVENT
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:
By MIME Type:
@@ -161,7 +161,7 @@ FILE VALIDATION
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
- "none" cursor: Shown when no valid target exists or when hovering
@@ -172,12 +172,12 @@ CURSOR FEEDBACK
IMPLEMENTATION NOTES
Drag Counter:
- The framework uses a drag counter to track when files enter and
- leave the window. This handles the common issue where dragenter
- and dragleave fire for child elements.
+ Droppable uses a drag counter to track when files enter and leave
+ the window. This handles the common issue where dragenter and
+ dragleave fire for child elements.
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.
Cleanup:
@@ -276,7 +276,7 @@ STYLING RECOMMENDATIONS
background: rgba(0, 123, 255, 0.15);
}
-RSX VS HTML5 DRAG-DROP
+DROPPABLE VS HTML5 DRAG-DROP
Standard HTML5:
- Must add dragenter, dragover, drop handlers to each element
@@ -284,9 +284,9 @@ RSX VS HTML5 DRAG-DROP
- Must track drag state per element
- No automatic multi-target coordination
- RSX:
+ Droppable:
- Add rsx-droppable class, handle file-drop event
- - Framework handles all drag events
+ - Droppable handles all drag events
- Automatic state management and cleanup
- Smart single vs multi-target behavior
@@ -315,4 +315,4 @@ SEE ALSO
VERSION
RSpade Framework 1.0
- Last Updated: 2025-01-14
+ Last Updated: 2026-01-15
diff --git a/app/RSpade/man/file_upload.txt b/app/RSpade/man/file_upload.txt
index 949df1c99..d04ecc123 100755
--- a/app/RSpade/man/file_upload.txt
+++ b/app/RSpade/man/file_upload.txt
@@ -1330,6 +1330,7 @@ FUTURE DEVELOPMENT
✓ Session-based attachment security with can_user_assign_this_file()
✓ Attachment API (attach_to, add_to, detach)
✓ Model helper methods (get_attachment, get_attachments)
+ ✓ Droppable - Global drag-and-drop file interception system
Planned Enhancements:
@@ -1367,7 +1368,7 @@ FUTURE DEVELOPMENT
- Automatic cleanup of unused thumbnails
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
- Image cropper/editor widget
- Camera capture widget
@@ -1426,10 +1427,11 @@ SECURITY
they own or have permission to view.
SEE ALSO
+ droppable.txt - Global drag-and-drop file interception system
model.txt - Model system documentation
storage_directories.txt - Storage directory conventions
migrations.txt - Database migration system
VERSION
RSpade Framework 1.0
- Last Updated: 2025-11-04
+ Last Updated: 2026-01-15
diff --git a/app/RSpade/man/file_upload_examples.txt b/app/RSpade/man/file_upload_examples.txt
index 33adda6e3..da5013667 100755
--- a/app/RSpade/man/file_upload_examples.txt
+++ b/app/RSpade/man/file_upload_examples.txt
@@ -385,6 +385,89 @@ DELETING FILES
->where('fileable_id', $project_id)
->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):
+
+
+