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:
@@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
167
app/RSpade/Core/Js/Rsx_Droppable.js
Executable file
167
app/RSpade/Core/Js/Rsx_Droppable.js
Executable 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user