fixes for hotkeys usage in docusaurus

This commit is contained in:
Erik Ziegler committed 2021-07-21 12:58:15 +02:00
1 parent 7a9f113b91
commit ff28a2264b
24 files changed
+61 -659

No files matched your search

@@ -72,7 +72,7 @@ const AboutModal = ({buildNumber, versionNumber}) => {
</Link>
</span>
<span className="ml-4">
<Link href="http://ohif.org/" showIcon={true}>
<Link href="https://ohif.org/" showIcon={true}>
More details
</Link>
</span>
@@ -2,7 +2,6 @@ import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import Input from '../Input';
import { hotkeys } from '../../utils/'
import { getKeys, formatKeysForInput } from './utils';
/**
@@ -16,7 +15,7 @@ import { getKeys, formatKeysForInput } from './utils';
* @param {string} props.className input classes
* @param {Array[]} props.modifierKeys
*/
const HotkeyField = ({ disabled, keys, onChange, className, modifierKeys }) => {
const HotkeyField = ({ disabled, keys, onChange, className, modifierKeys, hotkeys }) => {
const inputValue = formatKeysForInput(keys);
useEffect(() => {
@@ -57,6 +56,13 @@ HotkeyField.propTypes = {
className: PropTypes.string,
modifierKeys: PropTypes.array,
disabled: PropTypes.bool,
hotkeys: PropTypes.object({
initialize: PropTypes.func.isRequired,
pause: PropTypes.func.isRequired,
unpause: PropTypes.func.isRequired,
startRecording: PropTypes.func.isRequired,
record: PropTypes.func.isRequired,
}).isRequired
};
HotkeyField.defaultProps = {
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
import { MODIFIER_KEYS } from './hotkeysConfig';
import { validate, splitHotkeyDefinitionsAndCreateTuples } from './utils';
const HotkeysPreferences = ({ disabled, hotkeyDefinitions, errors: controlledErrors, onChange }) => {
const HotkeysPreferences = ({ disabled, hotkeyDefinitions, errors: controlledErrors, onChange, hotkeysModule }) => {
const { t } = useTranslation('UserPreferencesModal');
const visibleHotkeys = Object.keys(hotkeyDefinitions)
@@ -83,6 +83,7 @@ const HotkeysPreferences = ({ disabled, hotkeyDefinitions, errors: controlledErr
keys={definition.keys}
modifierKeys={MODIFIER_KEYS}
onChange={onChangeHandler}
hotkeys={hotkeysModule}
className='text-lg h-8'
/>
{error && <span className='p-2 text-left text-red-600 text-sm'>{error}</span>}
@@ -106,6 +107,13 @@ HotkeysPreferences.propTypes = {
onChange: PropTypes.func,
disabled: PropTypes.bool,
hotkeyDefinitions: PropTypes.object.isRequired,
hotkeysModule: PropTypes.object({
initialize: PropTypes.func.isRequired,
pause: PropTypes.func.isRequired,
unpause: PropTypes.func.isRequired,
startRecording: PropTypes.func.isRequired,
record: PropTypes.func.isRequired,
}).isRequired
};
HotkeysPreferences.defaultProps = {
@@ -46,7 +46,4 @@ const validate = ({ commandName, pressedKeys, hotkeys }) => {
return { error: undefined };
};
export {
validate,
splitHotkeyDefinitionsAndCreateTuples
};
export { validate, splitHotkeyDefinitionsAndCreateTuples };
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { Select, Typography, Button, HotkeysPreferences } from '../';
import { useTranslation } from 'react-i18next';
const UserPreferences = ({ availableLanguages, defaultLanguage, currentLanguage, disabled, hotkeyDefinitions, hotkeyDefaults, onCancel, onSubmit, onReset }) => {
const UserPreferences = ({ availableLanguages, defaultLanguage, currentLanguage, disabled, hotkeyDefinitions, hotkeyDefaults, onCancel, onSubmit, onReset, hotkeysModule }) => {
const { t } = useTranslation('UserPreferencesModal');
const [state, setState] = useState({
isDisabled: disabled,
@@ -85,6 +85,7 @@ const UserPreferences = ({ availableLanguages, defaultLanguage, currentLanguage,
hotkeyDefinitions={state.hotkeyDefinitions}
onChange={onHotkeysChangeHandler}
errors={state.hotkeyErrors}
hotkeysModule={hotkeysModule}
/>
</Section>
<div className="flex flex-row justify-between">
@@ -125,6 +126,13 @@ UserPreferences.propTypes = {
onCancel: PropTypes.func,
onSubmit: PropTypes.func,
onReset: PropTypes.func,
hotkeysModule: PropTypes.object({
initialize: PropTypes.func.isRequired,
pause: PropTypes.func.isRequired,
unpause: PropTypes.func.isRequired,
startRecording: PropTypes.func.isRequired,
record: PropTypes.func.isRequired,
}).isRequired
};
UserPreferences.defaultProps = {
+2 -5
View File
@@ -1,6 +1,6 @@
/** UTILS */
import utils from './utils';
export { utils };
//import utils from './utils';
//export { utils };
/** CONTEXT/HOOKS */
export {
@@ -102,6 +102,3 @@ export {
export { getIcon, ICONS } from './components/Icon/getIcon';
export { BackgroundColor } from './pages/Colors/BackgroundColor';
export { ModalComponent } from './contextProviders/ModalComponent';
/** VIEWS */
export { StudyList, Viewer } from './views';
-17
View File
@@ -1,17 +0,0 @@
import studyListMock from '../mocks/studyList.json';
/** Values can be env vars */
const DEFAULT_MOCKED_STUDIES_LIMIT = 1000;
/**
* Method to get a mocked study list
* @param {number} items Number of studies to be loaded
* @returns {array} Study list
*/
const getMockedStudies = (items = 50) => {
const num =
items > DEFAULT_MOCKED_STUDIES_LIMIT ? DEFAULT_MOCKED_STUDIES_LIMIT : items;
return new Array(num).fill(studyListMock.studies[0]);
};
export default getMockedStudies;
-14
View File
@@ -1,14 +0,0 @@
import Mousetrap from 'mousetrap';
import pausePlugin from './pausePlugin';
import recordPlugin from './recordPlugin';
Mousetrap.initialize = () => {
if (!Mousetrap._initialized) {
recordPlugin(Mousetrap);
pausePlugin(Mousetrap);
Mousetrap._initialized = true;
}
};
export default Mousetrap;
@@ -1,32 +0,0 @@
/**
* adds a pause and unpause method to Mousetrap
* this allows you to enable or disable keyboard shortcuts
* without having to reset Mousetrap and rebind everything
*
* https://github.com/ccampbell/mousetrap/blob/master/plugins/pause/mousetrap-pause.js
*/
export default function(Mousetrap) {
var _originalStopCallback = Mousetrap.prototype.stopCallback;
Mousetrap.prototype.stopCallback = function(e, element, combo) {
var self = this;
if (self.paused) {
return true;
}
return _originalStopCallback.call(self, e, element, combo);
};
Mousetrap.prototype.pause = function() {
var self = this;
self.paused = true;
};
Mousetrap.prototype.unpause = function() {
var self = this;
self.paused = false;
};
Mousetrap.init();
}
@@ -1,218 +0,0 @@
/**
* This extension allows you to record a sequence using Mousetrap.
* {@link https://craig.is/killing/mice}
*
* @author Dan Tao <daniel.tao@gmail.com>
*/
export default function (Mousetrap) {
/**
* the sequence currently being recorded
*
* @type {Array}
*/
var _recordedSequence = [],
/**
* a callback to invoke after recording a sequence
*
* @type {Function|null}
*/
_recordedSequenceCallback = null,
/**
* a list of all of the keys currently held down
*
* @type {Array}
*/
_currentRecordedKeys = [],
/**
* temporary state where we remember if we've already captured a
* character key in the current combo
*
* @type {boolean}
*/
_recordedCharacterKey = false,
/**
* a handle for the timer of the current recording
*
* @type {null|number}
*/
_recordTimer = null,
/**
* the original handleKey method to override when Mousetrap.record() is
* called
*
* @type {Function}
*/
_origHandleKey = Mousetrap.prototype.handleKey;
/**
* handles a character key event
*
* @param {string} character
* @param {Array} modifiers
* @param {Event} e
* @returns void
*/
function _handleKey(character, modifiers, e) {
var self = this;
if (!self.recording) {
_origHandleKey.apply(self, arguments);
return;
}
// remember this character if we're currently recording a sequence
if (e.type == 'keydown') {
if (character.length === 1 && _recordedCharacterKey) {
_recordCurrentCombo();
}
for (let i = 0; i < modifiers.length; ++i) {
_recordKey(modifiers[i]);
}
_recordKey(character);
// once a key is released, all keys that were held down at the time
// count as a keypress
} else if (e.type == 'keyup' && _currentRecordedKeys.length > 0) {
_recordCurrentCombo();
}
}
/**
* marks a character key as held down while recording a sequence
*
* @param {string} key
* @returns void
*/
function _recordKey(key) {
// one-off implementation of Array.indexOf, since IE6-9 don't support it
for (let i = 0; i < _currentRecordedKeys.length; ++i) {
if (_currentRecordedKeys[i] === key) {
return;
}
}
_currentRecordedKeys.push(key);
if (key.length === 1) {
_recordedCharacterKey = true;
}
}
/**
* marks whatever key combination that's been recorded so far as finished
* and gets ready for the next combo
*
* @returns void
*/
function _recordCurrentCombo() {
_recordedSequence.push(_currentRecordedKeys);
_currentRecordedKeys = [];
_recordedCharacterKey = false;
_finishRecording();
}
/**
* ensures each combo in a sequence is in a predictable order and formats
* key combos to be '+'-delimited
*
* modifies the sequence in-place
*
* @param {Array} sequence
* @returns void
*/
function _normalizeSequence(sequence) {
for (let i = 0; i < sequence.length; ++i) {
sequence[i].sort(function (x, y) {
// modifier keys always come first, in alphabetical order
if (x.length > 1 && y.length === 1) {
return -1;
} else if (x.length === 1 && y.length > 1) {
return 1;
}
// character keys come next (list should contain no duplicates,
// so no need for equality check)
return x > y ? 1 : -1;
});
sequence[i] = sequence[i].join('+');
}
}
/**
* finishes the current recording, passes the recorded sequence to the stored
* callback, and sets Mousetrap.handleKey back to its original function
*
* @returns void
*/
function _finishRecording() {
if (_recordedSequenceCallback) {
_normalizeSequence(_recordedSequence);
_recordedSequenceCallback(_recordedSequence);
}
// reset all recorded state
_recordedSequence = [];
_recordedSequenceCallback = null;
_currentRecordedKeys = [];
}
/**
* called to set a 1 second timeout on the current recording
*
* this is so after each key press in the sequence the recording will wait for
* 1 more second before executing the callback
*
* @returns void
*/
function _restartRecordTimer() {
clearTimeout(_recordTimer);
_recordTimer = setTimeout(_finishRecording, 1000);
}
/**
* records the next sequence and passes it to a callback once it's
* completed
*
* @param {Function} callback
* @returns void
*/
Mousetrap.prototype.record = function (callback) {
var self = this;
self.recording = true;
_recordedSequenceCallback = function () {
self.recording = false;
callback.apply(self, arguments);
};
};
/**
* stop recording
*
* @param {Function} callback
* @returns void
*/
Mousetrap.prototype.stopRecord = function () {
var self = this;
self.recording = false;
};
/**
* start recording
*
* @param {Function} callback
* @returns void
*/
Mousetrap.prototype.startRecording = function () {
var self = this;
self.recording = true;
};
Mousetrap.prototype.handleKey = function () {
var self = this;
_handleKey.apply(self, arguments);
};
Mousetrap.init();
}
-8
View File
@@ -1,8 +0,0 @@
import getMockedStudies from './getMockedStudies';
import hotkeys from './hotkeys';
const utils = { getMockedStudies, hotkeys };
export { getMockedStudies, hotkeys };
export default utils;
@@ -1,295 +0,0 @@
/**
* THIS IS A TEMPORARY FILE -- SHOULD BE REMOVED
*/
import React, { useState } from 'react';
import classnames from 'classnames';
import moment from 'moment';
import {
EmptyStudies,
Icon,
StudyListExpandedRow,
Button,
StudyListPagination,
StudyListTable,
StudyListFilter,
} from '../../components';
import utils from '../../utils';
// fix imports after refactor
import Header from './components/Header';
const filtersMeta = [
{
name: 'patientName',
displayName: 'Patient Name',
inputType: 'Text',
isSortable: true,
gridCol: 4,
},
{
name: 'mrn',
displayName: 'MRN',
inputType: 'Text',
isSortable: true,
gridCol: 2,
},
{
name: 'studyDate',
displayName: 'Study date',
inputType: 'DateRange',
isSortable: true,
gridCol: 5,
},
{
name: 'description',
displayName: 'Description',
inputType: 'Text',
isSortable: true,
gridCol: 4,
},
{
name: 'modality',
displayName: 'Modality',
inputType: 'MultiSelect',
inputProps: {
options: [
{ value: 'SEG', label: 'SEG' },
{ value: 'CT', label: 'CT' },
{ value: 'MR', label: 'MR' },
{ value: 'SR', label: 'SR' },
],
},
isSortable: true,
gridCol: 3,
},
{
name: 'accession',
displayName: 'Accession',
inputType: 'Text',
isSortable: true,
gridCol: 4,
},
{
name: 'instances',
displayName: 'Instances',
inputType: 'None',
isSortable: true,
gridCol: 2,
},
];
const defaultFilterValues = {
patientName: '',
mrn: '',
studyDate: {
startDate: null,
endDate: null,
},
description: '',
modality: undefined,
accession: '',
sortBy: '',
sortDirection: 'none',
page: 0,
resultsPerPage: 25,
};
const isFiltering = (filterValues, defaultFilterValues) => {
return Object.keys(defaultFilterValues).some(name => {
return filterValues[name] !== defaultFilterValues[name];
});
};
const StudyList = () => {
const [filterValues, setFilterValues] = useState(defaultFilterValues);
const studies = utils.getMockedStudies();
const numOfStudies = studies.length;
const [expandedRows, setExpandedRows] = useState([]);
const tableDataSource = studies.map((study, key) => {
const rowKey = key + 1;
const isExpanded = expandedRows.some(k => k === rowKey);
const {
AccessionNumber,
Modalities,
Instances,
StudyDescription,
PatientId,
PatientName,
StudyDate,
series,
} = study;
const seriesTableColumns = {
description: 'Description',
seriesNumber: 'Series',
modality: 'Modality',
Instances: 'Instances',
};
const seriesTableDataSource = series.map(seriesItem => {
const { SeriesNumber, Modality, instances } = seriesItem;
return {
description: 'Patient Protocol',
seriesNumber: SeriesNumber,
modality: Modality,
Instances: instances.length,
};
});
return {
row: [
{
key: 'patientName',
content: PatientName,
gridCol: 4,
},
{
key: 'mrn',
content: PatientId,
gridCol: 2,
},
{
key: 'studyDate',
content: (
<div>
<span className="mr-4">
{moment(StudyDate).format('MMM-DD-YYYY')}
</span>
<span>{moment(StudyDate).format('hh:mm A')}</span>
</div>
),
gridCol: 5,
},
{
key: 'description',
content: StudyDescription,
gridCol: 4,
},
{
key: 'modality',
content: Modalities,
gridCol: 3,
},
{
key: 'accession',
content: AccessionNumber,
gridCol: 4,
},
{
key: 'instances',
content: (
<>
<Icon
name="series-active"
className={classnames('inline-flex mr-2', {
'text-primary-active': isExpanded,
'text-secondary-light': !isExpanded,
})}
/>
{Instances}
</>
),
gridCol: 4,
},
],
expandedContent: (
<StudyListExpandedRow
seriesTableColumns={seriesTableColumns}
seriesTableDataSource={seriesTableDataSource}
>
<Button
rounded="full"
variant="contained"
className="mr-4 font-bold"
endIcon={<Icon name="launch-arrow" style={{ color: '#21a7c6' }} />}
>
Basic Viewer
</Button>
<Button
rounded="full"
variant="contained"
className="mr-4 font-bold"
endIcon={<Icon name="launch-arrow" style={{ color: '#21a7c6' }} />}
>
Segmentation
</Button>
<Button
rounded="full"
variant="outlined"
endIcon={<Icon name="launch-info" />}
className="font-bold"
>
Module 3
</Button>
<div className="ml-5 text-lg text-common-bright inline-flex items-center">
<Icon name="notificationwarning-diamond" className="mr-2 w-5 h-5" />
Feedback text lorem ipsum dolor sit amet
</div>
</StudyListExpandedRow>
),
onClickRow: () =>
setExpandedRows(s =>
isExpanded ? s.filter(n => rowKey !== n) : [...s, rowKey]
),
isExpanded,
};
});
const [currentPage, setCurrentPage] = useState(1);
const [perPage, setPerPage] = useState(25);
const totalPages = Math.floor(numOfStudies / perPage);
const onChangePage = page => {
if (page > totalPages) {
return;
}
setCurrentPage(page);
};
const onChangePerPage = perPage => {
setPerPage(perPage);
setCurrentPage(1);
};
const hasStudies = numOfStudies > 0;
return (
<div
className={classnames('bg-black h-full', {
'h-screen': !hasStudies,
})}
>
<Header />
<StudyListFilter
numOfStudies={numOfStudies}
filtersMeta={filtersMeta}
filterValues={filterValues}
onChange={setFilterValues}
clearFilters={() => setFilterValues(defaultFilterValues)}
isFiltering={isFiltering(filterValues, defaultFilterValues)}
/>
{hasStudies ? (
<>
<StudyListTable
tableDataSource={tableDataSource}
numOfStudies={numOfStudies}
filtersMeta={filtersMeta}
/>
<StudyListPagination
onChangePage={onChangePage}
onChangePerPage={onChangePerPage}
currentPage={currentPage}
perPage={perPage}
/>
</>
) : (
<div className="flex flex-col items-center justify-center pt-48">
<EmptyStudies />
</div>
)}
</div>
);
};
export default StudyList;