javascriptjqueryrshinyjqui

Resizing column widths in R-Shiny using jQuery


I'd like to use the resizable function in jQuery to resize column widths in a shiny application. Unfortunately, I'm not sure how to pass along options to the jqui_resizable function, so that I can pass along the also_resize function, or to set maxHeight/minHeight.

Ideally, I'd like for the columns to have a set height (i.e. full-screen), but synchronous re-sizable width's (which also take up the entire width of the screen).

Below is my very simple attempt at demonstrating what I'd like. Unfortunately, it doesn't work as, given the set column widths, resizing one column to make it bigger moves the other column down a row.

Any ideas on how to do this would be greatly appreciated! Thanks!

library(shiny)
library(shinyjqui)

ui <- fluidPage(
  fluidRow(

    jqui_resizable(column(6,"text")),
    jqui_resizable(column(6,"more text"))



  )
)

server <- function(input, output, session) {

}

shinyApp(ui, server)


Solution

  • Here is a solution based on this codepen.

    File resizableColumns.js:

    $(document).ready(function() {
      (function($, window, document, undefined) {
    
        $.widget('ce.resizableGrid', {
    
          _create: function() {
            this.resizing = false;
            this._on({
              'mousedown .resizable-column-handle': '_resizeStartHandler',
              'mousemove': '_resizeHandler',
              'mouseup': '_resizeStopHandler',
              'mouseleave': '_resizeStopHandler'
            });
          },
    
          _init: function() {
            this._createHelpers();
          },
    
          _createHelpers: function() {
            this.element.addClass('resizable-grid');
            this.element.find('> .row:not(.resizable-row)').each(function(rowIndex, rowElement) {
              var row = $(rowElement);
              row.addClass('resizable-row');
              row.find('> [class^="col-"]:not(.resizable-column)').each(function(columnIndex, columnElement) {
                var column = $(columnElement);
                column.addClass('resizable-column');
                column.append(
                  $('<div>', {
                    class: 'resizable-column-handle resizable-column-handle-w',
                    'data-is-west': 'true'
                  }),
                  $('<div>', {
                    class: 'resizable-column-handle resizable-column-handle-e',
                    'data-is-west': 'false'
                  })
                );
              });
            });
          },
    
          _resizeStartHandler: function(event) {
            this.resizing = {};
            this.resizing.handle = $(event.currentTarget).addClass('resizable-column-handle-resizing');
            this.resizing.column = this.resizing.handle.closest('.resizable-column').addClass('resizable-column-resizing');
            this.resizing.row = this.resizing.column.closest('.resizable-row').addClass('resizable-row-resizing');
            this.resizing.handleIsWest = this.resizing.handle.data('isWest');
            this.resizing.directionIsWest = this._getResizingDirectionIsWest(event.pageX);
            this.resizing.columnSize = this._getColumnSize(this.resizing.column);
            this.resizing.siblings = this._getResizingSiblings(this.resizing.column);
            this.resizing.offsets = this._getResizingOffsets();
            this.element.addClass('resizable-grid-resizing');
          },
    
          _resizeHandler: function(event) {
            if(!this.resizing) {
              return;
            }
            this.resizing.directionIsWest = this._getResizingDirectionIsWest(event.pageX);
            var resizingOffsetSize = this._getResizingOffsetSize(event.pageX);
            if(resizingOffsetSize && (this.resizing.columnSize !== resizingOffsetSize)) {
              if(resizingOffsetSize > this.resizing.columnSize) {
                var widestColumn = this._getWidestColumn(this.resizing.siblings),
                  widestColumnSize = this._getColumnSize(widestColumn);
                this._setColumnSize(widestColumn, (widestColumnSize - 1));
                this._setColumnSize(this.resizing.column, resizingOffsetSize);
              } else {
                var narrowestColumn = this._getNarrowestColumn(this.resizing.siblings),
                  narrowestColumnSize = this._getColumnSize(narrowestColumn);
                this._setColumnSize(narrowestColumn, (narrowestColumnSize + 1));
                this._setColumnSize(this.resizing.column, resizingOffsetSize);
              }
              this.resizing.columnSize = resizingOffsetSize;
            }
          },
    
          _resizeStopHandler: function(event) {
            if(!this.resizing) {
              return;
            }
            this.resizing.handle.removeClass('resizable-column-handle-resizing');
            this.resizing.column.removeClass('resizable-column-resizing');
            this.resizing.row.removeClass('resizable-row-resizing');
            this.element.removeClass('resizable-grid-resizing');
            this.resizing = false;
          },
    
          _getResizingDirectionIsWest: function(x) {
            var resizingDirectionIsWest;
            if(!this.resizing.directionLastX) {
              this.resizing.directionLastX = x;
              resizingDirectionIsWest = null;
            } else {
              if(x < this.resizing.directionLastX) {
                resizingDirectionIsWest = true;
              } else {
                resizingDirectionIsWest = false;
              }
              this.resizing.directionLastX = x;
            }
            return resizingDirectionIsWest;
          },
    
          _getResizingSiblings: function(column) {
            return ((this.resizing.handleIsWest) ? column.prevAll() : column.nextAll());
          },
    
          _getResizingOffsetSize: function(x) {
            var that = this,
              resizingOffsetSize;
            $.each(this.resizing.offsets, function(index, offset) {
              if((that.resizing.directionIsWest && ((x <= offset.end) && (x >= offset.start))) || (!that.resizing.directionIsWest && ((x >= offset.start) && (x <= offset.end)))) {
                resizingOffsetSize = offset.size;
              }
            });
            return resizingOffsetSize;
          },
    
          _getResizingOffsets: function() {
            var that = this,
              row = this.resizing.row.clone(),
              css = {
                'height': '1px',
                'min-height': '1px',
                'max-height': '1px'
              };
            row.removeClass('resizable-row resizable-row-resizing').css(css);
            row.children().empty().removeClass('resizable-column resizable-column-resizing').css(css);
            this.resizing.row.parent().append(row);
            var column = row.children().eq(this.resizing.row.children().index(this.resizing.column)),
              totalSize = this._getColumnSize(column);
            this._getResizingSiblings(column).each(function() {
              totalSize += (that._getColumnSize($(this)) - 1);
              that._setColumnSize($(this), 1);
            });
            var size = ((this.resizing.handleIsWest) ? totalSize : 1),
              sizeEnd = ((this.resizing.handleIsWest) ? 1 : totalSize),
              sizeOperator = ((this.resizing.handleIsWest) ? -1 : 1),
              offset = 0,
              offsetOperator = ((this.resizing.handleIsWest) ? 1 : 0);
            var columnGutter = ((column.outerWidth(true) - column.width()) / 2),
              columnWidth = ((this.resizing.handleIsWest) ? false : true);
            var resizingOffsets = [];
            while(true) {
              this._setColumnSize(column, size);
              this._setColumnOffset(column, offset);
              var left = (Math.floor((column.offset()).left) + columnGutter + ((columnWidth) ? column.width() : 0));
              resizingOffsets.push({
                start: (left + ((columnGutter * 3) * -1)),
                end: (left + (columnGutter * 3)),
                size: size
              });
              if(size === sizeEnd) {
                break;
              }
              size += sizeOperator;
              offset += offsetOperator;
            }
            row.remove();
            return resizingOffsets;
          },
    
          _getWidestColumn: function(columns) {
            var that = this,
              widestColumn;
            columns.each(function() {
              if(!widestColumn || (that._getColumnSize($(this)) > that._getColumnSize(widestColumn))) {
                widestColumn = $(this);
              }
            });
            return widestColumn;
          },
    
          _getNarrowestColumn: function(columns) {
            var that = this,
              narrowestColumn;
            columns.each(function() {
              if(!narrowestColumn || (that._getColumnSize($(this)) < that._getColumnSize(narrowestColumn))) {
                narrowestColumn = $(this);
              }
            });
            return narrowestColumn;
          },
    
          _getColumnSize: function(column) {
            var columnSize;
            $.each($.trim(column.attr('class')).split(' '), function(index, value) {
              if(value.match(/^col-/) && !value.match(/-offset-/)) {
                columnSize = parseInt($.trim(value).replace(/\D/g, ''), 10);
              }
            });
            return columnSize;
          },
    
          _setColumnSize: function(column, size) {
            column.toggleClass([
              ['col', 'sm', this._getColumnSize(column)].join('-'), ['col', 'sm', size].join('-')
            ].join(' '));
          },
    
          _getColumnOffset: function(column) {
            var columnOffset;
            $.each($.trim(column.attr('class')).split(' '), function(index, value) {
              if(value.match(/^col-/) && value.match(/-offset-/)) {
                columnOffset = parseInt($.trim(value).replace(/\D/g, ''), 10);
              }
            });
            return columnOffset;
          },
    
          _setColumnOffset: function(column, offset) {
            var currentColumnOffset,
              toggleClasses = [];
            if((currentColumnOffset = this._getColumnOffset(column)) !== undefined) {
              toggleClasses.push(['col', 'sm', 'offset', currentColumnOffset].join('-'));
            }
            toggleClasses.push(['col', 'sm', 'offset', offset].join('-'));
            column.toggleClass(toggleClasses.join(' '));
          },
    
          _destroy: function() {
            this._destroyHelpers();
          },
    
          _destroyHelpers: function() {
            this.element.find('.resizable-column-handle').remove();
            this.element.find('.resizable-column').removeClass('resizable-column resizable-column-resizing');
            this.element.find('.resizable-row').removeClass('resizable-row resizable-row-resizing');
            this.element.removeClass('resizable-grid resizable-grid-resizing');
          }
        });
    
      })(jQuery, window, document);
    
      $('#layout').resizableGrid();
    
    });
    

    File resizableColumns.css:

    .container {
      margin-top: 50px;
    }
    
    .row {
      background-color: rgba(189, 195, 199, 0.25);
    }
    
    .row:hover {
      background-color: rgba(189, 195, 199, 0.5);
    }
    
    .row > .col-sm-1,
    .row > .col-sm-2,
    .row > .col-sm-3,
    .row > .col-sm-4,
    .row > .col-sm-5,
    .row > .col-sm-6,
    .row > .col-sm-7,
    .row > .col-sm-8,
    .row > .col-sm-9,
    .row > .col-sm-10,
    .row > .col-sm-11,
    .row > .col-sm-12 {
      min-height: 100px;
    }
    
    .row > .col-sm-1:before,
    .row > .col-sm-2:before,
    .row > .col-sm-3:before,
    .row > .col-sm-4:before,
    .row > .col-sm-5:before,
    .row > .col-sm-6:before,
    .row > .col-sm-7:before,
    .row > .col-sm-8:before,
    .row > .col-sm-9:before,
    .row > .col-sm-10:before,
    .row > .col-sm-11:before,
    .row > .col-sm-12:before {
      content: '';
      display: block;
      position: absolute;
      top: 0;
      right: 15px;
      bottom: 0;
      left: 15px;
      background-color: rgba(52, 152, 219, 0.25);
    }
    
    .row > .col-sm-1:hover:before,
    .row > .col-sm-2:hover:before,
    .row > .col-sm-3:hover:before,
    .row > .col-sm-4:hover:before,
    .row > .col-sm-5:hover:before,
    .row > .col-sm-6:hover:before,
    .row > .col-sm-7:hover:before,
    .row > .col-sm-8:hover:before,
    .row > .col-sm-9:hover:before,
    .row > .col-sm-10:hover:before,
    .row > .col-sm-11:hover:before,
    .row > .col-sm-12:hover:before {
      background-color: rgba(52, 152, 219, 0.5);
    }
    
    /* Resizable Grid */
    
    .resizable-grid > .resizable-row > .resizable-column > .resizable-column-handle {
      z-index: 100;
      display: none;
      position: absolute;
      top: 0;
      height: 100%;
      width: 6px;
      cursor: col-resize;
      -webkit-touch-callout: none;
      -webkit-user-select: none;
      -khtml-user-select: none;
      -moz-user-select: none;
      -ms-user-select: none;
      user-select: none;
      -ms-touch-action: none;
      touch-action: none;
    }
    
    .resizable-grid > .resizable-row > .resizable-column > .resizable-column-handle-w {
      left: 12px;
    }
    
    .resizable-grid > .resizable-row > .resizable-column > .resizable-column-handle-e {
      right: 12px;
    }
    
    .resizable-grid > .resizable-row > .resizable-column:first-child:not(:last-child) > .resizable-column-handle-e,
    .resizable-grid > .resizable-row > .resizable-column:not(:first-child):not(:last-child) > .resizable-column-handle-w,
    .resizable-grid > .resizable-row > .resizable-column:not(:first-child):not(:last-child) > .resizable-column-handle-e,
    .resizable-grid > .resizable-row > .resizable-column:last-child:not(:first-child) > .resizable-column-handle-w {
      display: block;
    }
    
    .resizable-grid-resizing {
      cursor: col-resize;
    }
    
    .resizable-grid > .resizable-row-resizing > .resizable-column:not(.resizable-column-resizing) {
      opacity: 0.5;
    }
    

    Put the files resizableColumns.js and resizableColumns.css in the www subfolder of the app folder. The app:

    library(shiny)
    
    ui <- fluidPage(
      tags$head(
        tags$script(src = "https://code.jquery.com/ui/1.12.1/jquery-ui.js"),
        tags$link(rel = "stylesheet", href = "resizableColumns.css"),
        tags$script(src = "resizableColumns.js")
      ),
      tags$div(
        id = "layout",
        fluidRow(
          column(
            width = 3,
            h3("column1")
          ),
          column(
            width = 3,
            h3("column2")
          ),
          column(
            width = 3,
            h3("column3")
          ),
          column(
            width = 3,
            h3("column4")
          )
        )
      )
    )
    
    server <- function(input, output){}
    
    shinyApp(ui, server)