Skip to content

Commit 1ec71f4

Browse files
muzaffarmhdRed-Asuka
authored andcommitted
feat(desktop): add highlighting on active topic and payload search
1 parent 735f28f commit 1ec71f4

5 files changed

Lines changed: 190 additions & 10 deletions

File tree

src/components/MessageList.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,17 @@
1111
v-if="!item.out"
1212
:key="item.id"
1313
:msgId="item.id"
14+
:searchParams="searchParams"
15+
v-bind="item"
16+
@showmenu="handleShowContextMenu(arguments, item)"
17+
/>
18+
<MsgRightItem
19+
v-else
20+
:key="item.id"
21+
:searchParams="searchParams"
1422
v-bind="item"
1523
@showmenu="handleShowContextMenu(arguments, item)"
1624
/>
17-
<MsgRightItem v-else :key="item.id" v-bind="item" @showmenu="handleShowContextMenu(arguments, item)" />
1825
</template>
1926
</div>
2027
<span v-show="showAfterLoadingIcon" class="loading-icon after"><i class="el-icon-loading"></i></span>
@@ -40,6 +47,10 @@ export default class MessageList extends Vue {
4047
@Prop({ required: true }) height!: number
4148
@Prop({ required: true }) subscriptions!: SubscriptionModel[]
4249
@Prop({ required: true }) marginTop!: number
50+
@Prop({ required: false, default: () => ({ topic: '', payload: '' }) }) searchParams!: {
51+
topic: string
52+
payload: string
53+
}
4354
4455
public showMessages: MessageModel[] = []
4556
public showBeforeLoadingIcon: boolean = false

src/components/MsgLeftItem.vue

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@
3030
</span>
3131
<div ref="leftPayload" class="left-payload payload" @contextmenu.prevent="customMenu($event)">
3232
<p class="left-info">
33-
<span class="topic">Topic: {{ topic }}</span>
33+
<span class="topic">Topic: <span v-html="highlightedTopic"></span></span>
3434
<span class="qos">QoS: {{ qos }}</span>
3535
<span v-if="retain" class="retain">Retained</span>
3636
</p>
3737
<MqttProperties class="meta" :properties="properties" direction="left" />
3838
<template v-if="isLargeMsg">
39-
<div class="large-message { .el-button { margin-top: 12px;}}">
40-
<pre>{{ payload.substr(0, showMaxLen) }}<span><i class="iconfont icon-more"></i></span></pre>
39+
<div class="large-message">
40+
<pre v-html="highlightedPayloadPreview"></pre>
4141
<el-tooltip
4242
placement="bottom"
4343
:effect="theme !== 'light' ? 'light' : 'dark'"
@@ -51,8 +51,8 @@
5151
</div>
5252
</template>
5353
<template v-else>
54-
<pre v-if="!hightlight">{{ payload }}</pre>
55-
<pre v-else><code class="language-js" >{{ payload }}</code></pre>
54+
<pre v-if="!hightlight" v-html="highlightedPayload"></pre>
55+
<pre v-else ref="highlightedCode"><code class="language-js">{{ payload }}</code></pre>
5656
</template>
5757
</div>
5858
<p class="left-time time">{{ createAt }}</p>
@@ -61,12 +61,13 @@
6161
</template>
6262

6363
<script lang="ts">
64-
import { Component, Vue, Prop } from 'vue-property-decorator'
64+
import { Component, Vue, Prop, Watch } from 'vue-property-decorator'
6565
import FullMsgDialog from './FullMsgDialog.vue'
6666
import Prism from 'prismjs'
6767
import { Getter } from 'vuex-class'
6868
import { SHOW_MAX_LENGTH } from '@/utils/data'
6969
import MqttProperties from './MqttProperties.vue'
70+
import { highlightSearchTerm, highlightInPrismCode } from '@/utils/highlightSearch'
7071
7172
@Component({
7273
components: {
@@ -84,6 +85,10 @@ export default class MsgLeftItem extends Vue {
8485
@Prop({ required: false, default: false }) public retain!: boolean
8586
@Prop({ required: false, default: () => ({}) }) public properties!: PushPropertiesModel
8687
@Prop({ required: false, default: '' }) public color!: string
88+
@Prop({ required: false, default: () => ({ topic: '', payload: '' }) }) public searchParams!: {
89+
topic: string
90+
payload: string
91+
}
8792
8893
@Getter('jsonHighlight') private jsonHighlight!: boolean
8994
@Getter('currentTheme') private theme!: Theme
@@ -92,6 +97,35 @@ export default class MsgLeftItem extends Vue {
9297
9398
private showFullMsg: boolean = false
9499
100+
@Watch('searchParams', { deep: true })
101+
private onSearchParamsChanged() {
102+
this.$nextTick(() => {
103+
this.applyHighlighting()
104+
})
105+
}
106+
107+
get highlightedTopic(): string {
108+
if (this.searchParams.topic) {
109+
return highlightSearchTerm(this.topic, this.searchParams.topic, 'search-highlight')
110+
}
111+
return this.topic
112+
}
113+
114+
get highlightedPayload(): string {
115+
if (this.searchParams.payload) {
116+
return highlightSearchTerm(this.payload, this.searchParams.payload, 'search-highlight')
117+
}
118+
return this.payload
119+
}
120+
121+
get highlightedPayloadPreview(): string {
122+
const preview = this.payload.substr(0, this.showMaxLen)
123+
if (this.searchParams.payload) {
124+
return highlightSearchTerm(preview, this.searchParams.payload, 'search-highlight')
125+
}
126+
return preview
127+
}
128+
95129
public customMenu(event: MouseEvent) {
96130
this.$emit('showmenu', this.payload, event)
97131
}
@@ -131,6 +165,27 @@ export default class MsgLeftItem extends Vue {
131165
return SHOW_MAX_LENGTH
132166
}
133167
168+
private applyHighlighting() {
169+
if (this.hightlight && this.searchParams.payload) {
170+
const codeElement = this.$refs.highlightedCode as HTMLElement
171+
if (codeElement) {
172+
this.clearHighlights(codeElement)
173+
highlightInPrismCode(codeElement, this.searchParams.payload, 'search-highlight')
174+
}
175+
}
176+
}
177+
178+
private clearHighlights(element: HTMLElement) {
179+
const highlights = element.querySelectorAll('.search-highlight')
180+
highlights.forEach((highlight) => {
181+
const parent = highlight.parentNode
182+
if (parent) {
183+
parent.replaceChild(document.createTextNode(highlight.textContent || ''), highlight)
184+
parent.normalize()
185+
}
186+
})
187+
}
188+
134189
private hightlightJSON() {
135190
if (this.jsonHighlight === false) {
136191
return
@@ -140,6 +195,7 @@ export default class MsgLeftItem extends Vue {
140195
this.hightlight = true
141196
this.$nextTick(() => {
142197
Prism.highlightAllUnder(this.$refs.msgLeftItem as HTMLElement)
198+
this.applyHighlighting()
143199
})
144200
}
145201
} catch (e) {
@@ -212,5 +268,12 @@ body.night {
212268
}
213269
}
214270
}
271+
.search-highlight {
272+
background-color: #ffeb3b !important;
273+
color: #000 !important;
274+
padding: 1px 2px;
275+
border-radius: 2px;
276+
font-weight: bold;
277+
}
215278
}
216279
</style>

src/components/MsgRightItem.vue

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
</div>
1717
<div class="right-payload payload" @contextmenu.prevent="customMenu($event)">
1818
<p class="right-info">
19-
<span class="topic">Topic: {{ topic }}</span>
19+
<span class="topic">Topic: <span v-html="highlightedTopic"></span></span>
2020
<span class="qos">QoS: {{ qos }}</span>
2121
</p>
2222
<MqttProperties class="meta" :properties="properties" direction="right" />
23-
<pre>{{ payload }}</pre>
23+
<pre v-html="highlightedPayload"></pre>
2424
</div>
2525
<p class="right-time time">{{ createAt }}</p>
2626
</div>
@@ -29,19 +29,38 @@
2929
<script lang="ts">
3030
import { Component, Vue, Prop } from 'vue-property-decorator'
3131
import MqttProperties from './MqttProperties.vue'
32+
import { highlightSearchTerm } from '@/utils/highlightSearch'
3233
3334
@Component({
3435
components: {
3536
MqttProperties,
3637
},
3738
})
38-
export default class MsgrightItem extends Vue {
39+
export default class MsgRightItem extends Vue {
3940
@Prop({ required: true }) public topic!: string
4041
@Prop({ required: true }) public qos!: number
4142
@Prop({ required: true }) public payload!: string
4243
@Prop({ required: true }) public createAt!: string
4344
@Prop({ required: false }) public meta?: string
4445
@Prop({ required: false, default: () => ({}) }) public properties!: PushPropertiesModel
46+
@Prop({ required: false, default: () => ({ topic: '', payload: '' }) }) public searchParams!: {
47+
topic: string
48+
payload: string
49+
}
50+
51+
get highlightedTopic(): string {
52+
if (this.searchParams.topic) {
53+
return highlightSearchTerm(this.topic, this.searchParams.topic, 'search-highlight')
54+
}
55+
return this.topic
56+
}
57+
58+
get highlightedPayload(): string {
59+
if (this.searchParams.payload) {
60+
return highlightSearchTerm(this.payload, this.searchParams.payload, 'search-highlight')
61+
}
62+
return this.payload
63+
}
4564
4665
public customMenu(event: MouseEvent) {
4766
this.$emit('showmenu', this.payload, event)
@@ -81,5 +100,12 @@ export default class MsgrightItem extends Vue {
81100
border: 1px solid var(--color-border-right_metainfo) !important;
82101
}
83102
}
103+
.search-highlight {
104+
background-color: #ffeb3b !important;
105+
color: #000 !important;
106+
padding: 1px 2px;
107+
border-radius: 2px;
108+
font-weight: bold;
109+
}
84110
}
85111
</style>

src/utils/highlightSearch.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Highlights search terms in text while preserving HTML structure
3+
* @param text - The text to highlight
4+
* @param searchTerm - The term to search for
5+
* @param className - CSS class for highlighting
6+
* @returns HTML string with highlighted terms
7+
*/
8+
export function highlightSearchTerm(text: string, searchTerm: string, className: string = 'search-highlight'): string {
9+
if (!searchTerm || !text) {
10+
return escapeHtml(text)
11+
}
12+
13+
const escapedText = escapeHtml(text)
14+
const escapedSearchTerm = escapeRegExp(searchTerm)
15+
const regex = new RegExp(`(${escapedSearchTerm})`, 'gi')
16+
17+
return escapedText.replace(regex, `<span class="${className}">$1</span>`)
18+
}
19+
20+
/**
21+
* Highlights search terms in JSON-highlighted code
22+
* @param element - The DOM element containing Prism-highlighted code
23+
* @param searchTerm - The term to search for
24+
* @param className - CSS class for highlighting
25+
*/
26+
export function highlightInPrismCode(
27+
element: HTMLElement,
28+
searchTerm: string,
29+
className: string = 'search-highlight',
30+
): void {
31+
if (!searchTerm || !element) return
32+
33+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, {
34+
acceptNode: (node: Node) => NodeFilter.FILTER_ACCEPT,
35+
})
36+
37+
const textNodes: Text[] = []
38+
let node: Node | null
39+
40+
while ((node = walker.nextNode())) {
41+
textNodes.push(node as Text)
42+
}
43+
44+
textNodes.forEach((textNode) => {
45+
const text = textNode.textContent || ''
46+
if (text.toLowerCase().includes(searchTerm.toLowerCase())) {
47+
const highlightedHTML = highlightSearchTerm(text, searchTerm, className)
48+
const tempDiv = document.createElement('div')
49+
tempDiv.innerHTML = highlightedHTML
50+
51+
const fragment = document.createDocumentFragment()
52+
while (tempDiv.firstChild) {
53+
fragment.appendChild(tempDiv.firstChild)
54+
}
55+
textNode.parentNode?.replaceChild(fragment, textNode)
56+
}
57+
})
58+
}
59+
60+
function escapeHtml(text: string): string {
61+
const div = document.createElement('div')
62+
div.textContent = text
63+
return div.innerHTML
64+
}
65+
66+
function escapeRegExp(string: string): string {
67+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
68+
}

src/views/connections/ConnectionsDetail.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@
277277
:messages="recordMsgs.list"
278278
:height="messageListHeight"
279279
:marginTop="messageListMarginTop"
280+
:searchParams="searchParams"
280281
@showContextMenu="handleContextMenu"
281282
@loadMoreMsg="loadMoreMessages"
282283
@hideNewMsgsTip="hideNewMsgsTip"
@@ -2202,6 +2203,17 @@ export default class ConnectionsDetail extends Vue {
22022203
this.setMessageListHeight()
22032204
})
22042205
}
2206+
2207+
get activeSearchTerms() {
2208+
const terms = []
2209+
if (this.searchParams.payload && this.searchParams.payload.trim()) {
2210+
terms.push(this.searchParams.payload.trim())
2211+
}
2212+
if (this.searchParams.topic && this.searchParams.topic.trim()) {
2213+
terms.push(this.searchParams.topic.trim())
2214+
}
2215+
return terms
2216+
}
22052217
}
22062218
</script>
22072219

0 commit comments

Comments
 (0)