mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
1) Port query tool to React. Fixes #6131
2) Added status bar to the Query Tool. Fixes #3253 3) Ensure that row numbers should be visible in view when scrolling horizontally. Fixes #3989 4) Allow removing a single query history. Refs #4113 5) Partially fixed Macros usability issues. Ref #6969 6) Fixed an issue where the Query tool opens on minimum size if the user opens multiple query tool Window quickly. Fixes #6725 7) Relocate GIS Viewer Button to the Left Side of the Results Table. Fixes #6830 8) Fixed an issue where the connection bar is not visible. Fixes #7188 9) Fixed an issue where an Empty message popup after running a query. Fixes #7260 10) Ensure that Autocomplete should work after changing the connection. Fixes #7262 11) Fixed an issue where the copy and paste row does not work if the first column contains no data. Fixes #7294
This commit is contained in:
committed by
Akshay Joshi
parent
bf8e569bde
commit
b5b9ee46a1
@@ -42,6 +42,7 @@ const useStyles = makeStyles((theme)=>({
|
||||
}
|
||||
},
|
||||
iconButton: {
|
||||
minWidth: 0,
|
||||
padding: '3px 6px',
|
||||
'&.MuiButton-sizeSmall, &.MuiButton-outlinedSizeSmall, &.MuiButton-containedSizeSmall': {
|
||||
padding: '1px 4px',
|
||||
@@ -59,6 +60,7 @@ const useStyles = makeStyles((theme)=>({
|
||||
'&:hover': {
|
||||
backgroundColor: theme.custom.icon.hoverMain,
|
||||
color: theme.custom.icon.hoverContrastText,
|
||||
borderColor: theme.custom.icon.borderColor,
|
||||
},
|
||||
},
|
||||
splitButton: {
|
||||
@@ -72,6 +74,7 @@ const useStyles = makeStyles((theme)=>({
|
||||
xsButton: {
|
||||
padding: '2px 1px',
|
||||
height: '24px',
|
||||
minWidth: '24px',
|
||||
'& .MuiSvgIcon-root': {
|
||||
height: '0.8em',
|
||||
}
|
||||
@@ -127,27 +130,29 @@ DefaultButton.propTypes = {
|
||||
|
||||
|
||||
/* pgAdmin Icon button, takes Icon component as input */
|
||||
export const PgIconButton = forwardRef(({icon, title, shortcut, accessKey, className, splitButton, style, color, ...props}, ref)=>{
|
||||
export const PgIconButton = forwardRef(({icon, title, shortcut, className, splitButton, style, color, accesskey, ...props}, ref)=>{
|
||||
const classes = useStyles();
|
||||
|
||||
let shortcutTitle = null;
|
||||
if(accessKey || shortcut) {
|
||||
shortcutTitle = <ShortcutTitle title={title} accessKey={accessKey} shortcut={shortcut}/>;
|
||||
if(accesskey || shortcut) {
|
||||
shortcutTitle = <ShortcutTitle title={title} accesskey={accesskey} shortcut={shortcut}/>;
|
||||
}
|
||||
|
||||
/* Tooltip does not work for disabled items */
|
||||
if(props.disabled) {
|
||||
if(color == 'primary') {
|
||||
return (
|
||||
<PrimaryButton ref={ref} style={{minWidth: 0, ...style}}
|
||||
className={clsx(classes.iconButton, (splitButton ? classes.splitButton : ''), className)} {...props}>
|
||||
<PrimaryButton ref={ref} style={style}
|
||||
className={clsx(classes.iconButton, (splitButton ? classes.splitButton : ''), className)}
|
||||
accessKey={accesskey} {...props}>
|
||||
{icon}
|
||||
</PrimaryButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DefaultButton ref={ref} style={{minWidth: 0, ...style}}
|
||||
className={clsx(classes.iconButton, classes.iconButtonDefault, (splitButton ? classes.splitButton : ''), className)} {...props}>
|
||||
<DefaultButton ref={ref} style={style}
|
||||
className={clsx(classes.iconButton, classes.iconButtonDefault, (splitButton ? classes.splitButton : ''), className)}
|
||||
accessKey={accesskey} {...props}>
|
||||
{icon}
|
||||
</DefaultButton>
|
||||
);
|
||||
@@ -156,8 +161,9 @@ export const PgIconButton = forwardRef(({icon, title, shortcut, accessKey, class
|
||||
if(color == 'primary') {
|
||||
return (
|
||||
<Tooltip title={shortcutTitle || title || ''} aria-label={title || ''}>
|
||||
<PrimaryButton ref={ref} style={{minWidth: 0, ...style}}
|
||||
className={clsx(classes.iconButton, (splitButton ? classes.splitButton : ''), className)} {...props}>
|
||||
<PrimaryButton ref={ref} style={style}
|
||||
className={clsx(classes.iconButton, (splitButton ? classes.splitButton : ''), className)}
|
||||
accessKey={accesskey} {...props}>
|
||||
{icon}
|
||||
</PrimaryButton>
|
||||
</Tooltip>
|
||||
@@ -165,8 +171,9 @@ export const PgIconButton = forwardRef(({icon, title, shortcut, accessKey, class
|
||||
} else {
|
||||
return (
|
||||
<Tooltip title={shortcutTitle || title || ''} aria-label={title || ''}>
|
||||
<DefaultButton ref={ref} style={{minWidth: 0, ...style}}
|
||||
className={clsx(classes.iconButton, classes.iconButtonDefault, (splitButton ? classes.splitButton : ''), className)} {...props}>
|
||||
<DefaultButton ref={ref} style={style}
|
||||
className={clsx(classes.iconButton, classes.iconButtonDefault, (splitButton ? classes.splitButton : ''), className)}
|
||||
accessKey={accesskey} {...props}>
|
||||
{icon}
|
||||
</DefaultButton>
|
||||
</Tooltip>
|
||||
@@ -179,7 +186,7 @@ PgIconButton.propTypes = {
|
||||
icon: CustomPropTypes.children,
|
||||
title: PropTypes.string.isRequired,
|
||||
shortcut: CustomPropTypes.shortcut,
|
||||
accessKey: PropTypes.string,
|
||||
accesskey: PropTypes.string,
|
||||
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
style: PropTypes.object,
|
||||
color: PropTypes.oneOf(['primary', 'default', undefined]),
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {default as OrigCodeMirror} from 'bundled_codemirror';
|
||||
import OrigCodeMirror from 'bundled_codemirror';
|
||||
import {useOnScreen} from 'sources/custom_hooks';
|
||||
import PropTypes from 'prop-types';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import pgWindow from 'sources/window';
|
||||
import gettext from 'sources/gettext';
|
||||
import { Box, InputAdornment, makeStyles } from '@material-ui/core';
|
||||
import clsx from 'clsx';
|
||||
@@ -31,6 +31,11 @@ const useStyles = makeStyles((theme)=>({
|
||||
root: {
|
||||
position: 'relative',
|
||||
},
|
||||
hideCursor: {
|
||||
'& .CodeMirror-cursors': {
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
findDialog: {
|
||||
position: 'absolute',
|
||||
zIndex: 99,
|
||||
@@ -70,31 +75,40 @@ function parseQuery(query, useRegex=false, matchCase=false) {
|
||||
return query;
|
||||
}
|
||||
|
||||
function getRegexFinder(query) {
|
||||
return (stream) => {
|
||||
query.lastIndex = stream.pos;
|
||||
var match = query.exec(stream.string);
|
||||
if (match && match.index == stream.pos) {
|
||||
stream.pos += match[0].length || 1;
|
||||
return 'searching';
|
||||
} else if (match) {
|
||||
stream.pos = match.index;
|
||||
} else {
|
||||
stream.skipToEnd();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function getPlainStringFinder(query, matchCase) {
|
||||
return (stream) => {
|
||||
var matchIndex = (matchCase ? stream.string : stream.string.toLowerCase()).indexOf(query, stream.pos);
|
||||
if(matchIndex == -1) {
|
||||
stream.skipToEnd();
|
||||
} else if(matchIndex == stream.pos) {
|
||||
stream.pos += query.length;
|
||||
return 'searching';
|
||||
} else {
|
||||
stream.pos = matchIndex;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function searchOverlay(query, matchCase) {
|
||||
return {
|
||||
token: typeof query == 'string' ?
|
||||
(stream) =>{
|
||||
var matchIndex = (matchCase ? stream.string : stream.string.toLowerCase()).indexOf(query, stream.pos);
|
||||
if(matchIndex == -1) {
|
||||
stream.skipToEnd();
|
||||
} else if(matchIndex == stream.pos) {
|
||||
stream.pos += query.length;
|
||||
return 'searching';
|
||||
} else {
|
||||
stream.pos = matchIndex;
|
||||
}
|
||||
} : (stream) => {
|
||||
query.lastIndex = stream.pos;
|
||||
var match = query.exec(stream.string);
|
||||
if (match && match.index == stream.pos) {
|
||||
stream.pos += match[0].length || 1;
|
||||
return 'searching';
|
||||
} else if (match) {
|
||||
stream.pos = match.index;
|
||||
} else {
|
||||
stream.skipToEnd();
|
||||
}
|
||||
}
|
||||
getPlainStringFinder(query, matchCase) : getRegexFinder(query)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -260,28 +274,96 @@ FindDialog.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
function handleDrop(editor, e) {
|
||||
var dropDetails = null;
|
||||
try {
|
||||
dropDetails = JSON.parse(e.dataTransfer.getData('text'));
|
||||
|
||||
/* Stop firefox from redirecting */
|
||||
|
||||
if(e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.stopPropagation) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
} catch(error) {
|
||||
/* if parsing fails, it must be the drag internal of codemirror text */
|
||||
return;
|
||||
}
|
||||
|
||||
var cursor = editor.coordsChar({
|
||||
left: e.x,
|
||||
top: e.y,
|
||||
});
|
||||
editor.replaceRange(dropDetails.text, cursor);
|
||||
editor.focus();
|
||||
editor.setSelection({
|
||||
...cursor,
|
||||
ch: cursor.ch + dropDetails.cur.from,
|
||||
},{
|
||||
...cursor,
|
||||
ch: cursor.ch +dropDetails.cur.to,
|
||||
});
|
||||
}
|
||||
|
||||
function calcFontSize(fontSize) {
|
||||
if(fontSize) {
|
||||
fontSize = parseFloat((Math.round(parseFloat(fontSize + 'e+2')) + 'e-2'));
|
||||
let rounded = Number(fontSize);
|
||||
if(rounded > 0) {
|
||||
return rounded + 'em';
|
||||
}
|
||||
}
|
||||
return '1em';
|
||||
}
|
||||
|
||||
/* React wrapper for CodeMirror */
|
||||
export default function CodeMirror({currEditor, name, value, options, events, readonly, disabled, className}) {
|
||||
export default function CodeMirror({currEditor, name, value, options, events, readonly, disabled, className, autocomplete=false}) {
|
||||
const taRef = useRef();
|
||||
const editor = useRef();
|
||||
const cmWrapper = useRef();
|
||||
const isVisibleTrack = useRef();
|
||||
const classes = useStyles();
|
||||
const [[showFind, isReplace], setShowFind] = useState([false, false]);
|
||||
const defaultOptions = {
|
||||
tabindex: '0',
|
||||
lineNumbers: true,
|
||||
styleSelectedText: true,
|
||||
mode: 'text/x-pgsql',
|
||||
foldOptions: {
|
||||
widget: '\u2026',
|
||||
},
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
extraKeys: pgAdmin.Browser.editor_shortcut_keys,
|
||||
dragDrop: false,
|
||||
screenReaderLabel: gettext('SQL editor'),
|
||||
};
|
||||
const defaultOptions = useMemo(()=>{
|
||||
let goLeftKey = 'Ctrl-Alt-Left',
|
||||
goRightKey = 'Ctrl-Alt-Right',
|
||||
commentKey = 'Ctrl-/';
|
||||
if(isMac()) {
|
||||
goLeftKey = 'Cmd-Alt-Left';
|
||||
goRightKey = 'Cmd-Alt-Right';
|
||||
commentKey = 'Cmd-/';
|
||||
}
|
||||
return {
|
||||
tabindex: '0',
|
||||
lineNumbers: true,
|
||||
styleSelectedText: true,
|
||||
mode: 'text/x-pgsql',
|
||||
foldOptions: {
|
||||
widget: '\u2026',
|
||||
},
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
extraKeys: {
|
||||
// Autocomplete sql command
|
||||
...(autocomplete ? {
|
||||
'Ctrl-Space': 'autocomplete',
|
||||
}: {}),
|
||||
'Alt-Up': 'goLineUp',
|
||||
'Alt-Down': 'goLineDown',
|
||||
// Move word by word left/right
|
||||
[goLeftKey]: 'goGroupLeft',
|
||||
[goRightKey]: 'goGroupRight',
|
||||
// Allow user to delete Tab(s)
|
||||
'Shift-Tab': 'indentLess',
|
||||
//comment
|
||||
[commentKey]: 'toggleComment',
|
||||
},
|
||||
dragDrop: true,
|
||||
screenReaderLabel: gettext('SQL editor'),
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(()=>{
|
||||
const finalOptions = {...defaultOptions, ...options};
|
||||
@@ -317,29 +399,61 @@ export default function CodeMirror({currEditor, name, value, options, events, re
|
||||
setShowFind([false, false]);
|
||||
setShowFind([true, true]);
|
||||
}
|
||||
}
|
||||
},
|
||||
'Cmd-G': false,
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(events||{}).forEach((eventName)=>{
|
||||
editor.current.on(eventName, events[eventName]);
|
||||
});
|
||||
|
||||
editor.current.on('drop', handleDrop);
|
||||
initPreferences();
|
||||
return ()=>{
|
||||
editor.current?.toTextArea();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const initPreferences = ()=>{
|
||||
reflectPreferences();
|
||||
pgWindow?.pgAdmin?.Browser?.onPreferencesChange('sqleditor', function() {
|
||||
reflectPreferences();
|
||||
});
|
||||
};
|
||||
|
||||
const reflectPreferences = ()=>{
|
||||
let pref = pgWindow?.pgAdmin?.Browser?.get_preferences_for_module('sqleditor') || {};
|
||||
let wrapEle = editor?.current.getWrapperElement();
|
||||
wrapEle && (wrapEle.style.fontSize = calcFontSize(pref.sql_font_size));
|
||||
|
||||
if(pref.plain_editor_mode) {
|
||||
editor?.current.setOption('mode', 'text/plain');
|
||||
/* Although not required, setting explicitly as codemirror will remove code folding only on next edit */
|
||||
editor?.current.setOption('foldGutter', false);
|
||||
} else {
|
||||
editor?.current.setOption('mode', 'text/x-pgsql');
|
||||
editor?.current.setOption('foldGutter', pref.code_folding);
|
||||
}
|
||||
|
||||
editor?.current.setOption('indentWithTabs', pref.indent_with_tabs);
|
||||
editor?.current.setOption('indentUnit', pref.tab_size);
|
||||
editor?.current.setOption('tabSize', pref.tab_size);
|
||||
editor?.current.setOption('lineWrapping', pref.wrap_code);
|
||||
editor?.current.setOption('autoCloseBrackets', pref.insert_pair_brackets);
|
||||
editor?.current.setOption('matchBrackets', pref.brace_matching);
|
||||
editor?.current.refresh();
|
||||
};
|
||||
|
||||
useEffect(()=>{
|
||||
if(editor.current) {
|
||||
if(disabled) {
|
||||
cmWrapper.current.classList.add('cm_disabled');
|
||||
editor.current.setOption('readOnly', 'nocursor');
|
||||
editor.current.setOption('readOnly', true);
|
||||
cmWrapper.current.classList.add(classes.hideCursor);
|
||||
} else if(readonly) {
|
||||
cmWrapper.current.classList.add('cm_disabled');
|
||||
editor.current.setOption('readOnly', true);
|
||||
editor.current.addKeyMap({'Tab': false});
|
||||
editor.current.addKeyMap({'Shift-Tab': false});
|
||||
cmWrapper.current.classList.add(classes.hideCursor);
|
||||
} else {
|
||||
cmWrapper.current.classList.remove('cm_disabled');
|
||||
editor.current.setOption('readOnly', false);
|
||||
@@ -392,4 +506,5 @@ CodeMirror.propTypes = {
|
||||
readonly: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
className: CustomPropTypes.className,
|
||||
autocomplete: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -22,17 +22,16 @@ ExternalIcon.propTypes = {
|
||||
Icon: PropTypes.elementType.isRequired,
|
||||
};
|
||||
|
||||
export const QueryToolIcon = ()=><ExternalIcon Icon={QueryToolSvg} style={{height: '0.7em'}} />;
|
||||
export const SaveDataIcon = ()=><ExternalIcon Icon={SaveDataSvg} style={{height: '0.7em'}} />;
|
||||
export const QueryToolIcon = ()=><ExternalIcon Icon={QueryToolSvg} style={{height: '1rem'}} />;
|
||||
export const SaveDataIcon = ()=><ExternalIcon Icon={SaveDataSvg} style={{height: '1rem'}} />;
|
||||
export const PasteIcon = ()=><ExternalIcon Icon={PasteSvg} />;
|
||||
export const FilterIcon = ()=><ExternalIcon Icon={FilterSvg} />;
|
||||
export const CommitIcon = ()=><ExternalIcon Icon={CommitSvg} />;
|
||||
export const RollbackIcon = ()=><ExternalIcon Icon={RollbackSvg} />;
|
||||
export const ClearIcon = ()=><ExternalIcon Icon={ClearSvg} />;
|
||||
export const ConnectedIcon = ()=><ExternalIcon Icon={ConnectedSvg} style={{height: '0.7em'}} />;
|
||||
export const DisonnectedIcon = ()=><ExternalIcon Icon={DisconnectedSvg} style={{height: '0.7em'}} />;
|
||||
export const ConnectedIcon = ()=><ExternalIcon Icon={ConnectedSvg} style={{height: '1rem'}} />;
|
||||
export const DisonnectedIcon = ()=><ExternalIcon Icon={DisconnectedSvg} style={{height: '1rem'}} />;
|
||||
export const RegexIcon = ()=><ExternalIcon Icon={RegexSvg} />;
|
||||
export const FormatCaseIcon = ()=><ExternalIcon Icon={FormatCaseSvg} />;
|
||||
export const ExpandDialogIcon = ()=><ExternalIcon Icon={Expand} style={{height: '1.2em'}} />;
|
||||
export const MinimizeDialogIcon = ()=><ExternalIcon Icon={Collapse} style={{height: '1.4em'}} />;
|
||||
|
||||
export const ExpandDialogIcon = ()=><ExternalIcon Icon={Expand} style={{height: '1.2rem'}} />;
|
||||
export const MinimizeDialogIcon = ()=><ExternalIcon Icon={Collapse} style={{height: '1.4rem'}} />;
|
||||
|
||||
@@ -207,6 +207,7 @@ FormInputSQL.propTypes = {
|
||||
helpMessage: PropTypes.string,
|
||||
testcid: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
controlProps: PropTypes.object,
|
||||
noLabel: PropTypes.bool,
|
||||
change: PropTypes.func,
|
||||
};
|
||||
@@ -570,7 +571,7 @@ export function InputRadio({ helpid, value, onChange, controlProps, readonly, ..
|
||||
disableRipple
|
||||
{...props}
|
||||
/>
|
||||
|
||||
|
||||
}
|
||||
label={controlProps.label}
|
||||
className={(readonly || props.disabled) ? classes.readOnlySwitch : null}
|
||||
@@ -821,6 +822,7 @@ export const InputSelect = forwardRef(({
|
||||
const [[finalOptions, isLoading], setFinalOptions] = useState([[], true]);
|
||||
const theme = useTheme();
|
||||
|
||||
|
||||
/* React will always take options var as changed parameter. So,
|
||||
We cannot run the below effect with options dependency as it will keep on
|
||||
loading the options. optionsReloadBasis is helpful to avoid repeated
|
||||
@@ -1174,20 +1176,21 @@ const useStylesFormFooter = makeStyles((theme) => ({
|
||||
}));
|
||||
|
||||
/* The form footer used mostly for showing error */
|
||||
export function FormFooterMessage(props) {
|
||||
export function FormFooterMessage({style, ...props}) {
|
||||
const classes = useStylesFormFooter();
|
||||
|
||||
if (!props.message) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
<Box className={classes.root} style={style}>
|
||||
<NotifierMessage {...props}></NotifierMessage>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
FormFooterMessage.propTypes = {
|
||||
style: PropTypes.object,
|
||||
message: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import PropTypes from 'prop-types';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
|
||||
/* React wrapper for JsonEditor */
|
||||
export default function JsonEditor({currEditor, value, options, className}) {
|
||||
export default function JsonEditor({getEditor, value, options, className}) {
|
||||
const eleRef = useRef();
|
||||
const editor = useRef();
|
||||
const defaultOptions = {
|
||||
@@ -34,9 +34,10 @@ export default function JsonEditor({currEditor, value, options, className}) {
|
||||
}
|
||||
});
|
||||
editor.current.setText(value);
|
||||
currEditor && currEditor(editor.current);
|
||||
getEditor?.(editor.current);
|
||||
editor.current.focus();
|
||||
return ()=>editor.current?.destroy();
|
||||
/* Required by json editor */
|
||||
eleRef.current.style.height = eleRef.current.offsetHeight + 'px';
|
||||
}, []);
|
||||
|
||||
useMemo(() => {
|
||||
@@ -53,7 +54,7 @@ export default function JsonEditor({currEditor, value, options, className}) {
|
||||
}
|
||||
|
||||
JsonEditor.propTypes = {
|
||||
currEditor: PropTypes.func,
|
||||
getEditor: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
options: PropTypes.object,
|
||||
className: CustomPropTypes.className,
|
||||
|
||||
@@ -18,6 +18,9 @@ const useStyles = makeStyles((theme)=>({
|
||||
'& .szh-menu': {
|
||||
padding: '4px 0px',
|
||||
zIndex: 1000,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
border: `1px solid ${theme.otherVars.borderColor}`
|
||||
},
|
||||
'& .szh-menu__divider': {
|
||||
margin: 0,
|
||||
|
||||
@@ -4,8 +4,13 @@ import PropTypes from 'prop-types';
|
||||
import { isMac } from '../keyboard_shortcuts';
|
||||
import _ from 'lodash';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
import gettext from 'sources/gettext';
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
shortcutTitle: {
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
},
|
||||
shortcut: {
|
||||
justifyContent: 'center',
|
||||
marginTop: '0.125rem',
|
||||
@@ -19,10 +24,43 @@ const useStyles = makeStyles((theme)=>({
|
||||
},
|
||||
}));
|
||||
|
||||
export function getBrowserAccesskey() {
|
||||
/* Ref: https://github.com/tillsanders/access-key-label-polyfill/ */
|
||||
let ua = window.navigator.userAgent;
|
||||
// macOS
|
||||
if (ua.match(/macintosh/i)) {
|
||||
// Firefox
|
||||
if (ua.match(/firefox/i)) {
|
||||
const firefoxVersion = ua.match(/firefox[\s/](\d+)/i);
|
||||
// Firefox < v14
|
||||
if (firefoxVersion[1] && parseInt(firefoxVersion[1], 10) < 14) {
|
||||
return ['Ctrl'];
|
||||
}
|
||||
}
|
||||
return ['Option', 'Ctrl'];
|
||||
}
|
||||
|
||||
// Internet Explorer / Edge
|
||||
if (ua.match(/msie|trident/i) || ua.match(/\sedg/i)) {
|
||||
return ['Alt'];
|
||||
}
|
||||
|
||||
// iOS / iPadOS
|
||||
if (ua.match(/(ipod|iphone|ipad)/i)) {
|
||||
// accesskeyLabel is supported > v14, but we're not checking for versions here, since we use
|
||||
// feature support detection
|
||||
return ['Option', 'Ctrl'];
|
||||
}
|
||||
|
||||
// Fallback
|
||||
// Note: Apparently, Chrome for Android is not even supporting accesskey, so be prepared.
|
||||
return [gettext('Accesskey')];
|
||||
}
|
||||
|
||||
export function shortcutToString(shortcut, accesskey=null, asArray=false) {
|
||||
let keys = [];
|
||||
if(accesskey) {
|
||||
keys.push('Accesskey');
|
||||
keys = getBrowserAccesskey();
|
||||
keys.push(_.capitalize(accesskey?.toUpperCase()));
|
||||
} else if(shortcut) {
|
||||
shortcut.alt && keys.push((isMac() ? 'Option' : 'Alt'));
|
||||
@@ -41,12 +79,12 @@ export function shortcutToString(shortcut, accesskey=null, asArray=false) {
|
||||
}
|
||||
|
||||
/* The tooltip content to show shortcut details */
|
||||
export default function ShortcutTitle({title, shortcut, accessKey}) {
|
||||
export default function ShortcutTitle({title, shortcut, accesskey}) {
|
||||
const classes = useStyles();
|
||||
let keys = shortcutToString(shortcut, accessKey, true);
|
||||
let keys = shortcutToString(shortcut, accesskey, true);
|
||||
return (
|
||||
<>
|
||||
<div>{title}</div>
|
||||
<div className={classes.shortcutTitle}>{title}</div>
|
||||
<div className={classes.shortcut}>
|
||||
{keys.map((key, i)=>{
|
||||
return <div key={i} className={classes.key}>{key}</div>;
|
||||
@@ -59,5 +97,5 @@ export default function ShortcutTitle({title, shortcut, accessKey}) {
|
||||
ShortcutTitle.propTypes = {
|
||||
title: PropTypes.string,
|
||||
shortcut: CustomPropTypes.shortcut,
|
||||
accessKey: PropTypes.string,
|
||||
accesskey: PropTypes.string,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user