1/* 2 * insert_symbol-grid.tsx 3 * 4 * Copyright (C) 2019-20 by RStudio, PBC 5 * 6 * Unless you have received this program directly from RStudio pursuant 7 * to the terms of a commercial license agreement with RStudio, then 8 * this program is licensed to you under the terms of version 3 of the 9 * GNU Affero General Public License. This program is distributed WITHOUT 10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, 11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the 12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. 13 * 14 */ 15 16import React from 'react'; 17import { FixedSizeGrid } from 'react-window'; 18 19import debounce from 'lodash.debounce'; 20 21import { EditorUI } from '../../api/ui'; 22import { WidgetProps } from '../../api/widgets/react'; 23 24import { CharacterGridCellItemData, SymbolCharacterCell } from './insert_symbol-grid-cell'; 25import { SymbolCharacter } from './insert_symbol-dataprovider'; 26 27import './insert_symbol-grid-styles.css'; 28 29interface CharacterGridProps extends WidgetProps { 30 height: number; 31 width: number; 32 numberOfColumns: number; 33 symbolCharacters: SymbolCharacter[]; 34 selectedIndex: number; 35 onSelectionChanged: (selectedIndex: number) => void; 36 onSelectionCommitted: VoidFunction; 37 ui: EditorUI; 38} 39 40const selectedItemClassName = 'pm-grid-item-selected'; 41 42const SymbolCharacterGrid = React.forwardRef<any, CharacterGridProps>((props, ref) => { 43 const columnWidth = Math.floor(props.width / props.numberOfColumns); 44 const characterCellData: CharacterGridCellItemData = { 45 symbolCharacters: props.symbolCharacters, 46 numberOfColumns: props.numberOfColumns, 47 selectedIndex: props.selectedIndex, 48 onSelectionChanged: props.onSelectionChanged, 49 onSelectionCommitted: props.onSelectionCommitted, 50 selectedItemClassName, 51 }; 52 53 const gridRef = React.useRef<FixedSizeGrid>(null); 54 const handleScroll = debounce(() => { 55 gridRef.current?.scrollToItem({ rowIndex: Math.floor(props.selectedIndex / props.numberOfColumns) }); 56 }, 5); 57 58 React.useEffect(handleScroll, [props.selectedIndex]); 59 60 const handleKeyDown = (event: React.KeyboardEvent) => { 61 const newIndex = newIndexForKeyboardEvent( 62 event, 63 props.selectedIndex, 64 props.numberOfColumns, 65 props.symbolCharacters.length, 66 ); 67 if (newIndex !== undefined) { 68 props.onSelectionChanged(newIndex); 69 event.preventDefault(); 70 } 71 }; 72 73 return ( 74 <div onKeyDown={handleKeyDown} tabIndex={0} ref={ref}> 75 <FixedSizeGrid 76 columnCount={props.numberOfColumns} 77 rowCount={Math.ceil(props.symbolCharacters.length / props.numberOfColumns)} 78 height={props.height} 79 width={props.width + 1} 80 rowHeight={columnWidth} 81 columnWidth={columnWidth} 82 itemData={characterCellData} 83 className="pm-symbol-grid" 84 ref={gridRef} 85 > 86 {SymbolCharacterCell} 87 </FixedSizeGrid> 88 </div> 89 ); 90}); 91 92function previous(currentIndex: number, numberOfColumns: number, numberOfCells: number): number { 93 const newIndex = currentIndex - 1; 94 return Math.max(0, newIndex); 95} 96function next(currentIndex: number, numberOfColumns: number, numberOfCells: number): number { 97 const newIndex = currentIndex + 1; 98 return Math.min(numberOfCells - 1, newIndex); 99} 100function prevRow(currentIndex: number, numberOfColumns: number, numberOfCells: number): number { 101 const newIndex = currentIndex - numberOfColumns; 102 return newIndex >= 0 ? newIndex : currentIndex; 103} 104function nextRow(currentIndex: number, numberOfColumns: number, numberOfCells: number): number { 105 const newIndex = currentIndex + numberOfColumns; 106 return newIndex < numberOfCells ? newIndex : currentIndex; 107} 108function nextPage(currentIndex: number, numberOfColumns: number, numberOfCells: number): number { 109 const newIndex = currentIndex + 6 * numberOfColumns; 110 return Math.min(numberOfCells - 1, newIndex); 111} 112function prevPage(currentIndex: number, numberOfColumns: number, numberOfCells: number): number { 113 const newIndex = currentIndex - 6 * numberOfColumns; 114 return Math.max(0, newIndex); 115} 116 117export const newIndexForKeyboardEvent = ( 118 event: React.KeyboardEvent, 119 selectedIndex: number, 120 numberOfColumns: number, 121 numberOfCells: number, 122): number | undefined => { 123 switch (event.key) { 124 case 'ArrowLeft': // left 125 return previous(selectedIndex, numberOfColumns, numberOfCells); 126 127 case 'ArrowUp': // up 128 return prevRow(selectedIndex, numberOfColumns, numberOfCells); 129 130 case 'ArrowRight': // right 131 return next(selectedIndex, numberOfColumns, numberOfCells); 132 133 case 'ArrowDown': // down 134 return nextRow(selectedIndex, numberOfColumns, numberOfCells); 135 136 case 'PageDown': 137 return nextPage(selectedIndex, numberOfColumns, numberOfCells); 138 139 case 'PageUp': 140 return prevPage(selectedIndex, numberOfColumns, numberOfCells); 141 142 case 'Home': 143 return 0; 144 145 case 'End': 146 return numberOfCells - 1; 147 148 default: 149 return undefined; 150 } 151}; 152 153export default SymbolCharacterGrid; 154