cubash-archive/frontend/src/components/InputEditorCore.vue

403 lines
9.4 KiB
Vue

<template>
<div
class='input_editor_core'
>
<div>
<emoji-selector
v-model='emojiSelectorVisible'
style='margin-left: 0.25rem;'
@emoji='addEmoji'
:right-align='rightAlignEmoji'
></emoji-selector>
<div class='panel-block'>
<div
class='input_editor_core__format_button input_editor_core__format_button--emoji'
style='margin-left: 0.25rem;'
title='Emoji'
@click='emojiSelectorVisible = true'
>
<font-awesome-icon :icon='["fa", "grin"]' />
</div>
<div
class='input_editor_core__format_button'
style='margin-left: 0.25rem;'
title='Bold (CTRL + B)'
@click='replaceSelectedText("__", "__")'
>
B
</div>
<div
class='input_editor_core__format_button'
style='margin-left: 0.25rem;'
title='Italic (CTRL + I)'
@click='replaceSelectedText("*", "*")'
>
I
</div>
<div
class='input_editor_core__format_button'
style='margin-left: 0.25rem;'
title='New line (CTRL + Q)'
@click='replaceSelectedText("", `\\
`)'
>
</div>
<div
class='input_editor_core__format_button'
style='margin-left: 0.25rem;'
title='Link (CTRL + L)'
@click='setModalState("link", true)'
>
<font-awesome-icon :icon='["fa", "link"]' />
</div>
<div
class='input_editor_core__format_button'
style='margin-left: 0.25rem;'
title='Code (CTRL + K)'
@click='formatCode'
>
<font-awesome-icon :icon='["fa", "code"]' />
</div>
</div>
<textarea
class='input_editor_core__input'
placeholder='Type here - you can format using Markdown'
ref='textarea'
:value='value'
@input='setEditor($event.target.value)'
@focus='$emit("focus")'
@blur='$emit("blur")'
@keydown.ctrl.66.prevent='replaceSelectedText("__", "__")'
@keydown.ctrl.73.prevent='replaceSelectedText("*", "*")'
@keydown.ctrl.76.prevent='setModalState("link", true)'
@keydown.ctrl.81.prevent='replaceSelectedText("", `\\
`)'
@keydown.ctrl.75.prevent='formatCode'
>
</textarea>
</div>
<modal-window v-model='linkModalVisible'>
<div slot="header">
<h2>
Attach link to post
</h2>
</div>
<div slot='main' class="card-content">
<fancy-input placeholder='Text for link' width='100%' v-model='linkText'></fancy-input>
<fancy-input placeholder='Web address for link' width='100%' v-model='linkURL'></fancy-input>
</div>
<div slot='footer'>
<button class='button button--green button--modal' @click='addLink'>
Add link
</button>
<button class='button button--modal' @click='setModalState("link", false)'>
Cancel
</button>
</div>
</modal-window>
</div>
</template>
<script>
import ModalWindow from './ModalWindow'
import FancyInput from './FancyInput'
import EmojiSelector from './EmojiSelector'
let usernames = {}
export default {
name: 'InputEditorCore',
props: ['value', 'error', 'right-align-emoji'],
components: {
ModalWindow,
FancyInput,
EmojiSelector
},
data () {
return {
linkText: '',
linkURL: '',
linkModalVisible: false,
imageModalVisible: false,
emojiSelectorVisible: false
}
},
methods: {
setModalState (modal, state) {
if(modal === 'link') {
this.linkModalVisible = state
if(state) {
this.linkText = this.getSelectionData().val;
} else {
this.linkText = '';
this.linkURL = '';
}
} else if(modal === 'image') {
this.imageModalVisible = state
}
},
checkUsernames (matches) {
let doneCount = 0
let mentions = []
let done = res => {
doneCount++
if(res) mentions.push(res)
if(doneCount === matches.length) {
this.$emit('mentions', mentions)
}
}
matches.forEach(match => {
this.checkUsername(match, done)
})
},
checkUsername (match, cb) {
let username = match.trim().slice(1)
let checkedUsername = usernames[username]
if(checkedUsername !== undefined) {
cb(checkedUsername)
} else if(checkedUsername === undefined) {
this.axios
.get('/api/v1/user/' + username)
.then(() => {
usernames[username] = username
cb(username)
})
.catch(() => {
usernames[username] = null
cb(null)
})
}
},
setEditor (value) {
let matches = value.match(/(^|\s)@[^\s]+/g) || []
this.checkUsernames(matches)
this.$emit('input', value)
},
getSelectionData () {
var el = this.$refs.textarea,
start = el.selectionStart,
end = el.selectionEnd;
return {
val: el.value.slice(start, end),
start,
end
};
},
replaceSelectedText (before, after) {
var selectionData = this.getSelectionData();
var el = this.$refs.textarea;
if(
this.value.substr(selectionData.start - before.length, before.length) === before &&
this.value.substr(selectionData.end, after.length) === after
) {
this.setEditor(
this.value.slice(0, selectionData.start - before.length) +
selectionData.val +
this.value.slice(selectionData.end + after.length)
);
setTimeout(function() {
el.selectionStart = selectionData.start - before.length;
el.selectionEnd = selectionData.end - after.length;
}, 0);
} else {
this.setEditor(
this.value.slice(0, selectionData.start) +
before + selectionData.val + after +
this.value.slice(selectionData.end)
);
setTimeout(function() {
el.selectionStart = selectionData.start + before.length;
el.selectionEnd = selectionData.end + after.length;
}, 0);
}
el.focus(selectionData.end);
},
addLink () {
var linkTextLength = this.linkText.length;
var selectionData = this.getSelectionData();
var el = this.$refs.textarea;
this.setEditor(
this.value.slice(0, selectionData.start) +
'[' + this.linkText + '](' + this.linkURL + ')' +
this.value.slice(selectionData.end)
);
el.focus();
setTimeout(function() {
el.selectionStart = selectionData.start + 1;
el.selectionEnd = selectionData.start + 1 + linkTextLength;
}, 0);
this.setModalState('link', false);
},
addEmoji (emoji) {
var selectionData = this.getSelectionData();
var el = this.$refs.textarea;
this.setEditor(
this.value.slice(0, selectionData.start) +
emoji +
this.value.slice(selectionData.end)
);
el.focus();
setTimeout(function() {
el.selectionStart = selectionData.start + emoji.length;
el.selectionEnd = selectionData.start + emoji.length;
}, 0);
},
formatCode (e) {
e.preventDefault()
var selectionData = this.getSelectionData();
if(this.value[selectionData.start-1] === '\n' || selectionData.start === 0) {
var el = this.$refs.textarea;
var matches = ( selectionData.val.match(/\n/g) || [] ).length
var replacedText = ' ' + selectionData.val.replace(/\n/g, '\n ')
this.setEditor(
this.value.slice(0, selectionData.start) +
replacedText +
this.value.slice(selectionData.end)
);
el.focus();
setTimeout(function() {
el.selectionStart = selectionData.start + 4;
el.selectionEnd = selectionData.end + (matches + 1)*4;
}, 0);
} else {
this.replaceSelectedText('`', '`');
}
}
}
}
</script>
<style lang='scss' scoped>
@import '../assets/scss/variables.scss';
.input_editor_core {
@at-root #{&}__format_bar {
width: auto;
position: absolute;
height: 2rem;
top: 0.25rem;
right: 0;
background-color: transparent;
display: flex;
align-items: center;
padding: 0 0.125rem;
margin-right: 2.4rem;
}
@at-root #{&}__format_button {
height: 1.5rem;
width: 1.5rem;
border-radius: 0.25rem;
text-align: center;
line-height: 1.4rem;
cursor: pointer;
@include user-select(none);
@include text($font--role-default, 1rem, 600);
color: $color__darkgray--primary;
transition: background-color 0.2s;
&:hover {
background-color: $color__gray--darker;
}
&:active {
background-color: $color__gray--darkest;
}
}
@at-root #{&}__spacer {
width: 0.6rem;
}
@at-root #{&}__input {
width: 100%;
height: 8rem;
border: 0;
padding: 0.5rem;
@include text;
outline: none;
resize: none;
@include placeholder {
@include text($font--role-emphasis, 1rem);
display: flex;
align-content: center;
@include user-select(none);
cursor: default;
}
}
@at-root #{&}__error {
position: absolute;
background-color: #ffeff1;
border: 0.125rem solid #D32F2F;
font-size: 0.9rem;
padding: 0.1rem 0.25rem;
top: 0.2125rem;
left: calc(100% + 0.25rem);
white-space: nowrap;
&:first-letter{ text-transform: capitalize; }
opacity: 0;
pointer-events: none;
margin-top: -1rem;
transition: opacity 0.2s, margin-top 0.2s;
@at-root #{&}--show {
opacity: 1;
pointer-events: all;
margin-top: 0;
transition: opacity 0.2s, margin-top 0.2s;
}
&::after {
content: '';
position: relative;
width: 0;
height: 0;
display: inline-block;
right: calc(100% + 0.3rem);
border-top: 0.3rem solid transparent;
border-bottom: 0.3rem solid transparent;
border-right: 0.3rem solid #D32F2F;
}
}
}
@media (max-width: 420px) {
.input_editor_core__format_button--emoji {
display: none;
}
}
</style>