[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:
Fabio Manganiello 2024-09-05 01:37:11 +02:00
parent cc621cdca6
commit 4e5c740908
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
2 changed files with 160 additions and 26 deletions

View file

@ -9,10 +9,15 @@
<div class="editor-body">
<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>
<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 />
</div>
@ -54,6 +59,11 @@ export default {
default: false,
},
line: {
type: [String, Number],
default: null,
},
withSave: {
type: Boolean,
default: true,
@ -71,6 +81,7 @@ export default {
initialContentHash: 0,
loading: false,
saving: false,
selectedLine: null,
type: null,
}
},
@ -139,6 +150,12 @@ export default {
} finally {
this.loading = false
}
if (this.selectedLine) {
setTimeout(() => {
this.scrollToLine(this.selectedLine)
}, 1000)
}
},
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() {
this.highlighting = true
@ -243,7 +271,7 @@ export default {
},
reset() {
this.setUrlArgs({file: null})
this.setUrlArgs({file: null, line: null})
this.removeBeforeUnload()
this.removeKeyListener()
},
@ -276,9 +304,36 @@ export default {
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() {
const args = this.getUrlArgs()
const line = parseInt(this.line || args.line || 0)
if (line) {
if (!isNaN(line)) {
this.selectedLine = line
}
}
this.loadFile()
this.addBeforeUnload()
this.addKeyListener()
@ -332,8 +387,10 @@ $line-numbers-width: 2.5em;
}
.editor-body {
font-family: $monospace-font;
pre, textarea, code, .line-numbers {
font-family: 'Fira Code', 'Noto Sans Mono', 'Inconsolata', 'Courier New', monospace;
font-family: $monospace-font;
position: absolute;
top: 0;
height: 100%;
@ -341,6 +398,10 @@ $line-numbers-width: 2.5em;
white-space: pre;
}
pre, textarea, code {
background: transparent;
}
.line-numbers {
width: $line-numbers-width;
background: $tab-bg;
@ -356,6 +417,15 @@ $line-numbers-width: 2.5em;
width: 100%;
text-align: right;
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;
overflow-wrap: normal;
overflow-x: scroll;
z-index: 1;
z-index: 2;
color: rgba(0, 0, 0, 0);
caret-color: black;
border: 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) {
z-index: 5;
}
// Fix for some hljs styles that render white text on white background
:deep(code) {
.hljs-subst {
color: $selected-fg !important;
}
}
}
</style>

View file

@ -8,6 +8,7 @@
<FileEditor ref="fileEditor"
:file="file"
:is-new="isNew"
:line="line"
@save="$emit('save', $event)"
v-if="file" />
</div>
@ -26,10 +27,11 @@
import ConfirmDialog from "@/components/elements/ConfirmDialog";
import FileEditor from "./Editor";
import Modal from "@/components/Modal";
import Utils from '@/Utils'
export default {
emits: ['close', 'open', 'save'],
mixins: [Modal],
mixins: [Modal, Utils],
components: {
ConfirmDialog,
FileEditor,
@ -47,6 +49,11 @@ export default {
default: false,
},
line: {
type: [String, Number],
default: null,
},
withSave: {
type: Boolean,
default: true,
@ -113,15 +120,28 @@ export default {
onClose() {
this.$refs.fileEditor.reset()
this.setUrlArgs({ maximized: null })
this.$emit('close')
},
},
watch: {
maximized() {
this.setUrlArgs({ maximized: this.maximized })
},
},
mounted() {
this.maximized = !!this.getUrlArgs().maximized
},
}
</script>
<style lang="scss" scoped>
@import "src/style/items";
$maximized-modal-header-height: 2.75em;
.file-editor-root {
.file-editor-modal {
:deep(.modal) {
@ -133,45 +153,73 @@ export default {
.modal-body {
width: 100%;
height: 50em;
min-width: 30em;
max-height: calc(100vh - 2em);
}
.content {
@extend .expand;
}
}
&:not(.maximized) {
:deep(.body) {
@include until($tablet) {
width: calc(100vw - 2em);
}
:deep(.modal) {
.body {
@include until($tablet) {
width: calc(100vw - 2em);
}
@include from($tablet) {
width: 40em;
max-width: 100%;
}
@include from($tablet) {
width: 40em;
max-width: 100%;
}
@include from($desktop) {
width: 100%;
min-width: 50em;
@include from($desktop) {
width: 100%;
min-width: 50em;
}
.modal-body {
height: 50em;
}
}
}
}
&.maximized {
:deep(.body) {
width: 100vw;
height: 100vh;
:deep(.modal) {
width: calc(100vw - 2em);
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 {
:deep(.modal) {
.content {
.body {
min-width: 30em;
max-width: 100%;
}
width: 35em !important;
height: 9em !important;
max-width: 100%;
max-height: 100%;
.body {
width: 100% !important;
height: 100% !important;
justify-content: center;
}
}
}