1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-07-13 00:10:37 +02:00

Desktop, Mobile: Improve focus handling

This commit is contained in:
Laurent Cozic
2024-04-01 15:34:22 +01:00
parent 554fb7026a
commit 00084c5798
34 changed files with 119 additions and 92 deletions

View File

@ -322,9 +322,7 @@ packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js
packages/app-desktop/gui/NoteList/NoteList.js
packages/app-desktop/gui/NoteList/NoteList2.js packages/app-desktop/gui/NoteList/NoteList2.js
packages/app-desktop/gui/NoteList/NoteListSource.js
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js
packages/app-desktop/gui/NoteList/commands/index.js packages/app-desktop/gui/NoteList/commands/index.js
packages/app-desktop/gui/NoteList/utils/canManuallySortNotes.js packages/app-desktop/gui/NoteList/utils/canManuallySortNotes.js
@ -350,7 +348,6 @@ packages/app-desktop/gui/NoteListHeader/utils/getColumnTitle.js
packages/app-desktop/gui/NoteListHeader/utils/useContextMenu.js packages/app-desktop/gui/NoteListHeader/utils/useContextMenu.js
packages/app-desktop/gui/NoteListHeader/utils/validateColumns.test.js packages/app-desktop/gui/NoteListHeader/utils/validateColumns.test.js
packages/app-desktop/gui/NoteListHeader/utils/validateColumns.js packages/app-desktop/gui/NoteListHeader/utils/validateColumns.js
packages/app-desktop/gui/NoteListItem.js
packages/app-desktop/gui/NoteListItem/NoteListItem.js packages/app-desktop/gui/NoteListItem/NoteListItem.js
packages/app-desktop/gui/NoteListItem/utils/getNoteTitleHtml.js packages/app-desktop/gui/NoteListItem/utils/getNoteTitleHtml.js
packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.test.js packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.test.js
@ -1148,6 +1145,7 @@ packages/lib/types.js
packages/lib/utils/ActionLogger.test.js packages/lib/utils/ActionLogger.test.js
packages/lib/utils/ActionLogger.js packages/lib/utils/ActionLogger.js
packages/lib/utils/credentialFiles.js packages/lib/utils/credentialFiles.js
packages/lib/utils/focusHandler.js
packages/lib/utils/ipc/RemoteMessenger.test.js packages/lib/utils/ipc/RemoteMessenger.test.js
packages/lib/utils/ipc/RemoteMessenger.js packages/lib/utils/ipc/RemoteMessenger.js
packages/lib/utils/ipc/TestMessenger.js packages/lib/utils/ipc/TestMessenger.js

View File

@ -101,6 +101,17 @@ module.exports = {
'no-unneeded-ternary': 'error', 'no-unneeded-ternary': 'error',
'github/array-foreach': ['error'], 'github/array-foreach': ['error'],
'no-restricted-properties': ['error',
{
'property': 'focus',
'message': 'Please use focusHandler::focus() instead',
},
{
'property': 'blur',
'message': 'Please use focusHandler::blur() instead',
},
],
// ------------------------------- // -------------------------------
// Formatting // Formatting
// ------------------------------- // -------------------------------

4
.gitignore vendored
View File

@ -302,9 +302,7 @@ packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js
packages/app-desktop/gui/NoteList/NoteList.js
packages/app-desktop/gui/NoteList/NoteList2.js packages/app-desktop/gui/NoteList/NoteList2.js
packages/app-desktop/gui/NoteList/NoteListSource.js
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js
packages/app-desktop/gui/NoteList/commands/index.js packages/app-desktop/gui/NoteList/commands/index.js
packages/app-desktop/gui/NoteList/utils/canManuallySortNotes.js packages/app-desktop/gui/NoteList/utils/canManuallySortNotes.js
@ -330,7 +328,6 @@ packages/app-desktop/gui/NoteListHeader/utils/getColumnTitle.js
packages/app-desktop/gui/NoteListHeader/utils/useContextMenu.js packages/app-desktop/gui/NoteListHeader/utils/useContextMenu.js
packages/app-desktop/gui/NoteListHeader/utils/validateColumns.test.js packages/app-desktop/gui/NoteListHeader/utils/validateColumns.test.js
packages/app-desktop/gui/NoteListHeader/utils/validateColumns.js packages/app-desktop/gui/NoteListHeader/utils/validateColumns.js
packages/app-desktop/gui/NoteListItem.js
packages/app-desktop/gui/NoteListItem/NoteListItem.js packages/app-desktop/gui/NoteListItem/NoteListItem.js
packages/app-desktop/gui/NoteListItem/utils/getNoteTitleHtml.js packages/app-desktop/gui/NoteListItem/utils/getNoteTitleHtml.js
packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.test.js packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.test.js
@ -1128,6 +1125,7 @@ packages/lib/types.js
packages/lib/utils/ActionLogger.test.js packages/lib/utils/ActionLogger.test.js
packages/lib/utils/ActionLogger.js packages/lib/utils/ActionLogger.js
packages/lib/utils/credentialFiles.js packages/lib/utils/credentialFiles.js
packages/lib/utils/focusHandler.js
packages/lib/utils/ipc/RemoteMessenger.test.js packages/lib/utils/ipc/RemoteMessenger.test.js
packages/lib/utils/ipc/RemoteMessenger.js packages/lib/utils/ipc/RemoteMessenger.js
packages/lib/utils/ipc/TestMessenger.js packages/lib/utils/ipc/TestMessenger.js

View File

@ -441,6 +441,7 @@ class AppGui {
if (cmd === 'activate') { if (cmd === 'activate') {
const w = this.widget('mainWindow').focusedWidget; const w = this.widget('mainWindow').focusedWidget;
if (w.name === 'folderList') { if (w.name === 'folderList') {
// eslint-disable-next-line no-restricted-properties
this.widget('noteList').focus(); this.widget('noteList').focus();
} else if (w.name === 'noteList' || w.name === 'noteText') { } else if (w.name === 'noteList' || w.name === 'noteText') {
this.processPromptCommand('edit $n'); this.processPromptCommand('edit $n');

View File

@ -243,6 +243,7 @@ class AppComponent extends Component {
if (!ref) break; if (!ref) break;
lastRef = ref; lastRef = ref;
} }
// eslint-disable-next-line no-restricted-properties
if (lastRef) lastRef.focus(); if (lastRef) lastRef.focus();
} }
} }

View File

@ -428,6 +428,7 @@ export default class ElectronAppWrapper {
if (!win) return; if (!win) return;
if (win.isMinimized()) win.restore(); if (win.isMinimized()) win.restore();
win.show(); win.show();
// eslint-disable-next-line no-restricted-properties
win.focus(); win.focus();
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
const url = argv.find((arg) => isCallbackUrl(arg)); const url = argv.find((arg) => isCallbackUrl(arg));

View File

@ -13,6 +13,7 @@ import Button from '../Button/Button';
import bridge from '../../services/bridge'; import bridge from '../../services/bridge';
import shim from '@joplin/lib/shim'; import shim from '@joplin/lib/shim';
import FolderIconBox from '../FolderIconBox'; import FolderIconBox from '../FolderIconBox';
import { focus } from '@joplin/lib/utils/focusHandler';
interface Props { interface Props {
themeId: number; themeId: number;
@ -46,7 +47,7 @@ export default function(props: Props) {
}, [props.dispatch]); }, [props.dispatch]);
useEffect(() => { useEffect(() => {
titleInputRef.current.focus(); focus('Dialog::titleInputRef', titleInputRef.current);
setTimeout(() => { setTimeout(() => {
titleInputRef.current.select(); titleInputRef.current.select();

View File

@ -32,6 +32,7 @@ import useStyles from '../utils/useStyles';
import useContextMenu from '../utils/useContextMenu'; import useContextMenu from '../utils/useContextMenu';
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage'; import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
import useEditorSearchHandler from '../utils/useEditorSearchHandler'; import useEditorSearchHandler from '../utils/useEditorSearchHandler';
import { focus } from '@joplin/lib/utils/focusHandler';
function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions { function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions {
return { ...override }; return { ...override };
@ -142,7 +143,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
} }
} else if (cmd.name === 'editor.focus') { } else if (cmd.name === 'editor.focus') {
if (props.visiblePanes.indexOf('editor') >= 0) { if (props.visiblePanes.indexOf('editor') >= 0) {
editorRef.current.focus(); focus('v5/CodeMirror::editor.focus', editorRef.current);
} else { } else {
// If we just call focus() then the iframe is focused, // If we just call focus() then the iframe is focused,
// but not its content, such that scrolling up / down // but not its content, such that scrolling up / down
@ -188,7 +189,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
textItalic: () => wrapSelectionWithStrings('*', '*', _('emphasised text')), textItalic: () => wrapSelectionWithStrings('*', '*', _('emphasised text')),
textLink: async () => { textLink: async () => {
const url = await dialogs.prompt(_('Insert Hyperlink')); const url = await dialogs.prompt(_('Insert Hyperlink'));
editorRef.current.focus(); focus('v5/CodeMirror::textLink', editorRef.current);
if (url) wrapSelectionWithStrings('[', `](${url})`); if (url) wrapSelectionWithStrings('[', `](${url})`);
}, },
textCode: () => { textCode: () => {
@ -699,30 +700,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
return output; return output;
}, [styles.cellViewer, props.visiblePanes]); }, [styles.cellViewer, props.visiblePanes]);
// Disable this effect to fix this:
//
// https://github.com/laurent22/joplin/issues/6514 It doesn't seem essential
// to automatically focus the editor when the layout changes. The workaround
// is to toggle the layout Cmd+L, then manually focus the editor Cmd+Shift+B.
//
// On the other hand, if we automatically focus the editor, and the user
// does not want this, there's no workaround, so it's better to have this
// disabled.
// const editorPaneVisible = props.visiblePanes.indexOf('editor') >= 0;
// useEffect(() => {
// if (!editorRef.current) return;
// // Anytime the user toggles the visible panes AND the editor is visible as a result
// // we should focus the editor
// // The intuition is that a panel toggle (with editor in view) is the equivalent of
// // an editor interaction so users should expect the editor to be focused
// if (editorPaneVisible) {
// editorRef.current.focus();
// }
// }, [editorPaneVisible]);
useEffect(() => { useEffect(() => {
if (!editorRef.current) return; if (!editorRef.current) return;

View File

@ -33,6 +33,7 @@ import Setting from '@joplin/lib/models/Setting';
// import eventManager from '@joplin/lib/eventManager'; // import eventManager from '@joplin/lib/eventManager';
import { reg } from '@joplin/lib/registry'; import { reg } from '@joplin/lib/registry';
import { focus } from '@joplin/lib/utils/focusHandler';
// Based on http://pypl.github.io/PYPL.html // Based on http://pypl.github.io/PYPL.html
const topLanguages = [ const topLanguages = [
@ -137,7 +138,7 @@ function Editor(props: EditorProps, ref: any) {
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const editor_drop = useCallback((cm: any, _event: any) => { const editor_drop = useCallback((cm: any, _event: any) => {
cm.focus(); focus('v5/Editor::editor_drop', cm);
}, []); }, []);
const editor_drag = useCallback((cm: any, event: any) => { const editor_drag = useCallback((cm: any, event: any) => {

View File

@ -311,30 +311,6 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
return output; return output;
}, [styles.cellViewer, props.visiblePanes]); }, [styles.cellViewer, props.visiblePanes]);
// Disable this effect to fix this:
//
// https://github.com/laurent22/joplin/issues/6514 It doesn't seem essential
// to automatically focus the editor when the layout changes. The workaround
// is to toggle the layout Cmd+L, then manually focus the editor Cmd+Shift+B.
//
// On the other hand, if we automatically focus the editor, and the user
// does not want this, there's no workaround, so it's better to have this
// disabled.
// const editorPaneVisible = props.visiblePanes.indexOf('editor') >= 0;
// useEffect(() => {
// if (!editorRef.current) return;
// // Anytime the user toggles the visible panes AND the editor is visible as a result
// // we should focus the editor
// // The intuition is that a panel toggle (with editor in view) is the equivalent of
// // an editor interaction so users should expect the editor to be focused
// if (editorPaneVisible) {
// editorRef.current.focus();
// }
// }, [editorPaneVisible]);
useEditorSearchHandler({ useEditorSearchHandler({
setLocalSearchResultCount: props.setLocalSearchResultCount, setLocalSearchResultCount: props.setLocalSearchResultCount,
searchMarkers: props.searchMarkers, searchMarkers: props.searchMarkers,

View File

@ -8,6 +8,7 @@ import { EditorCommandType } from '@joplin/editor/types';
import Logger from '@joplin/utils/Logger'; import Logger from '@joplin/utils/Logger';
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl'; import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
import { MarkupLanguage } from '@joplin/renderer'; import { MarkupLanguage } from '@joplin/renderer';
import { focus } from '@joplin/lib/utils/focusHandler';
const logger = Logger.create('CodeMirror 6 commands'); const logger = Logger.create('CodeMirror 6 commands');
@ -88,7 +89,7 @@ const useEditorCommands = (props: Props) => {
}, },
textLink: async () => { textLink: async () => {
const url = await dialogs.prompt(_('Insert Hyperlink')); const url = await dialogs.prompt(_('Insert Hyperlink'));
editorRef.current.focus(); focus('useEditorCommands::textLink', editorRef.current);
if (url) wrapSelectionWithStrings(editorRef.current, '[', `](${url})`); if (url) wrapSelectionWithStrings(editorRef.current, '[', `](${url})`);
}, },
insertText: (value: any) => editorRef.current.insertText(value), insertText: (value: any) => editorRef.current.insertText(value),
@ -116,7 +117,7 @@ const useEditorCommands = (props: Props) => {
}, },
'editor.focus': () => { 'editor.focus': () => {
if (props.visiblePanes.indexOf('editor') >= 0) { if (props.visiblePanes.indexOf('editor') >= 0) {
editorRef.current.editor.focus(); focus('useEditorCommands::editor.focus', editorRef.current.editor);
} else { } else {
// If we just call focus() then the iframe is focused, // If we just call focus() then the iframe is focused,
// but not its content, such that scrolling up / down // but not its content, such that scrolling up / down

View File

@ -32,6 +32,7 @@ import markupRenderOptions from '../../utils/markupRenderOptions';
import { DropHandler } from '../../utils/useDropHandler'; import { DropHandler } from '../../utils/useDropHandler';
import Logger from '@joplin/utils/Logger'; import Logger from '@joplin/utils/Logger';
import useWebViewApi from './utils/useWebViewApi'; import useWebViewApi from './utils/useWebViewApi';
import { focus } from '@joplin/lib/utils/focusHandler';
const md5 = require('md5'); const md5 = require('md5');
const { clipboard } = require('electron'); const { clipboard } = require('electron');
const supportedLocales = require('./supportedLocales'); const supportedLocales = require('./supportedLocales');
@ -204,7 +205,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, cmd.value, markupRenderOptions({ bodyOnly: true })); const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, cmd.value, markupRenderOptions({ bodyOnly: true }));
editor.insertContent(result.html); editor.insertContent(result.html);
} else if (cmd.name === 'editor.focus') { } else if (cmd.name === 'editor.focus') {
editor.focus(); focus('TinyMCE::editor.focus', editor);
} else if (cmd.name === 'editor.execCommand') { } else if (cmd.name === 'editor.execCommand') {
if (!('ui' in cmd.value)) cmd.value.ui = false; if (!('ui' in cmd.value)) cmd.value.ui = false;
if (!('value' in cmd.value)) cmd.value.value = null; if (!('value' in cmd.value)) cmd.value.value = null;

View File

@ -1,6 +1,7 @@
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import { MarkupToHtml } from '@joplin/renderer'; import { MarkupToHtml } from '@joplin/renderer';
import { TinyMceEditorEvents } from './types'; import { TinyMceEditorEvents } from './types';
import { focus } from '@joplin/lib/utils/focusHandler';
const taboverride = require('taboverride'); const taboverride = require('taboverride');
interface SourceInfo { interface SourceInfo {
@ -13,7 +14,7 @@ interface SourceInfo {
function dialogTextArea_keyDown(event: any) { function dialogTextArea_keyDown(event: any) {
if (event.key === 'Tab') { if (event.key === 'Tab') {
window.requestAnimationFrame(() => event.target.focus()); window.requestAnimationFrame(() => focus('openEditDialog::dialogTextArea_keyDown', event.target));
} }
} }

View File

@ -1,5 +1,6 @@
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService'; import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import { focus } from '@joplin/lib/utils/focusHandler';
export const declaration: CommandDeclaration = { export const declaration: CommandDeclaration = {
name: 'focusElementNoteTitle', name: 'focusElementNoteTitle',
@ -11,7 +12,7 @@ export const runtime = (comp: any): CommandRuntime => {
return { return {
execute: async () => { execute: async () => {
if (!comp.titleInputRef.current) return; if (!comp.titleInputRef.current) return;
comp.titleInputRef.current.focus(); focus('focusElementNoteTitle', comp.titleInputRef.current);
}, },
enabledCondition: 'oneNoteSelected', enabledCondition: 'oneNoteSelected',
}; };

View File

@ -1,5 +1,6 @@
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService'; import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import { focus } from '@joplin/lib/utils/focusHandler';
export const declaration: CommandDeclaration = { export const declaration: CommandDeclaration = {
name: 'showLocalSearch', name: 'showLocalSearch',
@ -13,7 +14,7 @@ export const runtime = (comp: any): CommandRuntime => {
comp.editorRef.current.execCommand({ name: 'search' }); comp.editorRef.current.execCommand({ name: 'search' });
} else { } else {
if (comp.noteSearchBarRef.current) { if (comp.noteSearchBarRef.current) {
comp.noteSearchBarRef.current.focus(); focus('showLocalSearch', comp.noteSearchBarRef.current);
} else { } else {
comp.setShowLocalSearch(true); comp.setShowLocalSearch(true);
} }

View File

@ -14,6 +14,7 @@ import { reg } from '@joplin/lib/registry';
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker'; import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
import { NoteEntity } from '@joplin/lib/services/database/types'; import { NoteEntity } from '@joplin/lib/services/database/types';
import { focus } from '@joplin/lib/utils/focusHandler';
export interface OnLoadEvent { export interface OnLoadEvent {
formNote: FormNote; formNote: FormNote;
@ -191,7 +192,7 @@ export default function useFormNote(dependencies: HookDependencies) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (Setting.value(focusSettingName) === 'title') { if (Setting.value(focusSettingName) === 'title') {
if (titleInputRef.current) titleInputRef.current.focus(); if (titleInputRef.current) focus('useFormNote::handleAutoFocus', titleInputRef.current);
} else { } else {
if (editorRef.current) editorRef.current.execCommand({ name: 'editor.focus' }); if (editorRef.current) editorRef.current.execCommand({ name: 'editor.focus' });
} }

View File

@ -1,6 +1,7 @@
import { useState, useCallback, MutableRefObject, useEffect } from 'react'; import { useState, useCallback, MutableRefObject, useEffect } from 'react';
import Logger from '@joplin/utils/Logger'; import Logger from '@joplin/utils/Logger';
import { SearchMarkers } from './useSearchMarkers'; import { SearchMarkers } from './useSearchMarkers';
import { focus } from '@joplin/lib/utils/focusHandler';
const CommandService = require('@joplin/lib/services/CommandService').default; const CommandService = require('@joplin/lib/services/CommandService').default;
const logger = Logger.create('useNoteSearchBar'); const logger = Logger.create('useNoteSearchBar');
@ -36,7 +37,7 @@ export default function useNoteSearchBar({ noteSearchBarRef }: UseNoteSearchBarP
useEffect(() => { useEffect(() => {
if (showLocalSearch && noteSearchBarRef.current) { if (showLocalSearch && noteSearchBarRef.current) {
noteSearchBarRef.current.focus(); focus('useNoteSearchBar', noteSearchBarRef.current);
} }
}, [showLocalSearch, noteSearchBarRef]); }, [showLocalSearch, noteSearchBarRef]);

View File

@ -24,6 +24,7 @@ import useDragAndDrop from './utils/useDragAndDrop';
import usePrevious from '../hooks/usePrevious'; import usePrevious from '../hooks/usePrevious';
import { itemIsInTrash } from '@joplin/lib/services/trash'; import { itemIsInTrash } from '@joplin/lib/services/trash';
import Folder from '@joplin/lib/models/Folder'; import Folder from '@joplin/lib/models/Folder';
import { focus } from '@joplin/lib/utils/focusHandler';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const commands = { const commands = {
@ -159,7 +160,9 @@ const NoteList = (props: Props) => {
makeItemIndexVisible(i); makeItemIndexVisible(i);
if (doRefocus) { if (doRefocus) {
const ref = itemRefs.current[id]; const ref = itemRefs.current[id];
if (ref) ref.focus(); if (ref) {
focus('NoteList::doRefocus', ref);
}
} }
break; break;
} }

View File

@ -1,5 +1,6 @@
import shim from '@joplin/lib/shim'; import shim from '@joplin/lib/shim';
import { useRef, useCallback, MutableRefObject } from 'react'; import { useRef, useCallback, MutableRefObject } from 'react';
import { focus } from '@joplin/lib/utils/focusHandler';
export type FocusNote = (noteId: string)=> void; export type FocusNote = (noteId: string)=> void;
@ -16,14 +17,14 @@ const useFocusNote = (itemRefs: MutableRefObject<Record<string, HTMLDivElement>>
if (focusItemIID.current) shim.clearInterval(focusItemIID.current); if (focusItemIID.current) shim.clearInterval(focusItemIID.current);
focusItemIID.current = shim.setInterval(() => { focusItemIID.current = shim.setInterval(() => {
if (itemRefs.current[noteId]) { if (itemRefs.current[noteId]) {
itemRefs.current[noteId].focus(); focus('useFocusNote1', itemRefs.current[noteId]);
shim.clearInterval(focusItemIID.current); shim.clearInterval(focusItemIID.current);
focusItemIID.current = null; focusItemIID.current = null;
} }
}, 10); }, 10);
} else { } else {
if (focusItemIID.current) shim.clearInterval(focusItemIID.current); if (focusItemIID.current) shim.clearInterval(focusItemIID.current);
itemRefs.current[noteId].focus(); focus('useFocusNote2', itemRefs.current[noteId]);
} }
}, [itemRefs]); }, [itemRefs]);

View File

@ -7,6 +7,7 @@ import Note from '@joplin/lib/models/Note';
import bridge from '../services/bridge'; import bridge from '../services/bridge';
import shim from '@joplin/lib/shim'; import shim from '@joplin/lib/shim';
import { NoteEntity } from '@joplin/lib/services/database/types'; import { NoteEntity } from '@joplin/lib/services/database/types';
import { focus } from '@joplin/lib/utils/focusHandler';
const Datetime = require('react-datetime').default; const Datetime = require('react-datetime').default;
const { clipboard } = require('electron'); const { clipboard } = require('electron');
const formatcoords = require('formatcoords'); const formatcoords = require('formatcoords');
@ -77,7 +78,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
public componentDidUpdate() { public componentDidUpdate() {
if (this.state.editedKey === null) { if (this.state.editedKey === null) {
if (this.okButton.current) this.okButton.current.focus(); if (this.okButton.current) focus('NotePropertiesDialog::componentDidUpdate', this.okButton.current);
} }
} }
@ -220,7 +221,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
if ((this.refs.editField as any).openCalendar) { if ((this.refs.editField as any).openCalendar) {
(this.refs.editField as any).openCalendar(); (this.refs.editField as any).openCalendar();
} else { } else {
(this.refs.editField as any).focus(); focus('NotePropertiesDialog::editPropertyButtonClick', (this.refs.editField as any));
} }
}, 100); }, 100);
} }
@ -255,7 +256,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
public async cancelProperty() { public async cancelProperty() {
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
return new Promise((resolve: Function) => { return new Promise((resolve: Function) => {
if (this.okButton.current) this.okButton.current.focus(); if (this.okButton.current) focus('NotePropertiesDialog::focus', this.okButton.current);
this.setState({ this.setState({
editedKey: null, editedKey: null,
editedValue: null, editedValue: null,

View File

@ -1,6 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { themeStyle } from '@joplin/lib/theme'; import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import { focus } from '@joplin/lib/utils/focusHandler';
interface Props { interface Props {
themeId: number; themeId: number;
@ -32,6 +33,8 @@ class NoteSearchBar extends React.Component<Props> {
this.previousButton_click = this.previousButton_click.bind(this); this.previousButton_click = this.previousButton_click.bind(this);
this.nextButton_click = this.nextButton_click.bind(this); this.nextButton_click = this.nextButton_click.bind(this);
this.closeButton_click = this.closeButton_click.bind(this); this.closeButton_click = this.closeButton_click.bind(this);
// eslint-disable-next-line no-restricted-properties
this.focus = this.focus.bind(this); this.focus = this.focus.bind(this);
this.backgroundColor = undefined; this.backgroundColor = undefined;
@ -125,7 +128,7 @@ class NoteSearchBar extends React.Component<Props> {
} }
public focus() { public focus() {
(this.refs.searchInput as any).focus(); focus('NoteSearchBar::focus', this.refs.searchInput as any);
(this.refs.searchInput as any).select(); (this.refs.searchInput as any).select();
} }

View File

@ -1,6 +1,7 @@
import PostMessageService, { MessageResponse, ResponderComponentType } from '@joplin/lib/services/PostMessageService'; import PostMessageService, { MessageResponse, ResponderComponentType } from '@joplin/lib/services/PostMessageService';
import * as React from 'react'; import * as React from 'react';
import { reg } from '@joplin/lib/registry'; import { reg } from '@joplin/lib/registry';
import { focus } from '@joplin/lib/utils/focusHandler';
interface Props { interface Props {
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
@ -119,7 +120,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
public focus() { public focus() {
if (this.webviewRef_.current) { if (this.webviewRef_.current) {
this.webviewRef_.current.focus(); focus('NoteTextViewer::focus', this.webviewRef_.current);
} }
} }

View File

@ -6,6 +6,7 @@ const Datetime = require('react-datetime').default;
import CreatableSelect from 'react-select/creatable'; import CreatableSelect from 'react-select/creatable';
import Select from 'react-select'; import Select from 'react-select';
import makeAnimated from 'react-select/animated'; import makeAnimated from 'react-select/animated';
import { focus } from '@joplin/lib/utils/focusHandler';
interface Props { interface Props {
themeId: number; themeId: number;
defaultValue: any; defaultValue: any;
@ -67,7 +68,7 @@ export default class PromptDialog extends React.Component<Props, any> {
} }
public componentDidUpdate() { public componentDidUpdate() {
if (this.focusInput_ && this.answerInput_.current) this.answerInput_.current.focus(); if (this.focusInput_ && this.answerInput_.current) focus('PromptDialog::componentDidUpdate', this.answerInput_.current);
this.focusInput_ = false; this.focusInput_ = false;
} }

View File

@ -8,6 +8,7 @@ import uuid from '@joplin/lib/uuid';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
import Note from '@joplin/lib/models/Note'; import Note from '@joplin/lib/models/Note';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { blur, focus } from '@joplin/lib/utils/focusHandler';
const debounce = require('debounce'); const debounce = require('debounce');
const styled = require('styled-components').default; const styled = require('styled-components').default;
@ -117,7 +118,7 @@ function SearchBar(props: Props) {
const onKeyDown = useCallback((event: any) => { const onKeyDown = useCallback((event: any) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
if (document.activeElement) (document.activeElement as any).blur(); if (document.activeElement) blur('SearchBar::onKeyDown', document.activeElement as any);
void onExitSearch(); void onExitSearch();
} }
}, [onExitSearch]); }, [onExitSearch]);
@ -127,7 +128,7 @@ function SearchBar(props: Props) {
void onExitSearch(); void onExitSearch();
} else { } else {
setSearchStarted(true); setSearchStarted(true);
props.inputRef.current.focus(); focus('SearchBar::onSearchButtonClick', props.inputRef.current);
props.dispatch({ props.dispatch({
type: 'FOCUS_SET', type: 'FOCUS_SET',
field: 'globalSearch', field: 'globalSearch',

View File

@ -29,6 +29,7 @@ import { RuntimeProps } from './commands/focusElementSideBar';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
import { renderFolders, renderTags } from '@joplin/lib/components/shared/side-menu-shared'; import { renderFolders, renderTags } from '@joplin/lib/components/shared/side-menu-shared';
import { getTrashFolderIcon, getTrashFolderId } from '@joplin/lib/services/trash'; import { getTrashFolderIcon, getTrashFolderId } from '@joplin/lib/services/trash';
import { focus } from '@joplin/lib/utils/focusHandler';
const { themeStyle } = require('@joplin/lib/theme'); const { themeStyle } = require('@joplin/lib/theme');
const bridge = require('@electron/remote').require('./bridge').default; const bridge = require('@electron/remote').require('./bridge').default;
const Menu = bridge().Menu; const Menu = bridge().Menu;
@ -674,7 +675,7 @@ const SidebarComponent = (props: Props) => {
id: focusItem.id, id: focusItem.id,
}); });
focusItem.ref.current.focus(); focus('SideBar::onKeyDown', focusItem.ref.current);
} }
if (keyCode === 9) { if (keyCode === 9) {

View File

@ -2,6 +2,7 @@ import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import layoutItemProp from '../../ResizableLayout/utils/layoutItemProp'; import layoutItemProp from '../../ResizableLayout/utils/layoutItemProp';
import { AppState } from '../../../app.reducer'; import { AppState } from '../../../app.reducer';
import { focus } from '@joplin/lib/utils/focusHandler';
export const declaration: CommandDeclaration = { export const declaration: CommandDeclaration = {
name: 'focusElementSideBar', name: 'focusElementSideBar',
@ -24,10 +25,10 @@ export const runtime = (props: RuntimeProps): CommandRuntime => {
const item = props.getSelectedItem(); const item = props.getSelectedItem();
if (item) { if (item) {
const anchorRef = props.anchorItemRefs.current[item.type][item.id]; const anchorRef = props.anchorItemRefs.current[item.type][item.id];
if (anchorRef) anchorRef.current.focus(); if (anchorRef) focus('focusElementSideBar1', anchorRef.current);
} else { } else {
const anchorRef = props.getFirstAnchorItemRef('folder'); const anchorRef = props.getFirstAnchorItemRef('folder');
if (anchorRef) anchorRef.current.focus(); if (anchorRef) focus('focusElementSideBar2', anchorRef.current);
} }
} }
}, },

View File

@ -9,6 +9,7 @@ import useWebviewToPluginMessages from './hooks/useWebviewToPluginMessages';
import useScriptLoader from './hooks/useScriptLoader'; import useScriptLoader from './hooks/useScriptLoader';
import Logger from '@joplin/utils/Logger'; import Logger from '@joplin/utils/Logger';
import styled from 'styled-components'; import styled from 'styled-components';
import { focus } from '@joplin/lib/utils/focusHandler';
const logger = Logger.create('UserWebview'); const logger = Logger.create('UserWebview');
@ -99,7 +100,7 @@ function UserWebview(props: Props, ref: any) {
} }
}, },
focus: function() { focus: function() {
if (viewRef.current) viewRef.current.focus(); if (viewRef.current) focus('UserWebView::focus', viewRef.current);
}, },
}; };
}); });

View File

@ -5,6 +5,7 @@ import PluginService from '@joplin/lib/services/plugins/PluginService';
import WebviewController from '@joplin/lib/services/plugins/WebviewController'; import WebviewController from '@joplin/lib/services/plugins/WebviewController';
import UserWebview, { Props as UserWebviewProps } from './UserWebview'; import UserWebview, { Props as UserWebviewProps } from './UserWebview';
import UserWebviewDialogButtonBar from './UserWebviewDialogButtonBar'; import UserWebviewDialogButtonBar from './UserWebviewDialogButtonBar';
import { focus } from '@joplin/lib/utils/focusHandler';
const styled = require('styled-components').default; const styled = require('styled-components').default;
interface Props extends UserWebviewProps { interface Props extends UserWebviewProps {
@ -101,7 +102,7 @@ export default function UserWebviewDialog(props: Props) {
// We focus the dialog once it's ready to make sure that the ESC/Enter // We focus the dialog once it's ready to make sure that the ESC/Enter
// keyboard shortcuts are working. // keyboard shortcuts are working.
// https://github.com/laurent22/joplin/issues/4474 // https://github.com/laurent22/joplin/issues/4474
if (webviewRef.current) webviewRef.current.focus(); if (webviewRef.current) focus('UserWebviewDialog', webviewRef.current);
}, []); }, []);
return ( return (

View File

@ -2,7 +2,7 @@
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"include": [ "include": [
"**/*.ts", "**/*.ts",
"**/*.tsx", "**/*.tsx", "../lib/utils/focusHandler.ts",
], ],
"exclude": [ "exclude": [
"**/node_modules", "**/node_modules",

View File

@ -11,6 +11,7 @@ import { _ } from '@joplin/lib/locale';
import { EditorControl } from './types'; import { EditorControl } from './types';
import { useCallback } from 'react'; import { useCallback } from 'react';
import SelectionFormatting from '@joplin/editor/SelectionFormatting'; import SelectionFormatting from '@joplin/editor/SelectionFormatting';
import { focus } from '@joplin/lib/utils/focusHandler';
interface LinkDialogProps { interface LinkDialogProps {
editorControl: EditorControl; editorControl: EditorControl;
@ -100,7 +101,7 @@ const EditLinkDialog = (props: LinkDialogProps) => {
autoFocus autoFocus
onSubmitEditing={() => { onSubmitEditing={() => {
linkInputRef.current.focus(); focus('EditLinkDialog::onSubmitEditing', linkInputRef.current);
}} }}
onChangeText={(text: string) => setLinkLabel(text)} onChangeText={(text: string) => setLinkLabel(text)}
/> />

View File

@ -61,6 +61,7 @@ import { getDisplayParentTitle } from '@joplin/lib/services/trash';
import { PluginStates } from '@joplin/lib/services/plugins/reducer'; import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import pickDocument from '../../utils/pickDocument'; import pickDocument from '../../utils/pickDocument';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import { focus } from '@joplin/lib/utils/focusHandler';
const urlUtils = require('@joplin/lib/urlUtils'); const urlUtils = require('@joplin/lib/urlUtils');
const emptyArray: any[] = []; const emptyArray: any[] = [];
@ -1328,13 +1329,8 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
// Avoid writing `this.titleTextFieldRef.current` -- titleTextFieldRef may // Avoid writing `this.titleTextFieldRef.current` -- titleTextFieldRef may
// be undefined. // be undefined.
if (fieldToFocus === 'title' && this.titleTextFieldRef?.current) { if (fieldToFocus === 'title' && this.titleTextFieldRef?.current) {
this.titleTextFieldRef.current.focus(); focus('Note::focusUpdate', this.titleTextFieldRef.current);
} }
// if (fieldToFocus === 'body' && this.markdownEditorRef.current) {
// if (this.markdownEditorRef.current) {
// this.markdownEditorRef.current.focus();
// }
// }
} }
private async folderPickerOptions_valueChanged(itemValue: any) { private async folderPickerOptions_valueChanged(itemValue: any) {

View File

@ -11,6 +11,7 @@ import swapLine, { SwapLineDirection } from './swapLine';
import duplicateLine from './duplicateLine'; import duplicateLine from './duplicateLine';
import sortSelectedLines from './sortSelectedLines'; import sortSelectedLines from './sortSelectedLines';
import { closeSearchPanel, findNext, findPrevious, openSearchPanel, replaceAll, replaceNext } from '@codemirror/search'; import { closeSearchPanel, findNext, findPrevious, openSearchPanel, replaceAll, replaceNext } from '@codemirror/search';
import { focus } from '@joplin/lib/utils/focusHandler';
export type EditorCommandFunction = (editor: EditorView, ...args: any[])=> void|any; export type EditorCommandFunction = (editor: EditorView, ...args: any[])=> void|any;
@ -22,7 +23,7 @@ const editorCommands: Record<EditorCommandType, EditorCommandFunction> = {
[EditorCommandType.Undo]: undo, [EditorCommandType.Undo]: undo,
[EditorCommandType.Redo]: redo, [EditorCommandType.Redo]: redo,
[EditorCommandType.SelectAll]: selectAll, [EditorCommandType.SelectAll]: selectAll,
[EditorCommandType.Focus]: editor => editor.focus(), [EditorCommandType.Focus]: editor => focus('editorCommands::focus', editor),
[EditorCommandType.ToggleBolded]: toggleBolded, [EditorCommandType.ToggleBolded]: toggleBolded,
[EditorCommandType.ToggleItalicized]: toggleItalicized, [EditorCommandType.ToggleItalicized]: toggleItalicized,

View File

@ -0,0 +1,41 @@
// The purpose of this handler is to have all focus/blur calls go through the same place, which
// makes it easier to log what happens. This is useful when one unknown component is stealing focus
// from another component. Potentially it could also be used to resolve conflict situations when
// multiple components try to set the focus at the same time.
import Logger from '@joplin/utils/Logger';
const logger = Logger.create('setFocus');
enum ToggleFocusAction {
Focus = 'focus',
Blur = 'blur',
}
interface FocusableElement {
focus: ()=> void;
blur: ()=> void;
}
const toggleFocus = (source: string, element: FocusableElement, action: ToggleFocusAction) => {
if (!element) {
logger.warn(`Tried action "${action}" on an undefined element: ${source}`);
return;
}
if (!element[action]) {
logger.warn(`Element does not have a "${action}" method: ${source}`);
return;
}
logger.debug(`Action "${action}" from "${source}"`);
element[action]();
};
export const focus = (source: string, element: any) => {
toggleFocus(source, element, ToggleFocusAction.Focus);
};
export const blur = (source: string, element: any) => {
toggleFocus(source, element, ToggleFocusAction.Blur);
};

View File

@ -146,7 +146,8 @@ export default class PdfDocument {
frame.contentWindow.onafterprint = () => { frame.contentWindow.onafterprint = () => {
frame.remove(); frame.remove();
}; };
frame.focus(); console.warn('frame.focus() has been disabled!! Use focusHandler instead');
// frame.focus();
frame.contentWindow.print(); frame.contentWindow.print();
}; };
frame.src = this.url as string; frame.src = this.url as string;