react-infinite-grid-scroller (RIGS)
Heavy-duty vertical or horizontal infinite scroller
Key Features
- intra-list and inter-list drag and drop (optional)
- vertical or horizontal scrolling
- designed to display both "heavy" and "light" cell content (React components)
- supports both uniform and variable cell lengths (for both vertical and horizontal)
- single or multiple rows or columns
- dynamically variable virtual list size; prepend or append indefinitely
- limited sparse in-memory cache, to preserve content state, with an API
- repositioning mode when rapidly scrolling
- dynamic pivot (horizontal/vertical back and forth) while maintaining position in list
- automatic reconfiguration with viewport resize
- dynamic recalibration with async content refresh
- supports nested RIGS scrollers
Demo Site
See the demo site.
Key Technologies
RIGS uses these key technologies:
Therefore RIGS is best suited for modern browsers.
Architecture
Notes: The Cradle
is kept in view of the Viewport
, such that the axis
is always near the top or left of the Viewport
(depending on vertical or horizontal orientation). There are two CSS grids in the Cradle
, one on each side of the axis
. As CellFrame
s are added to or removed from the grids, the grid on the top or left expands toward or contracts away from the top or left of the Scrollblock
(depending on orientation), and the grid on the bottom or right expands toward or contracts away from the bottom or right of the Scrollblock
.
CellFrame
s display individual user components. CellFrame
s are created and destroyed on a rolling basis as the Cradle
re-configures and moves around the Scrollblock
to stay in view, but user components are maintained in the internal cache until they go out of scope. New CellFrame
s fetch user components from the internal cache (portals in the React virtual DOM) or from the host through the user-supplied getItemPack
function, as needed.
Not shown are two triggerlines (0 width
or height
div
s, depending on orientation) which straddle the top or left edge of the Viewport
. Whenever one of these triggerlines crosses the Viewport
edge (through scrolling), an IntersectionObserver
sends an interrupt to the Cradle
to update its content and configuration. Triggerlines are located in the last CellFrame
of the head grid, unless the scroller is at the very top of its list, in which case the triggerlines are located in the first CellFrame
of the tail grid.
Usage
This is the minimum configuration.
import Scroller from 'react-infinite-grid-scroller'
const lowindex = -50, highindex = 50
<div style = { containerstyle }>
<Scroller
cellHeight = { cellHeight }
cellWidth = { cellWidth }
startingListRange = { [lowindex, highindex] } // this constitutes the virtual list
getItemPack = { getItemPack } // a function called by RIGS to obtain a specified user component by index number
/>
</div>
See below for drag and drop configuration.
The scroller's highest level component, the Viewport
, is a div
with position:absolute
, and inset:0
, so the host container should be styled accordingly.
Note that scroller-generated elements show a data-type
attribute in browser inspectors (eg. 'viewport').
User components loaded to CellFrame
s are placed in a data-type = 'contentenvelope'
div
. In 'uniform' layout this has position:absolute
and inset:0
. In 'variable' layout it has width:100%
and max-height
= cellHeight
for 'vertical' orientation, and height:100%
and max-width
= cellWidth
for 'horizontal' orientation. These elements allow overflow, but the user components should have overflow:hidden
as appropriate.
See the "Content Types" section of the demodata.tsx module of the demo site for more code examples.
Compatible browsers
RIGS works on Chrome, Microsoft Edge, Firefox and Safari.
Scroller properties
Table of properties
property | value | notes |
---|
[CELL CONFIGURATION] | | |
cellHeight:integer | number of pixels for cell height | required. Applied to height for 'uniform' layout, 'vertical' orientation. Applied to max-height for 'variable' layout, 'vertical' orientation. Approximate, used for fr (fractional allocation) for 'horizontal' orientation |
cellWidth:integer | number of pixels for cell width | required. Applied to width for 'uniform' layout, 'horizontal' orientation. Applied to max-width for 'variable' layout, 'horizontal' orientation. Approximate, used for fr (fractional allocation) for 'vertical' orientation |
cellMinHeight:integer | default = 25, minimum = 25, maximum = cellHeight | used for 'variable' layout with 'vertical' orientation. Applied to min-height |
cellMinWidth:integer | default = 25, minimum = 25, maximum = cellWidth | used for 'variable' layout with 'horizontal' orientation. Applied to min-width |
gap:integer | [] | number of pixels between cells | there is no gap at start or end of rows or columns; default = 0; accepts an array of integers as well as a standalone integer. Values match standard CSS order. Standalone integer = gap (in pixels) for both of column-gap (horizontal) and row-gap (vertical). 1-item array, same as integer. 2-item array = [col-gap, row-gap] |
getItemPack(index:integer, itemID:integer, context:object): object | host-provided function. index signifies position in list; session itemID (integer) is for tracking and matching. context provides an accept property when dnd is installed. Arguments provided by system | required. Must return a simple object with three properties: component - a React component or promise of a component (React.isValidElement ), dndOptions (if dnd is enabled; see Drag and Drop section), and profile - a simple host-defined object which gets returned to the host for identification in various contexts |
[LIST CONFIGURATION] | | |
startingListRange:[lowindex, highindex] | [] | two part array , or empty array [] | lowindex must be <= highindex; both can be positive or negative integers. [] (empty array) creates an empty virtual list. Can be modified at runtime. |
startingIndex:integer | starting index when the scroller first loads | default = 0 |
orientation:string | 'vertical' (default) or 'horizontal' | direction of scroll |
layout:string | 'uniform' (default) or 'variable' | specifies handling of the height or width of cells, depending on orientation. 'uniform' is fixed cellHeight/cellWidth. 'variable' is constrained by cellHeight/cellWidth (maximum) and cellMinHeight/cellMinWidth (minimum) |
padding:integer | [] | number of pixels padding the Scrollblock | default = 0; accepts an array of integers as well as a standalone integer. Values match standard CSS order. Standalone integer = padding (in pixels) for all of top, right, bottom, left. 1-item array, same as integer. 2-item array = [t/b, r/l]. 3-item array = [t, r/l, b]. 4-item array = [t, r, b, l] |
getExpansionCount(position:string, index:ingeger): integer | function optionally provided by host. Called whenever the lowindex or highindex are loaded into the Cradle. | position = "SOL" or "EOL"; index = the lowindex or highindex. Should return the number by which to expand the virtual list |
getDropEffect(sourceScrollerID, targetScrollerID, context): 'move' | 'copy'| 'none' | undefined | function, optional, for RigsDnd component only | called whenever drag isOver and canDrop are true on a list; returns drop constraint. See Drag and Drop below. |
[SYSTEM SETTINGS] | | |
dndOptions:object | scroller settings for drag and drop | required if drag and drop is enabled. See Drag and Drop section |
runwaySize:integer | number of rows in the Cradle just out of view at head and tail of list | default = 1. minimum = 1. Gives time to assemble cellFrames before display |
cache:string | 'cradle' (default), 'keepload', 'preload' | 'cradle' matches the cache to the contents of the Cradle . 'keepload' keeps user components in the cache as loaded, up to cacheMax (and always Cradle user components). 'preload' loads user components up to cacheMax , then adjusts cache such that Cradle user components are always in the cache |
cacheMax:integer | at minimum (maintained by system) the number of user components in the Cradle | allows optimization of cache size for memory limits and performance |
useScrollTracker:boolean | default = true | allows suppression of system feedback on position within list while in reposition mode, if the host wants to provide alternative feedback based on data from callbacks |
placeholder:React.FC | a lightweight React component for cellFrame s to load while waiting for the intended cellFrame components | optional (replaces default placeholder). parameters are index, listsize, message, error, dndEnabled. Arguments set by system |
usePlaceholder:boolean | default = true | allows suppression of use of default or custom placeholder. Placeholders show messages to the user while user components are fetched, and report errors |
[OBJECT PROPERTIES] | | |
styles:object | collection of styles for scroller components | optional. These should be "passive" styles like backgroundColor. See below for details |
placeholderMessages:object | messages presented by the placeholder | optional, to replace default messages. See below for details |
callbacks:object | collection of functions for feedback, and interactions with scroller components | optional. See below for details |
technical:object | collection of values used to control system behaviour | use with caution. optional. See below for details |
Notes: For explicit cache management capability, a unique session itemID
(integer) is assigned to a user component as soon as it enters the cache. The itemID
is retired as soon as the user component is removed from the cache. If the same component is re-introduced into the cache, it is assigned a new session-unique itemID
.
The itemID
for a user component is given to the host with the getItemPack
call to obtain the component, so that the host can track the user component in the cache. If the user component is assigned to a new index
number (see the returned function object cache management section below) the host will still be able to track the user component with the itemID
.
The host can track removal of a user component and its itemID
from the cache through tracking its associated index removal through the deleteListCallback
return values, and the return values from cache management functions. These feedback mechanisms also return the host's item profile
object for further identification. See callback documentation below.
Most of the time the itemID
can be ignored.
Also, note that the cache is reloaded with a new getItemPack
function.
The styles
object
Create a style object for each of the elements you want to modify. The styles are not screened, though the RIGS essential styles pre-empt user styles. Be careful to only include "passive" styles (like color, backgroundColor) so as not to confuse the scroller. Do not add structural items like borders, padding etc.
const styles = {
viewport: {},
scrollblock: {},
cradle: {},
scrolltracker: {},
placeholderframe: {},
placeholderliner: {},
placeholdererrorframe: {},
placeholdererrorliner: {},
dndDragIcon: {},
dndHighlights:{
source,
target,
dropped,
scroller,
scrolltab,
},
You may want to add overscrollBehavior:'none'
to the top level viewport styles to prevent inadvertent reloading of your app in certain browsers when users scroll past the top of a vertical list. Also note that dndDragIcon needs zIndex
to be at least 1
.
The scrolltracker is the small rectangular component that appears at the top left of the viewport when the list is being rapidly repositioned. The scrolltracker shows the user the current virtual position (index + 1) and total listsize during the repositioning process.
The placeholder styles are applied only to the default placeholder.
The placeholderMessages
object
Replace any of the default messages used by the placeholder.
const placeholderMessages = {
loading:'(loading...)',
retrieving:'(retrieving from cache)',
undefined:'host returned "undefined"',
invalid:'invalid React element',
}
The callbacks
object
Callbacks are host-defined closure functions which RIGS calls to provide data back to the host. RIGS returns data by setting the arguments of the callbacks. Include only the callbacks in the callbacks
object that you want RIGS to use. The following callbacks are recognized by RIGS:
const callbacks = {
functionsCallback,
dragDropTransferCallback,
referenceIndexCallback,
repositioningIndexCallback,
preloadIndexCallback,
itemExceptionCallback,
changeListRangeCallback,
deleteListCallback,
boundaryCallback,
repositioningFlagCallback,
}
An example of a callback closure (functionsCallback
):
const scrollerFunctionsRef = useRef(null)
const functionsCallback = (functions) => {
scrollerFunctionsRef.current = functions
}
scrollerFunctionsRef.current.scrollToIndex(targetIndex)
Details about the callbacks:
callback function (parameters:datatypes) | context object parameter | notes |
---|
[GET FUNCTIONS] | | |
functionsCallback(functions: object) | | the object returned contains Cradle functions that the host can call directly. This is the API. functionsCallback is called once at startup. See below for details |
[TRACK INDEXES] | | |
dragDropTransferCallback(fromScrollerID:number, fromIndex:number, toScrollerID:number, toIndex:number, content:object) | contextType:'dragDropTransfer', scrollerID, item | item contains item source data: dndOptions (type, dragText), dropEffect, index, itemID, profile, scrollerID |
referenceIndexCallback(index: integer, context:object) | contextType: 'referenceIndex', action, cradleState, scrollerID | action can be 'setCradleContent' or 'updateCradleContent'. cradleState is the state change that triggered the action. Keeps the host up to date on the index number adjacent to the Cradle axis |
repositioningIndexCallback(index: integer, context:object) | contextType: 'repositioningIndex', scrollerID | the current index during repositioning. Useful for feedback to user when host sets useScrollTracker property to false |
preloadIndexCallback(index: integer, conext:object) | contextType: 'preloadIndex', scrollerID | during a preload operation, this stream gives the index number being preloaded |
deleteListCallback(deleteList: array, context:object) | contextType: 'deleteList', scrollerID, message | gives an array of objects with index numbers that have been deleted from the cache, together with itemID and profile .message gives the reason for the deletion(s) |
itemExceptionCallback(index: integer, context:object) | contextType: 'itemException', itemID, scrollerID, profile, dndOptions, component, action, error | action can be 'preload' or 'fetch'. Triggered whenever getItemPack does not return a valid React component |
[TRACK OPERATIONS] | | |
changeListRangeCallback(listrange:array, context:object) | contextType: 'changeListRange', scrollerID | notification of a change of list range. listrange is a two part array = lowindex, highindex |
boundaryCallback(position:string, index:integer, context:object) | contextType: 'boundary', scrollerID | called whenever the lowindex or highindex are loaded into the Cradle . position is "SOL" or "EOL", index is the corresponding boundary index |
repositioningFlagCallback(flag: boolean, context:object) | contextType: 'repositioningIndex', scrollerID | called with true when repositioning starts, and false when repositioning ends. Useful for feedback to user when host sets useScrollTracker property to false |
The returned API functions
object
Details about the functions returned in an object by functionsCallback
.
Functions with no return values:
function(parameters: datatypes):return value: datatype | notes |
---|
[OPERATIONS] | |
scrollToIndex(index:integer): void | places the requested index item at the top visible row or left visible column of the scroller, depending on orientation |
scrollToPixel(pixel:integer[,behavior:string = 'smooth']): void | scrolls the scroller to the provided pixel, along the current orientation. behavior = 'smooth' | 'instant' | 'auto'; default = 'smooth'. pixel must be >=0 |
scrollByPixel(pixel:integer[,behavior:string = 'smooth']): void | scrolls the scroller up or down by the number of provided pixels, along the current orientation. behavior = 'smooth' | 'instant' | 'auto'; default = 'smooth'. pixel can be positive (scroll down) or negative (scroll up) |
setListRange(array [lowindex, highindex] | []): void | lowindex must be <= highindex; lowindex and highindex can be positive or negative integers. [] (empty array) creates an empty virtual list |
prependIndexCount(count:integer): void | the number of indexes to expand the start of the virtual list |
appendIndexCount(count:integer): void | the number of indexes to expand the end of the virtual list |
[CACHE MANAGEMENT] | |
reload(): void | clears the cache and reloads the Cradle at its current position in the virtual list |
clearCache(): void | clears the cache and the Cradle (leaving nothing to display) |
Functions with return values
function(parameters: datatypes):return value: datatype | context object properties | notes |
---|
[SNAPSHOTS] | | |
getCacheIndexMap(): [Map, context] | contextType: 'cacheIndexMap', scrollerID | snapshot of cache index (=key) to itemID (=value) map for the subject scroller |
getCacheItemMap(): [Map, context] | contextType: 'cacheItemMap', scrollerID | snapshot of cache itemID (=key) to object (=value) map for the subject scroller. Object = {index, component} where component = user component |
getCradleIndexMap(): [Map, context] | contextType: 'cradleIndexMap', scrollerID | snapshot of Cradle index (=key) to itemID (=value) map |
getPropertiesSnapshot():[object, context] | contextType: 'propertiesSnapshot', scrollerID | object is a copy of scroller.current from scrollerContext object. See below. |
[CACHE MANAGEMENT] | | |
insertIndex(index:integer, rangehighindex: integer | null):array[[shiftedList:array, replacedList:array, removedList:array, deletedList:array], context:object] | contextType: 'insertIndex', scrollerID | can insert a range of indexes. Displaced indexes, and higher indexes, are renumbered; virtual list lowindex remains the same. Changes the list size by increasing virtual list highindex; synchronizes the Cradle . deletedList contains an object for each item, with index , itemID , and profile |
removeIndex(index:integer, rangehighindex:integer | null):array[[shiftedList:array, replacedList:array, removedList:array, deletedList:array], context:object] | contextType: 'removeIndex', scrollerID | a range of indexes can be removed. Higher indexes are renumbered; virtual list lowindex remains the same. Changes the list size by decreasing virtual list highindex; synchronizes to the Cradle |
moveIndex(toindex:integer, fromindex:integer, fromhighrange: integer | null): array[[processedIndexList:array, movedList:array], context:object] | contextType: 'moveIndex', scrollerID | a range of indexes can be moved. Displaced and higher indexes are renumbered. Synchronizes to the Cradle . movedList contains an object for each item moved with fromIndex , toIndex , itemID , and profile . Returns false for error or undefined for nothing to do |
Notes: cache management functions are provided to support drag-n-drop, sorting, and filtering operations.
Cache management functions operate on indexes and itemIDs in the cache, and generally ignore indexes and itemIDs that are not in the cache. They synchronize Cradle
cell content as appropriate.
This is a sparse in-memory cache, and indexes in the cache are not guaranteed to be contiguous.
The technical
object
These properties would rarely be changed.
property:datatype = default | notes |
---|
showAxis:boolean = false | axis can be made visible for debug |
triggerlineOffset:integer = 10 | distance from cell head or tail for content shifts above/below axis |
VIEWPORT_RESIZE_TIMEOUT:integer = 250 | milliseconds before the Viewport resizing state is cleared |
ONAFTERSCROLL_TIMEOUT:integer = 100 | milliseconds after last scroll event before onAfter scroll event is fired |
IDLECALLBACK_TIMEOUT:integer = 175 | milliseconds timeout for requestIdleCallback |
VARIABLE_MEASUREMENTS_TIMEOUT:integer = 250 | milliseconds to allow setCradleContent changes to render before being measured for 'variable' layout |
CACHE_PARTITION_SIZE:integer = 30 | the cache is partitioned for performance reasons |
MAX_CACHE_OVER_RUN:number = 1.5 | max streaming cache size over-run (while scrolling) as ratio to cacheMax |
SCROLLTAB_INTERVAL_MILLISECONDS:number = 100 | SetInterval milliseconds for ScrollTab |
SCROLLTAB_INTERVAL_PIXELS:number = 100 | SetInterval scrollBy pixels for dnd ScrollTab |
The cell component's acquired scrollerContext
property
Cell components can get access to dynamically updated parent RIGS properties, by requesting the scrollerContext object.
The scrollerContext
object is requested by host components by initializing a scrollerContext
component property to null
. The property is then recognized by RIGS and set to the scrollerContext object by the system on loading of the component to a CellFrame. Here is the internal code that does this:
if (usercontent.props.hasOwnProperty('scrollerContext')) {
component = React.cloneElement(usercontent, {scrollerContext})
} else {
component = usercontent
}
The scrollerContext object contains two properties:
{
cell,
scroller,
}
Each of these is a reference object, with values found in propertyRef.current
.
The cell.current
object is instantiated only when a component is instantiated in the Cradle. It contains two properties:
{
itemID,
index,
}
The scroller.current
object contains the following properties, which are identical to the properties set for the scroller (they are passed through):
orientation
, cellHeight
, cellWidth
, cellMinHeight
, cellMinWidth
, layout
, cache
, cacheMax
, startingIndex
It also contains scrollerID
, the internal session id (integer) of the current scroller, as well as dndInstalled
, and dndEnabled
.
Finally, it contains four objects with bundled properties: virtualListProps
, cradleContentProps
, gapProps
, and paddingProps
.
virtualListProps
is an object with the following properties:
{
size,
range,
lowindex,
highindex,
baserowblanks,
endrowblanks,
crosscount,
rowcount,
rowshift,
}
cradleContentProps
is an object with the following properties:
{
cradleRowcount,
viewportRowcount,
runwayRowcount,
SOL,
EOL,
lowindex,
highindex,
axisReferenceIndex,
size,
}
gapProps
is an object with the following properties:
{
CSS,
column,
row,
list,
original,
source,
}
paddingProps
is an object with the following properties:
{
CSS, the CSS to be applied to the Scrollblock
top,
right,
bottom,
left,
list,
original,
source,
}
Drag and drop
Overview
The following are the basic steps to implement drag and drop on RIGS. Note that RIGS drags and drops its CellFrame components. The user content (React components) come along for the ride. The following assumes multiple sub-scrollers, although using just the main scroller is fine.
- design a type system for scroller items and scrollers. Scroller item types must be included in scroller
accept
type lists - install the dnd system by invoking the specialized
RigsDnd
root component - provide every scroller and subscroller with a scroller
dndOptions
object as a scroller parameter. Also providing a host-defined scroller profile
object is highly recommended, for use during interaction with the system - provide every item fetched using
getItemPack
with a cell dndOptions
property through the returned object. Also providing items with a host-defined cell profile
object is highly recommended, for use during interaction with the system. (Remember that subscrollers require both a direct scroller dndOptions
property, and a cell dndOptions
object returned by getItemPack
. Sub-scrollers are both item repositories, and part of scroller items themselves). - prepare to respond to specialized 'dndFetchRequest'
getItemPack
requests, which are for dropped copies of existing items, or replacements for dropped moved items that have gone out of scope as the result of scrolling during the drag activity - request a
scrollerContext
object from the host scroller for each of your content items so that the items will be aware of dnd status in their host scrollers for layout purposes (see scrollerContext
section) - design and implement accommodating layout features on your cell components
- design and implement dnd configuration options as required
- create a
getDropEffect
function if needed, and pass this to the RigsDnd
root component - track drag and drop transfers with
dragDropTransferCallback
if desired
See below for details.
Installation
To install drag and drop ('dnd') capability,
- first invoke the
RigsDnd
component (instead of the default component) for the root of the RIGS tree to install the dnd Provider
. This can only be done once per environment. For embedded lists (sub-scrollers), use the default RIGS component.
import { RigsDnd as Scroller } from 'react-infinite-grid-scroller'
...
<div style = { containerstyle }>
<Scroller
cellHeight = { cellHeight }
cellWidth = { cellWidth }
startingListRange = { [lowindex, highindex] } // this constitutes the virtual list
getItemPack = { getItemPack } // a function called by RIGS to obtain a specified user component by index number
dndOptions = { dndOptions } // required for both the root instance and all child RIGS instances
/>
</div>
dndOptions
is a required property for all scrollers when dnd is enabled. It must include an accept
property, with an array of accepted dnd content types (strings or Symbols). For the root scroller it may also include a master
property, and a profile
property with a simple object to help identify the scroller when the host responds to the getDropEffect
function. A complete list here:
const dndOptions = {
accept:['type1','type2','type3',...],
master:{enabled},
enabled,
dropEffect,
}
- When dnd is enabled, all data packages returned to RIGS with
getItemPack
must include a dndOptions
object (together with the component
and profile
properties). The dndOptions
object must contain a type
property with a string that matches one of the accept
array strings of its containing scroller, and a dragText
property with text that will be shown in the drag image for the item.
...
return {
component,
dndOptions:{
type,
dragText,
}
profile,
}
RIGS does not check for matches of type
values returned with getItemPack
, with accept
values sent to scrollers via the dndOptions
scroller property.
With dnd enabled, the context
parameter of the getItemPack
function sent to the host will include the accept
list of the enclosing scroller, for convenience.
The canDrop
and dropEffect
properties, and the getDropEffect
function
canDrop
is an internal property controlled by react-dnd
, by comparing the dragged item's type
property (from the cell dndOptions
property) to the list of accept
values (from the scroller dndOptions
property) of the target scroller. If a match is found, then react-dnd
signals that the item can be dropped on the scroller.
canDrop
can be further constrained by the host-provided getDropEffect
function, by returning 'none'.
The internal dropEffect
property for a dragged item can be set by the dropEffect
property of the dndOptions
of the source scroller. If this is undefined
, then it becomes the default 'move' operation, or the 'copy' operation when the altKey
is pressed by the user on most browsers.
dropEffect
can be further constrained by the return value of the host-provided function getDropEffect
(see Scroller Properties above). getDropEffect
(if provided by the host) is called by RIGS whenever the drag location crosses into a scroller for which the internal isOver
and canDrop
properties are both true. This function has three parameters: sourceScrollerID
(the scrollerID
from the source of the drag), targetScrollerID
(the scrollerID
from the target of the drag), and context
. Here are the properties of the context
parameter:
const context = {
sourceDndOptions:{
accept,
enabled,
dropEffect
},
sourceProfile,
targetDndOptions:{
...
},
targetProfile,
itemData:{
itemID,
index,
profile,
dndOptions: {,
type,
dragText,
}
dropEffect,
}
}
The host can return 'move', 'copy', 'none', or undefined
from the getDropEffect
function. 'none' prevents a drop, 'move' and 'copy' override the item's dropEffect
value, and undefined
yields to the value of the scroller's calculated dropEffect
property.
The 'dndFetchRequest' specialized getItemPack
call
When the user drags and drops an item which exists in the RIGS cache, and with a 'move' dropEffect
, then RIGS takes care of the data transfer itself.
However, if the drop involves a 'copy', then the host has to supply what it considers to be a copy of the item (a RIGS cache item can only be used for one display instance). Also, if the item has been moved, but has gone out of scope as the result of scrolling during the drag operation and therefore removed from the cache, then a fresh instance of the moved item has to be obtained from the host. In those cases, RIGS sends out a specialized getItemPack(index, itemID, context)
call with sufficient information for the host to respond appropriately. In this case, the context
parameter contains the following:
const context = {
contextType:'dndFetchRequest',
accept,
scrollerID,
scrollerProfile,
item:{
scrollerID,
index,
itemID,
profile,
dndOptions,
dropEffect,
},
}
Layout
RIGS shows a drag icon for draggable items at the top left of each CellFrame. Since this icon can overlay some of the content of the host provided content, the component may want to move this content out of the way of the drag icon when it is present. Here is how that can be done.
First, obtain a scrollerContext
object from the host scroller. See the scrollerContext
section for details.
Then code something like this.
const isDnd = scrollerContext?.scroller.current.dndEnabled
const float = useMemo(() => {
return <div
style = {{float:'left', height: '28px', width:'34px'}}
data-type = 'dnd-float'
/>
},[])
return <div
... // properties
>
{isDnd && float}
... // content
</div>
Configuration
Drag and drop can be enabled or disabled for individual scrollers, by setting the scroller's dndOptions.enabled
property. If the scroller's enabled
property is undefined
, then it is set to the RigsDnd
component's dndOptions.master.enabled
property (which is itself set to default true
if undefined
. The enabled
property can be set at runtime.
This presents a number of options. For example the user can set the dnd capability of a scroller themselves, something like this:
const dndOptionsRef = useRef(dndOptions)
dndOptionsRef.current = dndOptions
...
const checkdnd = (event:React.ChangeEvent) => {
const
target = event.target as HTMLInputElement,
isChecked = target.checked
dndOptionsRef.current.enabled = isChecked
setSubscrollerState('revised')
}
return ...
<FormControl borderTop = '1px' style = {{clear:'left'}}>
<Checkbox
isChecked = {dndOptions.current.enabled}
size = 'sm'
mt = {2}
onChange = {checkdnd}
>
Drag and drop
</Checkbox>
</FormControl>}
...
<Scroller
...
dndOptions = {dndOptionsRef.current}
...
/>
Or you can create a global React context, say const DndEnabledContext = React.createContext(true)
. Then control that value through a global checkbox control. Finally, in your scroller component:
const dndEnabledContext = useContext( DndEnabledContext )
...
useEffect(()=>{
dndOptionsRef.current.enabled = dndEnabledContext
setSubscrollerState('revised')
},[dndEnabledContext])
...
The combination of the local and global settings would allow the user much flexibility in terms of how to use drag and drop. This could be particularly useful in settings involving dozens of subscrollers.
Performance
If there are dozens of subscrollers on screen (say by zooming out to 50%), then performance of drag and drop (notably for the drag icon) can degrade owing to the pub/sub system of react-dnd
. One way of dealing with this is to disable all drag and drop (see Configuration above), and let the user selectively enable drag and drop for the scrollers that they want to work with. That way, performance is quite good.
Restoring scroll positions coming out of cache
This is only of concern if your cell components support scrolling.
RIGS loads components into a cache (React portals), and into Cradle
cells from there. Moreover RIGS moves components from one side of the (hidden) axis to the other through the cache during scrolling. Plus caching can be extended (by RIGS property settings) beyond the Cradle
. So going into and out of cache happens a lot for components. While in cache, the component elements have their scrollTop
, scrollLeft
, width
, and height
values set to 0 by browsers. width
and height
values are restored by browsers when moved back into the visible DOM area, but scroll positions have to be manually restored.
Here is one way of restoring scroll positions. Basically, save scroll positions on an ongoing basis, detect going into cache when width
and height
values are both zero, and detect coming out of cache when width
and height
are no longer zero. When coming out of cache, restore the saved scroll positions.
This code is Typescript, in a function component.
const scrollerElementRef = useRef<any>(null),
scrollPositionsRef = useRef({scrollTop:0, scrollLeft:0}),
wasCachedRef = useRef(false)
const scrollerEventHandler = (event:React.UIEvent<HTMLElement>) => {
const scrollerElement = event.currentTarget
if (!(!scrollerElement.offsetHeight && !scrollerElement.offsetWidth)) {
const scrollPositions = scrollPositionsRef.current
scrollPositions.scrollTop = scrollerElement.scrollTop
scrollPositions.scrollLeft = scrollerElement.scrollLeft
}
}
useEffect( () => {
const scrollerElement = scrollerElementRef.current
scrollerElement.addEventListener('scroll', scrollerEventHandler)
return () => {
scrollerElement.removeEventListener('scroll', scrollerEventHandler)
}
},[])
const cacheSentinel = () => {
const scrollerElement = scrollerElementRef.current
if (!scrollerElement) return
const isCached = (!scrollerElement.offsetWidth && !scrollerElement.offsetHeight)
if (isCached != wasCachedRef.current) {
wasCachedRef.current = isCached
if (!isCached) {
const {scrollTop, scrollLeft} = scrollPositionsRef.current
scrollerElement.scrollTop = scrollTop
scrollerElement.scrollLeft = scrollLeft
}
}
}
cacheSentinel()
return <div ref = {scrollerElementRef} style = {scrollerstyles}>
{scrollercontent}
</div>
Licence
MIT © 2020-2023 Henrik Bechmann