Skip to content

Commit 5b411f0

Browse files
committed
feat(frontend:files): enhance text viewer with search panel toggle and keyboard shortcuts handling
1 parent 034c7a4 commit 5b411f0

File tree

5 files changed

+64
-8
lines changed

5 files changed

+64
-8
lines changed

backend/src/applications/files/utils/send-file.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class SendFile {
4444
async stream(req: FastifyRequest, res: FastifyReply): Promise<StreamableFile> {
4545
// SendStream manages HEAD (no including body in response) & GET request (with body)
4646
// Ranges, LastModified, Etag are also handled
47-
// Send function uses decodeURIComponent, but filePath is already decoded : we need to encode it again before passing it.
47+
// Send function uses decodeURIComponent, but filePath is already decoded: we need to encode it again before passing it.
4848
const encodedFilePath = encodeURIComponent(this.filePath)
4949
this.fileName = encodeURIComponent(this.downloadName ? this.downloadName : fileName(this.filePath))
5050
const sendResult: SendResult = await send(req.raw, encodedFilePath, this.sendOptions)

frontend/src/app/applications/files/components/viewers/files-viewer-text.component.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
<fa-icon [icon]="icons.faFloppyDisk"></fa-icon>
2727
</button>
2828
<button
29-
(click)="openSearchPanel(editor.view)"
29+
btnCheckbox
30+
[ngModel]="this.isSearchPanelOpen"
31+
(ngModelChange)="toggleSearch()"
3032
[tooltip]="'Search' | translate:locale.language"
3133
container="body"
32-
class="d-none d-sm-block btn btn-sm btn-secondary me-1"
34+
class="d-none d-sm-block btn btn-sm btn-custom me-1"
3335
type="button"
3436
[disabled]="!isReadable()">
3537
<fa-icon [icon]="icons.faMagnifyingGlass"></fa-icon>

frontend/src/app/applications/files/components/viewers/files-viewer-text.component.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66

77
import { CodeEditor } from '@acrodata/code-editor'
88
import { HttpClient, HttpErrorResponse } from '@angular/common/http'
9-
import { Component, inject, input, linkedSignal, OnDestroy, OnInit, signal, ViewEncapsulation } from '@angular/core'
9+
import { Component, HostListener, inject, input, linkedSignal, OnDestroy, OnInit, signal, ViewChild, ViewEncapsulation } from '@angular/core'
1010
import { FormsModule } from '@angular/forms'
1111
import { LanguageDescription } from '@codemirror/language'
1212
import { languages } from '@codemirror/language-data'
13-
import { openSearchPanel } from '@codemirror/search'
13+
import { closeSearchPanel, openSearchPanel } from '@codemirror/search'
1414
import { FaIconComponent } from '@fortawesome/angular-fontawesome'
1515
import { faFloppyDisk, faLock, faLockOpen, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'
1616
import { L10N_LOCALE, L10nLocale, L10nTranslatePipe } from 'angular-l10n'
17+
import { ButtonCheckboxDirective } from 'ngx-bootstrap/buttons'
1718
import { TooltipModule } from 'ngx-bootstrap/tooltip'
1819
import { themeDark } from '../../../../layout/layout.interfaces'
1920
import { LayoutService } from '../../../../layout/layout.service'
@@ -23,38 +24,72 @@ import { FilesUploadService } from '../../services/files-upload.service'
2324
@Component({
2425
selector: 'app-files-viewer-text',
2526
encapsulation: ViewEncapsulation.None,
26-
imports: [CodeEditor, TooltipModule, FormsModule, FaIconComponent, L10nTranslatePipe],
27+
imports: [CodeEditor, TooltipModule, FormsModule, FaIconComponent, L10nTranslatePipe, ButtonCheckboxDirective],
2728
styles: [
2829
`
2930
.code-editor {
3031
height: calc(100% - 40px);
31-
font-size: 0.75rem;
32+
font-size: 0.8rem;
3233
}
3334
`
3435
],
3536
templateUrl: 'files-viewer-text.component.html'
3637
})
3738
export class FilesViewerTextComponent implements OnInit, OnDestroy {
39+
@ViewChild('editor') editor: CodeEditor
3840
currentHeight = input<number>()
3941
file = input<FileModel>()
4042
mode = input<'view' | 'edit'>('view')
4143
isReadonly = linkedSignal(() => this.mode() === 'view')
4244
isReadable = signal(false)
4345
isModified = signal(false)
44-
protected readonly openSearchPanel = openSearchPanel
4546
protected content: string
4647
protected currentLanguage = undefined
4748
protected readonly languages: LanguageDescription[] = languages
4849
protected currentTheme: any = 'light'
4950
protected readonly icons = { faFloppyDisk, faLock, faLockOpen, faMagnifyingGlass }
5051
protected readonly locale = inject<L10nLocale>(L10N_LOCALE)
52+
protected isSearchPanelOpen = false
5153
private isContentReady = false
5254
private readonly layout = inject(LayoutService)
5355
private readonly http = inject(HttpClient)
5456
private readonly filesUpload = inject(FilesUploadService)
5557
private subscription = this.layout.switchTheme.subscribe((layout: string) => (this.currentTheme = layout === themeDark ? 'dark' : 'light'))
5658
private readonly maxSize = 5242880 // 5MB
5759

60+
@HostListener('document:keydown', ['$event'])
61+
onKeyDown(event: KeyboardEvent) {
62+
// ESC
63+
if (event.key === 'Escape' || event.key === 'Esc') {
64+
event.stopPropagation()
65+
event.preventDefault()
66+
if (this.isSearchPanelOpen) {
67+
this.toggleSearch()
68+
} else if (!this.isModified()) {
69+
if (this.isModified()) {
70+
// show dialog alert
71+
} else {
72+
this.layout.closeDialog()
73+
}
74+
}
75+
return
76+
}
77+
// Ctrl/Cmd+S | Ctrl/Cmd+F
78+
if (event.ctrlKey || event.metaKey) {
79+
switch (event.key.toLowerCase()) {
80+
case 's':
81+
event.preventDefault()
82+
this.save()
83+
return
84+
case 'f':
85+
event.preventDefault()
86+
event.stopPropagation()
87+
this.toggleSearch()
88+
return
89+
}
90+
}
91+
}
92+
5893
ngOnInit() {
5994
const language: LanguageDescription = LanguageDescription.matchFilename(languages, this.file().name)
6095
if (language?.name || this.file().size <= this.maxSize) {
@@ -78,6 +113,15 @@ export class FilesViewerTextComponent implements OnInit, OnDestroy {
78113
})
79114
}
80115

116+
toggleSearch() {
117+
this.isSearchPanelOpen = !this.isSearchPanelOpen
118+
if (this.isSearchPanelOpen) {
119+
openSearchPanel(this.editor.view)
120+
} else {
121+
closeSearchPanel(this.editor.view)
122+
}
123+
}
124+
81125
contentChange() {
82126
// Ignore first call
83127
if (this.isContentReady) {

frontend/src/styles/components/_fixes.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ hr {
3535
.cm-panel.cm-search {
3636
display: flex;
3737
align-items: center;
38+
text-wrap: nowrap;
3839

3940
label {
4041
display: flex;

frontend/src/styles/components/_theme_dark.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,4 +612,13 @@
612612
.avatar-img {
613613
background-color: darken($gray-light, 5%);
614614
}
615+
616+
// Code Mirror
617+
.cm-panel.cm-search {
618+
button[aria-label="close"] {
619+
right: 10px !important;
620+
font-size: 18px !important;
621+
color: $gray-light !important;
622+
}
623+
}
615624
}

0 commit comments

Comments
 (0)