Dragging rows in Perspective Table

Has anyone figured out how to drag rows around in a perspective table? Need user to be able to change the sort order of rows that will then be saved in the database.

I don't think this is possible, but I'd love to know. I have a perfect application for it

1 Like

Have once applied this functionality by adding a view to each row of the table that included and icon of an up and down arrow. Clicking an arrow would send a perspective message to the table that would change the order of its data by indexing the row up or down 1.

I did the same, but implemented with a flex repeater. I really would like a draggable list, just like a playlist on modern music services

1 Like

Maybe @victordcq knows if possible with magic hacks?

heh would require some tinkering, but ive done a drag and drop before
You could make a view for the first column but isntead with the arrow use something like this...

i can play around with this a bit more tmw :wink: Might be possible to not require a view/column and just make the whole row draggable but ill have to take a deeper look

2 Likes

Seems to me like it would be easier with a flex repeater...

ah it was easy, ignition puts its row id in a data atrribute so it was easy to read.

@jasoncoope
dragtable.zip (27.7 KB)

copy the markdown
and add in a custom property on the view
you probably will have to adapt the onchange script in the view.custom element to your needs.

sometimes the columns seem to move around too when you drag a row... not sure why or how to prevent that... probably clashing with the other drag events on the table xd
edit :
ahah, turn this off too, well unless you dont mind
image

update: a fix for unvirtualised tables and tables with multple pages... using the js mutation observer:

	#make the propName the key to write too in the view.custom 
	propName = "rowDroppedData"
	code =  """<img style='display:none' src='/favicon.ico' onload=\"
		const view = [...window.__client.page.views._data.values()].find(view => view.value.mountPath == this.parentNode.parentNode.parentNode.getAttributeNode('data-component-path').value.split('.')[0]).value; 
		function ondrop(ev) {
			ev.preventDefault(); 
			const draggedId = ev.dataTransfer.getData('text');
			const droppedId = this.getAttribute('data-row-id'); 
			view.custom.write('"""+propName+"""',{'draggedId':draggedId,'droppedId':droppedId});
		};
		function ondragstart(ev){
			
			ev.dataTransfer.setData('text', ev.target.getAttribute('data-row-id'));
		};
		function ondragover(ev){
			ev.preventDefault();
		};
		
		const callbackTable = (mutationList, observer) => {
		document.querySelectorAll('.tr-group.ia_table__rowGroup').forEach(e => {
					e.setAttribute('draggable',true);
					e.ondragstart = ondragstart;
					e.ondragover = ondragover;
					e.ondrop = ondrop;
				});	
		};
				
			
									
		const observerTable = new MutationObserver(callbackTable); 
		const tables = document.querySelectorAll('.ia_tableComponent .t.ia_table > .tb');
		tables.forEach(table => observerTable.observe(table, { attributes: false, childList: true, subtree: true }));	
		document.querySelectorAll('.tr-group.ia_table__rowGroup').forEach(e => {
						e.setAttribute('draggable',true);
						e.ondragstart = ondragstart;
						e.ondragover = ondragover;
						e.ondrop = ondrop;
					});		
				
	\"></img>""".replace("\n", "").replace("\t", "")
	return code
	

(also just gona link this to my other jsinjects post

4 Likes

Outstanding! Just need to figure out how this works. Thanks for your help here!

1 Like

I love this! This was super easy to implement and very straight forward. I did notice that if you're populating your table data from a named query you have to select to bring it in as a json and not a dataset to get this to work.

Another thing, do you think you could set this up to be able to drag a row from one table to another if they have the same column structure? Like say if I had two tables side by side and they both had "Name" and "Address" as the columns? @victordcq

ah yes, its a bit anonying to change positions in js for a dataset

yes that would be possible, but i dont really have time to set this up any time soon. Maybe i got time somewhere next month?
you'll have to combine this with the second script i put here

things to change in the second script are the query selectors for the scheduler to the one of the table you want to drop in (you should add in a domId to make this easier)

and ofc the ondrop event to instead change its data (or send a message or something)

Thanks for the info! I'll see if I can get this working. If I do I'll let you know.

1 Like

feel free to show you trials too, the code in the markdown that is.
i should have some time along the week to review that, just not to test it all myself

1 Like

mh it shouldnt be that difficult tbh,

just add a domId to both tables in perspective.

in the ondragstart, change the setData to include the domId of the origin in there (you'll need to use a strinigfied json as it only can transfer text. JSON.stringify(myObj)
in the onDrop, extract the domId and draggedId from the stringified json ev.dataTransfer.getData('text')
also find the domId of the target here JSON.parse(text)
and add both the origin and target domId to the view.custom.write.

from there its adjusting the python script, shouldnt be to hard if you match the domIds with the component names

I was able to get it to work. Had to do some extra work to allow it to drop into an empty table. I still need to figure out how to drop on to a table in the white space where there isn't any rows but wanted to update you on my progress from today.

propName = "rowDroppedData"
code =  """<img style='display:none' src='/favicon.ico' onload=\"
	const view = [...window.__client.page.views._data.values()].find(view => view.value.mountPath == this.parentNode.parentNode.parentNode.getAttributeNode('data-component-path').value.split('.')[0]).value; 
	async function ondrop(ev) {
		ev.preventDefault(); 
		const draggedData = ev.dataTransfer.getData('text');
      	const obj = JSON.parse(draggedData);
	  	const draggedId = obj.draggedId;
      	const draggedTable = obj.domID;	
  	    const droppedId = this.getAttribute('data-row-id');
  	    const droppedTable = this.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.getAttribute('id'); 
  	    const droppedTableEmpty = this.classList.contains('empty-data-source');
  	    const droppedTableEmptyID = this.parentNode.parentNode.parentNode.parentNode.getAttribute('id');
  	    view.custom.write('"""+propName+"""',{'draggedId':draggedId,'droppedId':droppedId,'draggedTable':draggedTable,'droppedTable':droppedTable,'droppedTableEmptyID':droppedTableEmptyID,'droppedTableEmpty':droppedTableEmpty});
		await sleep(500);
		resetTables();
	};
	function ondragstart(ev){
		resetTables();
		ev.dataTransfer.setData('text', JSON.stringify({draggedId:ev.target.getAttribute('data-row-id'), domID:ev.target.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.id}));
	};
	function ondragover(ev){
		ev.preventDefault();
	};
	
    function resetTables(){
        document.querySelectorAll('.tr-group.ia_table__rowGroup').forEach(e => {
            e.setAttribute('draggable',true);
            e.ondragstart = ondragstart;
            e.ondragover = ondragover;
            e.ondrop = ondrop;
        });	
        
        document.querySelectorAll('.empty-data-source').forEach(e => {
                e.setAttribute('draggable',true);
                e.ondragstart = ondragstart;
                e.ondragover = ondragover;
                e.ondrop = ondrop;
            });	
    };
    
        function sleep(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        }
    
	document.querySelectorAll('.tr-group.ia_table__rowGroup').forEach(e => {
		e.setAttribute('draggable',true);
		e.ondragstart = ondragstart;
		e.ondragover = ondragover;
		e.ondrop = ondrop;
	});	
\"></img>""".replace("\n", "").replace("\t", "")
if currentValue.value["droppedTableEmpty"].value:
	if int(currentValue.value["draggedId"].value) > -1:
		DraggedTable=self.getChild("rowDroppedData").getChild(currentValue.value["draggedTable"].value).props.data
		DroppedTable=self.getChild("rowDroppedData").getChild(currentValue.value["droppedTableEmptyID"].value).props.data
		DroppedTable.append(DraggedTable.pop(int(currentValue.value["draggedId"].value)))
else:
	if int(currentValue.value["droppedId"].value) > -1 and int(currentValue.value["draggedId"].value) > -1:
		DraggedTable=self.getChild("rowDroppedData").getChild(currentValue.value["draggedTable"].value).props.data
		DroppedTable=self.getChild("rowDroppedData").getChild(currentValue.value["droppedTable"].value).props.data
		if currentValue.value["draggedTable"].value == currentValue.value["droppedTable"].value:
			DroppedTable.insert(int(currentValue.value["droppedId"].value), DroppedTable.pop(int(currentValue.value["draggedId"].value)))
		else:
			DroppedTable.insert(int(currentValue.value["droppedId"].value), DraggedTable.pop(int(currentValue.value["draggedId"].value)))
self.custom.rowDroppedData.draggedId = -1
self.custom.rowDroppedData.droppedId = -1
self.custom.rowDroppedData.droppedTable = -1
self.custom.rowDroppedData.draggedTable = -1
self.custom.rowDroppedData.droppedTableEmpty = False
self.custom.rowDroppedData.droppedTableEmptyID = -1
1 Like

do not use sleep, use the mutation observer, i had updated the post a few days ago with the mutation observer in it, it will trigger the callback funciton anytime the content inside it changes, you might need to use a higher level selector though for the const tables because of the empty rows, i didnt test that (so try without the > tb)

you can use the queryselector
.ia_tableComponent .t.ia_table > .tb for the empty table and the empty space below the rows.
it might double trigger an ondrop event though
in which case you will need to add this to the ondrop function of the rows

ev.stopPropagation();

you should use a seperate ondrop function for the empty ones, as the id will not be found the same and there will be no data-row-id

and you dont need this in the empty row ones, so they cant accidently drag the whole table

e.setAttribute('draggable',true);
                e.ondragstart = ondragstart;

Okay, I think I have it all working now.

propName = "rowDroppedData"
code =  """<img style='display:none' src='/favicon.ico' onload=\"
	const view = [...window.__client.page.views._data.values()].find(view => view.value.mountPath == this.parentNode.parentNode.parentNode.getAttributeNode('data-component-path').value.split('.')[0]).value; 
	function ondrop(ev) {
		ev.preventDefault(); 
        ev.stopPropagation();
		const draggedData = ev.dataTransfer.getData('text');
      	const obj = JSON.parse(draggedData);
	  	const draggedId = obj.draggedId;
      	const draggedTable = obj.domID;	
  	    const droppedId = this.getAttribute('data-row-id');
	    const droppedTable = this.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.getAttribute('id');
        const droppedEmpty = this.classList.contains('tb');
        const droppedEmptyTable = this.parentNode.parentNode.parentNode.getAttribute('id');
        view.custom.write('"""+propName+"""',{'draggedId':draggedId,'droppedId':droppedId,'draggedTable':draggedTable,'droppedTable':droppedTable,'droppedEmptyTable':droppedEmptyTable,'droppedEmpty':droppedEmpty});
	};
	function ondragstart(ev){
        refreshEmptyTables();
		ev.dataTransfer.setData('text', JSON.stringify({draggedId:ev.target.getAttribute('data-row-id'), domID:ev.target.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.id}));
	};
	function ondragover(ev){
		ev.preventDefault();
	};
	
    const callbackTable = (mutationList, observer) => {
    		document.querySelectorAll('.tr-group.ia_table__rowGroup').forEach(e => {
			e.setAttribute('draggable',true);
			e.ondragstart = ondragstart;
			e.ondragover = ondragover;
			e.ondrop = ondrop;
		});	
	};
    									
        const observerTable = new MutationObserver(callbackTable); 
        const tables = document.querySelectorAll('.ia_tableComponent .t.ia_table > .tb');
        tables.forEach(table => observerTable.observe(table, { attributes: false, childList: true, subtree: true }));	
        document.querySelectorAll('.tr-group.ia_table__rowGroup').forEach(e => {
            e.setAttribute('draggable',true);
            e.ondragstart = ondragstart;
            e.ondragover = ondragover;
            e.ondrop = ondrop;
        });
        
        function refreshEmptyTables(){
            document.querySelectorAll('.ia_tableComponent .t.ia_table > .tb').forEach(e => {
                e.ondragover = ondragover;
                e.ondrop = ondrop;
            });
        }
    
\"></img>""".replace("\n", "").replace("\t", "")
return code

I had to add the refreshEmptyTables function because it seems that when the page first loads all the datasets are empty until they load in which was causing none of the rows to be draggable if I ran that at page load.

if currentValue.value["droppedEmpty"].value:
	if int(currentValue.value["draggedId"].value) > -1:
		if currentValue.value["droppedEmptyTable"].value == currentValue.value["draggedTable"].value:
			DraggedTable=self.getChild("rowDroppedData").getChild(currentValue.value["draggedTable"].value).props.data
			DraggedTable.append(DraggedTable.pop(int(currentValue.value["draggedId"].value)))
		else:
			DraggedTable=self.getChild("rowDroppedData").getChild(currentValue.value["draggedTable"].value).props.data
			DroppedTable=self.getChild("rowDroppedData").getChild(currentValue.value["droppedEmptyTable"].value).props.data
			DroppedTable.append(DraggedTable.pop(int(currentValue.value["draggedId"].value)))
else:
	if int(currentValue.value["droppedId"].value) > -1 and int(currentValue.value["draggedId"].value) > -1:
		DraggedTable=self.getChild("rowDroppedData").getChild(currentValue.value["draggedTable"].value).props.data
		DroppedTable=self.getChild("rowDroppedData").getChild(currentValue.value["droppedTable"].value).props.data
		if currentValue.value["draggedTable"].value == currentValue.value["droppedTable"].value:
			DroppedTable.insert(int(currentValue.value["droppedId"].value), DroppedTable.pop(int(currentValue.value["draggedId"].value)))
		else:
			DroppedTable.insert(int(currentValue.value["droppedId"].value), DraggedTable.pop(int(currentValue.value["draggedId"].value)))
self.custom.rowDroppedData.draggedId = -1
self.custom.rowDroppedData.droppedId = -1
self.custom.rowDroppedData.droppedTable = -1
self.custom.rowDroppedData.draggedTable = -1
self.custom.rowDroppedData.droppedEmpty = False
self.custom.rowDroppedData.droppedEmptyTable = -1
1 Like

I wish I understood even just a few lines of this code :frowning: I'll get time one of these days to look at javascript. But this sounds cool!

I need to allow operators to select multiple table rows with only a mouse, no keyboard. Reckon I can do that with js instead of hacking a checkbox into a new column?
I don't really know the mechanics I would use. Maybe click to select/deselect, then a button to unselect all?

like a button that mimicks you holding ctlr?

That could be one way, toggle it on off. Is that doable?