Skip to content

Commit

Permalink
feat(markup): added markup search (#295)
Browse files Browse the repository at this point in the history
* feat(markup): added markup search
  • Loading branch information
makhnatkin authored Jul 26, 2024
1 parent fb97667 commit 48372a6
Show file tree
Hide file tree
Showing 18 changed files with 442 additions and 17 deletions.
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
"@codemirror/commands": "6.5.0",
"@codemirror/lang-markdown": "6.2.5",
"@codemirror/language": "6.10.1",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.26.3",
"@gravity-ui/i18n": "^1.1.0",
Expand Down Expand Up @@ -198,9 +199,9 @@
"tslib": "^2.3.1"
},
"devDependencies": {
"@diplodoc/html-extension": "1.2.7",
"@diplodoc/latex-extension": "1.0.3",
"@diplodoc/mermaid-extension": "1.2.1",
"@diplodoc/html-extension": "1.2.7",
"@diplodoc/transform": "4.5.0",
"@gravity-ui/components": "3.0.0",
"@gravity-ui/eslint-config": "3.1.1",
Expand Down Expand Up @@ -268,9 +269,9 @@
}
},
"peerDependencies": {
"@diplodoc/html-extension": "^1.2.7",
"@diplodoc/latex-extension": "^1.0.3",
"@diplodoc/mermaid-extension": "^1.0.0",
"@diplodoc/html-extension": "^1.2.7",
"@diplodoc/transform": "^4.5.0",
"@gravity-ui/components": "^3.0.0",
"@gravity-ui/uikit": "^6.11.0",
Expand Down
3 changes: 2 additions & 1 deletion src/bundle/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type ToolbarActionData = {
attrs?: {[key: string]: any};
};

interface EventMap {
export interface EventMap {
change: null;
cancel: null;
submit: null;
Expand Down Expand Up @@ -273,6 +273,7 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
uploadHandler: this.fileUploadHandler,
needImgDimms: this.needToSetDimensionsForUploadedImages,
extraMarkupExtensions: this.#extraMarkupExtensions,
receiver: this,
}),
);
}
Expand Down
7 changes: 7 additions & 0 deletions 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,6 +290,7 @@ function Settings(props: EditorSettingsProps & {stickyToolbar: boolean}) {
})}
>
<EditorSettings {...props} />
<MarkupSearchAnchor {...props} />
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import type {TextInputProps} from '@gravity-ui/uikit';
import {TextInputFixed} from '../../../../../forms/TextInput';
import {UrlInputRow} from '../../../../../forms/UrlInputRow';
import Form from '../../../../../forms/base';
import {enterKeyHandler} from '../../../../../forms/utils';
import {i18n} from '../../../../../i18n/forms';
import {enterKeyHandler} from '../../../../../utils/handlers';

type LinkFormProps = {
href: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {EditorView} from 'prosemirror-view';
import {cn} from '../../../../../../classname';
import Form from '../../../../../../forms/base';
import {NumberInput} from '../../../../../../forms/components';
import {enterKeyHandler} from '../../../../../../forms/utils';
import {i18n} from '../../../../../../i18n/forms';
import {useAutoFocus} from '../../../../../../react-utils/useAutoFocus';
import {enterKeyHandler} from '../../../../../../utils/handlers';
import {LinkAttr, linkType} from '../../../../../markdown';
import {ImgSizeAttr} from '../../../../../specs';

Expand Down
2 changes: 1 addition & 1 deletion src/forms/FileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import {Tabs, TextInput, TextInputProps} from '@gravity-ui/uikit';
import {ClassNameProps, cn} from '../classname';
import {i18n} from '../i18n/forms';
import {isFunction} from '../lodash';
import {enterKeyHandler} from '../utils/handlers';

import {TextInputFixed} from './TextInput';
import Form from './base';
import {ButtonAttach} from './components';
import {enterKeyHandler} from './utils';

const b = cn('file-form');

Expand Down
2 changes: 1 addition & 1 deletion src/forms/ImageForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import {Tabs, TextInput, TextInputProps} from '@gravity-ui/uikit';
import {ClassNameProps, cn} from '../classname';
import {i18n} from '../i18n/forms';
import {isFunction} from '../lodash';
import {enterKeyHandler} from '../utils/handlers';

import {TextInputFixed} from './TextInput';
import Form from './base';
import {ButtonAttach, NumberInput} from './components';
import {enterKeyHandler} from './utils';

import './ImageForm.scss';

Expand Down
2 changes: 1 addition & 1 deletion src/forms/LinkForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import {TextInput, TextInputProps} from '@gravity-ui/uikit';

import {ClassNameProps} from '../classname';
import {i18n} from '../i18n/forms';
import {enterKeyHandler} from '../utils/handlers';

import {TextInputFixed} from './TextInput';
import {UrlInputRow} from './UrlInputRow';
import Form from './base';
import {enterKeyHandler} from './utils';

export type LinkFormSubmitParams = {
url: string;
Expand Down
9 changes: 0 additions & 9 deletions src/forms/utils.ts

This file was deleted.

5 changes: 5 additions & 0 deletions src/i18n/search/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"label_case-sensitive": "Case sensitive",
"label_whole-word": "Whole word",
"title": "Search in code"
}
8 changes: 8 additions & 0 deletions src/i18n/search/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {registerKeyset} from '../i18n';

import en from './en.json';
import ru from './ru.json';

const KEYSET = 'search';

export const i18n = registerKeyset(KEYSET, {en, ru});
5 changes: 5 additions & 0 deletions src/i18n/search/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"label_case-sensitive": "С учетом регистра",
"label_whole-word": "Слово целиком",
"title": "Найти в коде"
}
9 changes: 9 additions & 0 deletions src/markup/codemirror/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import {syntaxHighlighting} from '@codemirror/language';
import type {Extension, StateCommand} from '@codemirror/state';
import {EditorView, EditorViewConfig, keymap, placeholder} from '@codemirror/view';

import {EventMap} from '../../bundle/Editor';
import {ActionName} from '../../bundle/config/action-names';
import {ReactRenderStorage} from '../../extensions';
import {logger} from '../../logger';
import {Action as A, formatter as f} from '../../shortcuts';
import {Receiver} from '../../utils';
import {
insertLink,
toH1,
Expand All @@ -36,6 +38,7 @@ import {FileUploadHandler, FileUploadHandlerFacet} from './files-upload-facet';
import {gravityHighlightStyle, gravityTheme} from './gravity';
import {PairingCharactersExtension} from './pairing-chars';
import {ReactRendererFacet} from './react-facet';
import {SearchPanelPlugin} from './search-plugin/plugin';
import {yfmLang} from './yfm';

export type CreateCodemirrorParams = {
Expand All @@ -50,6 +53,7 @@ export type CreateCodemirrorParams = {
uploadHandler?: FileUploadHandler;
needImgDimms?: boolean;
extraMarkupExtensions?: Extension[];
receiver?: Receiver<EventMap>;
};

export function createCodemirror(params: CreateCodemirrorParams) {
Expand All @@ -63,6 +67,7 @@ export function createCodemirror(params: CreateCodemirrorParams) {
onChange,
onDocChange,
extraMarkupExtensions,
receiver,
} = params;

const extensions: Extension[] = [
Expand Down Expand Up @@ -118,6 +123,10 @@ export function createCodemirror(params: CreateCodemirrorParams) {
onScroll(event);
},
}),
SearchPanelPlugin({
anchorSelector: '.g-md-search-anchor',
receiver,
}),
];
if (params.uploadHandler) {
extensions.push(
Expand Down
166 changes: 166 additions & 0 deletions src/markup/codemirror/search-plugin/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import {
SearchQuery,
closeSearchPanel,
findNext,
findPrevious,
search,
searchKeymap,
searchPanelOpen,
setSearchQuery,
} from '@codemirror/search';
import {EditorView, PluginValue, ViewPlugin, ViewUpdate, keymap} from '@codemirror/view';

import {EditorMode, EventMap} from '../../../bundle/Editor';
import type {RendererItem} from '../../../extensions';
import {debounce} from '../../../lodash';
import {Receiver} from '../../../utils';
import {ReactRendererFacet} from '../react-facet';

import {renderSearchPopup} from './view/SearchPopup';

interface SearchQueryParams {
search: string;
caseSensitive?: boolean;
literal?: boolean;
regexp?: boolean;
replace?: string;
valid?: boolean;
wholeWord?: boolean;
}

const INPUT_DELAY = 200;

export interface SearchPanelPluginParams {
anchorSelector: string;
inputDelay?: number;
receiver?: Receiver<EventMap>;
}

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

anchor: HTMLElement | null;
renderer: RendererItem | null;
searchQuery: SearchQueryParams = {
search: '',
caseSensitive: false,
wholeWord: false,
};
receiver: Receiver<EventMap> | undefined;

setViewSearchWithDelay: (config: Partial<SearchQueryParams>) => void;

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

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.handleEditorModeChange = this.handleEditorModeChange.bind(this);

this.setViewSearchWithDelay = debounce(
this.setViewSearch,
this.params.inputDelay ?? INPUT_DELAY,
);
this.receiver?.on('change-editor-mode', this.handleEditorModeChange);
}

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

if (isPanelOpen && !this.renderer) {
this.anchor = document.querySelector(this.params.anchorSelector);
this.renderer = this.view.state
.facet(ReactRendererFacet)
.createItem('cm-search', () =>
renderSearchPopup({
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;
this.receiver?.off('change-editor-mode', this.handleEditorModeChange);
}

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

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

handleEditorModeChange({mode}: {mode: EditorMode}) {
if (mode === 'wysiwyg') {
closeSearchPanel(this.view);
}
}

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'),
}),
}),
],
},
);
Loading

0 comments on commit 48372a6

Please sign in to comment.