@gravity-ui/markdown-editor 13.4.2 → 13.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/build/cjs/bundle/Editor.d.ts +1 -2
  2. package/build/cjs/bundle/Editor.js +1 -0
  3. package/build/cjs/bundle/MarkdownEditorView.js +3 -1
  4. package/build/cjs/extensions/markdown/Link/plugins/LinkTooltipPlugin/TooltipView.js +2 -2
  5. package/build/cjs/extensions/yfm/ImgSize/plugins/ImgSizeNodeView/ImageForm/ImageForm.js +2 -2
  6. package/build/cjs/extensions/yfm/YfmHtmlBlock/YfmHtmlBlockSpecs/index.d.ts +1 -0
  7. package/build/cjs/extensions/yfm/YfmHtmlBlock/YfmHtmlBlockSpecs/index.js +2 -2
  8. package/build/cjs/extensions/yfm/YfmHtmlBlock/index.d.ts +1 -0
  9. package/build/cjs/extensions/yfm/YfmHtmlBlock/index.js +1 -0
  10. package/build/cjs/forms/FileForm.js +2 -2
  11. package/build/cjs/forms/ImageForm.js +2 -2
  12. package/build/cjs/forms/LinkForm.js +2 -2
  13. package/build/cjs/i18n/search/en.json +5 -0
  14. package/build/cjs/i18n/search/index.d.ts +7 -0
  15. package/build/cjs/i18n/search/index.js +9 -0
  16. package/build/cjs/i18n/search/ru.json +5 -0
  17. package/build/cjs/markup/codemirror/create.d.ts +3 -0
  18. package/build/cjs/markup/codemirror/create.js +6 -1
  19. package/build/cjs/markup/codemirror/search-plugin/plugin.d.ts +42 -0
  20. package/build/cjs/markup/codemirror/search-plugin/plugin.js +100 -0
  21. package/build/cjs/markup/codemirror/search-plugin/view/SearchPopup.css +9 -0
  22. package/build/cjs/markup/codemirror/search-plugin/view/SearchPopup.d.ts +29 -0
  23. package/build/cjs/markup/codemirror/search-plugin/view/SearchPopup.js +85 -0
  24. package/build/cjs/utils/handlers.d.ts +4 -0
  25. package/build/cjs/utils/handlers.js +29 -0
  26. package/build/cjs/version.js +1 -1
  27. package/build/esm/bundle/Editor.d.ts +1 -2
  28. package/build/esm/bundle/Editor.js +1 -0
  29. package/build/esm/bundle/MarkdownEditorView.js +3 -1
  30. package/build/esm/extensions/markdown/Link/plugins/LinkTooltipPlugin/TooltipView.js +1 -1
  31. package/build/esm/extensions/yfm/ImgSize/plugins/ImgSizeNodeView/ImageForm/ImageForm.js +1 -1
  32. package/build/esm/extensions/yfm/YfmHtmlBlock/YfmHtmlBlockSpecs/index.d.ts +1 -0
  33. package/build/esm/extensions/yfm/YfmHtmlBlock/YfmHtmlBlockSpecs/index.js +2 -2
  34. package/build/esm/extensions/yfm/YfmHtmlBlock/index.d.ts +1 -0
  35. package/build/esm/extensions/yfm/YfmHtmlBlock/index.js +1 -0
  36. package/build/esm/forms/FileForm.js +1 -1
  37. package/build/esm/forms/ImageForm.js +1 -1
  38. package/build/esm/forms/LinkForm.js +1 -1
  39. package/build/esm/i18n/search/en.json +5 -0
  40. package/build/esm/i18n/search/index.d.ts +7 -0
  41. package/build/esm/i18n/search/index.js +5 -0
  42. package/build/esm/i18n/search/ru.json +5 -0
  43. package/build/esm/markup/codemirror/create.d.ts +3 -0
  44. package/build/esm/markup/codemirror/create.js +6 -1
  45. package/build/esm/markup/codemirror/search-plugin/plugin.d.ts +42 -0
  46. package/build/esm/markup/codemirror/search-plugin/plugin.js +96 -0
  47. package/build/esm/markup/codemirror/search-plugin/view/SearchPopup.css +9 -0
  48. package/build/esm/markup/codemirror/search-plugin/view/SearchPopup.d.ts +30 -0
  49. package/build/esm/markup/codemirror/search-plugin/view/SearchPopup.js +80 -0
  50. package/build/esm/utils/handlers.d.ts +4 -0
  51. package/build/esm/utils/handlers.js +23 -0
  52. package/build/esm/version.js +1 -1
  53. package/build/styles.css +9 -0
  54. package/package.json +4 -3
  55. package/build/cjs/forms/utils.d.ts +0 -2
  56. package/build/cjs/forms/utils.js +0 -11
  57. package/build/esm/forms/utils.d.ts +0 -2
  58. package/build/esm/forms/utils.js +0 -7
@@ -19,7 +19,7 @@ export declare type ToolbarActionData = {
19
19
  [key: string]: any;
20
20
  };
21
21
  };
22
- interface EventMap {
22
+ export interface EventMap {
23
23
  change: null;
24
24
  cancel: null;
25
25
  submit: null;
@@ -70,4 +70,3 @@ export declare type EditorOptions = Pick<WysiwygEditorOptions, 'allowHTML' | 'li
70
70
  preset: EditorPreset;
71
71
  extraMarkupExtensions?: CodemirrorExtension[];
72
72
  };
73
- export {};
@@ -155,6 +155,7 @@ class EditorImpl extends event_emitter_1.SafeEventEmitter {
155
155
  uploadHandler: this.fileUploadHandler,
156
156
  needImgDimms: this.needToSetDimensionsForUploadedImages,
157
157
  extraMarkupExtensions: tslib_1.__classPrivateFieldGet(this, _EditorImpl_extraMarkupExtensions, "f"),
158
+ receiver: this,
158
159
  })), "f");
159
160
  }
160
161
  return tslib_1.__classPrivateFieldGet(this, _EditorImpl_markupEditor, "f");
@@ -113,6 +113,7 @@ exports.MarkdownEditorView = react_1.default.forwardRef((props, ref) => {
113
113
  react_1.default.createElement(SplitModeView_1.SplitModeView, { editor: editor, ref: splitModeViewWrapperRef })))))));
114
114
  });
115
115
  exports.MarkdownEditorView.displayName = 'MarkdownEditorView';
116
+ const MarkupSearchAnchor = ({ mode }) => (react_1.default.createElement(react_1.default.Fragment, null, mode === 'markup' && react_1.default.createElement("div", { className: "g-md-search-anchor" })));
116
117
  function Settings(props) {
117
118
  const wrapperRef = (0, react_1.useRef)(null);
118
119
  const isSticky = (0, useSticky_1.useSticky)(wrapperRef) && props.toolbarVisibility && props.stickyToolbar;
@@ -121,7 +122,8 @@ function Settings(props) {
121
122
  withToolbar: props.toolbarVisibility,
122
123
  stickyActive: isSticky,
123
124
  }) },
124
- react_1.default.createElement(settings_1.EditorSettings, Object.assign({}, props)))));
125
+ react_1.default.createElement(settings_1.EditorSettings, Object.assign({}, props)),
126
+ react_1.default.createElement(MarkupSearchAnchor, Object.assign({}, props)))));
125
127
  }
126
128
  function isPreviewKeyDown(e) {
127
129
  const modKey = (0, platform_1.isMac)() ? e.metaKey : e.ctrlKey;
@@ -6,14 +6,14 @@ const react_1 = tslib_1.__importDefault(require("react"));
6
6
  const TextInput_1 = require("../../../../../forms/TextInput");
7
7
  const UrlInputRow_1 = require("../../../../../forms/UrlInputRow");
8
8
  const base_1 = tslib_1.__importDefault(require("../../../../../forms/base"));
9
- const utils_1 = require("../../../../../forms/utils");
10
9
  const forms_1 = require("../../../../../i18n/forms");
10
+ const handlers_1 = require("../../../../../utils/handlers");
11
11
  exports.LinkForm = react_1.default.memo(function LinkForm({ href, autoFocus, onChange, onCancel, }) {
12
12
  const [url, setUrl] = react_1.default.useState(href);
13
13
  const handleSubmit = () => {
14
14
  onChange === null || onChange === void 0 ? void 0 : onChange({ href: url });
15
15
  };
16
- const inputEnterKeyHandler = (0, utils_1.enterKeyHandler)(handleSubmit);
16
+ const inputEnterKeyHandler = (0, handlers_1.enterKeyHandler)(handleSubmit);
17
17
  return (react_1.default.createElement(base_1.default.Form, null,
18
18
  react_1.default.createElement(base_1.default.Layout, null,
19
19
  react_1.default.createElement(base_1.default.Row, { label: (0, forms_1.i18n)('common_link'), help: (0, forms_1.i18n)('link_url_help'), control: react_1.default.createElement(UrlInputRow_1.UrlInputRow, { href: url, input: react_1.default.createElement(TextInput_1.TextInputFixed, { size: "s", hasClear: true, view: "normal", value: url, onUpdate: setUrl, placeholder: "https://", autoFocus: autoFocus, onKeyPress: inputEnterKeyHandler }) }) })),
@@ -8,9 +8,9 @@ const is_number_1 = tslib_1.__importDefault(require("is-number"));
8
8
  const classname_1 = require("../../../../../../classname");
9
9
  const base_1 = tslib_1.__importDefault(require("../../../../../../forms/base"));
10
10
  const components_1 = require("../../../../../../forms/components");
11
- const utils_1 = require("../../../../../../forms/utils");
12
11
  const forms_1 = require("../../../../../../i18n/forms");
13
12
  const useAutoFocus_1 = require("../../../../../../react-utils/useAutoFocus");
13
+ const handlers_1 = require("../../../../../../utils/handlers");
14
14
  const markdown_1 = require("../../../../../markdown");
15
15
  const specs_1 = require("../../../../../specs");
16
16
  const b = (0, classname_1.cn)('image-tooltip-form');
@@ -38,7 +38,7 @@ const ImageForm = ({ node, updateAttributes, view, unsetEdit, dom }) => {
38
38
  const linkRef = react_1.default.useRef(null);
39
39
  const imageNameRef = react_1.default.useRef(null);
40
40
  (0, useAutoFocus_1.useAutoFocus)(link ? linkRef : imageNameRef);
41
- const inputEnterKeyHandler = (0, utils_1.enterKeyHandler)(handleSubmit);
41
+ const inputEnterKeyHandler = (0, handlers_1.enterKeyHandler)(handleSubmit);
42
42
  return (react_1.default.createElement(uikit_1.Popup, { open: true, anchorRef: dom, placement: ['bottom-start', 'top-start', 'bottom-end', 'top-end'], onOutsideClick: unsetEdit },
43
43
  react_1.default.createElement(base_1.default.Form, { className: b() },
44
44
  react_1.default.createElement(base_1.default.Layout, null,
@@ -2,6 +2,7 @@ import type { ExtensionNodeSpec } from '../../../../core';
2
2
  export { yfmHtmlBlockNodeName } from './const';
3
3
  export declare type YfmHtmlBlockSpecsOptions = {
4
4
  nodeView?: ExtensionNodeSpec['view'];
5
+ sanitize?: (dirtyHtml: string) => string;
5
6
  };
6
7
  export declare const YfmHtmlBlockSpecs: import("../../../../core").ExtensionWithOptions<YfmHtmlBlockSpecsOptions> & {
7
8
  readonly NodeName: "yfm_html_block";
@@ -6,9 +6,9 @@ const html_extension_1 = require("@diplodoc/html-extension");
6
6
  const const_1 = require("./const");
7
7
  var const_2 = require("./const");
8
8
  Object.defineProperty(exports, "yfmHtmlBlockNodeName", { enumerable: true, get: function () { return const_2.yfmHtmlBlockNodeName; } });
9
- const YfmHtmlBlockSpecsExtension = (builder, { nodeView }) => {
9
+ const YfmHtmlBlockSpecsExtension = (builder, { nodeView, sanitize }) => {
10
10
  builder
11
- .configureMd((md) => md.use((0, html_extension_1.transform)({ bundle: false }), {}))
11
+ .configureMd((md) => md.use((0, html_extension_1.transform)({ bundle: false, sanitize }), {}))
12
12
  .addNode(const_1.YfmHtmlBlockConsts.NodeName, () => ({
13
13
  fromMd: {
14
14
  tokenSpec: {
@@ -3,6 +3,7 @@ import { Action, ExtensionAuto } from '../../../core';
3
3
  import { YfmHtmlBlockAction } from './YfmHtmlBlockSpecs/const';
4
4
  export declare type YfmHtmlBlockOptions = {
5
5
  useConfig?: () => IHTMLIFrameElementConfig | undefined;
6
+ sanitize?: (dirtyHtml: string) => string;
6
7
  };
7
8
  export declare const YfmHtmlBlock: ExtensionAuto<YfmHtmlBlockOptions>;
8
9
  declare global {
@@ -8,6 +8,7 @@ const actions_1 = require("./actions");
8
8
  const YfmHtmlBlock = (builder, options) => {
9
9
  builder.use(YfmHtmlBlockSpecs_1.YfmHtmlBlockSpecs, {
10
10
  nodeView: YfmHtmlBlockNodeViewFactory(options),
11
+ sanitize: options.sanitize,
11
12
  });
12
13
  builder.addAction(const_1.YfmHtmlBlockAction, () => actions_1.addYfmHtmlBlock);
13
14
  };
@@ -7,10 +7,10 @@ const uikit_1 = require("@gravity-ui/uikit");
7
7
  const classname_1 = require("../classname");
8
8
  const forms_1 = require("../i18n/forms");
9
9
  const lodash_1 = require("../lodash");
10
+ const handlers_1 = require("../utils/handlers");
10
11
  const TextInput_1 = require("./TextInput");
11
12
  const base_1 = tslib_1.__importDefault(require("./base"));
12
13
  const components_1 = require("./components");
13
- const utils_1 = require("./utils");
14
14
  const b = (0, classname_1.cn)('file-form');
15
15
  const FileForm = ({ className, autoFocus, onCancel, onSubmit, onAttach, loading, }) => {
16
16
  const [tabId, setTabId] = react_1.default.useState(() => (0, lodash_1.isFunction)(onAttach) ? "attach" /* TabId.Attach */ : "link" /* TabId.Link */);
@@ -28,7 +28,7 @@ const FileForm = ({ className, autoFocus, onCancel, onSubmit, onAttach, loading,
28
28
  name: name.trim(),
29
29
  });
30
30
  };
31
- const inputEnterKeyHandler = (0, utils_1.enterKeyHandler)(handleSubmit);
31
+ const inputEnterKeyHandler = (0, handlers_1.enterKeyHandler)(handleSubmit);
32
32
  return (react_1.default.createElement(base_1.default.Form, { className: b(null, [className]) },
33
33
  shouldRenderTabs && (react_1.default.createElement(uikit_1.Tabs, { activeTab: tabId, onSelectTab: setTabId, items: [
34
34
  { id: "attach" /* TabId.Attach */, title: (0, forms_1.i18n)('common_tab_attach') },
@@ -7,10 +7,10 @@ const uikit_1 = require("@gravity-ui/uikit");
7
7
  const classname_1 = require("../classname");
8
8
  const forms_1 = require("../i18n/forms");
9
9
  const lodash_1 = require("../lodash");
10
+ const handlers_1 = require("../utils/handlers");
10
11
  const TextInput_1 = require("./TextInput");
11
12
  const base_1 = tslib_1.__importDefault(require("./base"));
12
13
  const components_1 = require("./components");
13
- const utils_1 = require("./utils");
14
14
  const b = (0, classname_1.cn)('image-form');
15
15
  const ImageForm = ({ className, autoFocus, onCancel, onSubmit, onAttach, loading, }) => {
16
16
  const [tabId, setTabId] = react_1.default.useState(() => (0, lodash_1.isFunction)(onAttach) ? "attach" /* ImageTabId.Attach */ : "link" /* ImageTabId.Link */);
@@ -37,7 +37,7 @@ const ImageForm = ({ className, autoFocus, onCancel, onSubmit, onAttach, loading
37
37
  data.height = height;
38
38
  onSubmit(data);
39
39
  };
40
- const inputEnterKeyHandler = (0, utils_1.enterKeyHandler)(handleSubmit);
40
+ const inputEnterKeyHandler = (0, handlers_1.enterKeyHandler)(handleSubmit);
41
41
  return (react_1.default.createElement(base_1.default.Form, { className: b(null, [className]) },
42
42
  shouldRenderTabs && (react_1.default.createElement(uikit_1.Tabs, { activeTab: tabId, onSelectTab: setTabId, items: [
43
43
  { id: "attach" /* ImageTabId.Attach */, title: (0, forms_1.i18n)('common_tab_attach') },
@@ -5,17 +5,17 @@ const tslib_1 = require("tslib");
5
5
  const react_1 = tslib_1.__importDefault(require("react"));
6
6
  const uikit_1 = require("@gravity-ui/uikit");
7
7
  const forms_1 = require("../i18n/forms");
8
+ const handlers_1 = require("../utils/handlers");
8
9
  const TextInput_1 = require("./TextInput");
9
10
  const UrlInputRow_1 = require("./UrlInputRow");
10
11
  const base_1 = tslib_1.__importDefault(require("./base"));
11
- const utils_1 = require("./utils");
12
12
  exports.LinkForm = react_1.default.memo(function LinkForm({ className, autoFocus, initialUrl, initialText, readOnlyText, onSubmit, onCancel, }) {
13
13
  const [url, setUrl] = react_1.default.useState(initialUrl !== null && initialUrl !== void 0 ? initialUrl : '');
14
14
  const [text, setText] = react_1.default.useState(initialText !== null && initialText !== void 0 ? initialText : '');
15
15
  const handleSubmit = () => {
16
16
  onSubmit({ url, text });
17
17
  };
18
- const inputEnterKeyHandler = (0, utils_1.enterKeyHandler)(handleSubmit);
18
+ const inputEnterKeyHandler = (0, handlers_1.enterKeyHandler)(handleSubmit);
19
19
  return (react_1.default.createElement(base_1.default.Form, { className: className },
20
20
  react_1.default.createElement(base_1.default.Layout, null,
21
21
  react_1.default.createElement(base_1.default.Row, { label: (0, forms_1.i18n)('common_link'), help: (0, forms_1.i18n)('link_url_help'), control: react_1.default.createElement(UrlInputRow_1.UrlInputRow, { href: url, input: react_1.default.createElement(TextInput_1.TextInputFixed, { size: "s", view: "normal", value: url, onUpdate: setUrl, autoFocus: autoFocus, placeholder: "https://", onKeyPress: inputEnterKeyHandler }) }) }),
@@ -0,0 +1,5 @@
1
+ {
2
+ "label_case-sensitive": "Case sensitive",
3
+ "label_whole-word": "Whole word",
4
+ "title": "Search in code"
5
+ }
@@ -0,0 +1,7 @@
1
+ export declare const i18n: <G extends "title" | "label_case-sensitive" | "label_whole-word", S extends string>(key: G | (string extends S ? S : never), params?: {
2
+ [key: string]: any;
3
+ } | undefined) => S extends G ? {
4
+ "label_case-sensitive": string;
5
+ "label_whole-word": string;
6
+ title: string;
7
+ }[G] : string;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.i18n = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const i18n_1 = require("../i18n");
6
+ const en_json_1 = tslib_1.__importDefault(require("./en.json"));
7
+ const ru_json_1 = tslib_1.__importDefault(require("./ru.json"));
8
+ const KEYSET = 'search';
9
+ exports.i18n = (0, i18n_1.registerKeyset)(KEYSET, { en: en_json_1.default, ru: ru_json_1.default });
@@ -0,0 +1,5 @@
1
+ {
2
+ "label_case-sensitive": "С учетом регистра",
3
+ "label_whole-word": "Слово целиком",
4
+ "title": "Найти в коде"
5
+ }
@@ -1,6 +1,8 @@
1
1
  import type { Extension, StateCommand } from '@codemirror/state';
2
2
  import { EditorView, EditorViewConfig } from '@codemirror/view';
3
+ import { EventMap } from '../../bundle/Editor';
3
4
  import { ReactRenderStorage } from '../../extensions';
5
+ import { Receiver } from '../../utils';
4
6
  import { FileUploadHandler } from './files-upload-facet';
5
7
  export declare type CreateCodemirrorParams = {
6
8
  doc: EditorViewConfig['doc'];
@@ -14,6 +16,7 @@ export declare type CreateCodemirrorParams = {
14
16
  uploadHandler?: FileUploadHandler;
15
17
  needImgDimms?: boolean;
16
18
  extraMarkupExtensions?: Extension[];
19
+ receiver?: Receiver<EventMap>;
17
20
  };
18
21
  export declare function createCodemirror(params: CreateCodemirrorParams): EditorView;
19
22
  export declare function withLogger(action: string, command: StateCommand): StateCommand;
@@ -13,9 +13,10 @@ const files_upload_facet_1 = require("./files-upload-facet");
13
13
  const gravity_1 = require("./gravity");
14
14
  const pairing_chars_1 = require("./pairing-chars");
15
15
  const react_facet_1 = require("./react-facet");
16
+ const plugin_1 = require("./search-plugin/plugin");
16
17
  const yfm_1 = require("./yfm");
17
18
  function createCodemirror(params) {
18
- const { doc, placeholderText, reactRenderer, onCancel, onScroll, onSubmit, onChange, onDocChange, extraMarkupExtensions, } = params;
19
+ const { doc, placeholderText, reactRenderer, onCancel, onScroll, onSubmit, onChange, onDocChange, extraMarkupExtensions, receiver, } = params;
19
20
  const extensions = [
20
21
  gravity_1.gravityTheme,
21
22
  (0, view_1.placeholder)(placeholderText),
@@ -69,6 +70,10 @@ function createCodemirror(params) {
69
70
  onScroll(event);
70
71
  },
71
72
  }),
73
+ (0, plugin_1.SearchPanelPlugin)({
74
+ anchorSelector: '.g-md-search-anchor',
75
+ receiver,
76
+ }),
72
77
  ];
73
78
  if (params.uploadHandler) {
74
79
  extensions.push(files_upload_facet_1.FileUploadHandlerFacet.of({
@@ -0,0 +1,42 @@
1
+ import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
2
+ import { EditorMode, EventMap } from '../../../bundle/Editor';
3
+ import type { RendererItem } from '../../../extensions';
4
+ import { Receiver } from '../../../utils';
5
+ interface SearchQueryParams {
6
+ search: string;
7
+ caseSensitive?: boolean;
8
+ literal?: boolean;
9
+ regexp?: boolean;
10
+ replace?: string;
11
+ valid?: boolean;
12
+ wholeWord?: boolean;
13
+ }
14
+ export interface SearchPanelPluginParams {
15
+ anchorSelector: string;
16
+ inputDelay?: number;
17
+ receiver?: Receiver<EventMap>;
18
+ }
19
+ export declare const SearchPanelPlugin: (params: SearchPanelPluginParams) => ViewPlugin<{
20
+ readonly view: EditorView;
21
+ readonly params: SearchPanelPluginParams;
22
+ anchor: HTMLElement | null;
23
+ renderer: RendererItem | null;
24
+ searchQuery: SearchQueryParams;
25
+ receiver: Receiver<EventMap> | undefined;
26
+ setViewSearchWithDelay: (config: Partial<SearchQueryParams>) => void;
27
+ update(update: ViewUpdate): void;
28
+ destroy(): void;
29
+ setViewSearch(config: Partial<SearchQueryParams>): void;
30
+ handleEditorModeChange({ mode }: {
31
+ mode: EditorMode;
32
+ }): void;
33
+ handleChange(search: string): void;
34
+ handleClose(): void;
35
+ handleSearchNext(): void;
36
+ handleSearchPrev(): void;
37
+ handleSearchConfigChange({ isCaseSensitive, isWholeWord, }: {
38
+ isCaseSensitive?: boolean | undefined;
39
+ isWholeWord?: boolean | undefined;
40
+ }): void;
41
+ }>;
42
+ export {};
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SearchPanelPlugin = void 0;
4
+ const search_1 = require("@codemirror/search");
5
+ const view_1 = require("@codemirror/view");
6
+ const lodash_1 = require("../../../lodash");
7
+ const react_facet_1 = require("../react-facet");
8
+ const SearchPopup_1 = require("./view/SearchPopup");
9
+ const INPUT_DELAY = 200;
10
+ const SearchPanelPlugin = (params) => view_1.ViewPlugin.fromClass(class {
11
+ constructor(view) {
12
+ var _a, _b;
13
+ this.searchQuery = {
14
+ search: '',
15
+ caseSensitive: false,
16
+ wholeWord: false,
17
+ };
18
+ this.view = view;
19
+ this.anchor = null;
20
+ this.renderer = null;
21
+ this.params = params;
22
+ this.receiver = params.receiver;
23
+ this.handleClose = this.handleClose.bind(this);
24
+ this.handleChange = this.handleChange.bind(this);
25
+ this.handleSearchNext = this.handleSearchNext.bind(this);
26
+ this.handleSearchPrev = this.handleSearchPrev.bind(this);
27
+ this.handleSearchConfigChange = this.handleSearchConfigChange.bind(this);
28
+ this.handleEditorModeChange = this.handleEditorModeChange.bind(this);
29
+ this.setViewSearchWithDelay = (0, lodash_1.debounce)(this.setViewSearch, (_a = this.params.inputDelay) !== null && _a !== void 0 ? _a : INPUT_DELAY);
30
+ (_b = this.receiver) === null || _b === void 0 ? void 0 : _b.on('change-editor-mode', this.handleEditorModeChange);
31
+ }
32
+ update(update) {
33
+ var _a;
34
+ const isPanelOpen = (0, search_1.searchPanelOpen)(update.state);
35
+ if (isPanelOpen && !this.renderer) {
36
+ this.anchor = document.querySelector(this.params.anchorSelector);
37
+ this.renderer = this.view.state
38
+ .facet(react_facet_1.ReactRendererFacet)
39
+ .createItem('cm-search', () => (0, SearchPopup_1.renderSearchPopup)({
40
+ open: true,
41
+ anchor: this.anchor,
42
+ onChange: this.handleChange,
43
+ onClose: this.handleClose,
44
+ onSearchNext: this.handleSearchNext,
45
+ onSearchPrev: this.handleSearchPrev,
46
+ onConfigChange: this.handleSearchConfigChange,
47
+ }));
48
+ }
49
+ else if (!isPanelOpen && this.renderer) {
50
+ (_a = this.renderer) === null || _a === void 0 ? void 0 : _a.remove();
51
+ this.renderer = null;
52
+ }
53
+ }
54
+ destroy() {
55
+ var _a, _b;
56
+ (_a = this.renderer) === null || _a === void 0 ? void 0 : _a.remove();
57
+ this.renderer = null;
58
+ (_b = this.receiver) === null || _b === void 0 ? void 0 : _b.off('change-editor-mode', this.handleEditorModeChange);
59
+ }
60
+ setViewSearch(config) {
61
+ this.searchQuery = Object.assign(Object.assign({}, this.searchQuery), config);
62
+ const searchQuery = new search_1.SearchQuery(Object.assign({}, this.searchQuery));
63
+ this.view.dispatch({ effects: search_1.setSearchQuery.of(searchQuery) });
64
+ }
65
+ handleEditorModeChange({ mode }) {
66
+ if (mode === 'wysiwyg') {
67
+ (0, search_1.closeSearchPanel)(this.view);
68
+ }
69
+ }
70
+ handleChange(search) {
71
+ this.setViewSearchWithDelay({ search });
72
+ }
73
+ handleClose() {
74
+ this.setViewSearch({ search: '' });
75
+ (0, search_1.closeSearchPanel)(this.view);
76
+ }
77
+ handleSearchNext() {
78
+ (0, search_1.findNext)(this.view);
79
+ }
80
+ handleSearchPrev() {
81
+ (0, search_1.findPrevious)(this.view);
82
+ }
83
+ handleSearchConfigChange({ isCaseSensitive, isWholeWord, }) {
84
+ this.setViewSearch({
85
+ caseSensitive: isCaseSensitive,
86
+ wholeWord: isWholeWord,
87
+ });
88
+ }
89
+ }, {
90
+ provide: () => [
91
+ view_1.keymap.of(search_1.searchKeymap),
92
+ (0, search_1.search)({
93
+ createPanel: () => ({
94
+ // Create an empty search panel
95
+ dom: document.createElement('div'),
96
+ }),
97
+ }),
98
+ ],
99
+ });
100
+ exports.SearchPanelPlugin = SearchPanelPlugin;
@@ -0,0 +1,9 @@
1
+ .g-md-search-card {
2
+ padding: var(--g-spacing-2) var(--g-spacing-2) var(--g-spacing-3) var(--g-spacing-4);
3
+ }
4
+ .g-md-search-card__header {
5
+ display: flex;
6
+ justify-content: space-between;
7
+ align-items: center;
8
+ margin-bottom: var(--g-spacing-1);
9
+ }
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ interface SearchConfig {
3
+ isCaseSensitive: boolean;
4
+ isWholeWord: boolean;
5
+ }
6
+ interface SearchCardProps {
7
+ onSearchKeyDown?: (query: string) => void;
8
+ onChange?: (query: string) => void;
9
+ onClose?: (query: string) => void;
10
+ onSearchPrev?: (query: string) => void;
11
+ onSearchNext?: (query: string) => void;
12
+ onConfigChange?: (config: SearchConfig) => void;
13
+ }
14
+ export declare const SearchCard: React.FC<SearchCardProps>;
15
+ export interface SearchPopupProps {
16
+ anchor: HTMLElement;
17
+ onChange: (query: string) => void;
18
+ onClose: () => void;
19
+ onSearchNext: () => void;
20
+ onSearchPrev: () => void;
21
+ onConfigChange: (config: SearchConfig) => void;
22
+ open: boolean;
23
+ }
24
+ export declare const SearchPopup: React.FC<SearchPopupProps>;
25
+ interface SearchPopupWithRefProps extends Omit<SearchPopupProps, 'anchor'> {
26
+ anchor: HTMLElement | null;
27
+ }
28
+ export declare function renderSearchPopup({ anchor, open, onClose, ...props }: SearchPopupWithRefProps): JSX.Element;
29
+ export {};
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderSearchPopup = exports.SearchPopup = exports.SearchCard = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const react_1 = tslib_1.__importStar(require("react"));
6
+ const icons_1 = require("@gravity-ui/icons");
7
+ const uikit_1 = require("@gravity-ui/uikit");
8
+ const classname_1 = require("../../../../classname");
9
+ const search_1 = require("../../../../i18n/search");
10
+ const handlers_1 = require("../../../../utils/handlers");
11
+ const b = (0, classname_1.cn)('search-card');
12
+ const noop = () => { };
13
+ const SearchCard = ({ onChange = noop, onClose = noop, onSearchPrev = noop, onSearchNext = noop, onConfigChange = noop, }) => {
14
+ const [query, setQuery] = (0, react_1.useState)('');
15
+ const [isCaseSensitive, setIsCaseSensitive] = (0, react_1.useState)(false);
16
+ const [isWholeWord, setIsWholeWord] = (0, react_1.useState)(false);
17
+ const textInputRef = (0, react_1.useRef)(null);
18
+ const setInputFocus = () => {
19
+ var _a;
20
+ (_a = textInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
21
+ };
22
+ const handleInputChange = (event) => {
23
+ const { target: { value }, } = event;
24
+ setQuery(value);
25
+ onChange(value);
26
+ };
27
+ const handleClose = () => {
28
+ setQuery('');
29
+ onClose(query);
30
+ setInputFocus();
31
+ };
32
+ const handlePrev = () => {
33
+ onSearchPrev(query);
34
+ setInputFocus();
35
+ };
36
+ const handleNext = () => {
37
+ onSearchNext(query);
38
+ setInputFocus();
39
+ };
40
+ const handleIsCaseSensitive = () => {
41
+ onConfigChange({
42
+ isCaseSensitive: !isCaseSensitive,
43
+ isWholeWord,
44
+ });
45
+ setIsCaseSensitive((isCaseSensitive) => !isCaseSensitive);
46
+ setInputFocus();
47
+ };
48
+ const handleIsWholeWord = () => {
49
+ onConfigChange({
50
+ isCaseSensitive,
51
+ isWholeWord: !isWholeWord,
52
+ });
53
+ setIsWholeWord((isWholeWord) => !isWholeWord);
54
+ setInputFocus();
55
+ };
56
+ const handleSearchKeyPress = (0, handlers_1.enterKeyHandler)(handleNext);
57
+ return (react_1.default.createElement(uikit_1.Card, { className: b() },
58
+ react_1.default.createElement("div", { className: b('header') },
59
+ react_1.default.createElement("span", { className: b('title') },
60
+ " ",
61
+ (0, search_1.i18n)('title')),
62
+ react_1.default.createElement(uikit_1.Button, { onClick: handleClose, size: "s", view: "flat" },
63
+ react_1.default.createElement(uikit_1.Icon, { data: icons_1.Xmark, size: 14 }))),
64
+ react_1.default.createElement(uikit_1.TextInput, { controlRef: textInputRef, className: (0, uikit_1.sp)({ mb: 2 }), size: "s", autoFocus: true, onKeyPress: handleSearchKeyPress, onChange: handleInputChange, value: query, endContent: react_1.default.createElement(react_1.default.Fragment, null,
65
+ react_1.default.createElement(uikit_1.Button, { onClick: handlePrev },
66
+ react_1.default.createElement(uikit_1.Icon, { data: icons_1.ChevronUp, size: 12 })),
67
+ react_1.default.createElement(uikit_1.Button, { onClick: handleNext },
68
+ react_1.default.createElement(uikit_1.Icon, { data: icons_1.ChevronDown, size: 12 }))) }),
69
+ react_1.default.createElement(uikit_1.Checkbox, { size: "m", onUpdate: handleIsCaseSensitive, checked: isCaseSensitive, className: (0, uikit_1.sp)({ mr: 4 }) }, (0, search_1.i18n)('label_case-sensitive')),
70
+ react_1.default.createElement(uikit_1.Checkbox, { size: "m", onUpdate: handleIsWholeWord, checked: isWholeWord }, (0, search_1.i18n)('label_whole-word'))));
71
+ };
72
+ exports.SearchCard = SearchCard;
73
+ const SearchPopup = (_a) => {
74
+ var { open, anchor, onClose } = _a, props = tslib_1.__rest(_a, ["open", "anchor", "onClose"]);
75
+ const anchorRef = (0, react_1.useRef)(anchor);
76
+ return (react_1.default.createElement(uikit_1.Popup, { onEscapeKeyDown: onClose, open: anchorRef.current && open, anchorRef: anchorRef, placement: "bottom-end" },
77
+ react_1.default.createElement(exports.SearchCard, Object.assign({ onClose: onClose }, props))));
78
+ };
79
+ exports.SearchPopup = SearchPopup;
80
+ exports.SearchPopup.displayName = 'SearchPopup';
81
+ function renderSearchPopup(_a) {
82
+ var { anchor, open, onClose } = _a, props = tslib_1.__rest(_a, ["anchor", "open", "onClose"]);
83
+ return (react_1.default.createElement(react_1.default.Fragment, null, anchor && react_1.default.createElement(exports.SearchPopup, Object.assign({ open: open, onClose: onClose, anchor: anchor }, props))));
84
+ }
85
+ exports.renderSearchPopup = renderSearchPopup;
@@ -0,0 +1,4 @@
1
+ /// <reference types="react" />
2
+ export declare function enterKeyHandler<T>(handler: React.KeyboardEventHandler<T>): React.KeyboardEventHandler<T>;
3
+ export declare function escapeKeyHandler<T>(handler: React.KeyboardEventHandler<T>): React.KeyboardEventHandler<T>;
4
+ export declare function combinedKeyHandler<T>(handlers: Record<string, React.KeyboardEventHandler<T>>): React.KeyboardEventHandler<T>;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.combinedKeyHandler = exports.escapeKeyHandler = exports.enterKeyHandler = void 0;
4
+ const shortcuts_1 = require("../shortcuts");
5
+ function enterKeyHandler(handler) {
6
+ return (event) => {
7
+ if (event.key === shortcuts_1.Key.Enter) {
8
+ handler(event);
9
+ }
10
+ };
11
+ }
12
+ exports.enterKeyHandler = enterKeyHandler;
13
+ function escapeKeyHandler(handler) {
14
+ return (event) => {
15
+ if (event.key === shortcuts_1.Key.Esc) {
16
+ handler(event);
17
+ }
18
+ };
19
+ }
20
+ exports.escapeKeyHandler = escapeKeyHandler;
21
+ function combinedKeyHandler(handlers) {
22
+ return (event) => {
23
+ const handler = handlers[event.key];
24
+ if (handler) {
25
+ handler(event);
26
+ }
27
+ };
28
+ }
29
+ exports.combinedKeyHandler = combinedKeyHandler;
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
4
  /** During build process, the current version will be injected here */
5
- exports.VERSION = typeof '13.4.2' !== 'undefined' ? '13.4.2' : 'unknown';
5
+ exports.VERSION = typeof '13.5.1' !== 'undefined' ? '13.5.1' : 'unknown';
@@ -19,7 +19,7 @@ export declare type ToolbarActionData = {
19
19
  [key: string]: any;
20
20
  };
21
21
  };
22
- interface EventMap {
22
+ export interface EventMap {
23
23
  change: null;
24
24
  cancel: null;
25
25
  submit: null;
@@ -70,4 +70,3 @@ export declare type EditorOptions = Pick<WysiwygEditorOptions, 'allowHTML' | 'li
70
70
  preset: EditorPreset;
71
71
  extraMarkupExtensions?: CodemirrorExtension[];
72
72
  };
73
- export {};
@@ -152,6 +152,7 @@ export class EditorImpl extends SafeEventEmitter {
152
152
  uploadHandler: this.fileUploadHandler,
153
153
  needImgDimms: this.needToSetDimensionsForUploadedImages,
154
154
  extraMarkupExtensions: __classPrivateFieldGet(this, _EditorImpl_extraMarkupExtensions, "f"),
155
+ receiver: this,
155
156
  })), "f");
156
157
  }
157
158
  return __classPrivateFieldGet(this, _EditorImpl_markupEditor, "f");
@@ -110,6 +110,7 @@ export const MarkdownEditorView = React.forwardRef((props, ref) => {
110
110
  React.createElement(SplitModeView, { editor: editor, ref: splitModeViewWrapperRef })))))));
111
111
  });
112
112
  MarkdownEditorView.displayName = 'MarkdownEditorView';
113
+ const MarkupSearchAnchor = ({ mode }) => (React.createElement(React.Fragment, null, mode === 'markup' && React.createElement("div", { className: "g-md-search-anchor" })));
113
114
  function Settings(props) {
114
115
  const wrapperRef = useRef(null);
115
116
  const isSticky = useSticky(wrapperRef) && props.toolbarVisibility && props.stickyToolbar;
@@ -118,7 +119,8 @@ function Settings(props) {
118
119
  withToolbar: props.toolbarVisibility,
119
120
  stickyActive: isSticky,
120
121
  }) },
121
- React.createElement(EditorSettings, Object.assign({}, props)))));
122
+ React.createElement(EditorSettings, Object.assign({}, props)),
123
+ React.createElement(MarkupSearchAnchor, Object.assign({}, props)))));
122
124
  }
123
125
  function isPreviewKeyDown(e) {
124
126
  const modKey = isMac() ? e.metaKey : e.ctrlKey;
@@ -2,8 +2,8 @@ import React from 'react';
2
2
  import { TextInputFixed } from '../../../../../forms/TextInput';
3
3
  import { UrlInputRow } from '../../../../../forms/UrlInputRow';
4
4
  import Form from '../../../../../forms/base';
5
- import { enterKeyHandler } from '../../../../../forms/utils';
6
5
  import { i18n } from '../../../../../i18n/forms';
6
+ import { enterKeyHandler } from '../../../../../utils/handlers';
7
7
  export const LinkForm = React.memo(function LinkForm({ href, autoFocus, onChange, onCancel, }) {
8
8
  const [url, setUrl] = React.useState(href);
9
9
  const handleSubmit = () => {
@@ -4,9 +4,9 @@ import isNumber from 'is-number';
4
4
  import { cn } from '../../../../../../classname';
5
5
  import Form from '../../../../../../forms/base';
6
6
  import { NumberInput } from '../../../../../../forms/components';
7
- import { enterKeyHandler } from '../../../../../../forms/utils';
8
7
  import { i18n } from '../../../../../../i18n/forms';
9
8
  import { useAutoFocus } from '../../../../../../react-utils/useAutoFocus';
9
+ import { enterKeyHandler } from '../../../../../../utils/handlers';
10
10
  import { LinkAttr, linkType } from '../../../../../markdown';
11
11
  import { ImgSizeAttr } from '../../../../../specs';
12
12
  import './ImageForm.css';
@@ -2,6 +2,7 @@ import type { ExtensionNodeSpec } from '../../../../core';
2
2
  export { yfmHtmlBlockNodeName } from './const';
3
3
  export declare type YfmHtmlBlockSpecsOptions = {
4
4
  nodeView?: ExtensionNodeSpec['view'];
5
+ sanitize?: (dirtyHtml: string) => string;
5
6
  };
6
7
  export declare const YfmHtmlBlockSpecs: import("../../../../core").ExtensionWithOptions<YfmHtmlBlockSpecsOptions> & {
7
8
  readonly NodeName: "yfm_html_block";
@@ -2,9 +2,9 @@
2
2
  import { transform } from '@diplodoc/html-extension';
3
3
  import { YfmHtmlBlockConsts } from './const';
4
4
  export { yfmHtmlBlockNodeName } from './const';
5
- const YfmHtmlBlockSpecsExtension = (builder, { nodeView }) => {
5
+ const YfmHtmlBlockSpecsExtension = (builder, { nodeView, sanitize }) => {
6
6
  builder
7
- .configureMd((md) => md.use(transform({ bundle: false }), {}))
7
+ .configureMd((md) => md.use(transform({ bundle: false, sanitize }), {}))
8
8
  .addNode(YfmHtmlBlockConsts.NodeName, () => ({
9
9
  fromMd: {
10
10
  tokenSpec: {
@@ -3,6 +3,7 @@ import { Action, ExtensionAuto } from '../../../core';
3
3
  import { YfmHtmlBlockAction } from './YfmHtmlBlockSpecs/const';
4
4
  export declare type YfmHtmlBlockOptions = {
5
5
  useConfig?: () => IHTMLIFrameElementConfig | undefined;
6
+ sanitize?: (dirtyHtml: string) => string;
6
7
  };
7
8
  export declare const YfmHtmlBlock: ExtensionAuto<YfmHtmlBlockOptions>;
8
9
  declare global {
@@ -5,6 +5,7 @@ import { addYfmHtmlBlock } from './actions';
5
5
  export const YfmHtmlBlock = (builder, options) => {
6
6
  builder.use(YfmHtmlBlockSpecs, {
7
7
  nodeView: YfmHtmlBlockNodeViewFactory(options),
8
+ sanitize: options.sanitize,
8
9
  });
9
10
  builder.addAction(YfmHtmlBlockAction, () => addYfmHtmlBlock);
10
11
  };
@@ -3,10 +3,10 @@ import { Tabs, TextInput } from '@gravity-ui/uikit';
3
3
  import { cn } from '../classname';
4
4
  import { i18n } from '../i18n/forms';
5
5
  import { isFunction } from '../lodash';
6
+ import { enterKeyHandler } from '../utils/handlers';
6
7
  import { TextInputFixed } from './TextInput';
7
8
  import Form from './base';
8
9
  import { ButtonAttach } from './components';
9
- import { enterKeyHandler } from './utils';
10
10
  const b = cn('file-form');
11
11
  export const FileForm = ({ className, autoFocus, onCancel, onSubmit, onAttach, loading, }) => {
12
12
  const [tabId, setTabId] = React.useState(() => isFunction(onAttach) ? "attach" /* TabId.Attach */ : "link" /* TabId.Link */);
@@ -3,10 +3,10 @@ import { Tabs, TextInput } from '@gravity-ui/uikit';
3
3
  import { cn } from '../classname';
4
4
  import { i18n } from '../i18n/forms';
5
5
  import { isFunction } from '../lodash';
6
+ import { enterKeyHandler } from '../utils/handlers';
6
7
  import { TextInputFixed } from './TextInput';
7
8
  import Form from './base';
8
9
  import { ButtonAttach, NumberInput } from './components';
9
- import { enterKeyHandler } from './utils';
10
10
  import './ImageForm.css';
11
11
  const b = cn('image-form');
12
12
  export const ImageForm = ({ className, autoFocus, onCancel, onSubmit, onAttach, loading, }) => {
@@ -1,10 +1,10 @@
1
1
  import React from 'react';
2
2
  import { TextInput } from '@gravity-ui/uikit';
3
3
  import { i18n } from '../i18n/forms';
4
+ import { enterKeyHandler } from '../utils/handlers';
4
5
  import { TextInputFixed } from './TextInput';
5
6
  import { UrlInputRow } from './UrlInputRow';
6
7
  import Form from './base';
7
- import { enterKeyHandler } from './utils';
8
8
  export const LinkForm = React.memo(function LinkForm({ className, autoFocus, initialUrl, initialText, readOnlyText, onSubmit, onCancel, }) {
9
9
  const [url, setUrl] = React.useState(initialUrl !== null && initialUrl !== void 0 ? initialUrl : '');
10
10
  const [text, setText] = React.useState(initialText !== null && initialText !== void 0 ? initialText : '');
@@ -0,0 +1,5 @@
1
+ {
2
+ "label_case-sensitive": "Case sensitive",
3
+ "label_whole-word": "Whole word",
4
+ "title": "Search in code"
5
+ }
@@ -0,0 +1,7 @@
1
+ export declare const i18n: <G extends "title" | "label_case-sensitive" | "label_whole-word", S extends string>(key: G | (string extends S ? S : never), params?: {
2
+ [key: string]: any;
3
+ } | undefined) => S extends G ? {
4
+ "label_case-sensitive": string;
5
+ "label_whole-word": string;
6
+ title: string;
7
+ }[G] : string;
@@ -0,0 +1,5 @@
1
+ import { registerKeyset } from '../i18n';
2
+ import en from './en.json';
3
+ import ru from './ru.json';
4
+ const KEYSET = 'search';
5
+ export const i18n = registerKeyset(KEYSET, { en, ru });
@@ -0,0 +1,5 @@
1
+ {
2
+ "label_case-sensitive": "С учетом регистра",
3
+ "label_whole-word": "Слово целиком",
4
+ "title": "Найти в коде"
5
+ }
@@ -1,6 +1,8 @@
1
1
  import type { Extension, StateCommand } from '@codemirror/state';
2
2
  import { EditorView, EditorViewConfig } from '@codemirror/view';
3
+ import { EventMap } from '../../bundle/Editor';
3
4
  import { ReactRenderStorage } from '../../extensions';
5
+ import { Receiver } from '../../utils';
4
6
  import { FileUploadHandler } from './files-upload-facet';
5
7
  export declare type CreateCodemirrorParams = {
6
8
  doc: EditorViewConfig['doc'];
@@ -14,6 +16,7 @@ export declare type CreateCodemirrorParams = {
14
16
  uploadHandler?: FileUploadHandler;
15
17
  needImgDimms?: boolean;
16
18
  extraMarkupExtensions?: Extension[];
19
+ receiver?: Receiver<EventMap>;
17
20
  };
18
21
  export declare function createCodemirror(params: CreateCodemirrorParams): EditorView;
19
22
  export declare function withLogger(action: string, command: StateCommand): StateCommand;
@@ -10,9 +10,10 @@ import { FileUploadHandlerFacet } from './files-upload-facet';
10
10
  import { gravityHighlightStyle, gravityTheme } from './gravity';
11
11
  import { PairingCharactersExtension } from './pairing-chars';
12
12
  import { ReactRendererFacet } from './react-facet';
13
+ import { SearchPanelPlugin } from './search-plugin/plugin';
13
14
  import { yfmLang } from './yfm';
14
15
  export function createCodemirror(params) {
15
- const { doc, placeholderText, reactRenderer, onCancel, onScroll, onSubmit, onChange, onDocChange, extraMarkupExtensions, } = params;
16
+ const { doc, placeholderText, reactRenderer, onCancel, onScroll, onSubmit, onChange, onDocChange, extraMarkupExtensions, receiver, } = params;
16
17
  const extensions = [
17
18
  gravityTheme,
18
19
  placeholder(placeholderText),
@@ -66,6 +67,10 @@ export function createCodemirror(params) {
66
67
  onScroll(event);
67
68
  },
68
69
  }),
70
+ SearchPanelPlugin({
71
+ anchorSelector: '.g-md-search-anchor',
72
+ receiver,
73
+ }),
69
74
  ];
70
75
  if (params.uploadHandler) {
71
76
  extensions.push(FileUploadHandlerFacet.of({
@@ -0,0 +1,42 @@
1
+ import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
2
+ import { EditorMode, EventMap } from '../../../bundle/Editor';
3
+ import type { RendererItem } from '../../../extensions';
4
+ import { Receiver } from '../../../utils';
5
+ interface SearchQueryParams {
6
+ search: string;
7
+ caseSensitive?: boolean;
8
+ literal?: boolean;
9
+ regexp?: boolean;
10
+ replace?: string;
11
+ valid?: boolean;
12
+ wholeWord?: boolean;
13
+ }
14
+ export interface SearchPanelPluginParams {
15
+ anchorSelector: string;
16
+ inputDelay?: number;
17
+ receiver?: Receiver<EventMap>;
18
+ }
19
+ export declare const SearchPanelPlugin: (params: SearchPanelPluginParams) => ViewPlugin<{
20
+ readonly view: EditorView;
21
+ readonly params: SearchPanelPluginParams;
22
+ anchor: HTMLElement | null;
23
+ renderer: RendererItem | null;
24
+ searchQuery: SearchQueryParams;
25
+ receiver: Receiver<EventMap> | undefined;
26
+ setViewSearchWithDelay: (config: Partial<SearchQueryParams>) => void;
27
+ update(update: ViewUpdate): void;
28
+ destroy(): void;
29
+ setViewSearch(config: Partial<SearchQueryParams>): void;
30
+ handleEditorModeChange({ mode }: {
31
+ mode: EditorMode;
32
+ }): void;
33
+ handleChange(search: string): void;
34
+ handleClose(): void;
35
+ handleSearchNext(): void;
36
+ handleSearchPrev(): void;
37
+ handleSearchConfigChange({ isCaseSensitive, isWholeWord, }: {
38
+ isCaseSensitive?: boolean | undefined;
39
+ isWholeWord?: boolean | undefined;
40
+ }): void;
41
+ }>;
42
+ export {};
@@ -0,0 +1,96 @@
1
+ import { SearchQuery, closeSearchPanel, findNext, findPrevious, search, searchKeymap, searchPanelOpen, setSearchQuery, } from '@codemirror/search';
2
+ import { ViewPlugin, keymap } from '@codemirror/view';
3
+ import { debounce } from '../../../lodash';
4
+ import { ReactRendererFacet } from '../react-facet';
5
+ import { renderSearchPopup } from './view/SearchPopup';
6
+ const INPUT_DELAY = 200;
7
+ export const SearchPanelPlugin = (params) => ViewPlugin.fromClass(class {
8
+ constructor(view) {
9
+ var _a, _b;
10
+ this.searchQuery = {
11
+ search: '',
12
+ caseSensitive: false,
13
+ wholeWord: false,
14
+ };
15
+ this.view = view;
16
+ this.anchor = null;
17
+ this.renderer = null;
18
+ this.params = params;
19
+ this.receiver = params.receiver;
20
+ this.handleClose = this.handleClose.bind(this);
21
+ this.handleChange = this.handleChange.bind(this);
22
+ this.handleSearchNext = this.handleSearchNext.bind(this);
23
+ this.handleSearchPrev = this.handleSearchPrev.bind(this);
24
+ this.handleSearchConfigChange = this.handleSearchConfigChange.bind(this);
25
+ this.handleEditorModeChange = this.handleEditorModeChange.bind(this);
26
+ this.setViewSearchWithDelay = debounce(this.setViewSearch, (_a = this.params.inputDelay) !== null && _a !== void 0 ? _a : INPUT_DELAY);
27
+ (_b = this.receiver) === null || _b === void 0 ? void 0 : _b.on('change-editor-mode', this.handleEditorModeChange);
28
+ }
29
+ update(update) {
30
+ var _a;
31
+ const isPanelOpen = searchPanelOpen(update.state);
32
+ if (isPanelOpen && !this.renderer) {
33
+ this.anchor = document.querySelector(this.params.anchorSelector);
34
+ this.renderer = this.view.state
35
+ .facet(ReactRendererFacet)
36
+ .createItem('cm-search', () => renderSearchPopup({
37
+ open: true,
38
+ anchor: this.anchor,
39
+ onChange: this.handleChange,
40
+ onClose: this.handleClose,
41
+ onSearchNext: this.handleSearchNext,
42
+ onSearchPrev: this.handleSearchPrev,
43
+ onConfigChange: this.handleSearchConfigChange,
44
+ }));
45
+ }
46
+ else if (!isPanelOpen && this.renderer) {
47
+ (_a = this.renderer) === null || _a === void 0 ? void 0 : _a.remove();
48
+ this.renderer = null;
49
+ }
50
+ }
51
+ destroy() {
52
+ var _a, _b;
53
+ (_a = this.renderer) === null || _a === void 0 ? void 0 : _a.remove();
54
+ this.renderer = null;
55
+ (_b = this.receiver) === null || _b === void 0 ? void 0 : _b.off('change-editor-mode', this.handleEditorModeChange);
56
+ }
57
+ setViewSearch(config) {
58
+ this.searchQuery = Object.assign(Object.assign({}, this.searchQuery), config);
59
+ const searchQuery = new SearchQuery(Object.assign({}, this.searchQuery));
60
+ this.view.dispatch({ effects: setSearchQuery.of(searchQuery) });
61
+ }
62
+ handleEditorModeChange({ mode }) {
63
+ if (mode === 'wysiwyg') {
64
+ closeSearchPanel(this.view);
65
+ }
66
+ }
67
+ handleChange(search) {
68
+ this.setViewSearchWithDelay({ search });
69
+ }
70
+ handleClose() {
71
+ this.setViewSearch({ search: '' });
72
+ closeSearchPanel(this.view);
73
+ }
74
+ handleSearchNext() {
75
+ findNext(this.view);
76
+ }
77
+ handleSearchPrev() {
78
+ findPrevious(this.view);
79
+ }
80
+ handleSearchConfigChange({ isCaseSensitive, isWholeWord, }) {
81
+ this.setViewSearch({
82
+ caseSensitive: isCaseSensitive,
83
+ wholeWord: isWholeWord,
84
+ });
85
+ }
86
+ }, {
87
+ provide: () => [
88
+ keymap.of(searchKeymap),
89
+ search({
90
+ createPanel: () => ({
91
+ // Create an empty search panel
92
+ dom: document.createElement('div'),
93
+ }),
94
+ }),
95
+ ],
96
+ });
@@ -0,0 +1,9 @@
1
+ .g-md-search-card {
2
+ padding: var(--g-spacing-2) var(--g-spacing-2) var(--g-spacing-3) var(--g-spacing-4);
3
+ }
4
+ .g-md-search-card__header {
5
+ display: flex;
6
+ justify-content: space-between;
7
+ align-items: center;
8
+ margin-bottom: var(--g-spacing-1);
9
+ }
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import './SearchPopup.css';
3
+ interface SearchConfig {
4
+ isCaseSensitive: boolean;
5
+ isWholeWord: boolean;
6
+ }
7
+ interface SearchCardProps {
8
+ onSearchKeyDown?: (query: string) => void;
9
+ onChange?: (query: string) => void;
10
+ onClose?: (query: string) => void;
11
+ onSearchPrev?: (query: string) => void;
12
+ onSearchNext?: (query: string) => void;
13
+ onConfigChange?: (config: SearchConfig) => void;
14
+ }
15
+ export declare const SearchCard: React.FC<SearchCardProps>;
16
+ export interface SearchPopupProps {
17
+ anchor: HTMLElement;
18
+ onChange: (query: string) => void;
19
+ onClose: () => void;
20
+ onSearchNext: () => void;
21
+ onSearchPrev: () => void;
22
+ onConfigChange: (config: SearchConfig) => void;
23
+ open: boolean;
24
+ }
25
+ export declare const SearchPopup: React.FC<SearchPopupProps>;
26
+ interface SearchPopupWithRefProps extends Omit<SearchPopupProps, 'anchor'> {
27
+ anchor: HTMLElement | null;
28
+ }
29
+ export declare function renderSearchPopup({ anchor, open, onClose, ...props }: SearchPopupWithRefProps): JSX.Element;
30
+ export {};
@@ -0,0 +1,80 @@
1
+ import { __rest } from "tslib";
2
+ import React, { useRef, useState } from 'react';
3
+ import { ChevronDown, ChevronUp, Xmark } from '@gravity-ui/icons';
4
+ import { Button, Card, Checkbox, Icon, Popup, TextInput, sp, } from '@gravity-ui/uikit';
5
+ import { cn } from '../../../../classname';
6
+ import { i18n } from '../../../../i18n/search';
7
+ import { enterKeyHandler } from '../../../../utils/handlers';
8
+ import './SearchPopup.css';
9
+ const b = cn('search-card');
10
+ const noop = () => { };
11
+ export const SearchCard = ({ onChange = noop, onClose = noop, onSearchPrev = noop, onSearchNext = noop, onConfigChange = noop, }) => {
12
+ const [query, setQuery] = useState('');
13
+ const [isCaseSensitive, setIsCaseSensitive] = useState(false);
14
+ const [isWholeWord, setIsWholeWord] = useState(false);
15
+ const textInputRef = useRef(null);
16
+ const setInputFocus = () => {
17
+ var _a;
18
+ (_a = textInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
19
+ };
20
+ const handleInputChange = (event) => {
21
+ const { target: { value }, } = event;
22
+ setQuery(value);
23
+ onChange(value);
24
+ };
25
+ const handleClose = () => {
26
+ setQuery('');
27
+ onClose(query);
28
+ setInputFocus();
29
+ };
30
+ const handlePrev = () => {
31
+ onSearchPrev(query);
32
+ setInputFocus();
33
+ };
34
+ const handleNext = () => {
35
+ onSearchNext(query);
36
+ setInputFocus();
37
+ };
38
+ const handleIsCaseSensitive = () => {
39
+ onConfigChange({
40
+ isCaseSensitive: !isCaseSensitive,
41
+ isWholeWord,
42
+ });
43
+ setIsCaseSensitive((isCaseSensitive) => !isCaseSensitive);
44
+ setInputFocus();
45
+ };
46
+ const handleIsWholeWord = () => {
47
+ onConfigChange({
48
+ isCaseSensitive,
49
+ isWholeWord: !isWholeWord,
50
+ });
51
+ setIsWholeWord((isWholeWord) => !isWholeWord);
52
+ setInputFocus();
53
+ };
54
+ const handleSearchKeyPress = enterKeyHandler(handleNext);
55
+ return (React.createElement(Card, { className: b() },
56
+ React.createElement("div", { className: b('header') },
57
+ React.createElement("span", { className: b('title') },
58
+ " ",
59
+ i18n('title')),
60
+ React.createElement(Button, { onClick: handleClose, size: "s", view: "flat" },
61
+ React.createElement(Icon, { data: Xmark, size: 14 }))),
62
+ React.createElement(TextInput, { controlRef: textInputRef, className: sp({ mb: 2 }), size: "s", autoFocus: true, onKeyPress: handleSearchKeyPress, onChange: handleInputChange, value: query, endContent: React.createElement(React.Fragment, null,
63
+ React.createElement(Button, { onClick: handlePrev },
64
+ React.createElement(Icon, { data: ChevronUp, size: 12 })),
65
+ React.createElement(Button, { onClick: handleNext },
66
+ React.createElement(Icon, { data: ChevronDown, size: 12 }))) }),
67
+ React.createElement(Checkbox, { size: "m", onUpdate: handleIsCaseSensitive, checked: isCaseSensitive, className: sp({ mr: 4 }) }, i18n('label_case-sensitive')),
68
+ React.createElement(Checkbox, { size: "m", onUpdate: handleIsWholeWord, checked: isWholeWord }, i18n('label_whole-word'))));
69
+ };
70
+ export const SearchPopup = (_a) => {
71
+ var { open, anchor, onClose } = _a, props = __rest(_a, ["open", "anchor", "onClose"]);
72
+ const anchorRef = useRef(anchor);
73
+ return (React.createElement(Popup, { onEscapeKeyDown: onClose, open: anchorRef.current && open, anchorRef: anchorRef, placement: "bottom-end" },
74
+ React.createElement(SearchCard, Object.assign({ onClose: onClose }, props))));
75
+ };
76
+ SearchPopup.displayName = 'SearchPopup';
77
+ export function renderSearchPopup(_a) {
78
+ var { anchor, open, onClose } = _a, props = __rest(_a, ["anchor", "open", "onClose"]);
79
+ return (React.createElement(React.Fragment, null, anchor && React.createElement(SearchPopup, Object.assign({ open: open, onClose: onClose, anchor: anchor }, props))));
80
+ }
@@ -0,0 +1,4 @@
1
+ /// <reference types="react" />
2
+ export declare function enterKeyHandler<T>(handler: React.KeyboardEventHandler<T>): React.KeyboardEventHandler<T>;
3
+ export declare function escapeKeyHandler<T>(handler: React.KeyboardEventHandler<T>): React.KeyboardEventHandler<T>;
4
+ export declare function combinedKeyHandler<T>(handlers: Record<string, React.KeyboardEventHandler<T>>): React.KeyboardEventHandler<T>;
@@ -0,0 +1,23 @@
1
+ import { Key } from '../shortcuts';
2
+ export function enterKeyHandler(handler) {
3
+ return (event) => {
4
+ if (event.key === Key.Enter) {
5
+ handler(event);
6
+ }
7
+ };
8
+ }
9
+ export function escapeKeyHandler(handler) {
10
+ return (event) => {
11
+ if (event.key === Key.Esc) {
12
+ handler(event);
13
+ }
14
+ };
15
+ }
16
+ export function combinedKeyHandler(handlers) {
17
+ return (event) => {
18
+ const handler = handlers[event.key];
19
+ if (handler) {
20
+ handler(event);
21
+ }
22
+ };
23
+ }
@@ -1,2 +1,2 @@
1
1
  /** During build process, the current version will be injected here */
2
- export const VERSION = typeof '13.4.2' !== 'undefined' ? '13.4.2' : 'unknown';
2
+ export const VERSION = typeof '13.5.1' !== 'undefined' ? '13.5.1' : 'unknown';
package/build/styles.css CHANGED
@@ -1290,6 +1290,15 @@ img.ProseMirror-separator {
1290
1290
  .g-root_theme_dark .g-md-yfm-html-block_editing .g-text-area__content {
1291
1291
  color: var(--g-color-text-primary);
1292
1292
  }
1293
+ .g-md-search-card {
1294
+ padding: var(--g-spacing-2) var(--g-spacing-2) var(--g-spacing-3) var(--g-spacing-4);
1295
+ }
1296
+ .g-md-search-card__header {
1297
+ display: flex;
1298
+ justify-content: space-between;
1299
+ align-items: center;
1300
+ margin-bottom: var(--g-spacing-1);
1301
+ }
1293
1302
  .g-md-code-block-toolbar {
1294
1303
  margin: 2px 8px;
1295
1304
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/markdown-editor",
3
- "version": "13.4.2",
3
+ "version": "13.5.1",
4
4
  "description": "Markdown wysiwyg and markup editor",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -163,6 +163,7 @@
163
163
  "@codemirror/commands": "6.5.0",
164
164
  "@codemirror/lang-markdown": "6.2.5",
165
165
  "@codemirror/language": "6.10.1",
166
+ "@codemirror/search": "6.5.6",
166
167
  "@codemirror/state": "6.4.1",
167
168
  "@codemirror/view": "6.26.3",
168
169
  "@gravity-ui/i18n": "^1.1.0",
@@ -198,9 +199,9 @@
198
199
  "tslib": "^2.3.1"
199
200
  },
200
201
  "devDependencies": {
202
+ "@diplodoc/html-extension": "1.2.7",
201
203
  "@diplodoc/latex-extension": "1.0.3",
202
204
  "@diplodoc/mermaid-extension": "1.2.1",
203
- "@diplodoc/html-extension": "1.2.7",
204
205
  "@diplodoc/transform": "4.5.0",
205
206
  "@gravity-ui/components": "3.0.0",
206
207
  "@gravity-ui/eslint-config": "3.1.1",
@@ -268,9 +269,9 @@
268
269
  }
269
270
  },
270
271
  "peerDependencies": {
272
+ "@diplodoc/html-extension": "^1.2.7",
271
273
  "@diplodoc/latex-extension": "^1.0.3",
272
274
  "@diplodoc/mermaid-extension": "^1.0.0",
273
- "@diplodoc/html-extension": "^1.2.7",
274
275
  "@diplodoc/transform": "^4.5.0",
275
276
  "@gravity-ui/components": "^3.0.0",
276
277
  "@gravity-ui/uikit": "^6.11.0",
@@ -1,2 +0,0 @@
1
- /// <reference types="react" />
2
- export declare function enterKeyHandler<T>(handler: React.KeyboardEventHandler<T>): React.KeyboardEventHandler<T>;
@@ -1,11 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.enterKeyHandler = void 0;
4
- function enterKeyHandler(handler) {
5
- return (event) => {
6
- if (event.key === 'Enter') {
7
- handler(event);
8
- }
9
- };
10
- }
11
- exports.enterKeyHandler = enterKeyHandler;
@@ -1,2 +0,0 @@
1
- /// <reference types="react" />
2
- export declare function enterKeyHandler<T>(handler: React.KeyboardEventHandler<T>): React.KeyboardEventHandler<T>;
@@ -1,7 +0,0 @@
1
- export function enterKeyHandler(handler) {
2
- return (event) => {
3
- if (event.key === 'Enter') {
4
- handler(event);
5
- }
6
- };
7
- }