diff --git a/app/components/FileLoader.js b/app/components/FileLoader.js index f85388759..fb23f2401 100644 --- a/app/components/FileLoader.js +++ b/app/components/FileLoader.js @@ -12,6 +12,7 @@ import { Loader, Visibility, } from 'semantic-ui-react'; +import classNames from 'classnames'; import styles from './FileLoader.scss'; import FileRow from './FileRow'; import DismissibleMessage from './common/DismissibleMessage'; @@ -51,6 +52,8 @@ export default class FileLoader extends Component { this.state = { selections: [], + areAllSelected: false, + shiftPressed: false, }; } @@ -74,6 +77,10 @@ export default class FileLoader extends Component { this.refTableScroll.scrollTo(xPos, yPos); } + + // Listen for the shift key + document.addEventListener("keydown", this.shiftKeyListener); + document.addEventListener("keyup", this.shiftKeyListener); } componentDidUpdate(prevProps) { @@ -102,6 +109,18 @@ export default class FileLoader extends Component { }); this.props.dismissError('fileLoader-global'); + + // Stop listening for the shift key + document.removeEventListener("keydown", this.shiftKeyListener); + document.removeEventListener("keyup", this.shiftKeyListener); + } + + shiftKeyListener = (event) => { + if (event.key === "Shift") { + this.setState({ + shiftPressed: Boolean(event.type === "keydown"), + }); + } } refTableScroll = null; @@ -110,7 +129,13 @@ export default class FileLoader extends Component { this.refTableScroll = element; }; - onSelect = selectedFile => { + onSelect = (selectedFile, fileIndex) => { + // shift clicking has gmail behavior + if (this.state.shiftPressed) { + this.handleShiftSelect(selectedFile, fileIndex); + return; + } + const newSelections = []; let wasSeen = false; @@ -127,6 +152,45 @@ export default class FileLoader extends Component { this.setState({ selections: newSelections, + areAllSelected: newSelections.length === this.props.store.files.length, + }); + }; + + handleShiftSelect = (selectedFile, fileIndex) => { + // Shift clicking on an already selected file removes all consecutive selections on and after it + // eslint-disable-next-line react/no-access-state-in-setstate + let newSelections = [...this.state.selections]; + const files = this.props.store.files; + if (this.state.selections.indexOf(selectedFile) !== -1) { + const startingFileIndex = files.indexOf(selectedFile); + let numToRemove = 0; + for (let i = startingFileIndex; i < files.length; i++) { + if (this.state.selections.indexOf(files[i]) === -1) { + break; + } + numToRemove++; + } + newSelections.splice(this.state.selections.indexOf(selectedFile), numToRemove); + this.setState({ + selections: newSelections, + areAllSelected: newSelections.length === this.props.store.files.length, + }); + return; + } + + // Shift clicking on a not selected file selects all files before it up to another already selected file + let newFiles = []; + for (let i = fileIndex; i >= 0; i--) { + if (this.state.selections.indexOf(files[i]) !== -1) { + break; + } + newFiles.push(files[i]); + } + newFiles = newFiles.reverse(); + newSelections = [...this.state.selections, ...newFiles]; + this.setState({ + selections: newSelections, + areAllSelected: newSelections.length === this.props.store.files.length, }); }; @@ -162,6 +226,7 @@ export default class FileLoader extends Component { queueClear = () => { this.setState({ selections: [], + areAllSelected: false, }); }; @@ -172,6 +237,13 @@ export default class FileLoader extends Component { }); }; + selectAll = () => { + this.setState((prevState) => ({ + selections: prevState.areAllSelected ? [] : (this.props.store.filterReplays ? this.props.store.files : this.props.store.allFiles) || [], + areAllSelected: !prevState.areAllSelected, + })); + } + renderGlobalError() { const errors = this.props.errors || {}; const errorKey = 'fileLoader-global'; @@ -228,6 +300,7 @@ export default class FileLoader extends Component { this.props.setFilterReplays(false); this.setState({ selections: [], + areAllSelected: false, }); } @@ -335,12 +408,25 @@ export default class FileLoader extends Component { return this.renderEmptyLoader(); } + const cellStyles = classNames({ + [styles['select-cell']]: true, + [styles['selected']]: this.state.areAllSelected, + }); + + const iconStyle = classNames({ [styles['select-all-icon']]: true}) + + const selectAllIcon = this.state.areAllSelected ? ( + + ) : ( + + ) + // Generate header row const headerRow = ( - - Details - Time + {selectAllIcon} + Details + Time ); diff --git a/app/components/FileLoader.scss b/app/components/FileLoader.scss index b4ad32709..d0715fc94 100644 --- a/app/components/FileLoader.scss +++ b/app/components/FileLoader.scss @@ -1,4 +1,4 @@ -@import "../colors.global.scss"; +@import '../colors.global.scss'; .layout { display: grid; @@ -28,6 +28,7 @@ } .file-table { + user-select: none; min-width: 600px; } @@ -80,4 +81,46 @@ cursor: pointer; text-decoration: underline; } -} \ No newline at end of file +} + +.select-all-icon { + margin-left: 10px !important; +} + +.select-cell { + margin: 6px !important; + + i { + color: #94969a; + } + + i:hover { + color: #ffffff; + cursor: pointer; + } + + &.selected { + i { + color: rgba(255, 255, 255, 0.75); + } + + i:hover { + color: #ffffff; + cursor: pointer; + } + } + + .pos-text { + margin-left: 4px; + font-weight: bold; + color: rgba(255, 255, 255, 0.5); + } + + .select-content-wrapper { + margin-left: 10px; + } +} + +.table-header { + font-size: 1.2em; +} diff --git a/app/components/FileRow.js b/app/components/FileRow.js index c58bd09c3..4c6bccd38 100644 --- a/app/components/FileRow.js +++ b/app/components/FileRow.js @@ -57,7 +57,7 @@ export default class FileRow extends Component { }; onSelect = () => { - this.props.onSelect(this.props.file); + this.props.onSelect(this.props.file, this.props.fileIndex); }; viewStats = (e) => { diff --git a/app/components/FileRow.scss b/app/components/FileRow.scss index 445ee5111..e56cbe524 100644 --- a/app/components/FileRow.scss +++ b/app/components/FileRow.scss @@ -21,13 +21,13 @@ .actions-cell { text-align: center; - button>i { - color: #94969A; + button > i { + color: #94969a; } button:hover { i { - color: #FFFFFF; + color: #ffffff; } } } @@ -36,11 +36,11 @@ margin: 6px !important; i { - color: #94969A; + color: #94969a; } i:hover { - color: #FFFFFF; + color: #ffffff; cursor: pointer; } @@ -50,7 +50,7 @@ } i:hover { - color: #FFFFFF; + color: #ffffff; cursor: pointer; } } @@ -83,4 +83,4 @@ cursor: pointer; text-decoration: underline; } -} \ No newline at end of file +}