Skip to content

Commit

Permalink
feat(markup search): added MarkupSearchAnchor
Browse files Browse the repository at this point in the history
  • Loading branch information
makhnatkin committed Jul 25, 2024
1 parent 64a44df commit 53d9a53
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 121 deletions.
8 changes: 7 additions & 1 deletion src/bundle/MarkdownEditorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@ export const MarkdownEditorView = React.forwardRef<HTMLDivElement, MarkdownEdito
);
MarkdownEditorView.displayName = 'MarkdownEditorView';

interface MarkupSearchAnchorProps extends Pick<EditorSettingsProps, 'mode'> {}

const MarkupSearchAnchor: React.FC<MarkupSearchAnchorProps> = ({mode}) => (
<>{mode === 'markup' && <div className="g-md-search-anchor"></div>}</>
);

function Settings(props: EditorSettingsProps & {stickyToolbar: boolean}) {
const wrapperRef = useRef<HTMLDivElement>(null);
const isSticky = useSticky(wrapperRef) && props.toolbarVisibility && props.stickyToolbar;
Expand All @@ -284,7 +290,7 @@ function Settings(props: EditorSettingsProps & {stickyToolbar: boolean}) {
})}
>
<EditorSettings {...props} />
<div className="g-md-search-anchor"></div>
<MarkupSearchAnchor {...props} />
</div>
</div>
);
Expand Down
223 changes: 120 additions & 103 deletions src/markup/codemirror/search-plugin/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import {
setSearchQuery,
} from '@codemirror/search';
import {EditorView, PluginValue, ViewPlugin, ViewUpdate, keymap} from '@codemirror/view';
import debounce from 'lodash/debounce';

import type {RendererItem} from '../../../extensions';
import {debounce} from '../../../lodash';
import {ReactRendererFacet} from '../react-facet';

import {PortalWithPopup} from './view/SearchPopup';
Expand All @@ -30,111 +30,128 @@ interface SearchQueryParams {

const INPUT_DELAY = 200;

export const SearchPanelPlugin = ViewPlugin.fromClass(
class implements PluginValue {
readonly view: EditorView;

anchor: HTMLElement | null;
renderer: RendererItem | null;
searchQuery: SearchQueryParams = {
search: '',
caseSensitive: false,
wholeWord: false,
};
setViewSearchWithDelay: (config: Partial<SearchQueryParams>) => void;

constructor(view: EditorView) {
this.view = view;
this.anchor = null;
this.renderer = null;

this.handleClose = this.handleClose.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleSearchNext = this.handleSearchNext.bind(this);
this.handleSearchPrev = this.handleSearchPrev.bind(this);
this.handleSearchConfigChange = this.handleSearchConfigChange.bind(this);
this.setViewSearchWithDelay = debounce(this.setViewSearch, INPUT_DELAY);
}

update(update: ViewUpdate): void {
const isPanelOpen = searchPanelOpen(update.state);

if (isPanelOpen && !this.renderer) {
this.anchor = this.anchor ?? document.querySelector('.g-md-search-anchor');
this.renderer = this.view.state
.facet(ReactRendererFacet)
.createItem('cm-search', () =>
React.createElement(PortalWithPopup, {
open: true,
anchor: this.anchor,
onChange: this.handleChange,
onClose: this.handleClose,
onSearchNext: this.handleSearchNext,
onSearchPrev: this.handleSearchPrev,
onConfigChange: this.handleSearchConfigChange,
}),
);
} else if (!isPanelOpen && this.renderer) {
export interface Options {
inputDelay?: number;
anchorSelector?: string;
}

const defaultOptions: Options = {
inputDelay: 200,
};

export const SearchPanelPlugin = (options: Options = defaultOptions) =>
ViewPlugin.fromClass(
class implements PluginValue {
readonly view: EditorView;
readonly options: Options;

anchor: HTMLElement | null;
renderer: RendererItem | null;
searchQuery: SearchQueryParams = {
search: '',
caseSensitive: false,
wholeWord: false,
};
setViewSearchWithDelay: (config: Partial<SearchQueryParams>) => void;

constructor(view: EditorView) {
this.view = view;
this.anchor = null;
this.renderer = null;
this.options = options;

this.handleClose = this.handleClose.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleSearchNext = this.handleSearchNext.bind(this);
this.handleSearchPrev = this.handleSearchPrev.bind(this);
this.handleSearchConfigChange = this.handleSearchConfigChange.bind(this);
this.setViewSearchWithDelay = debounce(
this.setViewSearch,
this.options.inputDelay ?? INPUT_DELAY,
);
}

update(update: ViewUpdate): void {
const isPanelOpen = searchPanelOpen(update.state);

if (isPanelOpen && !this.renderer) {
this.anchor =
this.anchor ?? document.querySelector(this.options.anchorSelector ?? '');
this.renderer = this.view.state
.facet(ReactRendererFacet)
.createItem('cm-search', () =>
React.createElement(PortalWithPopup, {
open: true,
anchor: this.anchor,
onChange: this.handleChange,
onClose: this.handleClose,
onSearchNext: this.handleSearchNext,
onSearchPrev: this.handleSearchPrev,
onConfigChange: this.handleSearchConfigChange,
}),
);
} else if (!isPanelOpen && this.renderer) {
this.renderer?.remove();
this.renderer = null;
}
}

destroy() {
this.renderer?.remove();
this.renderer = null;
}
}

destroy() {
this.renderer?.remove();
}
setViewSearch(config: Partial<SearchQueryParams>) {
this.searchQuery = {
...this.searchQuery,
...config,
};
const searchQuery = new SearchQuery({
...this.searchQuery,
});

setViewSearch(config: Partial<SearchQueryParams>) {
this.searchQuery = {
...this.searchQuery,
...config,
};
const searchQuery = new SearchQuery({
...this.searchQuery,
});

this.view.dispatch({effects: setSearchQuery.of(searchQuery)});
}

handleChange(search: string) {
this.setViewSearchWithDelay({search});
}

handleClose() {
this.setViewSearch({search: ''});
closeSearchPanel(this.view);
}

handleSearchNext() {
findNext(this.view);
}

handleSearchPrev() {
findPrevious(this.view);
}

handleSearchConfigChange({
isCaseSensitive,
isWholeWord,
}: {
isCaseSensitive?: boolean;
isWholeWord?: boolean;
}) {
this.setViewSearch({
caseSensitive: isCaseSensitive,
wholeWord: isWholeWord,
});
}
},
{
provide: () => [
keymap.of(searchKeymap),
search({
createPanel: () => ({
// Create an empty search panel
dom: document.createElement('div'),
this.view.dispatch({effects: setSearchQuery.of(searchQuery)});
}

handleChange(search: string) {
this.setViewSearchWithDelay({search});
}

handleClose() {
this.setViewSearch({search: ''});
closeSearchPanel(this.view);
}

handleSearchNext() {
findNext(this.view);
}

handleSearchPrev() {
findPrevious(this.view);
}

handleSearchConfigChange({
isCaseSensitive,
isWholeWord,
}: {
isCaseSensitive?: boolean;
isWholeWord?: boolean;
}) {
this.setViewSearch({
caseSensitive: isCaseSensitive,
wholeWord: isWholeWord,
});
}
},
{
provide: () => [
keymap.of(searchKeymap),
search({
createPanel: () => ({
// Create an empty search panel
dom: document.createElement('div'),
}),
}),
}),
],
},
);
],
},
);
28 changes: 11 additions & 17 deletions src/markup/codemirror/search-plugin/view/SearchPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ const b = cn('search-card');

const noop = () => {};

const i18nSearch = i18n.bind(null);

export const SearchCard: React.FC<SearchCardProps> = ({
onChange = noop,
onClose = noop,
Expand Down Expand Up @@ -103,7 +101,7 @@ export const SearchCard: React.FC<SearchCardProps> = ({
return (
<Card className={b()}>
<div className={b('header')}>
<span className={b('title')}> {i18nSearch('title')}</span>
<span className={b('title')}> {i18n('title')}</span>
<Button onClick={handleClose} size="s" view="flat">
<Icon data={Xmark} size={14} />
</Button>
Expand Down Expand Up @@ -133,10 +131,10 @@ export const SearchCard: React.FC<SearchCardProps> = ({
checked={isCaseSensitive}
className={sp({mr: 4})}
>
{i18nSearch('label_case-sensitive')}
{i18n('label_case-sensitive')}
</Checkbox>
<Checkbox size="m" onUpdate={handleIsWholeWord} checked={isWholeWord}>
{i18nSearch('label_whole-word')}
{i18n('label_whole-word')}
</Checkbox>
</Card>
);
Expand All @@ -156,18 +154,14 @@ export const SearchPopup: React.FC<SearchPopupProps> = ({open, anchor, onClose,
const anchorRef = useRef<HTMLElement>(anchor);

return (
<>
{anchorRef && (
<Popup
onEscapeKeyDown={onClose}
open={open}
anchorRef={anchorRef as PopoverAnchorRef}
placement="bottom-end"
>
<SearchCard onClose={onClose} {...props} />
</Popup>
)}
</>
<Popup
onEscapeKeyDown={onClose}
open={anchorRef.current && open}
anchorRef={anchorRef as PopoverAnchorRef}
placement="bottom-end"
>
<SearchCard onClose={onClose} {...props} />
</Popup>
);
};

Expand Down

0 comments on commit 53d9a53

Please sign in to comment.