mirror of
https://github.com/nasa/openmct.git
synced 2025-01-31 16:36:13 +00:00
[Notebook] Improve Search and [TextHighlight] Ability to highlight text (#3760)
This commit is contained in:
parent
f9bd31deee
commit
ac0e1d3161
@ -1,16 +1,38 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-notebook">
|
||||
<div class="c-notebook__head">
|
||||
<Search class="c-notebook__search"
|
||||
:value="search"
|
||||
@input="throttledSearchItem"
|
||||
@clear="throttledSearchItem"
|
||||
@input="search = $event"
|
||||
@clear="resetSearch()"
|
||||
/>
|
||||
</div>
|
||||
<SearchResults v-if="search.length"
|
||||
ref="searchResults"
|
||||
:domain-object="internalDomainObject"
|
||||
:results="searchedEntries"
|
||||
:results="searchResults"
|
||||
@changeSectionPage="changeSelectedSection"
|
||||
@updateEntries="updateEntries"
|
||||
/>
|
||||
@ -115,9 +137,13 @@ import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaul
|
||||
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
|
||||
import objectUtils from 'objectUtils';
|
||||
|
||||
import { throttle } from 'lodash';
|
||||
import { debounce } from 'lodash';
|
||||
import objectLink from '../../../ui/mixins/object-link';
|
||||
|
||||
function objectCopy(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NotebookEntry,
|
||||
@ -134,6 +160,7 @@ export default {
|
||||
focusEntryId: null,
|
||||
internalDomainObject: this.domainObject,
|
||||
search: '',
|
||||
searchResults: [],
|
||||
showTime: 0,
|
||||
showNav: false,
|
||||
sidebarCoversEntries: false
|
||||
@ -156,9 +183,6 @@ export default {
|
||||
pages() {
|
||||
return this.getPages() || [];
|
||||
},
|
||||
searchedEntries() {
|
||||
return this.getSearchResults();
|
||||
},
|
||||
sections() {
|
||||
return this.internalDomainObject.configuration.sections || [];
|
||||
},
|
||||
@ -178,8 +202,13 @@ export default {
|
||||
return this.sections.find(section => section.isSelected);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
search() {
|
||||
this.getSearchResults();
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.throttledSearchItem = throttle(this.searchItem, 500);
|
||||
this.getSearchResults = debounce(this.getSearchResults, 500);
|
||||
},
|
||||
mounted() {
|
||||
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
|
||||
@ -228,7 +257,7 @@ export default {
|
||||
});
|
||||
|
||||
this.sectionsChanged({ sections });
|
||||
this.throttledSearchItem('');
|
||||
this.resetSearch();
|
||||
},
|
||||
createNotebookStorageObject() {
|
||||
const notebookMeta = {
|
||||
@ -377,25 +406,79 @@ export default {
|
||||
const output = [];
|
||||
const entries = this.internalDomainObject.configuration.entries;
|
||||
const sectionKeys = Object.keys(entries);
|
||||
const searchTextLower = this.search.toLowerCase();
|
||||
const originalSearchText = this.search;
|
||||
let sectionTrackPageHit;
|
||||
let pageTrackEntryHit;
|
||||
let sectionTrackEntryHit;
|
||||
|
||||
sectionKeys.forEach(sectionKey => {
|
||||
const pages = entries[sectionKey];
|
||||
const pageKeys = Object.keys(pages);
|
||||
const section = this.getSection(sectionKey);
|
||||
let resultMetadata = {
|
||||
originalSearchText,
|
||||
sectionHit: section.name && section.name.toLowerCase().includes(searchTextLower)
|
||||
};
|
||||
sectionTrackPageHit = false;
|
||||
sectionTrackEntryHit = false;
|
||||
|
||||
pageKeys.forEach(pageKey => {
|
||||
const pageEntries = entries[sectionKey][pageKey];
|
||||
const page = this.getPage(section, pageKey);
|
||||
resultMetadata.pageHit = page.name && page.name.toLowerCase().includes(searchTextLower);
|
||||
pageTrackEntryHit = false;
|
||||
|
||||
if (resultMetadata.pageHit) {
|
||||
sectionTrackPageHit = true;
|
||||
}
|
||||
|
||||
pageEntries.forEach(entry => {
|
||||
if (entry.text && entry.text.toLowerCase().includes(this.search.toLowerCase())) {
|
||||
const section = this.getSection(sectionKey);
|
||||
output.push({
|
||||
const entryHit = entry.text && entry.text.toLowerCase().includes(searchTextLower);
|
||||
|
||||
// any entry hit goes in, it's the most unique of the hits
|
||||
if (entryHit) {
|
||||
resultMetadata.entryHit = entryHit;
|
||||
pageTrackEntryHit = true;
|
||||
sectionTrackEntryHit = true;
|
||||
|
||||
output.push(objectCopy({
|
||||
metadata: resultMetadata,
|
||||
section,
|
||||
page: this.getPage(section, pageKey),
|
||||
page,
|
||||
entry
|
||||
});
|
||||
}));
|
||||
}
|
||||
});
|
||||
// all entries checked, now in pages,
|
||||
// if page hit, but not in results, need to add
|
||||
if (resultMetadata.pageHit && !pageTrackEntryHit) {
|
||||
resultMetadata.entryHit = false;
|
||||
|
||||
output.push(objectCopy({
|
||||
metadata: resultMetadata,
|
||||
section,
|
||||
page
|
||||
}));
|
||||
}
|
||||
|
||||
});
|
||||
// all pages checked, now in sections,
|
||||
// if section hit, but not in results, need to add and default page
|
||||
if (resultMetadata.sectionHit && !sectionTrackPageHit && !sectionTrackEntryHit) {
|
||||
resultMetadata.entryHit = false;
|
||||
resultMetadata.pageHit = false;
|
||||
|
||||
output.push(objectCopy({
|
||||
metadata: resultMetadata,
|
||||
section,
|
||||
page: this.getPage(section, pageKeys[0])
|
||||
}));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return output;
|
||||
this.searchResults = output;
|
||||
},
|
||||
getPages() {
|
||||
const selectedSection = this.getSelectedSection();
|
||||
@ -456,12 +539,11 @@ export default {
|
||||
this.sectionsChanged({ sections });
|
||||
},
|
||||
newEntry(embed = null) {
|
||||
this.search = '';
|
||||
this.resetSearch();
|
||||
const notebookStorage = this.createNotebookStorageObject();
|
||||
this.updateDefaultNotebook(notebookStorage);
|
||||
const id = addNotebookEntry(this.openmct, this.internalDomainObject, notebookStorage, embed);
|
||||
this.focusEntryId = id;
|
||||
this.search = '';
|
||||
},
|
||||
orientationChange() {
|
||||
this.formatSidebar();
|
||||
@ -491,8 +573,9 @@ export default {
|
||||
|
||||
this.openmct.status.delete(domainObject.identifier);
|
||||
},
|
||||
searchItem(input) {
|
||||
this.search = input;
|
||||
resetSearch() {
|
||||
this.search = '';
|
||||
this.searchResults = [];
|
||||
},
|
||||
toggleNav() {
|
||||
this.showNav = !this.showNav;
|
||||
|
@ -1,3 +1,25 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-notebook__entry c-ne has-local-controls"
|
||||
@dragover="changeCursor"
|
||||
@ -10,17 +32,32 @@
|
||||
<span>{{ createdOnTime }}</span>
|
||||
</div>
|
||||
<div class="c-ne__content">
|
||||
<div :id="entry.id"
|
||||
class="c-ne__text"
|
||||
tabindex="0"
|
||||
:class="{ 'c-ne__input' : !readOnly }"
|
||||
:contenteditable="!readOnly"
|
||||
@blur="updateEntryValue($event)"
|
||||
@keydown.enter.exact.prevent
|
||||
@keyup.enter.exact.prevent="forceBlur($event)"
|
||||
v-text="entry.text"
|
||||
>
|
||||
</div>
|
||||
<template v-if="readOnly && result">
|
||||
<div
|
||||
:id="entry.id"
|
||||
class="c-ne__text highlight"
|
||||
tabindex="0"
|
||||
>
|
||||
<TextHighlight
|
||||
:text="entryText"
|
||||
:highlight="highlightText"
|
||||
:highlight-class="'search-highlight'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
:id="entry.id"
|
||||
class="c-ne__text c-ne__input"
|
||||
tabindex="0"
|
||||
contenteditable
|
||||
@blur="updateEntryValue($event)"
|
||||
@keydown.enter.exact.prevent
|
||||
@keyup.enter.exact.prevent="forceBlur($event)"
|
||||
v-text="entry.text"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<div class="c-snapshots c-ne__embeds">
|
||||
<NotebookEmbed v-for="embed in entry.embeds"
|
||||
:key="embed.id"
|
||||
@ -45,14 +82,18 @@
|
||||
<div v-if="readOnly"
|
||||
class="c-ne__section-and-page"
|
||||
>
|
||||
<a class="c-click-link"
|
||||
@click="navigateToSection()"
|
||||
<a
|
||||
class="c-click-link"
|
||||
:class="{ 'search-highlight': result.metadata.sectionHit }"
|
||||
@click="navigateToSection()"
|
||||
>
|
||||
{{ result.section.name }}
|
||||
</a>
|
||||
<span class="icon-arrow-right"></span>
|
||||
<a class="c-click-link"
|
||||
@click="navigateToPage()"
|
||||
<a
|
||||
class="c-click-link"
|
||||
:class="{ 'search-highlight': result.metadata.pageHit }"
|
||||
@click="navigateToPage()"
|
||||
>
|
||||
{{ result.page.name }}
|
||||
</a>
|
||||
@ -64,10 +105,12 @@
|
||||
import NotebookEmbed from './NotebookEmbed.vue';
|
||||
import { createNewEmbed } from '../utils/notebook-entries';
|
||||
import Moment from 'moment';
|
||||
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NotebookEmbed
|
||||
NotebookEmbed,
|
||||
TextHighlight
|
||||
},
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
props: {
|
||||
@ -114,6 +157,24 @@ export default {
|
||||
},
|
||||
createdOnTime() {
|
||||
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
|
||||
},
|
||||
entryText() {
|
||||
let text = this.entry.text;
|
||||
|
||||
if (!this.result.metadata.entryHit) {
|
||||
text = `[ no result for '${this.result.metadata.originalSearchText}' in entry ]`;
|
||||
}
|
||||
|
||||
return text;
|
||||
},
|
||||
highlightText() {
|
||||
let text = '';
|
||||
|
||||
if (this.result.metadata.entryHit) {
|
||||
text = this.result.metadata.originalSearchText;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -1,3 +1,25 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-notebook__search-results">
|
||||
<div class="c-notebook__search-results__header">Search Results</div>
|
||||
|
@ -21,6 +21,12 @@
|
||||
*****************************************************************************/
|
||||
|
||||
/*********************************************** NOTEBOOK */
|
||||
@mixin searchHighlight {
|
||||
background: rgba($colorBtnSelectedBg, 0.4);
|
||||
color: $colorBtnSelectedFg;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.c-notebook {
|
||||
$headerFontSize: 1.3em;
|
||||
display: flex;
|
||||
@ -274,7 +280,14 @@
|
||||
|
||||
&__text {
|
||||
min-height: 22px; // Needed in Firefox when field is blank
|
||||
white-space: pre-wrap;
|
||||
|
||||
&:not(.highlight) {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.search-highlight {
|
||||
@include searchHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
@ -305,6 +318,10 @@
|
||||
align-self: flex-start;
|
||||
padding: $interiorMargin;
|
||||
|
||||
.search-highlight {
|
||||
@include searchHighlight();
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMargin;
|
||||
}
|
||||
|
123
src/utils/textHighlight/TextHighlight.vue
Normal file
123
src/utils/textHighlight/TextHighlight.vue
Normal file
@ -0,0 +1,123 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
|
||||
<span>
|
||||
<span
|
||||
v-for="segment in segments"
|
||||
:key="segment.id"
|
||||
:style="getStyles(segment)"
|
||||
:class="{ [highlightClass] : segment.type === 'highlight' }"
|
||||
>
|
||||
{{ segment.text }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import uuid from 'uuid';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
highlight: {
|
||||
type: String,
|
||||
default() {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
highlightClass: {
|
||||
type: String,
|
||||
default() {
|
||||
return 'highlight';
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
segments: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
highlight() {
|
||||
this.highlightText();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.highlightText();
|
||||
},
|
||||
methods: {
|
||||
addHighlightSegment(segment) {
|
||||
this.segments.push({
|
||||
id: uuid(),
|
||||
text: segment,
|
||||
type: 'highlight',
|
||||
spaceBefore: segment.startsWith(' '),
|
||||
spaceAfter: segment.endsWith(' ')
|
||||
});
|
||||
},
|
||||
addTextSegment(segment) {
|
||||
this.segments.push({
|
||||
id: uuid(),
|
||||
text: segment,
|
||||
type: 'text',
|
||||
spaceBefore: segment.startsWith(' '),
|
||||
spaceAfter: segment.endsWith(' ')
|
||||
});
|
||||
},
|
||||
getStyles(segment) {
|
||||
let styles = {
|
||||
display: 'inline-block'
|
||||
};
|
||||
|
||||
if (segment.spaceBefore) {
|
||||
styles.paddingLeft = '.33em';
|
||||
}
|
||||
|
||||
if (segment.spaceAfter) {
|
||||
styles.paddingRight = '.33em';
|
||||
}
|
||||
|
||||
return styles;
|
||||
},
|
||||
highlightText() {
|
||||
this.segments = [];
|
||||
let regex = new RegExp('(' + this.highlight + ')', 'gi');
|
||||
let textSegments = this.text.split(regex);
|
||||
|
||||
for (let i = 0; i < textSegments.length; i++) {
|
||||
if (textSegments[i].toLowerCase() === this.highlight.toLowerCase()) {
|
||||
this.addHighlightSegment(textSegments[i]);
|
||||
} else {
|
||||
this.addTextSegment(textSegments[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
Loading…
x
Reference in New Issue
Block a user