From 45f832037bc071bae7e602f15408b22a287cec82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:54:07 +0000 Subject: [PATCH 1/2] Add drag-and-drop functionality to x0 framework --- example/drag_drop/README.md | 63 +++++ example/drag_drop/sql/insert-txt.sql | 31 +++ example/drag_drop/sql/sys-config.sql | 9 + example/drag_drop/static/menu.json | 8 + example/drag_drop/static/object.json | 327 +++++++++++++++++++++++++ example/drag_drop/static/skeleton.json | 50 ++++ python/Index.py | 1 + www/static/globalstyles.css | 6 + www/sysDragDropHandler.js | 66 +++++ www/sysInitOnLoad.js | 7 + www/sysObjFormfieldList.js | 57 +++++ www/sysObjList.js | 84 +++++++ www/sysObjTreeSimple.js | 101 +++++++- 13 files changed, 806 insertions(+), 4 deletions(-) create mode 100644 example/drag_drop/README.md create mode 100644 example/drag_drop/sql/insert-txt.sql create mode 100644 example/drag_drop/sql/sys-config.sql create mode 100644 example/drag_drop/static/menu.json create mode 100644 example/drag_drop/static/object.json create mode 100644 example/drag_drop/static/skeleton.json create mode 100644 www/sysDragDropHandler.js diff --git a/example/drag_drop/README.md b/example/drag_drop/README.md new file mode 100644 index 00000000..733dedd6 --- /dev/null +++ b/example/drag_drop/README.md @@ -0,0 +1,63 @@ +# Example #16: Drag and Drop + +This example demonstrates how to implement drag-and-drop functionality in the **x0-framework**, +enabling objects to be dragged between UI components for seamless data transfer. + +## URL + +Open URL: `http://x0-app.x0.localnet/python/Index.py?appid=example16` + +## Scenarios Demonstrated + +### 1. List → List (Append by Drag) + +- **Source**: `sysObjList.js` (`SourceList`) configured with `"DragSource": true`. +- **Target**: `sysObjList.js` (`DestinationList`) configured with `"DropTarget": true`. +- Drag any row from the **Source List** tab and drop it onto the **Destination List** in the + **Drop Targets** tab. The row data is appended to the destination list. + +### 2. List → FormfieldList (Set Form Data by Drag) + +- **Source**: same `SourceList` above. +- **Target**: `sysObjFormfieldList.js` (`DestinationForm`) configured with `"DropTarget": true`. +- Drag any row from the **Source List** tab and drop it onto the **Form Drop Target** in the + **Drop Targets** tab. The row data populates the form fields. + +### 3. Tree Item → Tree Node (Move by Drag) + +- **Tree**: `sysObjTreeSimple.js` (`DragDropTree`) configured with both `"DragSource": true` + and `"DropTarget": true`. +- In the **Tree** tab, drag any item from one node and drop it onto another node's header. + The item is moved to the target node's children list. + +## Configuration + +All drag-and-drop behaviour is declared declaratively in `object.json`: + +| Object type | Attribute | Effect | +|------------------|----------------|-------------------------------------------| +| `List` | `DragSource: true` | Rows can be dragged as data sources | +| `List` | `DropTarget: true` | Rows from other lists can be dropped | +| `FormfieldList` | `DropTarget: true` | Dragged row data sets the form fields | +| `TreeSimple` | `DragSource: true` | Tree items can be dragged | +| `TreeSimple` | `DropTarget: true` | Tree nodes accept dropped items | + +## Visual Feedback + +Drop targets highlight with a dashed blue outline (`.sysDragDropOver`) while a draggable +object is hovered over them. + +## Main Components + +- [`sysObjList.js`](../../../www/sysObjList.js) — drag source rows and drop target list +- [`sysObjFormfieldList.js`](../../../www/sysObjFormfieldList.js) — drop target form +- [`sysObjTreeSimple.js`](../../../www/sysObjTreeSimple.js) — drag source items and drop target nodes +- [`sysDragDropHandler.js`](../../../www/sysDragDropHandler.js) — central coordinator + +## Object Metadata + +All UI and logic are defined in `./static/object.json`. + +--- + +**Note:** *x0-app* and *x0-db* docker containers must be up and running for viewing. diff --git a/example/drag_drop/sql/insert-txt.sql b/example/drag_drop/sql/insert-txt.sql new file mode 100644 index 00000000..bb49fe9f --- /dev/null +++ b/example/drag_drop/sql/insert-txt.sql @@ -0,0 +1,31 @@ +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.LINK1', 'menu', 'Drag and Drop'); + +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.TAB1', 'tabcontainer', 'Source List'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.TAB2', 'tabcontainer', 'Drop Targets'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.TAB3', 'tabcontainer', 'Tree'); + +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.HEADER.COL1', 'table-header', 'Col1'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.HEADER.COL2', 'table-header', 'Col2'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.HEADER.COL3', 'table-header', 'Col3'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.HEADER.COL4', 'table-header', 'Col4'); + +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.FORM.HEADER', 'form-section', 'Form Drop Target'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.FORM.SUBHEADER', 'form-section', 'Drag a row from the Source List onto this form'); + +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.FORM.LABEL.COL1', 'form-label', 'Column 1'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.FORM.LABEL.COL2', 'form-label', 'Column 2'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.FORM.LABEL.COL3', 'form-label', 'Column 3'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.FORM.LABEL.COL4', 'form-label', 'Column 4'); + +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.PULLDOWN.OPT1', 'pulldown-option', 'Option 10'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.PULLDOWN.OPT2', 'pulldown-option', 'Option 20'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.PULLDOWN.OPT3', 'pulldown-option', 'Option 30'); + +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.TREE.NODE1', 'tree-node', 'Group A'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.TREE.NODE2', 'tree-node', 'Group B'); + +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.TREE.ITEM1', 'tree-item', 'Document 1'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.TREE.ITEM2', 'tree-item', 'Document 2'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.TREE.ITEM3', 'tree-item', 'Document 3'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.TREE.ITEM4', 'tree-item', 'Document 4'); +INSERT INTO webui.text (id, "group", value_en) VALUES ('TXT.DRAGDROP.TREE.ITEM5', 'tree-item', 'Document 5'); diff --git a/example/drag_drop/sql/sys-config.sql b/example/drag_drop/sql/sys-config.sql new file mode 100644 index 00000000..59650949 --- /dev/null +++ b/example/drag_drop/sql/sys-config.sql @@ -0,0 +1,9 @@ +INSERT INTO system.config (app_id, config_group, "value") VALUES ('example16', 'index_title', 'x0 Example Drag and Drop'); +INSERT INTO system.config (app_id, config_group, "value") VALUES ('example16', 'debug_level', '10'); +INSERT INTO system.config (app_id, config_group, "value") VALUES ('example16', 'display_language', 'en'); +INSERT INTO system.config (app_id, config_group, "value") VALUES ('example16', 'default_screen', 'Screen1'); +INSERT INTO system.config (app_id, config_group, "value") VALUES ('example16', 'parent_window_url', 'null'); +INSERT INTO system.config (app_id, config_group, "value") VALUES ('example16', 'subdir', '/examples/drag_drop'); +INSERT INTO system.config (app_id, config_group, "value") VALUES ('example16', 'config_file_menu', 'menu.json'); +INSERT INTO system.config (app_id, config_group, "value") VALUES ('example16', 'config_file_object', 'object.json'); +INSERT INTO system.config (app_id, config_group, "value") VALUES ('example16', 'config_file_skeleton', 'skeleton.json'); diff --git a/example/drag_drop/static/menu.json b/example/drag_drop/static/menu.json new file mode 100644 index 00000000..9807a154 --- /dev/null +++ b/example/drag_drop/static/menu.json @@ -0,0 +1,8 @@ +[ + { + "Link1": + { + "RefID": "sysMenu" + } + } +] diff --git a/example/drag_drop/static/object.json b/example/drag_drop/static/object.json new file mode 100644 index 00000000..9b7ed1e4 --- /dev/null +++ b/example/drag_drop/static/object.json @@ -0,0 +1,327 @@ +{ + "Link1": + { + "Type": "Link", + "Attributes": { + "Style": "btn btn-primary d-none", + "TextID": "TXT.DRAGDROP.LINK1", + "ScreenID": "Screen1" + } + }, + + "TabContainer1": + { + "Type": "TabContainer", + "Attributes": + { + "Tabs": [ + { + "ID": "Tab1", + "Attributes": + { + "Default": true, + "TextID": "TXT.DRAGDROP.TAB1", + "Style": "col-md-5" + } + }, + { + "ID": "Tab2", + "Attributes": + { + "TextID": "TXT.DRAGDROP.TAB2", + "Style": "col-md-7" + } + }, + { + "ID": "Tab3", + "Attributes": + { + "TextID": "TXT.DRAGDROP.TAB3", + "Style": "col-md-5" + } + } + ] + } + }, + + "SourceListConnector": + { + "Type": "ServiceConnector", + "Attributes": + { + "OnEvent": + { + "Events": [ "InitSystem" ], + "ServiceCall": "/python/IntegrationTestGetTableData2.py" + } + } + }, + + "SourceList": + { + "Type": "List", + "Attributes": + { + "Style": "border rounded-top rounded-bottom", + "HeaderRowStyle": "row fw-bold fs-6 m-1 border-bottom", + "RowCount": 10, + "DragSource": true, + "SrcDataObjects": + { + "Dummy": + { + "Type": "HardcodedValues", + "Values": { + "LimitRows": "10" + } + } + }, + "Columns": [ + { + "ID": "col1", + "HeaderTextID": "TXT.DRAGDROP.HEADER.COL1", + "HeaderStyle": "col-md-3 p-2 border-end" + }, + { + "ID": "col2", + "HeaderTextID": "TXT.DRAGDROP.HEADER.COL2", + "HeaderStyle": "col-md-3 p-2 border-end" + }, + { + "ID": "col3", + "HeaderTextID": "TXT.DRAGDROP.HEADER.COL3", + "HeaderStyle": "col-md-3 p-2 border-end" + }, + { + "ID": "col4", + "HeaderTextID": "TXT.DRAGDROP.HEADER.COL4", + "HeaderStyle": "col-md-3 p-2" + } + ], + "RowStyle": "row m-1 border-bottom", + "RowAfterElements": 4, + "ColStyle": [ + "col-md-3 p-2 border-end", + "col-md-3 p-2 border-end", + "col-md-3 p-2 border-end", + "col-md-3 p-2" + ] + } + }, + + "DestinationList": + { + "Type": "List", + "Attributes": + { + "Style": "border rounded-top rounded-bottom", + "HeaderRowStyle": "row fw-bold fs-6 m-1 border-bottom", + "RowCount": 20, + "DropTarget": true, + "DataMaxRows": 50, + "Columns": [ + { + "ID": "col1", + "HeaderTextID": "TXT.DRAGDROP.HEADER.COL1", + "HeaderStyle": "col-md-3 p-2 border-end" + }, + { + "ID": "col2", + "HeaderTextID": "TXT.DRAGDROP.HEADER.COL2", + "HeaderStyle": "col-md-3 p-2 border-end" + }, + { + "ID": "col3", + "HeaderTextID": "TXT.DRAGDROP.HEADER.COL3", + "HeaderStyle": "col-md-3 p-2 border-end" + }, + { + "ID": "col4", + "HeaderTextID": "TXT.DRAGDROP.HEADER.COL4", + "HeaderStyle": "col-md-3 p-2" + } + ], + "RowStyle": "row m-1 border-bottom", + "RowAfterElements": 4, + "ColStyle": [ + "col-md-3 p-2 border-end", + "col-md-3 p-2 border-end", + "col-md-3 p-2 border-end", + "col-md-3 p-2" + ] + } + }, + + "DestinationForm": { + "Type": "FormfieldList", + "Attributes": { + "DropTarget": true, + "Sections": [ + { + "ID": "Section1", + "Object": "FormSectionHeader", + "ObjectAttributes": { + "Style": "card card-line bg-body-tertiary border border-dark border-2 mb-3", + "SubStyle": "card-body mt-n6", + "HeaderIcon": "fa-solid fa-down-to-line", + "HeaderTextID": "TXT.DRAGDROP.FORM.HEADER", + "SubHeaderTextID": "TXT.DRAGDROP.FORM.SUBHEADER" + }, + "Formfields": [ + "col1Descr", + "col1", + "col2Descr", + "col2", + "col3Descr", + "col3", + "col4Descr", + "col4" + ], + "RowStyle": "row", + "RowAfterElements": 4, + "ColStyle": "col-md-6", + "ColAfterElements": 2 + } + ] + } + }, + + "col1Descr": { + "Type": "Formfield", + "Attributes": { + "Type": "label", + "TextID": "TXT.DRAGDROP.FORM.LABEL.COL1" + } + }, + + "col1": { + "Type": "Formfield", + "Attributes": { + "Type": "checkbox", + "Value": true, + "Style": "form-check" + } + }, + + "col2Descr": { + "Type": "Formfield", + "Attributes": { + "Type": "label", + "TextID": "TXT.DRAGDROP.FORM.LABEL.COL2" + } + }, + + "col2": { + "Type": "Formfield", + "Attributes": { + "Type": "text", + "Disabled": true, + "Style": "form-control mb-1 p-1 w-100" + } + }, + + "col3Descr": { + "Type": "Formfield", + "Attributes": { + "Type": "label", + "TextID": "TXT.DRAGDROP.FORM.LABEL.COL3" + } + }, + + "col3": { + "Type": "Formfield", + "Attributes": { + "Type": "pulldown", + "Style": "form-control mb-1 p-1 w-100", + "Options": [ + { + "TextID": "TXT.DRAGDROP.PULLDOWN.OPT1", + "Value": "10" + }, + { + "TextID": "TXT.DRAGDROP.PULLDOWN.OPT2", + "Value": "20", + "Default": true + }, + { + "TextID": "TXT.DRAGDROP.PULLDOWN.OPT3", + "Value": "30" + } + ] + } + }, + + "col4Descr": { + "Type": "Formfield", + "Attributes": { + "Type": "label", + "TextID": "TXT.DRAGDROP.FORM.LABEL.COL4" + } + }, + + "col4": { + "Type": "Formfield", + "Attributes": { + "Type": "textarea", + "Style": "form-control mb-1 p-1 w-100" + } + }, + + "DragDropTree": + { + "Type": "TreeSimple", + "Attributes": + { + "DragSource": true, + "DropTarget": true, + "TreeItems": + [ + { + "Type": "Node", + "TextID": "TXT.DRAGDROP.TREE.NODE1", + "Children": + [ + { + "Type": "Item", + "TextID": "TXT.DRAGDROP.TREE.ITEM1", + "Icon": "fa-solid fa-file", + "ScreenID": "Screen1" + }, + { + "Type": "Item", + "TextID": "TXT.DRAGDROP.TREE.ITEM2", + "Icon": "fa-solid fa-file", + "ScreenID": "Screen1" + }, + { + "Type": "Item", + "TextID": "TXT.DRAGDROP.TREE.ITEM3", + "Icon": "fa-solid fa-file", + "ScreenID": "Screen1" + } + ] + }, + { + "Type": "Node", + "TextID": "TXT.DRAGDROP.TREE.NODE2", + "Children": + [ + { + "Type": "Item", + "TextID": "TXT.DRAGDROP.TREE.ITEM4", + "Icon": "fa-solid fa-file", + "ScreenID": "Screen1" + }, + { + "Type": "Item", + "TextID": "TXT.DRAGDROP.TREE.ITEM5", + "Icon": "fa-solid fa-file", + "ScreenID": "Screen1" + } + ] + } + ] + } + } + +} diff --git a/example/drag_drop/static/skeleton.json b/example/drag_drop/static/skeleton.json new file mode 100644 index 00000000..d4bad5b2 --- /dev/null +++ b/example/drag_drop/static/skeleton.json @@ -0,0 +1,50 @@ +{ + "Screen1": + [ + { + "TabContainer1": + { + "RefID": "Screen1" + } + }, + + { + "SourceListConnector": + { + "RefID": "TabContainer1", + "ElementID": "Tab1Content" + } + }, + + { + "SourceList": + { + "RefID": "SourceListConnector" + } + }, + + { + "DestinationList": + { + "RefID": "TabContainer1", + "ElementID": "Tab2Content" + } + }, + + { + "DestinationForm": + { + "RefID": "TabContainer1", + "ElementID": "Tab2Content" + } + }, + + { + "DragDropTree": + { + "RefID": "TabContainer1", + "ElementID": "Tab3Content" + } + } + ] +} diff --git a/python/Index.py b/python/Index.py index 9930fbda..1aaeb6ba 100755 --- a/python/Index.py +++ b/python/Index.py @@ -60,6 +60,7 @@ + diff --git a/www/static/globalstyles.css b/www/static/globalstyles.css index 1bd8aedf..1002ed98 100644 --- a/www/static/globalstyles.css +++ b/www/static/globalstyles.css @@ -133,3 +133,9 @@ a:active { width: 160px; float: right; } + +.sysDragDropOver { + outline: 2px dashed #2b5575; + background-color: rgba(43, 85, 117, 0.12); + cursor: copy; +} diff --git a/www/sysDragDropHandler.js b/www/sysDragDropHandler.js new file mode 100644 index 00000000..761a74ea --- /dev/null +++ b/www/sysDragDropHandler.js @@ -0,0 +1,66 @@ +//-------1---------2---------3---------4---------5---------6---------7--------// +//- Copyright WEB/codeX, clickIT 2011 - 2025 -// +//-------1---------2---------3---------4---------5---------6---------7--------// +//- -// +//-------1---------2---------3---------4---------5---------6---------7--------// +//- SYSTEM OBJECT "DragDropHandler" -// +//-------1---------2---------3---------4---------5---------6---------7--------// +//- -// +//- Central coordinator for HTML5 drag-and-drop operations. -// +//- Tracks the currently dragged object and its data payload so that -// +//- any registered drop-target can retrieve them on the "drop" event. -// +//- -// +//-------1---------2---------3---------4---------5---------6---------7--------// + + +//------------------------------------------------------------------------------ +//- CONSTRUCTOR "sysDragDropHandler" +//------------------------------------------------------------------------------ + +function sysDragDropHandler() +{ + this.DragSourceObject = null; //- Currently dragged source object reference + this.DragSourceData = null; //- Currently dragged data payload +} + + +//------------------------------------------------------------------------------ +//- METHOD "setDragSource" +//------------------------------------------------------------------------------ + +sysDragDropHandler.prototype.setDragSource = function(SourceObject, SourceData) +{ + this.DragSourceObject = SourceObject; + this.DragSourceData = SourceData; +} + + +//------------------------------------------------------------------------------ +//- METHOD "getDragData" +//------------------------------------------------------------------------------ + +sysDragDropHandler.prototype.getDragData = function() +{ + return this.DragSourceData; +} + + +//------------------------------------------------------------------------------ +//- METHOD "getDragSourceObject" +//------------------------------------------------------------------------------ + +sysDragDropHandler.prototype.getDragSourceObject = function() +{ + return this.DragSourceObject; +} + + +//------------------------------------------------------------------------------ +//- METHOD "clearDragSource" +//------------------------------------------------------------------------------ + +sysDragDropHandler.prototype.clearDragSource = function() +{ + this.DragSourceObject = null; + this.DragSourceData = null; +} diff --git a/www/sysInitOnLoad.js b/www/sysInitOnLoad.js index fd109aed..7636e04a 100644 --- a/www/sysInitOnLoad.js +++ b/www/sysInitOnLoad.js @@ -185,6 +185,13 @@ function InitOk(XHR) { sysFactory.Reactor = new sysReactor(); + //---------------------------------------------------------------------------- + //- Construct Global Drag and Drop Handler + //---------------------------------------------------------------------------- + + sysFactory.DragDropHandler = new sysDragDropHandler(); + + //---------------------------------------------------------------------------- //- Construct Global Async Notify Indicator //---------------------------------------------------------------------------- diff --git a/www/sysObjFormfieldList.js b/www/sysObjFormfieldList.js index e0ccc250..15ba4020 100644 --- a/www/sysObjFormfieldList.js +++ b/www/sysObjFormfieldList.js @@ -85,6 +85,23 @@ sysFormfieldList.prototype.init = function() EventListenerObj['Element'] = this.EventListenerRightClick.bind(this); this.EventListeners['ContextMenuOpen'] = EventListenerObj; + if (Attributes.DropTarget === true) { + var EventListenerObj = new Object(); + EventListenerObj['Type'] = 'dragover'; + EventListenerObj['Element'] = this.onDragOver.bind(this); + this.EventListeners['DragOver'] = EventListenerObj; + + var EventListenerObj = new Object(); + EventListenerObj['Type'] = 'dragleave'; + EventListenerObj['Element'] = this.onDragLeave.bind(this); + this.EventListeners['DragLeave'] = EventListenerObj; + + var EventListenerObj = new Object(); + EventListenerObj['Type'] = 'drop'; + EventListenerObj['Element'] = this.onDrop.bind(this); + this.EventListeners['Drop'] = EventListenerObj; + } + this.render(); } @@ -459,3 +476,43 @@ sysFormfieldList.prototype.initOnChangeItems = function() this.FormfieldItems[ItemKey].processOnChangeItem(); } } + + +//------------------------------------------------------------------------------ +//- METHOD "onDragOver" +//------------------------------------------------------------------------------ + +sysFormfieldList.prototype.onDragOver = function(Event) +{ + Event.preventDefault(); + this.addDOMElementStyle('sysDragDropOver'); +} + + +//------------------------------------------------------------------------------ +//- METHOD "onDragLeave" +//------------------------------------------------------------------------------ + +sysFormfieldList.prototype.onDragLeave = function(Event) +{ + const Element = this.getElement(); + if (Element !== null && !Element.contains(Event.relatedTarget)) { + this.removeDOMElementStyle('sysDragDropOver'); + } +} + + +//------------------------------------------------------------------------------ +//- METHOD "onDrop" +//------------------------------------------------------------------------------ + +sysFormfieldList.prototype.onDrop = function(Event) +{ + Event.preventDefault(); + this.removeDOMElementStyle('sysDragDropOver'); + const DragData = sysFactory.DragDropHandler.getDragData(); + if (DragData !== null) { + this.setData(DragData); + } + sysFactory.DragDropHandler.clearDragSource(); +} diff --git a/www/sysObjList.js b/www/sysObjList.js index cb88357e..2e76ec5e 100644 --- a/www/sysObjList.js +++ b/www/sysObjList.js @@ -59,6 +59,15 @@ sysListRow.prototype.init = function() EventListenerObj['Type'] = 'mousedown'; EventListenerObj['Element'] = this.EventListenerSelect.bind(this); this.EventListeners['RowSelect'] = EventListenerObj; + + if (this.ParentObject.JSONConfig.Attributes.DragSource === true) { + this.DOMAttributes = { 'draggable': 'true' }; + + var EventListenerObj = new Object(); + EventListenerObj['Type'] = 'dragstart'; + EventListenerObj['Element'] = this.onDragStart.bind(this); + this.EventListeners['DragStart'] = EventListenerObj; + } } @@ -269,6 +278,17 @@ sysListRow.prototype.updateIndex = function(UpdateIndex) } +//------------------------------------------------------------------------------ +//- METHOD "onDragStart" +//------------------------------------------------------------------------------ + +sysListRow.prototype.onDragStart = function(Event) +{ + sysFactory.DragDropHandler.setDragSource(this, this.RowData); + Event.dataTransfer.effectAllowed = 'copy'; +} + + //------------------------------------------------------------------------------ //- CONSTRUCTOR "sysList" //------------------------------------------------------------------------------ @@ -296,6 +316,8 @@ function sysList() this.ChildObjects = new Array(); //- Child Objects + this.EventListeners = new Object(); //- Event Listeners + this.PaginationObject = new sysPagination(this); //- Pagination Processing this.RowsSelectable = true; //- Multi Row Selection Default @@ -381,6 +403,23 @@ sysList.prototype.init = function() this.DOMStyle = Attributes.Style; + if (Attributes.DropTarget === true) { + var EventListenerObj = new Object(); + EventListenerObj['Type'] = 'dragover'; + EventListenerObj['Element'] = this.onDragOver.bind(this); + this.EventListeners['DragOver'] = EventListenerObj; + + var EventListenerObj = new Object(); + EventListenerObj['Type'] = 'dragleave'; + EventListenerObj['Element'] = this.onDragLeave.bind(this); + this.EventListeners['DragLeave'] = EventListenerObj; + + var EventListenerObj = new Object(); + EventListenerObj['Type'] = 'drop'; + EventListenerObj['Element'] = this.onDrop.bind(this); + this.EventListeners['Drop'] = EventListenerObj; + } + this.renderPage(); } @@ -710,3 +749,48 @@ sysList.prototype.appendData = function(DataObj) this.addRow(AppendRowObj, this.RowItems.length+1); this.renderPage(); } + + +//------------------------------------------------------------------------------ +//- METHOD "onDragOver" +//------------------------------------------------------------------------------ + +sysList.prototype.onDragOver = function(Event) +{ + Event.preventDefault(); + this.addDOMElementStyle('sysDragDropOver'); +} + + +//------------------------------------------------------------------------------ +//- METHOD "onDragLeave" +//------------------------------------------------------------------------------ + +sysList.prototype.onDragLeave = function(Event) +{ + const Element = this.getElement(); + if (Element !== null && !Element.contains(Event.relatedTarget)) { + this.removeDOMElementStyle('sysDragDropOver'); + } +} + + +//------------------------------------------------------------------------------ +//- METHOD "onDrop" +//------------------------------------------------------------------------------ + +sysList.prototype.onDrop = function(Event) +{ + Event.preventDefault(); + this.removeDOMElementStyle('sysDragDropOver'); + const DragSourceObj = sysFactory.DragDropHandler.getDragSourceObject(); + if (DragSourceObj !== null && DragSourceObj !== undefined) { + if (DragSourceObj.ParentObject !== this) { + const DragData = sysFactory.DragDropHandler.getDragData(); + if (DragData !== null) { + this.appendData(DragData); + } + } + } + sysFactory.DragDropHandler.clearDragSource(); +} diff --git a/www/sysObjTreeSimple.js b/www/sysObjTreeSimple.js index 67494c7c..5d53ea44 100644 --- a/www/sysObjTreeSimple.js +++ b/www/sysObjTreeSimple.js @@ -40,7 +40,7 @@ sysObjTreeSimple.prototype.init = function() for (const RootItem of Attributes.TreeItems) { console.debug('TreeSimple RootItem:%o', RootItem); - let NodeItem = new sysObjTreeSimpleNode(this.IndentLevel); + let NodeItem = new sysObjTreeSimpleNode(this, this.IndentLevel); NodeItem.JSONConfig = { "Attributes": { @@ -78,7 +78,7 @@ sysObjTreeSimple.prototype.addTreeItems = function(RootObj, NodeItem, ChildItems } if (ChildItem.Type == 'Node') { - TreeItem = new sysObjTreeSimpleNode(this.IndentLevel); + TreeItem = new sysObjTreeSimpleNode(RootObj, this.IndentLevel); } TreeItem.JSONConfig = { @@ -103,11 +103,12 @@ sysObjTreeSimple.prototype.addTreeItems = function(RootObj, NodeItem, ChildItems //- CONSTRUCTOR "sysObjTreeSimpleNode" //------------------------------------------------------------------------------ -function sysObjTreeSimpleNode(IndentLevel) +function sysObjTreeSimpleNode(TreeRootObj, IndentLevel) { this.ChildObjects = new Array(); //- Child Objects this.DOMStyle = 'sysTreeNodeRoot'; //- CSS Style this.IndentLevel = IndentLevel; //- Tree Indent Level + this.TreeRootObj = TreeRootObj; //- Tree Root Object Reference } //- inherit sysBaseObject @@ -146,6 +147,26 @@ sysObjTreeSimpleNode.prototype.init = function() EventListenerObj['Element'] = this.toggleVisibleState.bind(this.OpenCloseIcon); this.OpenCloseIcon.EventListeners["OpenClose"] = EventListenerObj; + //- define node header div (visible area carrying drop-target listeners) + this.NodeHeaderDiv = new sysObjDiv(); + + if (this.TreeRootObj !== undefined && this.TreeRootObj.JSONConfig.Attributes.DropTarget === true) { + var DragOverEvent = new Object(); + DragOverEvent['Type'] = 'dragover'; + DragOverEvent['Element'] = this.onDragOver.bind(this); + this.NodeHeaderDiv.EventListeners['DragOver'] = DragOverEvent; + + var DragLeaveEvent = new Object(); + DragLeaveEvent['Type'] = 'dragleave'; + DragLeaveEvent['Element'] = this.onDragLeave.bind(this); + this.NodeHeaderDiv.EventListeners['DragLeave'] = DragLeaveEvent; + + var DropEvent = new Object(); + DropEvent['Type'] = 'drop'; + DropEvent['Element'] = this.onDrop.bind(this); + this.NodeHeaderDiv.EventListeners['Drop'] = DropEvent; + } + //- setup recursive object structure const ObjDefs = [ { @@ -157,7 +178,7 @@ sysObjTreeSimpleNode.prototype.init = function() }, { "id": "item-container", - "SysObject": new sysObjDiv(), + "SysObject": this.NodeHeaderDiv, "JSONAttributes": { "Style": "sysTreeItemContainer sysTreeItemIndent" + this.IndentLevel }, @@ -216,6 +237,58 @@ sysObjTreeSimpleNode.prototype.toggleVisibleState = function() } +//------------------------------------------------------------------------------ +//- METHOD "onDragOver" +//------------------------------------------------------------------------------ + +sysObjTreeSimpleNode.prototype.onDragOver = function(Event) +{ + Event.preventDefault(); + Event.stopPropagation(); + this.NodeHeaderDiv.addDOMElementStyle('sysDragDropOver'); +} + + +//------------------------------------------------------------------------------ +//- METHOD "onDragLeave" +//------------------------------------------------------------------------------ + +sysObjTreeSimpleNode.prototype.onDragLeave = function(Event) +{ + const Element = this.NodeHeaderDiv.getElement(); + if (Element !== null && !Element.contains(Event.relatedTarget)) { + this.NodeHeaderDiv.removeDOMElementStyle('sysDragDropOver'); + } +} + + +//------------------------------------------------------------------------------ +//- METHOD "onDrop" +//------------------------------------------------------------------------------ + +sysObjTreeSimpleNode.prototype.onDrop = function(Event) +{ + Event.preventDefault(); + Event.stopPropagation(); + this.NodeHeaderDiv.removeDOMElementStyle('sysDragDropOver'); + + const DragSourceObj = sysFactory.DragDropHandler.getDragSourceObject(); + + if (DragSourceObj !== null && DragSourceObj !== undefined) { + if (DragSourceObj instanceof sysObjTreeSimpleItem) { + //- prevent dropping onto the same parent node + if (DragSourceObj.ParentObject !== this.ItemContainerObj) { + DragSourceObj.remove(); + this.ItemContainerObj.addObject(DragSourceObj); + DragSourceObj.renderObject(this.ItemContainerObj.DOMObjectID); + } + } + } + + sysFactory.DragDropHandler.clearDragSource(); +} + + //------------------------------------------------------------------------------ //- CONSTRUCTOR "sysObjTreeSimpleItem" //------------------------------------------------------------------------------ @@ -269,6 +342,15 @@ sysObjTreeSimpleItem.prototype.init = function() EventLinkClick['Element'] = this.activateSelected.bind(this.SelectedIndicator); this.ItemContainerObj.EventListeners["LinkClick"] = EventLinkClick; + if (this.TreeRootObj !== undefined && this.TreeRootObj.JSONConfig.Attributes.DragSource === true) { + this.ItemContainerObj.DOMAttributes = { 'draggable': 'true' }; + + var DragStartEvent = new Object(); + DragStartEvent['Type'] = 'dragstart'; + DragStartEvent['Element'] = this.onDragStart.bind(this); + this.ItemContainerObj.EventListeners['DragStart'] = DragStartEvent; + } + //- setup recursive object structure const ObjDefs = [ { @@ -348,3 +430,14 @@ sysObjTreeSimpleItem.prototype.activateSelected = function() this.ParentObject.TreeRootObj.LastSelectedItem = this; } + + +//------------------------------------------------------------------------------ +//- METHOD "onDragStart" +//------------------------------------------------------------------------------ + +sysObjTreeSimpleItem.prototype.onDragStart = function(Event) +{ + sysFactory.DragDropHandler.setDragSource(this, this.JSONConfig.Attributes); + Event.dataTransfer.effectAllowed = 'move'; +} From 639ba4cefc9144a0708cb574940f23f17d24d1e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:10:31 +0000 Subject: [PATCH 2/2] Use RuntimeSetDataFunc/RuntimeAppendDataFunc in drag-and-drop drop handlers --- www/sysObjFormfieldList.js | 2 +- www/sysObjList.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/www/sysObjFormfieldList.js b/www/sysObjFormfieldList.js index 15ba4020..67bc6c44 100644 --- a/www/sysObjFormfieldList.js +++ b/www/sysObjFormfieldList.js @@ -512,7 +512,7 @@ sysFormfieldList.prototype.onDrop = function(Event) this.removeDOMElementStyle('sysDragDropOver'); const DragData = sysFactory.DragDropHandler.getDragData(); if (DragData !== null) { - this.setData(DragData); + this.RuntimeSetDataFunc(DragData); } sysFactory.DragDropHandler.clearDragSource(); } diff --git a/www/sysObjList.js b/www/sysObjList.js index 2e76ec5e..2c1b1a88 100644 --- a/www/sysObjList.js +++ b/www/sysObjList.js @@ -788,7 +788,7 @@ sysList.prototype.onDrop = function(Event) if (DragSourceObj.ParentObject !== this) { const DragData = sysFactory.DragDropHandler.getDragData(); if (DragData !== null) { - this.appendData(DragData); + this.RuntimeAppendDataFunc(DragData); } } }