Experiments (#1174)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
35
components/ui/__tests__/url-field.spec.js
Normal file
35
components/ui/__tests__/url-field.spec.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import urlField from "../url-field"
|
||||
import { mount } from "@vue/test-utils"
|
||||
|
||||
const factory = (props) =>
|
||||
mount(urlField, {
|
||||
propsData: props,
|
||||
})
|
||||
|
||||
/*
|
||||
* NOTE : jsdom as of yet doesn't support contenteditable features
|
||||
* hence, the test suite is pretty limited as it is not easy to test
|
||||
* inputting values.
|
||||
*/
|
||||
|
||||
describe("url-field", () => {
|
||||
test("mounts properly", () => {
|
||||
const wrapper = factory({
|
||||
value: "test",
|
||||
})
|
||||
|
||||
expect(wrapper.vm).toBeTruthy()
|
||||
})
|
||||
test("highlights environment variables", () => {
|
||||
const wrapper = factory({
|
||||
value: "https://hoppscotch.io/<<testa>>/<<testb>>",
|
||||
})
|
||||
|
||||
const highlights = wrapper.findAll(".highlight-VAR").wrappers
|
||||
|
||||
expect(highlights).toHaveLength(2)
|
||||
|
||||
expect(highlights[0].text()).toEqual("<<testa>>")
|
||||
expect(highlights[1].text()).toEqual("<<testb>>")
|
||||
})
|
||||
})
|
||||
124
components/ui/url-field.vue
Normal file
124
components/ui/url-field.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div contenteditable class="url-field" ref="editor" spellcheck="false"></div>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.highlight-VAR {
|
||||
color: var(--ac-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: { type: String },
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.editor.addEventListener("input", this.updateEditor)
|
||||
this.$refs.editor.textContent = this.value || ""
|
||||
|
||||
this.updateEditor()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$refs.editor.removeEventListener("input", this.updateEditor)
|
||||
},
|
||||
methods: {
|
||||
renderText(text) {
|
||||
const fixedText = text.replace(/(\r\n|\n|\r)/gm, "").trim()
|
||||
const parseMap = this.parseURL(fixedText)
|
||||
|
||||
const convertSpan = document.createElement("span")
|
||||
|
||||
const output = parseMap.map(([start, end, protocol]) => {
|
||||
convertSpan.textContent = fixedText.substring(start, end + 1)
|
||||
return `<span class='highlight-${protocol}'>${convertSpan.innerHTML}</span>`
|
||||
})
|
||||
|
||||
return output.join("")
|
||||
},
|
||||
parseURL(text) {
|
||||
const map = []
|
||||
const regex = /<<\w+>>/
|
||||
|
||||
let match
|
||||
let index = 0
|
||||
while ((match = text.substring(index).match(regex))) {
|
||||
map.push([index, index + (match.index - 1), "TEXT"])
|
||||
map.push([index + match.index, index + match.index + match[0].length - 1, "VAR"])
|
||||
index += match.index + match[0].length
|
||||
|
||||
if (index >= text.length - 1) break
|
||||
}
|
||||
|
||||
if (text.length > index && !text.substring(index).match(regex)) {
|
||||
map.push([index, text.length, "TEXT"])
|
||||
}
|
||||
|
||||
return map
|
||||
},
|
||||
getTextSegments(element) {
|
||||
const textSegments = []
|
||||
Array.from(element.childNodes).forEach((node) => {
|
||||
switch (node.nodeType) {
|
||||
case Node.TEXT_NODE:
|
||||
textSegments.push({ text: node.nodeValue, node })
|
||||
break
|
||||
|
||||
case Node.ELEMENT_NODE:
|
||||
textSegments.splice(textSegments.length, 0, ...this.getTextSegments(node))
|
||||
break
|
||||
}
|
||||
})
|
||||
return textSegments
|
||||
},
|
||||
restoreSelection(absoluteAnchorIndex, absoluteFocusIndex) {
|
||||
const sel = window.getSelection()
|
||||
const textSegments = this.getTextSegments(this.$refs.editor)
|
||||
let anchorNode = this.$refs.editor
|
||||
let anchorIndex = 0
|
||||
let focusNode = this.$refs.editor
|
||||
let focusIndex = 0
|
||||
let currentIndex = 0
|
||||
textSegments.forEach(({ text, node }) => {
|
||||
const startIndexOfNode = currentIndex
|
||||
const endIndexOfNode = startIndexOfNode + text.length
|
||||
if (startIndexOfNode <= absoluteAnchorIndex && absoluteAnchorIndex <= endIndexOfNode) {
|
||||
anchorNode = node
|
||||
anchorIndex = absoluteAnchorIndex - startIndexOfNode
|
||||
}
|
||||
if (startIndexOfNode <= absoluteFocusIndex && absoluteFocusIndex <= endIndexOfNode) {
|
||||
focusNode = node
|
||||
focusIndex = absoluteFocusIndex - startIndexOfNode
|
||||
}
|
||||
currentIndex += text.length
|
||||
})
|
||||
|
||||
sel.setBaseAndExtent(anchorNode, anchorIndex, focusNode, focusIndex)
|
||||
},
|
||||
updateEditor() {
|
||||
const sel = window.getSelection()
|
||||
const textSegments = this.getTextSegments(this.$refs.editor)
|
||||
const textContent = textSegments.map(({ text }) => text).join("")
|
||||
|
||||
let anchorIndex = null
|
||||
let focusIndex = null
|
||||
let currentIndex = 0
|
||||
|
||||
textSegments.forEach(({ text, node }) => {
|
||||
if (node === sel.anchorNode) {
|
||||
anchorIndex = currentIndex + sel.anchorOffset
|
||||
}
|
||||
if (node === sel.focusNode) {
|
||||
focusIndex = currentIndex + sel.focusOffset
|
||||
}
|
||||
currentIndex += text.length
|
||||
})
|
||||
|
||||
this.$refs.editor.innerHTML = this.renderText(textContent)
|
||||
|
||||
this.restoreSelection(anchorIndex, focusIndex)
|
||||
|
||||
this.$emit("input", this.$refs.editor.textContent)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user