Skip to content

Instantly share code, notes, and snippets.

@marianopaulin02
Last active November 11, 2021 22:44
Show Gist options
  • Save marianopaulin02/e8a9b3171fd46481477963a2ba63a321 to your computer and use it in GitHub Desktop.
Save marianopaulin02/e8a9b3171fd46481477963a2ba63a321 to your computer and use it in GitHub Desktop.
VueJS component to provide highlight of hashtags in a given string and allow edit the string using HTML contenteditable

🏷 Highlight & Edit Hashtags in VueJS

Lightweight and zero dependency Vuejs component

Example:

image

Usage

<script>
import HashtagsLine from './HashtagsLine.vue'

export default {
	data: () => ({
		lineText: 'Hello #There #myclass',
	}),
	// ...
	components: { HashtagsLine },
}
</script>
<template>
	<hashtags-line v-model="lineText" :validHashtags="['myclass']" />
</template>

🎨 Customize

All optional

:validHashtags: Array - List of lowercased allowed tag names

:hashtagClass: String - Class name to apply to the highlighted tags

:containerClassName: String - Class name for the container "input"

<template>
<div v-html="html" ref="content" @keyup="keyup" contenteditable :class="containerClassName" />
</template>
<style scoped>
.hashtags-line-container {
padding: 10px;
border: solid rgba(0, 0, 0, 0.5) 1px;
border-radius: 4px;
transition: 0.25s;
margin: 6px;
}
.hashtags-line-container:hover {
border: solid rgba(0, 0, 0, 0.87) 1px;
}
.hashtags-line-container:focus {
border-color: transparent;
outline: solid rgb(75, 95, 247) 2px;
}
</style>
<style>
.hashtag-class {
color: rgb(75, 95, 247);
}
</style>
<script>
const HASHTAG_REGEXP = /#+([a-zA-Z0-9_]+)/gi
const REGEXP_LESSTHAN = /</g
const SCAPED_KEY_CODES = [
8, // Backspace
13, // Enter
]
const highlightHashtags = (text, hashtagClass, validHashtags) => {
const tagStart = `<span class="${hashtagClass}">`
const html = text.replace(HASHTAG_REGEXP, (hashtag) => {
if ((validHashtags && validHashtags.includes(hashtag.substr(1).toLowerCase())) || !validHashtags) {
return `${tagStart}${hashtag}</span>`
}
return hashtag
})
return html
}
const getTree = (node, container) => {
if (node == container) return null
const nodeIndex = Array.prototype.indexOf.call(node.parentNode.childNodes, node)
if (node.parentNode === container) {
return [nodeIndex]
}
return [nodeIndex, ...getTree(node.parentNode, container)]
}
const getChild = (container, path) => {
try {
let elem = container
path.forEach((index) => {
elem = elem.childNodes[index]
})
return elem
} catch (e) {
return null
}
}
export default {
props: {
hashtagClass: { type: String, default: 'hashtag-class' },
validHashtags: { type: Array, default: null },
containerClassName: { type: String, default: 'hashtags-line-container' },
value: { type: String, default: '' },
},
data() {
return {
open: true,
html: '',
}
},
methods: {
keyup($event) {
const { keyCode } = $event
if (SCAPED_KEY_CODES.includes(keyCode)) {
// On backspace
return
}
const vm = this
const el = vm.$refs.content
const { anchorOffset, anchorNode } = window.getSelection()
const treePrototype = getTree(anchorNode, el)
const tree = treePrototype ? treePrototype.reverse() : []
const text = el.innerText
vm.render(text)
vm.$emit('input', text)
vm.$nextTick(() => {
setTimeout(() => {
const selection = window.getSelection()
const range = document.createRange()
let node = treePrototype ? getChild(el, tree) : el
if (!node) return
if (anchorOffset > node.textContent.length) {
// Jump to the next node
const nodeIndex = Array.prototype.indexOf.call(node.parentNode.childNodes, node)
if (node.parentNode.childNodes[nodeIndex + 1]) {
node = node.parentNode.childNodes[nodeIndex + 1]
} else {
const parentNodeIndex = Array.prototype.indexOf.call(
node.parentNode.parentNode.childNodes,
node.parentNode
)
const siblingNode = node.parentNode.parentNode.childNodes[parentNodeIndex + 1]
if (siblingNode) node = siblingNode
}
if (node) range.setStart(node, 1)
} else {
range.setStart(node, anchorOffset)
}
range.collapse(true)
selection.removeAllRanges()
selection.addRange(range)
el.focus()
}, 1)
})
},
render(text = '') {
const vm = this
const scapedText = text.replace(REGEXP_LESSTHAN, '&lt;')
vm.html = highlightHashtags(scapedText, vm.hashtagClass, vm.validHashtags)
},
},
watch: {
value(newValue) {
this.render(newValue)
},
},
mounted() {
this.render(this.value)
},
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment