Experiments (#1174)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
@@ -394,6 +394,7 @@ input[type="radio"],
|
||||
}
|
||||
|
||||
.method,
|
||||
.url-field,
|
||||
kbd,
|
||||
select,
|
||||
input,
|
||||
@@ -409,7 +410,7 @@ code {
|
||||
font-size: 16px;
|
||||
font-family: "Roboto Mono", monospace;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
line-height: 1.25;
|
||||
transition: all 0.2s ease-in-out;
|
||||
user-select: text;
|
||||
width: calc(100% - 8px);
|
||||
|
||||
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>
|
||||
@@ -302,5 +302,8 @@
|
||||
"select_head_method": "Select HEAD method",
|
||||
"select_post_method": "Select POST method",
|
||||
"select_put_method": "Select PUT method",
|
||||
"select_delete_method": "Select DELETE method"
|
||||
"select_delete_method": "Select DELETE method",
|
||||
"experiments": "Experiments",
|
||||
"experiments_notice": "This is a collection of experiments we're working on that might turn out to be useful, fun, both, or neither. They're not final and may not be stable, so if something overly weird happens, don't panic. Just turn the dang thing off. Jokes aside, ",
|
||||
"use_experimental_url_bar": "Use experimental URL bar with environment highlighting"
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@
|
||||
<li>
|
||||
<label for="url">{{ $t("url") }}</label>
|
||||
<input
|
||||
v-if="!this.$store.state.postwoman.settings.EXPERIMENTAL_URL_BAR_ENABLED"
|
||||
:class="{ error: !isValidURL }"
|
||||
@keyup.enter="isValidURL ? sendRequest() : null"
|
||||
id="url"
|
||||
@@ -154,6 +155,7 @@
|
||||
spellcheck="false"
|
||||
@input="pathInputHandler"
|
||||
/>
|
||||
<url-field v-model="uri" v-else />
|
||||
</li>
|
||||
<li class="shrink">
|
||||
<label class="hide-on-small-screen" for="send"> </label>
|
||||
|
||||
@@ -209,6 +209,35 @@
|
||||
</ul>
|
||||
-->
|
||||
</pw-section>
|
||||
|
||||
<pw-section class="red" :label="$t('experiments')" ref="experiments">
|
||||
<ul class="info">
|
||||
<li>
|
||||
<p>
|
||||
{{ $t("experiments_notice") }}
|
||||
<a
|
||||
class="link"
|
||||
href="https://github.com/hoppscotch/hoppscotch/issues/new/choose"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ $t("contact_us") }}</a
|
||||
>.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<div class="flex-wrap">
|
||||
<pw-toggle
|
||||
:on="settings.EXPERIMENTAL_URL_BAR_ENABLED"
|
||||
@change="toggleSetting('EXPERIMENTAL_URL_BAR_ENABLED')"
|
||||
>
|
||||
{{ $t("use_experimental_url_bar") }}
|
||||
</pw-toggle>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</pw-section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -325,6 +354,11 @@ export default {
|
||||
typeof this.$store.state.postwoman.settings.EXTENSIONS_ENABLED !== "undefined"
|
||||
? this.$store.state.postwoman.settings.EXTENSIONS_ENABLED
|
||||
: true,
|
||||
|
||||
EXPERIMENTAL_URL_BAR_ENABLED:
|
||||
typeof this.$store.state.postwoman.settings.EXPERIMENTAL_URL_BAR_ENABLED !== "undefined"
|
||||
? this.$store.state.postwoman.settings.EXPERIMENTAL_URL_BAR_ENABLED
|
||||
: false,
|
||||
},
|
||||
|
||||
doneButton: '<i class="material-icons">done</i>',
|
||||
|
||||
@@ -69,6 +69,11 @@ export const SETTINGS_KEYS = [
|
||||
* to run the requests
|
||||
*/
|
||||
"EXTENSIONS_ENABLED",
|
||||
|
||||
/**
|
||||
* A boolean value indicating whether to use the URL bar experiments
|
||||
*/
|
||||
"EXPERIMENTAL_URL_BAR_ENABLED",
|
||||
]
|
||||
|
||||
export const state = () => ({
|
||||
|
||||
Reference in New Issue
Block a user