🚚 Moving or renaming files
This commit is contained in:
147
components/ui/ace-editor.vue
Normal file
147
components/ui/ace-editor.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div class="show-if-initialized" :class="{ initialized }">
|
||||
<pre ref="editor"></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.show-if-initialized {
|
||||
opacity: 0;
|
||||
|
||||
&.initialized {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
& > * {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const DEFAULT_THEME = "twilight"
|
||||
|
||||
import ace from "ace-builds"
|
||||
import "ace-builds/webpack-resolver"
|
||||
import jsonParse from "../../functions/jsonParse"
|
||||
import debounce from "../../functions/utils/debounce"
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
lang: {
|
||||
type: String,
|
||||
default: "json",
|
||||
},
|
||||
lint: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
required: false,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
|
||||
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
|
||||
})
|
||||
})
|
||||
},
|
||||
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
|
||||
|
||||
editor.on("change", () => {
|
||||
const content = editor.getValue()
|
||||
this.$emit("input", content)
|
||||
this.cacheValue = content
|
||||
if (this.lint) this.provideLinting(content)
|
||||
})
|
||||
|
||||
// Disable linting, if lint prop is false
|
||||
if (this.lint) this.provideLinting(this.value)
|
||||
},
|
||||
|
||||
methods: {
|
||||
defineTheme() {
|
||||
if (this.theme) {
|
||||
return this.theme
|
||||
}
|
||||
return this.$store.state.postwoman.settings.THEME_ACE_EDITOR || DEFAULT_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),
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
217
components/ui/autocomplete.vue
Normal file
217
components/ui/autocomplete.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<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="spellcheck"
|
||||
:autocorrect="spellcheck"
|
||||
/>
|
||||
<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 {
|
||||
position: relative;
|
||||
|
||||
input:focus + ul.suggestions,
|
||||
ul.suggestions:hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ul.suggestions {
|
||||
display: none;
|
||||
background-color: var(--atc-color);
|
||||
position: absolute;
|
||||
top: calc(100% - 4px);
|
||||
margin: 0 4px;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
border-radius: 0 0 8px 8px;
|
||||
z-index: 9999;
|
||||
transition: transform 0.2s ease-out;
|
||||
box-shadow: 0 5px 30px rgba(black, 0.1);
|
||||
|
||||
li {
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: 8px 16px;
|
||||
font-size: 16px;
|
||||
font-family: "Roboto Mono", monospace;
|
||||
font-weight: 400;
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: var(--ac-color);
|
||||
color: var(--act-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const KEY_TAB = 9
|
||||
const KEY_ESC = 27
|
||||
|
||||
const KEY_ARROW_UP = 38
|
||||
const KEY_ARROW_DOWN = 40
|
||||
|
||||
export default {
|
||||
props: {
|
||||
spellcheck: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
required: false,
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
|
||||
source: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
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.which && event.which === KEY_ESC) {
|
||||
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.which) {
|
||||
case KEY_ARROW_UP:
|
||||
event.preventDefault()
|
||||
this.currentSuggestionIndex =
|
||||
this.currentSuggestionIndex - 1 >= 0 ? this.currentSuggestionIndex - 1 : 0
|
||||
break
|
||||
|
||||
case KEY_ARROW_DOWN:
|
||||
event.preventDefault()
|
||||
this.currentSuggestionIndex =
|
||||
this.currentSuggestionIndex < this.suggestions.length - 1
|
||||
? this.currentSuggestionIndex + 1
|
||||
: this.suggestions.length - 1
|
||||
break
|
||||
|
||||
case KEY_TAB:
|
||||
event.preventDefault()
|
||||
let activeSuggestion = this.suggestions[
|
||||
this.currentSuggestionIndex >= 0 ? this.currentSuggestionIndex : 0
|
||||
]
|
||||
if (activeSuggestion) {
|
||||
let input = this.text.substring(0, this.selectionStart)
|
||||
this.text = input + activeSuggestion
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
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 => {
|
||||
return (
|
||||
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>
|
||||
79
components/ui/modal.vue
Normal file
79
components/ui/modal.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<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>
|
||||
<div class="modal-footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
z-index: 998;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.32);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
margin: 16px;
|
||||
padding: 16px;
|
||||
transition: all 0.2s ease;
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0px 16px 70px rgba(0, 0, 0, 0.5);
|
||||
max-height: calc(100vh - 128px);
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter .modal-container,
|
||||
.modal-leave-active .modal-container {
|
||||
transform: scale(0.8);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
98
components/ui/toggle.vue
Normal file
98
components/ui/toggle.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div @click="toggle()">
|
||||
<label class="toggle" :class="{ on: on }" ref="toggle">
|
||||
<span class="handle"></span>
|
||||
</label>
|
||||
<label class="caption">
|
||||
<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;
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label.caption {
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label.toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: $width;
|
||||
height: $height;
|
||||
border: if($useBorder, 2px solid $borderColor, none);
|
||||
background-color: if($useBorder, transparent, $inactiveColor);
|
||||
vertical-align: middle;
|
||||
|
||||
border-radius: 32px;
|
||||
transition: $transition;
|
||||
box-sizing: initial;
|
||||
padding: 0;
|
||||
margin: 8px 4px;
|
||||
cursor: pointer;
|
||||
|
||||
.handle {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: $handleSpacing;
|
||||
background-color: $inactiveHandleColor;
|
||||
|
||||
width: #{$height - ($handleSpacing * 2)};
|
||||
height: #{$height - ($handleSpacing * 2)};
|
||||
border-radius: 100px;
|
||||
|
||||
pointer-events: none;
|
||||
transition: $transition;
|
||||
}
|
||||
|
||||
&.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>
|
||||
Reference in New Issue
Block a user