Skip to content

Commit 154c8e6

Browse files
authored
Merge pull request #498 from Harbour-Enterprises/har-9619_search
HAR-9619 Search and highlight matches
2 parents 3cffc42 + d9c97fd commit 154c8e6

11 files changed

Lines changed: 202 additions & 23 deletions

File tree

package-lock.json

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/super-editor/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"prosemirror-model": "^1.21.0",
5959
"prosemirror-schema-basic": "^1.2.2",
6060
"prosemirror-schema-list": "^1.3.0",
61+
"prosemirror-search": "^1.1.0",
6162
"prosemirror-state": "^1.4.3",
6263
"prosemirror-tables": "^1.4.0",
6364
"prosemirror-transform": "^1.9.0",

packages/super-editor/src/assets/styles/elements/prosemirror.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,10 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html
317317
align-items: baseline;
318318
margin-top: -5px;
319319
}
320+
321+
.ProseMirror-search-match {
322+
background-color: #ffff0054;
323+
}
324+
.ProseMirror-active-search-match {
325+
background-color: #ff6a0054;
326+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<script setup>
2+
import { ref } from 'vue';
3+
4+
const props = defineProps({
5+
searchRef: {
6+
type: Object,
7+
}
8+
})
9+
10+
const searchValue = ref('');
11+
const emit = defineEmits(['submit']);
12+
13+
const handleSubmit = () => {
14+
emit('submit', { value: searchValue.value });
15+
};
16+
</script>
17+
18+
<template>
19+
<div class="search-input-ctn">
20+
<div class="row">
21+
<input
22+
:ref="searchRef"
23+
v-model="searchValue"
24+
class="search-input"
25+
type="text"
26+
name="search"
27+
placeholder="Type search string"
28+
@keydown.enter.stop.prevent="handleSubmit"
29+
/>
30+
</div>
31+
<div class="row submit">
32+
<button class="submit-btn" @click="handleSubmit">
33+
Apply
34+
</button>
35+
</div>
36+
</div>
37+
</template>
38+
39+
<style lang="postcss" scoped>
40+
.search-input-ctn {
41+
padding: 10px;
42+
border-radius: 5px;
43+
.search-input {
44+
min-width: 200px;
45+
font-size: 13px;
46+
flex-grow: 1;
47+
padding: 10px;
48+
border-radius: 8px;
49+
color: #666;
50+
border: 1px solid #ddd;
51+
box-sizing: border-box;
52+
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, .15);
53+
&:active, &:focus {
54+
outline: none;
55+
border: 1px solid #1355ff;
56+
}
57+
}
58+
.row {
59+
display: flex;
60+
&.submit {
61+
margin-top: 10px;
62+
flex-direction: row-reverse;
63+
}
64+
}
65+
.submit-btn {
66+
display: flex;
67+
justify-content: center;
68+
align-items: center;
69+
padding: 10px 16px;
70+
border-radius: 8px;
71+
outline: none;
72+
border: none;
73+
background-color: #1355ff;
74+
color: white;
75+
font-weight: 400;
76+
font-size: 13px;
77+
cursor: pointer;
78+
transition: all 0.2s ease;
79+
box-sizing: border-box;
80+
}
81+
}
82+
</style>

packages/super-editor/src/components/toolbar/Toolbar.vue

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup>
2-
import { ref, getCurrentInstance, onMounted, onDeactivated } from 'vue';
2+
import { ref, getCurrentInstance, onMounted, onDeactivated, nextTick } from 'vue';
33
import { throttle } from './helpers.js';
44
import ButtonGroup from './ButtonGroup.vue';
55
@@ -18,12 +18,28 @@ const getFilteredItems = (position) => {
1818
1919
onMounted(() => {
2020
window.addEventListener('resize', onResizeThrottled);
21+
window.addEventListener('keydown', onKeyDown);
2122
});
2223
2324
onDeactivated(() => {
2425
window.removeEventListener('resize', onResizeThrottled);
26+
window.removeEventListener('keydown', onKeyDown);
2527
});
2628
29+
const onKeyDown = async (e) => {
30+
if (e.metaKey && e.key === 'f') {
31+
e.preventDefault();
32+
const searchItem = proxy.$toolbar.getToolbarItemByName('search');
33+
if (searchItem) {
34+
searchItem.expand.value = true;
35+
await nextTick();
36+
if (searchItem.inputRef.value) {
37+
searchItem.inputRef.value.focus();
38+
}
39+
}
40+
}
41+
}
42+
2743
const onWindowResized = async () => {
2844
await proxy.$toolbar.onToolbarResize();
2945
toolbarKey.value += 1;

packages/super-editor/src/components/toolbar/defaultItems.js

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { undoDepth, redoDepth } from 'prosemirror-history';
2-
import { h } from 'vue';
2+
import { h, ref } from 'vue';
33

44
import { scrollToElement } from './scroll-helpers';
55
import { sanitizeNumber } from './helpers';
@@ -14,6 +14,7 @@ import TableGrid from './TableGrid.vue';
1414
import TableActions from './TableActions.vue';
1515

1616
import checkIconSvg from '@harbour-enterprises/common/icons/check.svg?raw';
17+
import SearchInput from './SearchInput.vue';
1718

1819
const closeDropdown = (dropdown) => {
1920
dropdown.expand.value = false;
@@ -257,6 +258,39 @@ export const makeDefaultItems = (superToolbar, isDev = false, windowWidth, role,
257258
},
258259
onDeactivate: () => (colorButton.iconColor.value = '#000'),
259260
});
261+
262+
// search
263+
const searchRef = ref(null);
264+
const search = useToolbarItem({
265+
type: 'dropdown',
266+
name: 'search',
267+
active: false,
268+
icon: toolbarIcons.search,
269+
tooltip: 'Search',
270+
group: 'right',
271+
inputRef: searchRef,
272+
options: [
273+
{
274+
type: 'render',
275+
key: 'searchDropdown',
276+
render: () => renderSearchDropdown(),
277+
},
278+
],
279+
});
280+
281+
const renderSearchDropdown = () => {
282+
283+
const handleSubmit = ({ value }) => {
284+
superToolbar.activeEditor.doSearch(value);
285+
};
286+
287+
return h('div', {}, [
288+
h(SearchInput, {
289+
onSubmit: handleSubmit,
290+
searchRef,
291+
}),
292+
]);
293+
};
260294

261295
// link
262296
const link = useToolbarItem({
@@ -669,18 +703,6 @@ export const makeDefaultItems = (superToolbar, isDev = false, windowWidth, role,
669703
icon: toolbarIcons.trackChangesFinal,
670704
group: 'left',
671705
});
672-
//
673-
674-
// search
675-
// const search = useToolbarItem({
676-
// type: "button",
677-
// allowWithoutEditor: true,
678-
// name: "search",
679-
// tooltip: "Search",
680-
// disabled: true,
681-
// icon: "fas fa-magnifying-glass", // change to svg
682-
// group: "right",
683-
// });
684706

685707
const clearFormatting = useToolbarItem({
686708
type: 'button',
@@ -696,7 +718,7 @@ export const makeDefaultItems = (superToolbar, isDev = false, windowWidth, role,
696718
underline,
697719
indentRight,
698720
indentLeft,
699-
// search,
721+
search,
700722
overflow,
701723
].map((item) => item.name);
702724

@@ -942,7 +964,7 @@ export const makeDefaultItems = (superToolbar, isDev = false, windowWidth, role,
942964
aiButton,
943965
overflow,
944966
documentMode,
945-
// search,
967+
search,
946968
];
947969

948970
if (!superToolbar.config?.superdoc?.config?.modules?.ai) {
@@ -978,7 +1000,7 @@ export const makeDefaultItems = (superToolbar, isDev = false, windowWidth, role,
9781000
}
9791001

9801002
// always visible items
981-
const toolbarItemsSticky = [undo, overflow, documentMode].map((item) => item.name);
1003+
const toolbarItemsSticky = [search, undo, overflow, documentMode].map((item) => item.name);
9821004
const isStickyItem = (item) => toolbarItemsSticky.includes(item.name);
9831005

9841006
const overflowItems = [];

packages/super-editor/src/components/toolbar/super-toolbar.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,10 @@ export class SuperToolbar extends EventEmitter {
271271
getToolbarItemByGroup(groupName) {
272272
return this.toolbarItems.filter((item) => item.group.value === groupName);
273273
}
274+
275+
getToolbarItemByName(name) {
276+
return this.toolbarItems.find((item) => item.name.value === name);
277+
}
274278

275279
#makeToolbarItems(superToolbar, icons, isDev = false) {
276280
const documentWidth = document.documentElement.clientWidth; // take into account the scrollbar

packages/super-editor/src/components/toolbar/toolbarIcons.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import trashIconSvg from '@harbour-enterprises/common/icons/trash-can-solid.svg?
4444
import wrenchIconSvg from '@harbour-enterprises/common/icons/wrench-solid.svg?raw';
4545
import borderNoneIconSvg from '@harbour-enterprises/common/icons/border-none-solid.svg?raw';
4646
import upDownIconSvg from '@harbour-enterprises/common/icons/up-down.svg?raw';
47+
import magnifyingGlassSvg from '@harbour-enterprises/common/icons/magnifying-glass.svg?raw';
4748

4849
export const toolbarIcons = {
4950
undo: rotateLeftIconSvg,
@@ -100,4 +101,5 @@ export const toolbarIcons = {
100101
deleteBorders: borderNoneIconSvg,
101102
fixTables: wrenchIconSvg,
102103
lineHeight: upDownIconSvg,
104+
search: magnifyingGlassSvg,
103105
}

packages/super-editor/src/components/toolbar/use-toolbar-item.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export const useToolbarItem = (options) => {
6666
// Dropdown item
6767
const selectedValue = ref(options.selectedValue);
6868
const dropdownValueKey = ref(options.dropdownValueKey);
69+
70+
const inputRef = ref(options.inputRef || null);
6971

7072
const nestedOptions = ref([]);
7173
if (options.options) {
@@ -145,7 +147,8 @@ export const useToolbarItem = (options) => {
145147

146148
allowWithoutEditor,
147149
dropdownValueKey,
148-
selectedValue
150+
selectedValue,
151+
inputRef
149152
};
150153

151154
return {

packages/super-editor/src/core/Editor.js

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { EditorState } from 'prosemirror-state';
22
import { EditorView } from 'prosemirror-view';
33
import { DOMParser, DOMSerializer } from 'prosemirror-model';
4+
import { search, SearchQuery, setSearchState, getMatchHighlights } from 'prosemirror-search';
45
import { yXmlFragmentToProseMirrorRootNode } from 'y-prosemirror';
56
import { helpers } from '@core/index.js';
67
import { EventEmitter } from './EventEmitter.js';
@@ -186,7 +187,7 @@ export class Editor extends EventEmitter {
186187
if (!this.options.ydoc) {
187188
this.#initPagination();
188189
this.#initComments();
189-
};
190+
}
190191

191192
window.setTimeout(() => {
192193
if (this.isDestroyed) return;
@@ -197,7 +198,7 @@ export class Editor extends EventEmitter {
197198
#initRichText(options) {
198199
if (!options.extensions || !options.extensions.length) {
199200
this.options.extensions = getRichTextExtensions();
200-
};
201+
}
201202

202203
this.#createExtensionService();
203204
this.#createCommandService();
@@ -231,6 +232,30 @@ export class Editor extends EventEmitter {
231232
this.options.onFocus({ editor, event });
232233
}
233234

235+
goToFirstMatch() {
236+
const highlights = getMatchHighlights(this.view.state);
237+
if (!highlights || !highlights.children?.length) return;
238+
239+
const match = highlights.children.find(item => item.local);
240+
const firstSearchItemPosition = highlights.children[0] + match.local[0].from + 1;
241+
this.view.domAtPos(firstSearchItemPosition)?.node?.scrollIntoView(true);
242+
}
243+
244+
doSearch(text) {
245+
const query = new SearchQuery({
246+
search: text,
247+
caseSensitive: false,
248+
regexp: false,
249+
wholeWord: false
250+
});
251+
const tr = this.view.state.tr;
252+
setSearchState(tr, query);
253+
this.view.dispatch(tr);
254+
255+
this.goToFirstMatch();
256+
return getMatchHighlights(this.view.state);
257+
}
258+
234259
setToolbar(toolbar) {
235260
this.toolbar = toolbar;
236261
}
@@ -644,9 +669,14 @@ export class Editor extends EventEmitter {
644669
dispatchTransaction: this.#dispatchTransaction.bind(this),
645670
state: EditorState.create(state),
646671
});
647-
672+
673+
const searchPlugin = search();
674+
648675
const newState = this.state.reconfigure({
649-
plugins: this.extensionService.plugins,
676+
plugins: [
677+
...this.extensionService.plugins,
678+
searchPlugin,
679+
],
650680
});
651681

652682
this.view.updateState(newState);

0 commit comments

Comments
 (0)