[File UI] Added support for custom line positioning in file editor.
- Adds the ability to select lines from the editor, which in turn will highlight them. - Adds the ability to load a file and scroll at a specific line if the URL has with the `line` argument. - Adds the ability to maximize the file editor modal.
This commit is contained in:
parent
cc621cdca6
commit
4e5c740908
2 changed files with 160 additions and 26 deletions
|
@ -9,10 +9,15 @@
|
||||||
|
|
||||||
<div class="editor-body">
|
<div class="editor-body">
|
||||||
<div class="line-numbers" ref="lineNumbers">
|
<div class="line-numbers" ref="lineNumbers">
|
||||||
<span class="line-number" v-for="n in lines" :key="n" v-text="n" />
|
<span class="line-number"
|
||||||
|
:class="{selected: selectedLine === n}"
|
||||||
|
v-for="n in lines"
|
||||||
|
:key="n"
|
||||||
|
@click="selectedLine = selectedLine === n ? null : n"
|
||||||
|
v-text="n" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pre ref="pre"><code ref="content" v-html="displayedContent" /></pre>
|
<pre ref="pre"><code ref="content" v-html="displayedContent" /><div class="selected-line" ref="selectedLine" v-if="selectedLine != null" /></pre>
|
||||||
<textarea ref="textarea" v-model="content" @scroll="syncScroll" @input.stop />
|
<textarea ref="textarea" v-model="content" @scroll="syncScroll" @input.stop />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -54,6 +59,11 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
line: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
withSave: {
|
withSave: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
@ -71,6 +81,7 @@ export default {
|
||||||
initialContentHash: 0,
|
initialContentHash: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
|
selectedLine: null,
|
||||||
type: null,
|
type: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -139,6 +150,12 @@ export default {
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.selectedLine) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.scrollToLine(this.selectedLine)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveFile() {
|
async saveFile() {
|
||||||
|
@ -190,6 +207,17 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
scrollToLine(line) {
|
||||||
|
const offset = (line - 1) * parseFloat(getComputedStyle(this.$refs.pre).lineHeight)
|
||||||
|
this.$refs.textarea.scrollTo({
|
||||||
|
top: offset,
|
||||||
|
left: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
})
|
||||||
|
|
||||||
|
return offset
|
||||||
|
},
|
||||||
|
|
||||||
highlightContent() {
|
highlightContent() {
|
||||||
this.highlighting = true
|
this.highlighting = true
|
||||||
|
|
||||||
|
@ -243,7 +271,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.setUrlArgs({file: null})
|
this.setUrlArgs({file: null, line: null})
|
||||||
this.removeBeforeUnload()
|
this.removeBeforeUnload()
|
||||||
this.removeKeyListener()
|
this.removeKeyListener()
|
||||||
},
|
},
|
||||||
|
@ -276,9 +304,36 @@ export default {
|
||||||
this.highlightedContent = this.content
|
this.highlightedContent = this.content
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
selectedLine(line) {
|
||||||
|
line = parseInt(line)
|
||||||
|
if (isNaN(line)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = this.$refs.textarea
|
||||||
|
const lines = this.content.split('\n')
|
||||||
|
const cursor = lines.slice(0, line - 1).join('\n').length + 1
|
||||||
|
|
||||||
|
textarea.setSelectionRange(cursor, cursor)
|
||||||
|
textarea.focus()
|
||||||
|
this.setUrlArgs({line})
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const offset = this.scrollToLine(line)
|
||||||
|
this.$refs.selectedLine.style.top = `${offset}px`
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
const args = this.getUrlArgs()
|
||||||
|
const line = parseInt(this.line || args.line || 0)
|
||||||
|
if (line) {
|
||||||
|
if (!isNaN(line)) {
|
||||||
|
this.selectedLine = line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.loadFile()
|
this.loadFile()
|
||||||
this.addBeforeUnload()
|
this.addBeforeUnload()
|
||||||
this.addKeyListener()
|
this.addKeyListener()
|
||||||
|
@ -332,8 +387,10 @@ $line-numbers-width: 2.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-body {
|
.editor-body {
|
||||||
|
font-family: $monospace-font;
|
||||||
|
|
||||||
pre, textarea, code, .line-numbers {
|
pre, textarea, code, .line-numbers {
|
||||||
font-family: 'Fira Code', 'Noto Sans Mono', 'Inconsolata', 'Courier New', monospace;
|
font-family: $monospace-font;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -341,6 +398,10 @@ $line-numbers-width: 2.5em;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre, textarea, code {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.line-numbers {
|
.line-numbers {
|
||||||
width: $line-numbers-width;
|
width: $line-numbers-width;
|
||||||
background: $tab-bg;
|
background: $tab-bg;
|
||||||
|
@ -356,6 +417,15 @@ $line-numbers-width: 2.5em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding-right: 0.25em;
|
padding-right: 0.25em;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $hover-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: $selected-bg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -372,16 +442,32 @@ $line-numbers-width: 2.5em;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
overflow-wrap: normal;
|
overflow-wrap: normal;
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
color: rgba(0, 0, 0, 0);
|
color: rgba(0, 0, 0, 0);
|
||||||
caret-color: black;
|
caret-color: black;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-line {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 1.5em;
|
||||||
|
background: rgba(110, 255, 160, 0.25);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.floating-btn) {
|
:deep(.floating-btn) {
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix for some hljs styles that render white text on white background
|
||||||
|
:deep(code) {
|
||||||
|
.hljs-subst {
|
||||||
|
color: $selected-fg !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<FileEditor ref="fileEditor"
|
<FileEditor ref="fileEditor"
|
||||||
:file="file"
|
:file="file"
|
||||||
:is-new="isNew"
|
:is-new="isNew"
|
||||||
|
:line="line"
|
||||||
@save="$emit('save', $event)"
|
@save="$emit('save', $event)"
|
||||||
v-if="file" />
|
v-if="file" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,10 +27,11 @@
|
||||||
import ConfirmDialog from "@/components/elements/ConfirmDialog";
|
import ConfirmDialog from "@/components/elements/ConfirmDialog";
|
||||||
import FileEditor from "./Editor";
|
import FileEditor from "./Editor";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
|
import Utils from '@/Utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['close', 'open', 'save'],
|
emits: ['close', 'open', 'save'],
|
||||||
mixins: [Modal],
|
mixins: [Modal, Utils],
|
||||||
components: {
|
components: {
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
FileEditor,
|
FileEditor,
|
||||||
|
@ -47,6 +49,11 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
line: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
withSave: {
|
withSave: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
@ -113,15 +120,28 @@ export default {
|
||||||
|
|
||||||
onClose() {
|
onClose() {
|
||||||
this.$refs.fileEditor.reset()
|
this.$refs.fileEditor.reset()
|
||||||
|
this.setUrlArgs({ maximized: null })
|
||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
maximized() {
|
||||||
|
this.setUrlArgs({ maximized: this.maximized })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.maximized = !!this.getUrlArgs().maximized
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "src/style/items";
|
@import "src/style/items";
|
||||||
|
|
||||||
|
$maximized-modal-header-height: 2.75em;
|
||||||
|
|
||||||
.file-editor-root {
|
.file-editor-root {
|
||||||
.file-editor-modal {
|
.file-editor-modal {
|
||||||
:deep(.modal) {
|
:deep(.modal) {
|
||||||
|
@ -133,45 +153,73 @@ export default {
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50em;
|
|
||||||
min-width: 30em;
|
min-width: 30em;
|
||||||
max-height: calc(100vh - 2em);
|
max-height: calc(100vh - 2em);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
@extend .expand;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.maximized) {
|
&:not(.maximized) {
|
||||||
:deep(.body) {
|
:deep(.modal) {
|
||||||
@include until($tablet) {
|
.body {
|
||||||
width: calc(100vw - 2em);
|
@include until($tablet) {
|
||||||
}
|
width: calc(100vw - 2em);
|
||||||
|
}
|
||||||
|
|
||||||
@include from($tablet) {
|
@include from($tablet) {
|
||||||
width: 40em;
|
width: 40em;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include from($desktop) {
|
@include from($desktop) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 50em;
|
min-width: 50em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
height: 50em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.maximized {
|
&.maximized {
|
||||||
:deep(.body) {
|
:deep(.modal) {
|
||||||
width: 100vw;
|
width: calc(100vw - 2em);
|
||||||
height: 100vh;
|
height: calc(100vh - 2em);
|
||||||
|
|
||||||
|
.content, .modal-body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
height: $maximized-modal-header-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
height: calc(100% - #{$maximized-modal-header-height});
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm-dialog-container {
|
.confirm-dialog-container {
|
||||||
:deep(.modal) {
|
:deep(.modal) {
|
||||||
.content {
|
width: 35em !important;
|
||||||
.body {
|
height: 9em !important;
|
||||||
min-width: 30em;
|
max-width: 100%;
|
||||||
max-width: 100%;
|
max-height: 100%;
|
||||||
}
|
|
||||||
|
.body {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue