[Notebook] Improve Search and [TextHighlight] Ability to highlight text (#3760)

This commit is contained in:
Jamie V 2021-03-29 13:47:25 -07:00 committed by GitHub
parent f9bd31deee
commit ac0e1d3161
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 342 additions and 36 deletions

View File

@ -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;

View File

@ -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() {

View File

@ -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>

View File

@ -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;
}

View 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>