[Notebook] Convert full links in entries, into clickable links (#6090)

* Automatically promote urls to hyperlinks if matches whitelist
* Disable v-html lint warning for notebook entries
* Check whether domain endswith given partial

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
Jamie V
2023-01-20 18:27:19 -08:00
committed by GitHub
parent 20c7b23a4f
commit 4d84b16d8b
6 changed files with 139 additions and 12 deletions

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
@@ -75,12 +76,14 @@
class="c-ne__text c-ne__input"
aria-label="Notebook Entry Input"
tabindex="0"
contenteditable="true"
:contenteditable="canEdit"
@mouseover="checkEditability($event)"
@mouseleave="canEdit = true"
@focus="editingEntry()"
@blur="updateEntryValue($event)"
@keydown.enter.exact.prevent
@keyup.enter.exact.prevent="forceBlur($event)"
v-text="entry.text"
v-html="formattedText"
>
</div>
</template>
@@ -91,7 +94,7 @@
class="c-ne__text"
contenteditable="false"
tabindex="0"
v-text="entry.text"
v-html="formattedText"
>
</div>
</template>
@@ -156,10 +159,16 @@ import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
import { createNewEmbed } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
import sanitizeHtml from 'sanitize-html';
import _ from 'lodash';
import Moment from 'moment';
const SANITIZATION_SCHEMA = {
allowedTags: [],
allowedAttributes: {}
};
const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/g;
const UNKNOWN_USER = 'Unknown';
export default {
@@ -167,7 +176,7 @@ export default {
NotebookEmbed,
TextHighlight
},
inject: ['openmct', 'snapshotContainer'],
inject: ['openmct', 'snapshotContainer', 'entryUrlWhitelist'],
props: {
domainObject: {
type: Object,
@@ -224,6 +233,8 @@ export default {
},
data() {
return {
editMode: false,
canEdit: true,
enableEmbedsWrapperScroll: false
};
},
@@ -234,6 +245,31 @@ export default {
createdOnTime() {
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
},
formattedText() {
// remove ANY tags
let text = sanitizeHtml(this.entry.text, SANITIZATION_SCHEMA);
if (this.editMode || !this.urlWhitelist) {
return text;
}
text = text.replace(URL_REGEX, (match) => {
const url = new URL(match);
const domain = url.hostname;
let result = match;
let isMatch = this.urlWhitelist.find((partialDomain) => {
return domain.endsWith(partialDomain);
});
if (isMatch) {
result = `<a class="c-hyperlink" target="_blank" href="${match}">${match}</a>`;
}
return result;
});
return text;
},
isSelectedEntry() {
return this.selectedEntryId === this.entry.id;
},
@@ -271,6 +307,9 @@ export default {
this.manageEmbedLayout();
this.dropOnEntry = this.dropOnEntry.bind(this);
if (this.entryUrlWhitelist?.length > 0) {
this.urlWhitelist = this.entryUrlWhitelist;
}
},
beforeDestroy() {
if (this.embedsWrapperResizeObserver) {
@@ -307,6 +346,11 @@ export default {
event.dataTransfer.effectAllowed = 'none';
}
},
checkEditability($event) {
if ($event.target.nodeName === 'A') {
this.canEdit = false;
}
},
deleteEntry() {
this.$emit('deleteEntry', this.entry.id);
},
@@ -405,9 +449,11 @@ export default {
this.$emit('updateEntry', this.entry);
},
editingEntry() {
this.editMode = true;
this.$emit('editingEntry');
},
updateEntryValue($event) {
this.editMode = false;
const value = $event.target.innerText;
if (value !== this.entry.text && value.match(/\S/)) {
this.entry.text = value;