Skip to Main Content

Drag & Drop Interactive Grid rows

Here's an example that lets you drag and drop the rows of an interactive grid to arrange the records the way you want. A table used in an interactive grid must have a column that stores the sort order. If your table doesn't already have a column that can be used for this, edit the table and add a new column.

alter table <your table name> add (display_seq number);
update <your table name> set display_seq = rownum;
alter table <your table name> modify display_seq not null;

First, create an editable interactive grid and run the page. Sort the interactive grid in ascending order by display sequence column.

Then hide the display sequence column from the report.

Save the report.

Create new column to the interactive grid .

Enter a column name. Change the type to “HTML expression”. Add to the HTML Expression:

<span aria-hidden="true" class="fa fa-sort z-sortable--handle"></span>

Change the Source Type to None. Add a Column Initialization JavaScript Function:

sortableIG.initHandleColumn

Edit the other interactive grid columns and disable Sorting for all columns except the display sequence column.

Edit the display sequence column and add a Column Initialization JavaScript Function:

sortableIG.initSequenceColumn

Add to the interactive grid region Initialization JavaScript Function:

sortableIG.initRegion

 

Add to the page JavaScript Function and Global Variable Declaration:

// create a namespace for sortable IG
var sortableIG = {};

( function( $, region, sortableIG ) {

  // Set defaults
  sortableIG.options = {
    region: {
      defaultModelOptions: {
        sequenceStep: 10
      }
      ,defaultGridViewOptions: {
        reorderColumns: false
      }
    }
    ,handleColumn: {
      defaultGridColumnOptions: {
        noHeaderActivate: true
      }
      ,layout: {
        columnAlignment: "center"
        ,noStretch: true
      }
    }
    ,sequenceColumn: {
      features: {
      	sort: true
        ,canHide: false
        ,aggregate: false
        ,compute: false
        ,controlBreak: false
        ,groupBy: false
        ,highlight: false
        ,pivot: false
      }
    }
  }

  // sortable IG handle column Initialization code
  // call function in IG column Advanced: Column JavaScript Initialization Code
  sortableIG.initHandleColumn = function( options ) {

    return $.extend( true, options, sortableIG.options.handleColumn );

  }

  // sortable IG sequence column Initialization code
  // call function in IG column Advanced: Column JavaScript Initialization Code
  sortableIG.initSequenceColumn = function( options ) {

		// get sequence column name and store it for region Initialization JavaScript Function
    sortableIG.options.region.defaultModelOptions.sequenceField = options.name;

    return $.extend( true, options, sortableIG.options.sequenceColumn );

  }

  // sortable IG initialization code
  // call function in IG region Advanced: JavaScript Initialization Code
  sortableIG.initRegion = function( options ) {

    $( $x( options.regionStaticId ) ).on( "interactivegridviewchange interactivegridreportsettingschange", function( _event, ui ) {

      var view          = region( this.id ).call( "getViews", "grid" )
        , sortableArea$ = view.view$.find( ".a-GV-w-scroll tbody" )
        , helperBgClass = "u-color-29-bg"
      ;

      // https://api.jqueryui.com/sortable/
      sortableArea$.sortable({
        handle:       ".z-sortable--handle"
        ,items:       "tr:has(.z-sortable--handle)"
        ,axis:        "y"
        ,tolerance:   "pointer"
        ,scrollSpeed: 8
        ,placeholder: "a-GV-cell u-color-1-bg"
        
        ,helper: function( _event, ui ){
          ui.children().each( function(){
            $( this ).width( $( this ).width() ).addClass( helperBgClass )
          })
          return ui;
        }
        
        ,update: function( _event, ui ){
          
          var model = view.model
            , item$ = $( ui.item )
            // get moved record
            , item = ( item$.length ) ? model.getRecord( item$.data( "id" ) ) : null
            // get next or previous row from visible table depending is record moved up or down
            , after$ = ( ui.originalPosition.top > ui.position.top ) ? item$.next() : item$.prev()
            , after = ( after$.length ) ? model.getRecord( after$.data( "id" ) ) : null
          ;
          
          // remove helper background class, if row isn't actually moved
          item$.children().removeClass( helperBgClass );
          // check if we have found record and place where record is moved
          if( item && after ){
            // if record is moved upwards, we need get previous record from model
            after = ( ui.originalPosition.top > ui.position.top ) ? model.recordAt( model.indexOf( after ) - 1 ) : after;
            // move record
             if( item !== after ){
              model.moveRecords( [item], null, after );
            }
          }
          
        }
      
      });

    });

    return $.extend( true, options, sortableIG.options.region );

  }

})( apex.jQuery, apex.region, sortableIG );

Add to the Page CSS Inline:

.z-sortable--handle{
  cursor: move;
}
.ui-sortable-helper{
  display: inline-flex;
}

Now when you run a page, you can drag and drop to arrange the rows of the interactive grid from the handle column.

See working example in apex.oracle.com.

If you need support for touch events, you can try jQuery UI Touch Punch. Download jquery.ui.touch-punch.js and upload the file to Static Application Files. Reference the file e.g. on page JavaScript File URLs:

#APP_FILES#jquery.ui.touch-punch.js

Comments

  • Jari Laine 8 Apr 2024

    Hi Nilton,

    I might have find solution.

    Make sure that display sequence column data type is NUMBER. Set column Format Mask to 999999999999990.99999999999999. This seems to work with APEX 23.2.4.

    If that don't work make huge caps to sequence column values to prevent decimals. Update statement could look like

    merge into <your table> t1
    using (
      select <primary key column>
        ,row_number() over( order by <display seq column> ) * 10000 as new_display_seq
      from <your table>
      where 1 = 1
    ) v1
    on ( t1.<primary key column> = v1.<primary key column> )
    when matched then
      update set t1.<display seq column> = v1.new_display_seq
        where t1.<display seq column> != v1.new_display_seq
    ;

    Regards, Jari

  • Jari Laine 6 Apr 2024

    Hi Nilton,

    Unfortunately I have not able resolve this issue :(. It's something that IG does internally.

    Regards,
    Jari

  • Nilton 4 Apr 2024

    Hi Jari, many thanks for the code explaining how to drag and drop in IG.
    I live in Brazil, where '.' is the thousand separator, and the ',' is the decimal point, exactly as Kai mentioned in 04/01/2024 to Germany.
    Do we have a solution to this case? Sorry!! I didn't find your answer to kai, so I'm questioning once again, because it is exactly my case.

  • Jari Laine 6 Jan 2024

    Hi Kai,

    Thanks for informing this.

    I did try check, but quickly didn't find solution. IG handles internally calculating that sequence field and there isn't anything in custom code that sets that sequence number.

    I still keep checking and let you know if I find solution.

    Regards,
    Jari

  • Kai 4 Jan 2024

    Hi Jari,
    nice pice of code. But it dosn't work, if the application primary language ist set to german. In Germany the '.' is the thousand separator, and the ',' is the decimal point. So the calculation for the sort column leads to totally other results. you can see it, if you set sequnce column to visible.
    Is there a way to use the apex.local in the javascriopt as described in <a href="https://blog.cloudnueva.com/easy-number-handling-in-javascript-with-apexlocale" target ="_blank">https://blog.cloudnueva.com/easy-number-handling-in-javascript-with-apexlocale</a>?
    Greetings from germany
    Kai

  • Mario 14 Nov 2023

    Thank Jari, I took a look at the IG Cookbook and found the use of the >defaultModelOptions.sequenceField option but it seems that it's not documented in the Apex API document though. Thanks.

  • Jari Laine 12 Nov 2023

    Hi Mario,

    Good point.

    Newer checked is it documented. I did take it from John Snyders IG Cookbook sample application page 11.

    Regards,
    Jari

  • Mario 10 Nov 2023

    Hello,
    This is a great implementation. The only part that I don't get is the sequenceField in defaultModelOptions.sequenceField. I can not find this option in the Apex JS API. Can you explain it more in detail please ?

  • Chris 21 Aug 2023

    Thank you Jari. Works like a charm.