rename all components to new namespace (#1515)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
39
components/smart/AccentModePicker.vue
Normal file
39
components/smart/AccentModePicker.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<label>{{ $t("color") }}: {{ active.charAt(0).toUpperCase() + active.slice(1) }}</label>
|
||||
<div>
|
||||
<span
|
||||
v-for="(color, index) of colors"
|
||||
:key="`color-${index}`"
|
||||
v-tooltip="`${color.charAt(0).toUpperCase()}${color.slice(1)}`"
|
||||
class="inline-flex items-center justify-center p-3 m-2 transition duration-150 ease-in-out bg-transparent rounded-full cursor-pointer border-collapseer-2 hover:shadow-none"
|
||||
:class="[{ 'bg-bgDarkColor': color === active }, `text-${color}-400`]"
|
||||
@click="setActiveColor(color)"
|
||||
>
|
||||
<i class="material-icons">lens</i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
active: localStorage.getItem("THEME_COLOR") || "green",
|
||||
colors: ["blue", "green", "teal", "purple", "orange", "pink", "red", "yellow"],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setActiveColor(color) {
|
||||
document.documentElement.setAttribute("data-accent", color)
|
||||
this.active = color
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
active(color) {
|
||||
localStorage.setItem("THEME_COLOR", color)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
295
components/smart/AceEditor.vue
Normal file
295
components/smart/AceEditor.vue
Normal file
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<div class="show-if-initialized" :class="{ initialized }">
|
||||
<div class="outline" v-if="lang == 'json'">
|
||||
<div class="block" v-for="(p, index) in currPath" :key="index">
|
||||
<div class="label" @click="onBlockClick(index)">
|
||||
{{ p }}
|
||||
</div>
|
||||
<i v-if="index + 1 !== currPath.length" class="material-icons">chevron_right</i>
|
||||
<div
|
||||
class="siblings"
|
||||
v-if="sibDropDownIndex == index"
|
||||
@mouseleave="clearSibList"
|
||||
:ref="`sibling-${index}`"
|
||||
>
|
||||
<div class="sib" v-for="(sib, i) in currSib" :key="i" @click="goToSib(sib)">
|
||||
{{ sib.key ? sib.key.value : i }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<pre ref="editor" :class="styles"></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.show-if-initialized {
|
||||
@apply opacity-0;
|
||||
|
||||
&.initialized {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
& > * {
|
||||
@apply transition-none;
|
||||
}
|
||||
}
|
||||
|
||||
.outline {
|
||||
@apply flex;
|
||||
@apply flex-no-wrap;
|
||||
@apply w-full;
|
||||
@apply overflow-auto;
|
||||
@apply font-mono;
|
||||
@apply shadow-lg;
|
||||
@apply px-4;
|
||||
|
||||
.block {
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
@apply flex-grow-0;
|
||||
@apply flex-shrink-0;
|
||||
@apply text-fgLightColor;
|
||||
@apply text-sm;
|
||||
|
||||
&:hover {
|
||||
@apply text-fgColor;
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
.label {
|
||||
@apply p-2;
|
||||
@apply transition;
|
||||
@apply ease-in-out;
|
||||
@apply duration-150;
|
||||
}
|
||||
|
||||
.siblings {
|
||||
@apply absolute;
|
||||
@apply z-50;
|
||||
@apply top-9;
|
||||
@apply bg-bgColor;
|
||||
@apply max-h-60;
|
||||
@apply overflow-auto;
|
||||
@apply shadow-lg;
|
||||
@apply text-fgLightColor;
|
||||
@apply overscroll-none;
|
||||
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.sib {
|
||||
@apply px-4;
|
||||
@apply py-1;
|
||||
|
||||
&:hover {
|
||||
@apply text-fgColor;
|
||||
@apply bg-bgLightColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import ace from "ace-builds"
|
||||
import "ace-builds/webpack-resolver"
|
||||
import jsonParse from "~/helpers/jsonParse"
|
||||
import debounce from "~/helpers/utils/debounce"
|
||||
import outline from "~/helpers/outline"
|
||||
|
||||
export default {
|
||||
props: {
|
||||
provideJSONOutline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
lang: {
|
||||
type: String,
|
||||
default: "json",
|
||||
},
|
||||
lint: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
required: false,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
styles: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
initialized: false,
|
||||
editor: null,
|
||||
cacheValue: "",
|
||||
outline: outline(),
|
||||
currPath: [],
|
||||
currSib: [],
|
||||
sibDropDownIndex: null,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(value) {
|
||||
if (value !== this.cacheValue) {
|
||||
this.editor.session.setValue(value, 1)
|
||||
this.cacheValue = value
|
||||
if (this.lint) this.provideLinting(value)
|
||||
}
|
||||
},
|
||||
theme() {
|
||||
this.initialized = false
|
||||
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
|
||||
this.$nextTick().then(() => {
|
||||
this.initialized = true
|
||||
})
|
||||
})
|
||||
},
|
||||
lang(value) {
|
||||
this.editor.getSession().setMode(`ace/mode/${value}`)
|
||||
},
|
||||
options(value) {
|
||||
this.editor.setOptions(value)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const editor = ace.edit(this.$refs.editor, {
|
||||
mode: `ace/mode/${this.lang}`,
|
||||
...this.options,
|
||||
})
|
||||
|
||||
// Set the theme and show the editor only after it's been set to prevent FOUC.
|
||||
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
|
||||
this.$nextTick().then(() => {
|
||||
this.initialized = true
|
||||
})
|
||||
})
|
||||
|
||||
if (this.value) editor.setValue(this.value, 1)
|
||||
|
||||
this.editor = editor
|
||||
this.cacheValue = this.value
|
||||
|
||||
if (this.lang === "json" && this.provideJSONOutline) this.initOutline(this.value)
|
||||
|
||||
editor.on("change", () => {
|
||||
const content = editor.getValue()
|
||||
this.$emit("input", content)
|
||||
this.cacheValue = content
|
||||
|
||||
if (this.provideJSONOutline) debounce(this.initOutline(content), 500)
|
||||
|
||||
if (this.lint) this.provideLinting(content)
|
||||
})
|
||||
|
||||
if (this.lang === "json" && this.provideJSONOutline) {
|
||||
editor.session.selection.on("changeCursor", (e) => {
|
||||
const index = editor.session.doc.positionToIndex(editor.selection.getCursor(), 0)
|
||||
const path = this.outline.genPath(index)
|
||||
if (path.success) {
|
||||
this.currPath = path.res
|
||||
}
|
||||
})
|
||||
document.addEventListener("touchstart", this.onTouchStart)
|
||||
}
|
||||
|
||||
// Disable linting, if lint prop is false
|
||||
if (this.lint) this.provideLinting(this.value)
|
||||
},
|
||||
|
||||
methods: {
|
||||
defineTheme() {
|
||||
if (this.theme) {
|
||||
return this.theme
|
||||
}
|
||||
const strip = (str) => str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
|
||||
return strip(
|
||||
window.getComputedStyle(document.documentElement).getPropertyValue("--editor-theme")
|
||||
)
|
||||
},
|
||||
|
||||
provideLinting: debounce(function (code) {
|
||||
if (this.lang === "json") {
|
||||
try {
|
||||
jsonParse(code)
|
||||
this.editor.session.setAnnotations([])
|
||||
} catch (e) {
|
||||
const pos = this.editor.session.getDocument().indexToPosition(e.start, 0)
|
||||
this.editor.session.setAnnotations([
|
||||
{
|
||||
row: pos.row,
|
||||
column: pos.column,
|
||||
text: e.message,
|
||||
type: "error",
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
}, 2000),
|
||||
onBlockClick(index) {
|
||||
if (this.sibDropDownIndex == index) {
|
||||
this.clearSibList()
|
||||
} else {
|
||||
this.currSib = this.outline.getSiblings(index)
|
||||
if (this.currSib.length) this.sibDropDownIndex = index
|
||||
}
|
||||
},
|
||||
clearSibList() {
|
||||
this.currSib = []
|
||||
this.sibDropDownIndex = null
|
||||
},
|
||||
goToSib(obj) {
|
||||
this.clearSibList()
|
||||
if (obj.start) {
|
||||
let pos = this.editor.session.doc.indexToPosition(obj.start, 0)
|
||||
if (pos) {
|
||||
this.editor.session.selection.moveCursorTo(pos.row, pos.column, true)
|
||||
this.editor.session.selection.clearSelection()
|
||||
this.editor.scrollToLine(pos.row, false, true, null)
|
||||
}
|
||||
}
|
||||
},
|
||||
initOutline: debounce(function (content) {
|
||||
if (this.lang == "json") {
|
||||
try {
|
||||
this.outline.init(content)
|
||||
|
||||
if (content[0] == "[") this.currPath.push("[]")
|
||||
else this.currPath.push("{}")
|
||||
} catch (e) {
|
||||
console.log("Outline error: ", e)
|
||||
}
|
||||
}
|
||||
}),
|
||||
onTouchStart(e) {
|
||||
if (this.sibDropDownIndex == null) return
|
||||
else {
|
||||
if (e.target.parentElement != this.$refs[`sibling-${this.sibDropDownIndex}`][0]) {
|
||||
this.clearSibList()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
destroyed() {
|
||||
this.editor.destroy()
|
||||
document.removeEventListener("touchstart", this.onTouchStart)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
225
components/smart/AutoComplete.vue
Normal file
225
components/smart/AutoComplete.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<div class="autocomplete-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="placeholder"
|
||||
v-model="text"
|
||||
@input="updateSuggestions"
|
||||
@keyup="updateSuggestions"
|
||||
@click="updateSuggestions"
|
||||
@keydown="handleKeystroke"
|
||||
ref="acInput"
|
||||
:spellcheck="spellcheck"
|
||||
:autocapitalize="autocapitalize"
|
||||
:autocorrect="spellcheck"
|
||||
:class="styles"
|
||||
/>
|
||||
<ul
|
||||
class="suggestions"
|
||||
v-if="suggestions.length > 0 && suggestionsVisible"
|
||||
:style="{ transform: `translate(${suggestionsOffsetLeft}px, 0)` }"
|
||||
>
|
||||
<li
|
||||
v-for="(suggestion, index) in suggestions"
|
||||
@click.prevent="forceSuggestion(suggestion)"
|
||||
:class="{ active: currentSuggestionIndex === index }"
|
||||
:key="index"
|
||||
>
|
||||
{{ suggestion }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.autocomplete-wrapper {
|
||||
@apply relative;
|
||||
|
||||
input:focus + ul.suggestions,
|
||||
ul.suggestions:hover {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
ul.suggestions {
|
||||
@apply hidden;
|
||||
@apply bg-actColor;
|
||||
@apply absolute;
|
||||
@apply mx-2;
|
||||
@apply left-0;
|
||||
@apply z-50;
|
||||
@apply transition;
|
||||
@apply ease-in-out;
|
||||
@apply duration-150;
|
||||
@apply shadow-lg;
|
||||
|
||||
top: calc(100% - 8px);
|
||||
border-radius: 0 0 8px 8px;
|
||||
|
||||
li {
|
||||
@apply w-full;
|
||||
@apply block;
|
||||
@apply py-2;
|
||||
@apply px-4;
|
||||
@apply text-sm;
|
||||
@apply font-mono;
|
||||
@apply font-normal;
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
@apply bg-acColor;
|
||||
@apply text-actColor;
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
spellcheck: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
required: false,
|
||||
},
|
||||
|
||||
autocapitalize: {
|
||||
type: String,
|
||||
default: "off",
|
||||
required: false,
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
|
||||
source: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
|
||||
styles: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
text() {
|
||||
this.$emit("input", this.text)
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
text: this.value,
|
||||
selectionStart: 0,
|
||||
suggestionsOffsetLeft: 0,
|
||||
currentSuggestionIndex: -1,
|
||||
suggestionsVisible: false,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateSuggestions(event) {
|
||||
// Hide suggestions if ESC pressed.
|
||||
if (event.code && event.code === "Escape") {
|
||||
event.preventDefault()
|
||||
this.suggestionsVisible = false
|
||||
this.currentSuggestionIndex = -1
|
||||
return
|
||||
}
|
||||
|
||||
// As suggestions is a reactive property, this implicitly
|
||||
// causes suggestions to update.
|
||||
this.selectionStart = this.$refs.acInput.selectionStart
|
||||
this.suggestionsOffsetLeft = 12 * this.selectionStart
|
||||
this.suggestionsVisible = true
|
||||
},
|
||||
|
||||
forceSuggestion(text) {
|
||||
let input = this.text.substring(0, this.selectionStart)
|
||||
this.text = input + text
|
||||
|
||||
this.selectionStart = this.text.length
|
||||
this.suggestionsVisible = true
|
||||
this.currentSuggestionIndex = -1
|
||||
},
|
||||
|
||||
handleKeystroke(event) {
|
||||
switch (event.code) {
|
||||
case "ArrowUp":
|
||||
event.preventDefault()
|
||||
this.currentSuggestionIndex =
|
||||
this.currentSuggestionIndex - 1 >= 0 ? this.currentSuggestionIndex - 1 : 0
|
||||
break
|
||||
|
||||
case "ArrowDown":
|
||||
event.preventDefault()
|
||||
this.currentSuggestionIndex =
|
||||
this.currentSuggestionIndex < this.suggestions.length - 1
|
||||
? this.currentSuggestionIndex + 1
|
||||
: this.suggestions.length - 1
|
||||
break
|
||||
|
||||
case "Tab":
|
||||
let activeSuggestion = this.suggestions[
|
||||
this.currentSuggestionIndex >= 0 ? this.currentSuggestionIndex : 0
|
||||
]
|
||||
|
||||
if (!activeSuggestion) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
let input = this.text.substring(0, this.selectionStart)
|
||||
this.text = input + activeSuggestion
|
||||
break
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Gets the suggestions list to be displayed under the input box.
|
||||
*
|
||||
* @returns {default.props.source|{type, required}}
|
||||
*/
|
||||
suggestions() {
|
||||
let input = this.text.substring(0, this.selectionStart)
|
||||
|
||||
return (
|
||||
this.source
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.toLowerCase().startsWith(input.toLowerCase()) &&
|
||||
input.toLowerCase() !== entry.toLowerCase()
|
||||
)
|
||||
// Cut off the part that's already been typed.
|
||||
.map((entry) => entry.substring(this.selectionStart))
|
||||
// We only want the top 6 suggestions.
|
||||
.slice(0, 6)
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateSuggestions({
|
||||
target: this.$refs.acInput,
|
||||
})
|
||||
},
|
||||
}
|
||||
</script>
|
||||
54
components/smart/ColorModePicker.vue
Normal file
54
components/smart/ColorModePicker.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<label>
|
||||
<ColorScheme placeholder="..." tag="span">
|
||||
{{ $t("background") }}:
|
||||
{{ $colorMode.preference.charAt(0).toUpperCase() + $colorMode.preference.slice(1) }}
|
||||
<span v-if="$colorMode.preference === 'system'">
|
||||
({{ $colorMode.value }} mode detected)
|
||||
</span>
|
||||
</ColorScheme>
|
||||
</label>
|
||||
<div>
|
||||
<span
|
||||
v-for="(color, index) of colors"
|
||||
:key="`color-${index}`"
|
||||
v-tooltip="`${color.charAt(0).toUpperCase()}${color.slice(1)}`"
|
||||
class="inline-flex items-center justify-center p-3 m-2 transition duration-150 ease-in-out bg-transparent rounded-full cursor-pointer border-collapseer-2 text-fgLightColor hover:text-fgColor hover:shadow-none"
|
||||
:class="[
|
||||
{ 'bg-bgDarkColor': color === $colorMode.preference },
|
||||
{ 'text-acColor hover:text-acColor': color === $colorMode.value },
|
||||
]"
|
||||
@click="$colorMode.preference = color"
|
||||
>
|
||||
<i class="material-icons">{{ getIcon(color) }}</i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
colors: ["system", "light", "dark", "black"],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getIcon(color) {
|
||||
switch (color) {
|
||||
case "system":
|
||||
return "desktop_windows"
|
||||
case "light":
|
||||
return "wb_sunny"
|
||||
case "dark":
|
||||
return "nights_stay"
|
||||
case "black":
|
||||
return "bedtime"
|
||||
default:
|
||||
return "desktop_windows"
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
71
components/smart/ConfirmModal.vue
Normal file
71
components/smart/ConfirmModal.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<SmartModal v-if="show" @close="hideModal">
|
||||
<div slot="header">
|
||||
<div class="row-wrapper">
|
||||
<h3 class="title">{{ $t("confirm") }}</h3>
|
||||
<div>
|
||||
<button class="icon" @click="hideModal">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="body" class="flex flex-col">
|
||||
<label>{{ title }}</label>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<div class="row-wrapper">
|
||||
<span></span>
|
||||
<span>
|
||||
<button class="icon" @click="hideModal">
|
||||
{{ no }}
|
||||
</button>
|
||||
<button class="icon primary" @click="resolve">
|
||||
{{ yes }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
show: Boolean,
|
||||
title: "",
|
||||
yes: {
|
||||
type: String,
|
||||
default: function () {
|
||||
return this.$t("yes")
|
||||
},
|
||||
},
|
||||
no: {
|
||||
type: String,
|
||||
default: function () {
|
||||
return this.$t("no")
|
||||
},
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this._keyListener = function (e) {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
this.hideModal()
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", this._keyListener.bind(this))
|
||||
},
|
||||
methods: {
|
||||
hideModal() {
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
resolve() {
|
||||
this.$emit("resolve")
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener("keydown", this._keyListener)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
32
components/smart/DeletableChip.vue
Normal file
32
components/smart/DeletableChip.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<span class="chip">
|
||||
<span><slot></slot></span>
|
||||
<button class="p-2 icon" @click="$emit('chip-delete')">
|
||||
<i class="material-icons close-button"> close </i>
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chip {
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply rounded-lg;
|
||||
@apply m-1;
|
||||
@apply pl-4;
|
||||
@apply bg-bgDarkColor;
|
||||
@apply text-fgColor;
|
||||
@apply font-mono;
|
||||
@apply font-normal;
|
||||
@apply transition;
|
||||
@apply ease-in-out;
|
||||
@apply duration-150;
|
||||
@apply border;
|
||||
@apply border-brdColor;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
@apply text-base;
|
||||
}
|
||||
</style>
|
||||
261
components/smart/JsEditor.vue
Normal file
261
components/smart/JsEditor.vue
Normal file
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<div class="show-if-initialized" :class="{ initialized }">
|
||||
<pre ref="editor" :class="styles"></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.show-if-initialized {
|
||||
@apply opacity-0;
|
||||
|
||||
&.initialized {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
& > * {
|
||||
@apply transition-none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import ace from "ace-builds"
|
||||
import "ace-builds/webpack-resolver"
|
||||
import "ace-builds/src-noconflict/ext-language_tools"
|
||||
import "ace-builds/src-noconflict/mode-graphqlschema"
|
||||
import debounce from "~/helpers/utils/debounce"
|
||||
import {
|
||||
getPreRequestScriptCompletions,
|
||||
getTestScriptCompletions,
|
||||
performPreRequestLinting,
|
||||
} from "~/helpers/tern"
|
||||
|
||||
import * as esprima from "esprima"
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
styles: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
completeMode: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "none",
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
initialized: false,
|
||||
editor: null,
|
||||
cacheValue: "",
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(value) {
|
||||
if (value !== this.cacheValue) {
|
||||
this.editor.session.setValue(value, 1)
|
||||
this.cacheValue = value
|
||||
if (this.lint) this.provideLinting(value)
|
||||
}
|
||||
},
|
||||
theme() {
|
||||
this.initialized = false
|
||||
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
|
||||
this.$nextTick()
|
||||
.then(() => {
|
||||
this.initialized = true
|
||||
})
|
||||
.catch(() => {
|
||||
// nextTick shouldn't really ever throw but still
|
||||
this.initialized = true
|
||||
})
|
||||
})
|
||||
},
|
||||
options(value) {
|
||||
this.editor.setOptions(value)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// const langTools = ace.require("ace/ext/language_tools")
|
||||
|
||||
const editor = ace.edit(this.$refs.editor, {
|
||||
mode: `ace/mode/javascript`,
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true,
|
||||
...this.options,
|
||||
})
|
||||
|
||||
// Set the theme and show the editor only after it's been set to prevent FOUC.
|
||||
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
|
||||
this.$nextTick()
|
||||
.then(() => {
|
||||
this.initialized = true
|
||||
})
|
||||
.catch(() => {
|
||||
// nextTIck shouldn't really ever throw but still
|
||||
this.initalized = true
|
||||
})
|
||||
})
|
||||
|
||||
const completer = {
|
||||
getCompletions: (editor, _session, { row, column }, _prefix, callback) => {
|
||||
if (this.completeMode === "pre") {
|
||||
getPreRequestScriptCompletions(editor.getValue(), row, column)
|
||||
.then((res) => {
|
||||
callback(
|
||||
null,
|
||||
res.completions.map((r, index, arr) => ({
|
||||
name: r.name,
|
||||
value: r.name,
|
||||
score: (arr.length - index) / arr.length,
|
||||
meta: r.type,
|
||||
}))
|
||||
)
|
||||
})
|
||||
.catch(() => callback(null, []))
|
||||
} else if (this.completeMode === "test") {
|
||||
getTestScriptCompletions(editor.getValue(), row, column)
|
||||
.then((res) => {
|
||||
callback(
|
||||
null,
|
||||
res.completions.map((r, index, arr) => ({
|
||||
name: r.name,
|
||||
value: r.name,
|
||||
score: (arr.length - index) / arr.length,
|
||||
meta: r.type,
|
||||
}))
|
||||
)
|
||||
})
|
||||
.catch(() => callback(null, []))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
editor.completers = [completer]
|
||||
|
||||
if (this.value) editor.setValue(this.value, 1)
|
||||
|
||||
this.editor = editor
|
||||
this.cacheValue = this.value
|
||||
|
||||
editor.on("change", () => {
|
||||
const content = editor.getValue()
|
||||
this.$emit("input", content)
|
||||
this.cacheValue = content
|
||||
this.provideLinting(content)
|
||||
})
|
||||
|
||||
this.provideLinting(this.value)
|
||||
},
|
||||
|
||||
methods: {
|
||||
defineTheme() {
|
||||
if (this.theme) {
|
||||
return this.theme
|
||||
}
|
||||
const strip = (str) => str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
|
||||
return strip(
|
||||
window.getComputedStyle(document.documentElement).getPropertyValue("--editor-theme")
|
||||
)
|
||||
},
|
||||
|
||||
provideLinting: debounce(function (code) {
|
||||
let results = []
|
||||
|
||||
performPreRequestLinting(code)
|
||||
.then((semanticLints) => {
|
||||
results = results.concat(
|
||||
semanticLints.map((lint) => ({
|
||||
row: lint.from.line,
|
||||
column: lint.from.ch,
|
||||
text: `[semantic] ${lint.message}`,
|
||||
type: "error",
|
||||
}))
|
||||
)
|
||||
|
||||
try {
|
||||
const res = esprima.parseScript(code, { tolerant: true })
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
results = results.concat(
|
||||
res.errors.map((err) => {
|
||||
const pos = this.editor.session.getDocument().indexToPosition(err.index, 0)
|
||||
|
||||
return {
|
||||
row: pos.row,
|
||||
column: pos.column,
|
||||
text: `[syntax] ${err.description}`,
|
||||
type: "error",
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
const pos = this.editor.session.getDocument().indexToPosition(e.index, 0)
|
||||
results = results.concat([
|
||||
{
|
||||
row: pos.row,
|
||||
column: pos.column,
|
||||
text: `[syntax] ${e.description}`,
|
||||
type: "error",
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
this.editor.session.setAnnotations(results)
|
||||
})
|
||||
.catch(() => {
|
||||
try {
|
||||
const res = esprima.parseScript(code, { tolerant: true })
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
results = results.concat(
|
||||
res.errors.map((err) => {
|
||||
const pos = this.editor.session.getDocument().indexToPosition(err.index, 0)
|
||||
|
||||
return {
|
||||
row: pos.row,
|
||||
column: pos.column,
|
||||
text: `[syntax] ${err.description}`,
|
||||
type: "error",
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
const pos = this.editor.session.getDocument().indexToPosition(e.index, 0)
|
||||
results = results.concat([
|
||||
{
|
||||
row: pos.row,
|
||||
column: pos.column,
|
||||
text: `[syntax] ${e.description}`,
|
||||
type: "error",
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
this.editor.session.setAnnotations(results)
|
||||
})
|
||||
}, 2000),
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
130
components/smart/Modal.vue
Normal file
130
components/smart/Modal.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<transition name="modal" appear>
|
||||
<div class="modal-backdrop">
|
||||
<div class="modal-wrapper">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot name="body"></slot>
|
||||
<!-- <div class="fade top"></div>
|
||||
<div class="fade bottom"></div> -->
|
||||
</div>
|
||||
<div v-if="hasFooterSlot" class="modal-footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-backdrop {
|
||||
@apply fixed;
|
||||
@apply inset-0;
|
||||
@apply z-50;
|
||||
@apply w-full;
|
||||
@apply h-full;
|
||||
@apply flex;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply transition;
|
||||
@apply ease-in-out;
|
||||
@apply duration-150;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
.modal-wrapper {
|
||||
@apply flex;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply flex-1;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
@apply relative;
|
||||
@apply flex;
|
||||
@apply flex-1;
|
||||
@apply flex-col;
|
||||
@apply m-2;
|
||||
@apply transition;
|
||||
@apply ease-in-out;
|
||||
@apply duration-150;
|
||||
@apply bg-bgColor;
|
||||
@apply rounded-lg;
|
||||
@apply shadow-2xl;
|
||||
|
||||
max-height: calc(100vh - 128px);
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@apply pl-2;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
@apply overflow-auto;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
@apply p-2;
|
||||
}
|
||||
|
||||
/*
|
||||
* The following styles are auto-applied to elements with
|
||||
* transition="modal" when their visibility is toggled
|
||||
* by Vue.js.
|
||||
*
|
||||
* You can easily play with the modal transition by editing
|
||||
* these styles.
|
||||
*/
|
||||
|
||||
.modal-enter,
|
||||
.modal-leave-active {
|
||||
@apply opacity-0;
|
||||
}
|
||||
|
||||
.modal-enter .modal-container,
|
||||
.modal-leave-active .modal-container {
|
||||
@apply transform;
|
||||
@apply scale-90;
|
||||
@apply transition;
|
||||
@apply ease-in-out;
|
||||
@apply duration-150;
|
||||
}
|
||||
|
||||
.fade {
|
||||
@apply absolute;
|
||||
@apply block;
|
||||
@apply transition;
|
||||
@apply ease-in-out;
|
||||
@apply duration-150;
|
||||
|
||||
left: 16px;
|
||||
right: 20px;
|
||||
height: 32px;
|
||||
|
||||
&.top {
|
||||
top: 68px;
|
||||
background: linear-gradient(to bottom, var(--bg-color), transparent);
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
bottom: 16px;
|
||||
background: linear-gradient(to top, var(--bg-color), transparent);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
hasFooterSlot() {
|
||||
return !!this.$slots.footer
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
35
components/smart/Tab.vue
Normal file
35
components/smart/Tab.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div v-show="isActive">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: { type: String, default: "" },
|
||||
icon: { type: String, default: "" },
|
||||
id: { required: true },
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isActive: false,
|
||||
}
|
||||
},
|
||||
|
||||
// computed: {
|
||||
// href() {
|
||||
// return `#${this.label.toLowerCase().replace(/ /g, "-")}`
|
||||
// },
|
||||
// },
|
||||
|
||||
mounted() {
|
||||
this.isActive = this.selected
|
||||
},
|
||||
}
|
||||
</script>
|
||||
122
components/smart/Tabs.vue
Normal file
122
components/smart/Tabs.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="tabs-wrapper">
|
||||
<div class="tabs" :class="styles">
|
||||
<ul>
|
||||
<li
|
||||
v-for="(tab, index) in tabs"
|
||||
:class="{ 'is-active': tab.isActive }"
|
||||
:key="index"
|
||||
:tabindex="0"
|
||||
@keyup.enter="selectTab(tab)"
|
||||
>
|
||||
<a :href="tab.href" @click="selectTab(tab)">
|
||||
<i v-if="tab.icon" class="material-icons">
|
||||
{{ tab.icon }}
|
||||
</i>
|
||||
<span v-if="tab.label">{{ tab.label }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tabs-details">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tabs-wrapper {
|
||||
@apply flex;
|
||||
@apply flex-col;
|
||||
@apply flex-no-wrap;
|
||||
@apply flex-1;
|
||||
|
||||
.tabs {
|
||||
@apply scrolling-touch;
|
||||
@apply flex;
|
||||
@apply whitespace-no-wrap;
|
||||
@apply overflow-auto;
|
||||
@apply mt-4;
|
||||
|
||||
ul {
|
||||
@apply flex;
|
||||
@apply w-0;
|
||||
}
|
||||
|
||||
li {
|
||||
@apply inline-flex;
|
||||
@apply outline-none;
|
||||
@apply border-none;
|
||||
|
||||
a {
|
||||
@apply flex;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply py-2;
|
||||
@apply px-4;
|
||||
@apply text-fgLightColor;
|
||||
@apply text-sm;
|
||||
@apply rounded-lg;
|
||||
@apply cursor-pointer;
|
||||
@apply transition-colors;
|
||||
@apply ease-in-out;
|
||||
@apply duration-150;
|
||||
|
||||
.material-icons {
|
||||
@apply m-4;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply text-fgColor;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus a {
|
||||
@apply text-fgColor;
|
||||
}
|
||||
|
||||
&.is-active a {
|
||||
@apply bg-brdColor;
|
||||
@apply text-fgColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
ul,
|
||||
ol {
|
||||
@apply flex-row;
|
||||
@apply flex-no-wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
styles: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
tabs: [],
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.tabs = this.$children
|
||||
},
|
||||
|
||||
methods: {
|
||||
selectTab({ id }) {
|
||||
this.tabs.forEach((tab) => {
|
||||
tab.isActive = tab.id == id
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
83
components/smart/Toggle.vue
Normal file
83
components/smart/Toggle.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div @click="toggle()" class="inline-block cursor-pointer">
|
||||
<label class="toggle" :class="{ on: on }" ref="toggle">
|
||||
<span class="handle"></span>
|
||||
</label>
|
||||
<label class="pl-0 align-middle cursor-pointer">
|
||||
<slot />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$useBorder: false;
|
||||
$borderColor: var(--fg-light-color);
|
||||
$activeColor: var(--ac-color);
|
||||
$inactiveColor: var(--fg-light-color);
|
||||
$inactiveHandleColor: var(--bg-color);
|
||||
$activeHandleColor: var(--act-color);
|
||||
$width: 32px;
|
||||
$height: 16px;
|
||||
$handleSpacing: 4px;
|
||||
$transition: all 0.2s ease-in-out;
|
||||
|
||||
.toggle {
|
||||
@apply relative;
|
||||
@apply inline-block;
|
||||
@apply align-middle;
|
||||
@apply rounded-full;
|
||||
@apply p-0;
|
||||
@apply m-4;
|
||||
@apply cursor-pointer;
|
||||
@apply flex-shrink-0;
|
||||
|
||||
width: $width;
|
||||
height: $height;
|
||||
border: if($useBorder, 2px solid $borderColor, none);
|
||||
background-color: if($useBorder, transparent, $inactiveColor);
|
||||
transition: $transition;
|
||||
box-sizing: initial;
|
||||
|
||||
.handle {
|
||||
@apply absolute;
|
||||
@apply inline-block;
|
||||
@apply inset-0;
|
||||
@apply rounded-full;
|
||||
@apply pointer-events-none;
|
||||
|
||||
transition: $transition;
|
||||
margin: $handleSpacing;
|
||||
background-color: $inactiveHandleColor;
|
||||
width: #{$height - ($handleSpacing * 2)};
|
||||
height: #{$height - ($handleSpacing * 2)};
|
||||
}
|
||||
|
||||
&.on {
|
||||
background-color: $activeColor;
|
||||
border-color: $activeColor;
|
||||
|
||||
.handle {
|
||||
background-color: $activeHandleColor;
|
||||
left: #{$width - $height};
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
on: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggle() {
|
||||
const containsOnClass = this.$refs.toggle.classList.toggle("on")
|
||||
this.$emit("change", containsOnClass)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
163
components/smart/UrlField.vue
Normal file
163
components/smart/UrlField.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div contenteditable class="url-field" ref="editor" spellcheck="false"></div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.url-field {
|
||||
@apply border-dashed;
|
||||
@apply md:border-l;
|
||||
@apply border-brdColor;
|
||||
}
|
||||
|
||||
.highlight-VAR {
|
||||
@apply font-bold;
|
||||
@apply text-acColor;
|
||||
}
|
||||
|
||||
.highlight-TEXT {
|
||||
@apply overflow-auto;
|
||||
@apply break-all;
|
||||
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.highlight-TEXT::-webkit-scrollbar {
|
||||
@apply hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: { type: String, default: "" },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
cacheValue: null,
|
||||
unwatchValue: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.editor.addEventListener("input", this.updateEditor)
|
||||
this.$refs.editor.textContent = this.value || ""
|
||||
|
||||
this.cacheValue = this.value || ""
|
||||
|
||||
this.unwatchValue = this.$watch(
|
||||
() => this.value,
|
||||
(newVal) => {
|
||||
if (this.$refs.editor && this.cacheValue !== newVal)
|
||||
this.$refs.editor.textContent = newVal || ""
|
||||
this.updateEditor()
|
||||
}
|
||||
)
|
||||
|
||||
this.updateEditor()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.unwatchValue()
|
||||
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({ childNodes }) {
|
||||
const textSegments = []
|
||||
Array.from(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() {
|
||||
this.cacheValue = this.$refs.editor.textContent
|
||||
|
||||
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>
|
||||
299
components/smart/__tests__/AutoComplete.spec.js
Normal file
299
components/smart/__tests__/AutoComplete.spec.js
Normal file
@@ -0,0 +1,299 @@
|
||||
import autocomplete from "../AutoComplete"
|
||||
import { mount } from "@vue/test-utils"
|
||||
|
||||
const props = {
|
||||
placeholder: "",
|
||||
value: "",
|
||||
spellcheck: false,
|
||||
source: ["app", "apple", "appliance", "brian", "bob", "alice"],
|
||||
}
|
||||
|
||||
// ["pp", "pple", "ppliance", "lice"]
|
||||
const suggestionStr = props.source.filter((str) => str.startsWith("a")).map((str) => str.slice(1))
|
||||
|
||||
const factory = (props) =>
|
||||
mount(autocomplete, {
|
||||
propsData: props,
|
||||
})
|
||||
|
||||
describe("autocomplete", () => {
|
||||
test("mounts properly", () => {
|
||||
const wrapper = factory(props)
|
||||
|
||||
expect(wrapper).toBeTruthy()
|
||||
})
|
||||
|
||||
test("emits input event on text update [v-model compat]", async () => {
|
||||
const wrapper = factory(props)
|
||||
|
||||
const input = wrapper.find("input")
|
||||
|
||||
await input.setValue("testval")
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.emitted("input")).toBeTruthy()
|
||||
expect(wrapper.emitted("input").length).toEqual(1)
|
||||
})
|
||||
|
||||
test("shows matching suggestions", async () => {
|
||||
const wrapper = factory(props)
|
||||
|
||||
const input = wrapper.find("input")
|
||||
|
||||
await input.setValue("a")
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const suggestions = wrapper.findAll("li").wrappers.map((el) => el.text())
|
||||
|
||||
suggestionStr.forEach((str) => expect(suggestions).toContain(str))
|
||||
})
|
||||
|
||||
test("doesnt show non-matching suggestions", async () => {
|
||||
const wrapper = factory(props)
|
||||
|
||||
const input = wrapper.find("input")
|
||||
|
||||
await input.setValue("b")
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const suggestions = wrapper.findAll("li").wrappers.map((el) => el.text())
|
||||
|
||||
suggestionStr.forEach((str) => expect(suggestions).not.toContain(str))
|
||||
})
|
||||
|
||||
test("updates suggestions on input", async () => {
|
||||
const wrapper = factory(props)
|
||||
|
||||
const input = wrapper.find("input")
|
||||
|
||||
await input.setValue("b")
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const suggestions = wrapper.findAll("li").wrappers.map((el) => el.text())
|
||||
|
||||
suggestionStr.forEach((str) => expect(suggestions).not.toContain(str))
|
||||
})
|
||||
|
||||
test("applies suggestion on clicking", async () => {
|
||||
const wrapper = factory(props)
|
||||
|
||||
const input = wrapper.find("input")
|
||||
|
||||
await input.setValue("b")
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const selectedSuggestion = wrapper.findAll("li").at(0)
|
||||
const selectedText = selectedSuggestion.text()
|
||||
|
||||
await selectedSuggestion.trigger("click")
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(input.element.value).toEqual(`b${selectedText}`)
|
||||
})
|
||||
|
||||
test("hide selection on pressing ESC", async () => {
|
||||
const wrapper = factory(props)
|
||||
const input = wrapper.find("input")
|
||||
|
||||
await input.setValue("b")
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await input.trigger("keyup", { code: "Escape" })
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.find("ul").exists()).toEqual(false)
|
||||
})
|
||||
|
||||
test("pressing up when nothing is selected selects the first in the list", async () => {
|
||||
const wrapper = factory(props)
|
||||
|
||||
const input = wrapper.find("input")
|
||||
|
||||
await input.setValue("a")
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "ArrowUp",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.findAll("li").at(0).element.classList.contains("active")).toEqual(true)
|
||||
})
|
||||
|
||||
test("pressing down arrow when nothing is selected selects the first in the list", async () => {
|
||||
const wrapper = factory(props)
|
||||
|
||||
const input = wrapper.find("input")
|
||||
|
||||
await input.setValue("a")
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "ArrowDown",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.findAll("li").at(0).element.classList.contains("active")).toEqual(true)
|
||||
})
|
||||
|
||||
test("pressing down arrow moves down the selection list", async () => {
|
||||
const wrapper = factory(props)
|
||||
const input = wrapper.find("input")
|
||||
|
||||
await input.setValue("a")
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "ArrowDown",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "ArrowDown",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.findAll("li").at(1).element.classList.contains("active")).toEqual(true)
|
||||
})
|
||||
|
||||
test("pressing up arrow moves up the selection list", async () => {
|
||||
const wrapper = factory(props)
|
||||
const input = wrapper.find("input")
|
||||
|
||||
await input.setValue("a")
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "ArrowDown",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "ArrowDown",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "ArrowUp",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.findAll("li").at(0).element.classList.contains("active")).toEqual(true)
|
||||
})
|
||||
|
||||
test("pressing down arrow at the end of the list doesn't update the selection", async () => {
|
||||
const wrapper = factory(props)
|
||||
const input = wrapper.find("input")
|
||||
|
||||
await input.setValue("b")
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "ArrowDown",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "ArrowDown",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.findAll("li").at(1).element.classList.contains("active")).toEqual(true)
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "ArrowDown",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.findAll("li").at(1).element.classList.contains("active")).toEqual(true)
|
||||
})
|
||||
|
||||
test("pressing up arrow at the top of the list doesn't update the selection", async () => {
|
||||
const wrapper = factory(props)
|
||||
const input = wrapper.find("input")
|
||||
|
||||
await input.setValue("b")
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "ArrowDown",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "ArrowUp",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.findAll("li").at(0).element.classList.contains("active")).toEqual(true)
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "ArrowUp",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.findAll("li").at(0).element.classList.contains("active")).toEqual(true)
|
||||
})
|
||||
|
||||
test("pressing tab performs the current completion", async () => {
|
||||
const wrapper = factory(props)
|
||||
const input = wrapper.find("input")
|
||||
|
||||
await input.setValue("a")
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "ArrowDown",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const selectedSuggestion = wrapper.find("li.active").text()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "Tab",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(input.element.value).toEqual(`a${selectedSuggestion}`)
|
||||
})
|
||||
|
||||
test("pressing tab when nothing is selected selects the first suggestion", async () => {
|
||||
const wrapper = factory(props)
|
||||
const input = wrapper.find("input")
|
||||
|
||||
await input.setValue("a")
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const firstSuggestionText = wrapper.findAll("li").at(0).text()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "Tab",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(input.element.value).toEqual(`a${firstSuggestionText}`)
|
||||
})
|
||||
|
||||
test("pressing any non-special key doesn't do anything", async () => {
|
||||
const wrapper = factory(props)
|
||||
const input = wrapper.find("input")
|
||||
|
||||
await input.setValue("a")
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "ArrowDown",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const selectedSuggestion = wrapper.find("li.active").text()
|
||||
|
||||
await input.trigger("keydown", {
|
||||
code: "Tab",
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(input.element.value).toEqual(`a${selectedSuggestion}`)
|
||||
})
|
||||
})
|
||||
85
components/smart/__tests__/Tab.spec.js
Normal file
85
components/smart/__tests__/Tab.spec.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import tab from "../Tab"
|
||||
import { mount } from "@vue/test-utils"
|
||||
|
||||
const factory = (props, data) => {
|
||||
if (data) {
|
||||
return mount(tab, {
|
||||
propsData: props,
|
||||
data: () => data,
|
||||
slots: {
|
||||
default: '<div id="testdiv"></div>',
|
||||
},
|
||||
})
|
||||
} else {
|
||||
return mount(tab, {
|
||||
propsData: props,
|
||||
slots: {
|
||||
default: '<div id="testdiv"></div>',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
describe("tab", () => {
|
||||
test("mounts properly when needed props are passed in", () => {
|
||||
const wrapper = factory({
|
||||
label: "TestLabel",
|
||||
icon: "TestIcon",
|
||||
id: "testid",
|
||||
selected: true,
|
||||
})
|
||||
|
||||
expect(wrapper).toBeTruthy()
|
||||
})
|
||||
|
||||
test("mounts properly when selected prop is not passed", () => {
|
||||
const wrapper = factory({
|
||||
label: "TestLabel",
|
||||
icon: "TestIcon",
|
||||
id: "testid",
|
||||
})
|
||||
|
||||
expect(wrapper).toBeTruthy()
|
||||
})
|
||||
|
||||
test("if 'selected' prop is not passed, it is set to false by default", () => {
|
||||
const wrapper = factory({
|
||||
label: "TestLabel",
|
||||
icon: "TestIcon",
|
||||
id: "testid",
|
||||
})
|
||||
|
||||
expect(wrapper.props("selected")).toEqual(false)
|
||||
})
|
||||
|
||||
test("if set active, the slot is shown", () => {
|
||||
const wrapper = factory(
|
||||
{
|
||||
label: "TestLabel",
|
||||
icon: "TestIcon",
|
||||
id: "testid",
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
isActive: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(wrapper.find("#testdiv").element.parentElement).toBeVisible()
|
||||
})
|
||||
|
||||
test("if not set active, the slot is not rendered", () => {
|
||||
const wrapper = factory(
|
||||
{
|
||||
label: "TestLabel",
|
||||
icon: "TestIcon",
|
||||
id: "testid",
|
||||
},
|
||||
{
|
||||
isActive: false,
|
||||
}
|
||||
)
|
||||
|
||||
expect(wrapper.find("#testdiv").element.parentElement).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
68
components/smart/__tests__/Tabs.spec.js
Normal file
68
components/smart/__tests__/Tabs.spec.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import tabs from "../Tabs"
|
||||
import tab from "../Tab"
|
||||
|
||||
import { mount } from "@vue/test-utils"
|
||||
|
||||
const factory = () =>
|
||||
mount(tabs, {
|
||||
slots: {
|
||||
default: [
|
||||
`<Tab id="tab1" href="#" :label="'tab 1'" :icon="'testicon1'" :selected=true><div id="tab1render">tab1</div></Tab>`,
|
||||
`<Tab id="tab2" href="#" :label="'tab 2'" :icon="'testicon2'"><div id="tab2render">tab1</div></Tab>`,
|
||||
`<Tab id="tab3" href="#" :label="'tab 3'" :icon="'testicon3'"><div id="tab3render">tab1</div></Tab>`,
|
||||
],
|
||||
},
|
||||
stubs: {
|
||||
"Tab": tab,
|
||||
},
|
||||
})
|
||||
|
||||
describe("tabs", () => {
|
||||
test("mounts properly", async () => {
|
||||
const wrapper = factory()
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper).toBeTruthy()
|
||||
})
|
||||
|
||||
test("tab labels shown", async () => {
|
||||
const wrapper = factory()
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const labels = wrapper.findAll("li a span").wrappers.map((w) => w.text())
|
||||
expect(labels).toEqual(["tab 1", "tab 2", "tab 3"])
|
||||
})
|
||||
|
||||
test("tab icons are shown", async () => {
|
||||
const wrapper = factory()
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const labels = wrapper.findAll("li a i").wrappers.map((w) => w.text())
|
||||
expect(labels).toEqual(["testicon1", "testicon2", "testicon3"])
|
||||
})
|
||||
|
||||
test("clicking on tab labels switches the selected page", async () => {
|
||||
const wrapper = factory()
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.vm.selectTab({ id: "tab2" })
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.vm.$data.tabs[1].$data.isActive).toEqual(true)
|
||||
})
|
||||
|
||||
test("switched tab page is rendered and the other page is not rendered", async () => {
|
||||
const wrapper = factory()
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.vm.selectTab({ id: "tab2" })
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.vm.$data.tabs[0].$data.isActive).toEqual(false)
|
||||
expect(wrapper.vm.$data.tabs[1].$data.isActive).toEqual(true)
|
||||
expect(wrapper.vm.$data.tabs[2].$data.isActive).toEqual(false)
|
||||
})
|
||||
})
|
||||
52
components/smart/__tests__/Toggle.spec.js
Normal file
52
components/smart/__tests__/Toggle.spec.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import pwToggle from "../Toggle"
|
||||
import { mount } from "@vue/test-utils"
|
||||
|
||||
const factory = (props, slot) =>
|
||||
mount(pwToggle, {
|
||||
propsData: props,
|
||||
slots: {
|
||||
default: slot,
|
||||
},
|
||||
})
|
||||
|
||||
describe("pwToggle", () => {
|
||||
test("mounts properly", () => {
|
||||
const wrapper = factory({ on: true }, "test")
|
||||
|
||||
expect(wrapper).toBeTruthy()
|
||||
})
|
||||
|
||||
test("mounts even without the on prop", () => {
|
||||
const wrapper = factory({}, "test")
|
||||
|
||||
expect(wrapper).toBeTruthy()
|
||||
})
|
||||
|
||||
test("state is set correctly through the prop", () => {
|
||||
const wrapper1 = factory({ on: true }, "test")
|
||||
expect(wrapper1.vm.$refs.toggle.classList.contains("on")).toEqual(true)
|
||||
|
||||
const wrapper2 = factory({ on: false }, "test")
|
||||
expect(wrapper2.vm.$refs.toggle.classList.contains("on")).toEqual(false)
|
||||
})
|
||||
|
||||
test("caption label is rendered", () => {
|
||||
const wrapper = factory({ on: true }, "<span id='testcaption'></span>")
|
||||
|
||||
expect(wrapper.find("#testcaption").exists()).toEqual(true)
|
||||
})
|
||||
|
||||
test("clicking the button toggles the state", async () => {
|
||||
const wrapper = factory({ on: true }, "test")
|
||||
|
||||
wrapper.vm.toggle()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.vm.$refs.toggle.classList.contains("on")).toEqual(false)
|
||||
|
||||
wrapper.vm.toggle()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.vm.$refs.toggle.classList.contains("on")).toEqual(true)
|
||||
})
|
||||
})
|
||||
35
components/smart/__tests__/UrlField.spec.js
Normal file
35
components/smart/__tests__/UrlField.spec.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import urlField from "../UrlField"
|
||||
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>>")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user