mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-08 12:45:55 +08:00
refactor: find in page (#7086)
- refactor rxjs data flow - use canvas text to mitigate searchable search box input text issue
This commit is contained in:
@@ -45,13 +45,21 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
autoFocus,
|
autoFocus,
|
||||||
...otherProps
|
...otherProps
|
||||||
}: InputProps,
|
}: InputProps,
|
||||||
ref: ForwardedRef<HTMLInputElement>
|
upstreamRef: ForwardedRef<HTMLInputElement>
|
||||||
) {
|
) {
|
||||||
const handleAutoFocus = useCallback((ref: HTMLInputElement | null) => {
|
const handleAutoFocus = useCallback(
|
||||||
if (ref) {
|
(ref: HTMLInputElement | null) => {
|
||||||
window.setTimeout(() => ref.focus(), 0);
|
if (ref) {
|
||||||
}
|
window.setTimeout(() => ref.focus(), 0);
|
||||||
}, []);
|
if (typeof upstreamRef === 'function') {
|
||||||
|
upstreamRef(ref);
|
||||||
|
} else if (upstreamRef) {
|
||||||
|
upstreamRef.current = ref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[upstreamRef]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -78,7 +86,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
large: size === 'large',
|
large: size === 'large',
|
||||||
'extra-large': size === 'extraLarge',
|
'extra-large': size === 'extraLarge',
|
||||||
})}
|
})}
|
||||||
ref={autoFocus ? handleAutoFocus : ref}
|
ref={autoFocus ? handleAutoFocus : upstreamRef}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
onChange={useCallback(
|
onChange={useCallback(
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
withoutCloseButton = false,
|
withoutCloseButton = false,
|
||||||
|
modal,
|
||||||
|
|
||||||
portalOptions,
|
portalOptions,
|
||||||
contentOptions: {
|
contentOptions: {
|
||||||
@@ -63,13 +64,13 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => (
|
) => (
|
||||||
<Dialog.Root {...props}>
|
<Dialog.Root modal={modal} {...props}>
|
||||||
<Dialog.Portal {...portalOptions}>
|
<Dialog.Portal {...portalOptions}>
|
||||||
<Dialog.Overlay
|
<Dialog.Overlay
|
||||||
className={clsx(styles.modalOverlay, overlayClassName)}
|
className={clsx(styles.modalOverlay, overlayClassName)}
|
||||||
{...otherOverlayOptions}
|
{...otherOverlayOptions}
|
||||||
/>
|
/>
|
||||||
<div className={styles.modalContentWrapper}>
|
<div data-modal={modal} className={clsx(styles.modalContentWrapper)}>
|
||||||
<Dialog.Content
|
<Dialog.Content
|
||||||
className={clsx(styles.modalContent, contentClassName)}
|
className={clsx(styles.modalContent, contentClassName)}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { createVar, style } from '@vanilla-extract/css';
|
import { createVar, globalStyle, style } from '@vanilla-extract/css';
|
||||||
export const widthVar = createVar('widthVar');
|
export const widthVar = createVar('widthVar');
|
||||||
export const heightVar = createVar('heightVar');
|
export const heightVar = createVar('heightVar');
|
||||||
export const minHeightVar = createVar('minHeightVar');
|
export const minHeightVar = createVar('minHeightVar');
|
||||||
@@ -17,6 +17,7 @@ export const modalContentWrapper = style({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
zIndex: cssVar('zIndexModal'),
|
zIndex: cssVar('zIndexModal'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const modalContent = style({
|
export const modalContent = style({
|
||||||
vars: {
|
vars: {
|
||||||
[widthVar]: '',
|
[widthVar]: '',
|
||||||
@@ -82,3 +83,11 @@ export const confirmModalContainer = style({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
globalStyle(`[data-modal="false"]${modalContentWrapper}`, {
|
||||||
|
pointerEvents: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
globalStyle(`[data-modal="false"] ${modalContent}`, {
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { useCallback, useEffect } from 'react';
|
|||||||
export function useRegisterFindInPageCommands() {
|
export function useRegisterFindInPageCommands() {
|
||||||
const findInPage = useService(FindInPageService).findInPage;
|
const findInPage = useService(FindInPageService).findInPage;
|
||||||
const toggleVisible = useCallback(() => {
|
const toggleVisible = useCallback(() => {
|
||||||
findInPage.toggleVisible();
|
// get the selected text in page
|
||||||
|
const selection = window.getSelection();
|
||||||
|
const selectedText = selection?.toString();
|
||||||
|
|
||||||
|
findInPage.toggleVisible(selectedText);
|
||||||
}, [findInPage]);
|
}, [findInPage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,81 +1,98 @@
|
|||||||
import { cmdFind } from '@affine/electron-api';
|
import { DebugLogger } from '@affine/debug';
|
||||||
|
import { apis } from '@affine/electron-api';
|
||||||
import { Entity, LiveData } from '@toeverything/infra';
|
import { Entity, LiveData } from '@toeverything/infra';
|
||||||
import { Observable, of, switchMap } from 'rxjs';
|
import {
|
||||||
|
debounceTime,
|
||||||
|
distinctUntilChanged,
|
||||||
|
of,
|
||||||
|
shareReplay,
|
||||||
|
switchMap,
|
||||||
|
tap,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
|
const logger = new DebugLogger('affine:find-in-page');
|
||||||
|
|
||||||
type FindInPageResult = {
|
|
||||||
requestId: number;
|
|
||||||
activeMatchOrdinal: number;
|
|
||||||
matches: number;
|
|
||||||
finalUpdate: boolean;
|
|
||||||
};
|
|
||||||
export class FindInPage extends Entity {
|
export class FindInPage extends Entity {
|
||||||
// modal open/close
|
|
||||||
|
|
||||||
readonly searchText$ = new LiveData<string | null>(null);
|
readonly searchText$ = new LiveData<string | null>(null);
|
||||||
private readonly direction$ = new LiveData<'forward' | 'backward'>('forward');
|
|
||||||
readonly isSearching$ = new LiveData(false);
|
readonly isSearching$ = new LiveData(false);
|
||||||
|
private readonly direction$ = new LiveData<'forward' | 'backward'>('forward');
|
||||||
readonly visible$ = new LiveData(false);
|
readonly visible$ = new LiveData(false);
|
||||||
|
|
||||||
readonly result$ = LiveData.from(
|
readonly result$ = LiveData.from(
|
||||||
this.searchText$.pipe(
|
this.visible$.pipe(
|
||||||
switchMap(searchText => {
|
distinctUntilChanged(),
|
||||||
if (!searchText) {
|
switchMap(visible => {
|
||||||
|
if (!visible) {
|
||||||
return of(null);
|
return of(null);
|
||||||
} else {
|
|
||||||
return new Observable<FindInPageResult>(subscriber => {
|
|
||||||
const handleResult = (result: FindInPageResult) => {
|
|
||||||
subscriber.next(result);
|
|
||||||
if (result.finalUpdate) {
|
|
||||||
subscriber.complete();
|
|
||||||
this.isSearching$.next(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.isSearching$.next(true);
|
|
||||||
cmdFind
|
|
||||||
?.findInPage(searchText, {
|
|
||||||
forward: this.direction$.value === 'forward',
|
|
||||||
})
|
|
||||||
.then(() => cmdFind?.onFindInPageResult(handleResult))
|
|
||||||
.catch(e => {
|
|
||||||
console.error(e);
|
|
||||||
this.isSearching$.next(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cmdFind?.offFindInPageResult(handleResult);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
let searchId = 0;
|
||||||
|
return this.searchText$.pipe(
|
||||||
|
tap(() => {
|
||||||
|
this.isSearching$.next(false);
|
||||||
|
}),
|
||||||
|
debounceTime(500),
|
||||||
|
switchMap(searchText => {
|
||||||
|
if (!searchText) {
|
||||||
|
return of(null);
|
||||||
|
} else {
|
||||||
|
let findNext = true;
|
||||||
|
return this.direction$.pipe(
|
||||||
|
switchMap(direction => {
|
||||||
|
if (apis?.findInPage) {
|
||||||
|
this.isSearching$.next(true);
|
||||||
|
const currentId = ++searchId;
|
||||||
|
return apis?.findInPage
|
||||||
|
.find(searchText, {
|
||||||
|
forward: direction === 'forward',
|
||||||
|
findNext,
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (currentId === searchId) {
|
||||||
|
this.isSearching$.next(false);
|
||||||
|
findNext = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return of(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
shareReplay({
|
||||||
|
bufferSize: 1,
|
||||||
|
refCount: true,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
{ requestId: 0, activeMatchOrdinal: 0, matches: 0, finalUpdate: true }
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
// todo: hide on navigation
|
||||||
}
|
}
|
||||||
|
|
||||||
findInPage(searchText: string) {
|
findInPage(searchText: string) {
|
||||||
|
this.onChangeVisible(true);
|
||||||
this.searchText$.next(searchText);
|
this.searchText$.next(searchText);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateResult(result: FindInPageResult) {
|
|
||||||
this.result$.next(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeVisible(visible: boolean) {
|
onChangeVisible(visible: boolean) {
|
||||||
this.visible$.next(visible);
|
this.visible$.next(visible);
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
this.stopFindInPage('clearSelection');
|
this.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleVisible() {
|
toggleVisible(text?: string) {
|
||||||
const nextVisible = !this.visible$.value;
|
const nextVisible = !this.visible$.value;
|
||||||
this.visible$.next(nextVisible);
|
this.visible$.next(nextVisible);
|
||||||
if (!nextVisible) {
|
if (!nextVisible) {
|
||||||
this.stopFindInPage('clearSelection');
|
this.clear();
|
||||||
|
} else if (text) {
|
||||||
|
this.searchText$.next(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,8 +101,6 @@ export class FindInPage extends Entity {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.direction$.next('backward');
|
this.direction$.next('backward');
|
||||||
this.searchText$.next(this.searchText$.value);
|
|
||||||
cmdFind?.onFindInPageResult(result => this.updateResult(result));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
forward() {
|
forward() {
|
||||||
@@ -93,16 +108,10 @@ export class FindInPage extends Entity {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.direction$.next('forward');
|
this.direction$.next('forward');
|
||||||
this.searchText$.next(this.searchText$.value);
|
|
||||||
cmdFind?.onFindInPageResult(result => this.updateResult(result));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stopFindInPage(
|
clear() {
|
||||||
action: 'clearSelection' | 'keepSelection' | 'activateSelection'
|
logger.debug('clear');
|
||||||
) {
|
apis?.findInPage.clear().catch(logger.error);
|
||||||
if (action === 'clearSelection') {
|
|
||||||
this.searchText$.next(null);
|
|
||||||
}
|
|
||||||
cmdFind?.stopFindInPage(action).catch(e => console.error(e));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,16 +20,40 @@ export const container = style({
|
|||||||
export const leftContent = style({
|
export const leftContent = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
flex: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const inputContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
flex: 1,
|
||||||
|
height: '32px',
|
||||||
|
position: 'relative',
|
||||||
|
margin: '0 8px',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const input = style({
|
export const input = style({
|
||||||
padding: '0 10px',
|
position: 'absolute',
|
||||||
height: '32px',
|
padding: '0',
|
||||||
gap: '8px',
|
inset: 0,
|
||||||
color: cssVar('iconColor'),
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
color: 'transparent',
|
||||||
background: cssVar('white10'),
|
background: cssVar('white10'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const inputHack = style([
|
||||||
|
input,
|
||||||
|
{
|
||||||
|
'::placeholder': {
|
||||||
|
color: cssVar('iconColor'),
|
||||||
|
},
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
export const count = style({
|
export const count = style({
|
||||||
color: cssVar('textSecondaryColor'),
|
color: cssVar('textSecondaryColor'),
|
||||||
fontSize: cssVar('fontXs'),
|
fontSize: cssVar('fontXs'),
|
||||||
@@ -41,6 +65,7 @@ export const arrowButton = style({
|
|||||||
fontSize: '24px',
|
fontSize: '24px',
|
||||||
width: '32px',
|
width: '32px',
|
||||||
height: '32px',
|
height: '32px',
|
||||||
|
flexShrink: 0,
|
||||||
border: '1px solid',
|
border: '1px solid',
|
||||||
borderColor: cssVar('borderColor'),
|
borderColor: cssVar('borderColor'),
|
||||||
alignItems: 'baseline',
|
alignItems: 'baseline',
|
||||||
|
|||||||
@@ -1,24 +1,68 @@
|
|||||||
import { Button, Input, Modal } from '@affine/component';
|
import { Button, Modal } from '@affine/component';
|
||||||
import { rightSidebarWidthAtom } from '@affine/core/atoms';
|
import { rightSidebarWidthAtom } from '@affine/core/atoms';
|
||||||
import {
|
import { ArrowDownSmallIcon, ArrowUpSmallIcon } from '@blocksuite/icons';
|
||||||
ArrowDownSmallIcon,
|
|
||||||
ArrowUpSmallIcon,
|
|
||||||
SearchIcon,
|
|
||||||
} from '@blocksuite/icons';
|
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useDebouncedValue } from 'foxact/use-debounced-value';
|
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback, useDeferredValue, useEffect, useState } from 'react';
|
import {
|
||||||
|
type KeyboardEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { RightSidebarService } from '../../right-sidebar';
|
import { RightSidebarService } from '../../right-sidebar';
|
||||||
import { FindInPageService } from '../services/find-in-page';
|
import { FindInPageService } from '../services/find-in-page';
|
||||||
import * as styles from './find-in-page-modal.css';
|
import * as styles from './find-in-page-modal.css';
|
||||||
|
|
||||||
|
const drawText = (canvas: HTMLCanvasElement, text: string) => {
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
canvas.width = canvas.getBoundingClientRect().width * dpr;
|
||||||
|
canvas.height = canvas.getBoundingClientRect().height * dpr;
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.font = '15px Inter';
|
||||||
|
ctx.fillText(text, 0, 22);
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.textBaseline = 'ideographic';
|
||||||
|
};
|
||||||
|
|
||||||
|
const CanvasText = ({
|
||||||
|
text,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
text: string;
|
||||||
|
className: string;
|
||||||
|
}) => {
|
||||||
|
const ref = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = ref.current;
|
||||||
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drawText(canvas, text);
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
drawText(canvas, text);
|
||||||
|
});
|
||||||
|
resizeObserver.observe(canvas);
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
}, [text]);
|
||||||
|
|
||||||
|
return <canvas className={className} ref={ref} />;
|
||||||
|
};
|
||||||
|
|
||||||
export const FindInPageModal = () => {
|
export const FindInPageModal = () => {
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const debouncedValue = useDebouncedValue(value, 300);
|
|
||||||
const deferredValue = useDeferredValue(debouncedValue);
|
|
||||||
|
|
||||||
const findInPage = useService(FindInPageService).findInPage;
|
const findInPage = useService(FindInPageService).findInPage;
|
||||||
const visible = useLiveData(findInPage.visible$);
|
const visible = useLiveData(findInPage.visible$);
|
||||||
@@ -29,10 +73,48 @@ export const FindInPageModal = () => {
|
|||||||
const rightSidebar = useService(RightSidebarService).rightSidebar;
|
const rightSidebar = useService(RightSidebarService).rightSidebar;
|
||||||
const frontView = useLiveData(rightSidebar.front$);
|
const frontView = useLiveData(rightSidebar.front$);
|
||||||
const open = useLiveData(rightSidebar.isOpen$) && frontView !== undefined;
|
const open = useLiveData(rightSidebar.isOpen$) && frontView !== undefined;
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const handleSearch = useCallback(() => {
|
const handleValueChange = useCallback(
|
||||||
findInPage.findInPage(deferredValue);
|
(v: string) => {
|
||||||
}, [deferredValue, findInPage]);
|
setValue(v);
|
||||||
|
findInPage.findInPage(v);
|
||||||
|
if (v.length === 0) {
|
||||||
|
findInPage.clear();
|
||||||
|
}
|
||||||
|
inputRef.current?.focus();
|
||||||
|
},
|
||||||
|
[findInPage]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
setValue(findInPage.searchText$.value || '');
|
||||||
|
const onEsc = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
findInPage.onChangeVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', onEsc);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', onEsc);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return () => {};
|
||||||
|
}, [findInPage, findInPage.searchText$.value, visible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = findInPage.isSearching$.subscribe(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsub.unsubscribe();
|
||||||
|
};
|
||||||
|
}, [findInPage.isSearching$]);
|
||||||
|
|
||||||
const handleBackWard = useCallback(() => {
|
const handleBackWard = useCallback(() => {
|
||||||
findInPage.backward();
|
findInPage.backward();
|
||||||
@@ -45,7 +127,7 @@ export const FindInPageModal = () => {
|
|||||||
const onChangeVisible = useCallback(
|
const onChangeVisible = useCallback(
|
||||||
(visible: boolean) => {
|
(visible: boolean) => {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
findInPage.stopFindInPage('clearSelection');
|
findInPage.clear();
|
||||||
}
|
}
|
||||||
findInPage.onChangeVisible(visible);
|
findInPage.onChangeVisible(visible);
|
||||||
},
|
},
|
||||||
@@ -55,53 +137,27 @@ export const FindInPageModal = () => {
|
|||||||
onChangeVisible(false);
|
onChangeVisible(false);
|
||||||
}, [onChangeVisible]);
|
}, [onChangeVisible]);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleKeydown: KeyboardEventHandler = useCallback(
|
||||||
// add keyboard event listener for arrow up and down
|
e => {
|
||||||
const keyArrowDown = (event: KeyboardEvent) => {
|
if (e.key === 'Enter' || e.key === 'ArrowDown') {
|
||||||
if (event.key === 'ArrowDown') {
|
|
||||||
handleForward();
|
handleForward();
|
||||||
}
|
}
|
||||||
};
|
if (e.key === 'ArrowUp') {
|
||||||
const keyArrowUp = (event: KeyboardEvent) => {
|
|
||||||
if (event.key === 'ArrowUp') {
|
|
||||||
handleBackWard();
|
handleBackWard();
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
document.addEventListener('keydown', keyArrowDown);
|
[handleBackWard, handleForward]
|
||||||
document.addEventListener('keydown', keyArrowUp);
|
);
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', keyArrowDown);
|
|
||||||
document.removeEventListener('keydown', keyArrowUp);
|
|
||||||
};
|
|
||||||
}, [findInPage, handleBackWard, handleForward]);
|
|
||||||
|
|
||||||
const panelWidth = assignInlineVars({
|
const panelWidth = assignInlineVars({
|
||||||
[styles.panelWidthVar]: open ? `${rightSidebarWidth}px` : '0',
|
[styles.panelWidthVar]: open ? `${rightSidebarWidth}px` : '0',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// auto search when value change
|
|
||||||
if (deferredValue) {
|
|
||||||
handleSearch();
|
|
||||||
}
|
|
||||||
}, [deferredValue, handleSearch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// clear highlight when value is empty
|
|
||||||
if (value.length === 0) {
|
|
||||||
findInPage.stopFindInPage('keepSelection');
|
|
||||||
}
|
|
||||||
}, [value, findInPage]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={visible}
|
open={visible}
|
||||||
onOpenChange={onChangeVisible}
|
modal={false}
|
||||||
overlayOptions={{
|
|
||||||
hidden: true,
|
|
||||||
}}
|
|
||||||
withoutCloseButton
|
withoutCloseButton
|
||||||
width={398}
|
width={400}
|
||||||
height={48}
|
height={48}
|
||||||
minHeight={48}
|
minHeight={48}
|
||||||
contentOptions={{
|
contentOptions={{
|
||||||
@@ -110,33 +166,32 @@ export const FindInPageModal = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.leftContent}>
|
<div className={styles.leftContent}>
|
||||||
<Input
|
<div className={styles.inputContainer}>
|
||||||
onChange={setValue}
|
<input
|
||||||
value={isSearching ? '' : value}
|
type="text"
|
||||||
onEnter={handleSearch}
|
autoFocus
|
||||||
autoFocus
|
value={value}
|
||||||
preFix={<SearchIcon fontSize={20} />}
|
ref={inputRef}
|
||||||
endFix={
|
style={{
|
||||||
<div className={styles.count}>
|
visibility: isSearching ? 'hidden' : 'visible',
|
||||||
{value.length > 0 && result && result.matches !== 0 ? (
|
}}
|
||||||
<>
|
className={styles.input}
|
||||||
<span>{result?.activeMatchOrdinal || 0}</span>
|
onKeyDown={handleKeydown}
|
||||||
<span>/</span>
|
onChange={e => handleValueChange(e.target.value)}
|
||||||
<span>{result?.matches || 0}</span>
|
/>
|
||||||
</>
|
<CanvasText className={styles.inputHack} text={value} />
|
||||||
) : (
|
</div>
|
||||||
<span>No matches</span>
|
<div className={styles.count}>
|
||||||
)}
|
{value.length > 0 && result && result.matches !== 0 ? (
|
||||||
</div>
|
<>
|
||||||
}
|
<span>{result?.activeMatchOrdinal || 0}</span>
|
||||||
style={{
|
<span>/</span>
|
||||||
width: 239,
|
<span>{result?.matches || 0}</span>
|
||||||
}}
|
</>
|
||||||
className={styles.input}
|
) : value.length ? (
|
||||||
inputStyle={{
|
<span>No matches</span>
|
||||||
padding: '0',
|
) : null}
|
||||||
}}
|
</div>
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className={clsx(styles.arrowButton, 'backward')}
|
className={clsx(styles.arrowButton, 'backward')}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
affine as exposedAffineGlobal,
|
affine as exposedAffineGlobal,
|
||||||
appInfo as exposedAppInfo,
|
appInfo as exposedAppInfo,
|
||||||
cmdFind as exposedCmdFind,
|
|
||||||
} from '@affine/electron/preload/electron-api';
|
} from '@affine/electron/preload/electron-api';
|
||||||
|
|
||||||
type MainHandlers = typeof mainHandlers;
|
type MainHandlers = typeof mainHandlers;
|
||||||
@@ -40,8 +39,5 @@ export const events = (globalThis as any).events as ClientEvents | null;
|
|||||||
export const affine = (globalThis as any).affine as
|
export const affine = (globalThis as any).affine as
|
||||||
| typeof exposedAffineGlobal
|
| typeof exposedAffineGlobal
|
||||||
| null;
|
| null;
|
||||||
export const cmdFind = (globalThis as any).cmdFind as
|
|
||||||
| typeof exposedCmdFind
|
|
||||||
| null;
|
|
||||||
|
|
||||||
export type { UpdateMeta } from '@affine/electron/main/updater/event';
|
export type { UpdateMeta } from '@affine/electron/main/updater/event';
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
|
import type { NamespaceHandlers } from '../type';
|
||||||
|
|
||||||
export const findInPageHandlers = {
|
export const findInPageHandlers = {
|
||||||
findInPage: async (
|
find: async (event, text: string, options?: Electron.FindInPageOptions) => {
|
||||||
event: Electron.IpcMainInvokeEvent,
|
const { promise, resolve } =
|
||||||
text: string,
|
Promise.withResolvers<Electron.Result | null>();
|
||||||
options?: Electron.FindInPageOptions
|
|
||||||
) => {
|
|
||||||
const webContents = event.sender;
|
const webContents = event.sender;
|
||||||
return webContents.findInPage(text, options);
|
let requestId: number = -1;
|
||||||
|
webContents.once('found-in-page', (_, result) => {
|
||||||
|
resolve(result.requestId === requestId ? result : null);
|
||||||
|
});
|
||||||
|
requestId = webContents.findInPage(text, options);
|
||||||
|
return promise;
|
||||||
},
|
},
|
||||||
stopFindInPage: async (
|
clear: async event => {
|
||||||
event: Electron.IpcMainInvokeEvent,
|
|
||||||
action: 'clearSelection' | 'keepSelection' | 'activateSelection'
|
|
||||||
) => {
|
|
||||||
const webContents = event.sender;
|
const webContents = event.sender;
|
||||||
return webContents.stopFindInPage(action);
|
webContents.stopFindInPage('keepSelection');
|
||||||
},
|
},
|
||||||
};
|
} satisfies NamespaceHandlers;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import assert from 'node:assert';
|
import assert from 'node:assert';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
import type { CookiesSetDetails } from 'electron';
|
import { type CookiesSetDetails } from 'electron';
|
||||||
import { BrowserWindow, nativeTheme } from 'electron';
|
import { BrowserWindow, nativeTheme } from 'electron';
|
||||||
import electronWindowState from 'electron-window-state';
|
import electronWindowState from 'electron-window-state';
|
||||||
|
|
||||||
@@ -169,16 +169,6 @@ async function createWindow(additionalArguments: string[]) {
|
|||||||
uiSubjects.onFullScreen$.next(false);
|
uiSubjects.onFullScreen$.next(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
browserWindow.webContents.on('found-in-page', (_event, result) => {
|
|
||||||
const { requestId, activeMatchOrdinal, matches, finalUpdate } = result;
|
|
||||||
browserWindow.webContents.send('found-in-page-result', {
|
|
||||||
requestId,
|
|
||||||
activeMatchOrdinal,
|
|
||||||
matches,
|
|
||||||
finalUpdate,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL for main window.
|
* URL for main window.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { contextBridge } from 'electron';
|
import { contextBridge } from 'electron';
|
||||||
|
|
||||||
import { affine, appInfo, cmdFind, getElectronAPIs } from './electron-api';
|
import { affine, appInfo, getElectronAPIs } from './electron-api';
|
||||||
|
|
||||||
const { apis, events } = getElectronAPIs();
|
const { apis, events } = getElectronAPIs();
|
||||||
|
|
||||||
@@ -10,7 +10,6 @@ contextBridge.exposeInMainWorld('events', events);
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld('affine', affine);
|
contextBridge.exposeInMainWorld('affine', affine);
|
||||||
contextBridge.exposeInMainWorld('cmdFind', cmdFind);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to expose affine APIs to window object!', error);
|
console.error('Failed to expose affine APIs to window object!', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,20 +47,6 @@ export const affine = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cmdFind = {
|
|
||||||
findInPage: (text: string, options?: Electron.FindInPageOptions) =>
|
|
||||||
ipcRenderer.invoke('findInPage:findInPage', text, options),
|
|
||||||
stopFindInPage: (
|
|
||||||
action: 'clearSelection' | 'keepSelection' | 'activateSelection'
|
|
||||||
) => ipcRenderer.invoke('findInPage:stopFindInPage', action),
|
|
||||||
onFindInPageResult: (callBack: (data: any) => void) =>
|
|
||||||
ipcRenderer.on('found-in-page-result', (_event, data) => callBack(data)),
|
|
||||||
offFindInPageResult: (callBack: (data: any) => void) =>
|
|
||||||
ipcRenderer.removeListener('found-in-page-result', (_event, data) =>
|
|
||||||
callBack(data)
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getElectronAPIs() {
|
export function getElectronAPIs() {
|
||||||
const mainAPIs = getMainAPIs();
|
const mainAPIs = getMainAPIs();
|
||||||
const helperAPIs = getHelperAPIs();
|
const helperAPIs = getHelperAPIs();
|
||||||
|
|||||||
Reference in New Issue
Block a user