CSS-only tree view. (#753)
* Make tree-view work with CSS only Changed the file list tree-view to use recursive templating instead of an external function, and improved it so that it works with only CSS. Striped lines won't work though. * Remove inline-block from folder label It breaks with the text-overflow: ellipsis. * Rename makeFolderData to makeTreeViewData
Cette révision appartient à :
Parent
f22d11b35d
révision
8fbdeed9f5
7 fichiers modifiés avec 89 ajouts et 194 suppressions
|
@ -549,16 +549,31 @@ td.tr-le { color: #E84C4C; }
|
|||
width: 24px;
|
||||
}
|
||||
/* Filelist */
|
||||
#filelist-control {
|
||||
.filelist-control {
|
||||
cursor: pointer;
|
||||
}
|
||||
#filelist-control::before {
|
||||
.filelist-control::before {
|
||||
content: "\25B6 ";
|
||||
}
|
||||
#filelist-control[data-filelist-open="true"]::before {
|
||||
input#show-filelist:checked ~ .filelist-control::before {
|
||||
content: "\25BC ";
|
||||
}
|
||||
|
||||
input#show-filelist {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#filelist {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input#show-filelist:checked ~ #filelist {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#filelist tr {
|
||||
background: none; /* Striped lines will look really ugly due to how it's drawn */
|
||||
}
|
||||
.tr-filelist {
|
||||
--nest-level: 0;
|
||||
}
|
||||
|
@ -580,10 +595,18 @@ td.tr-le { color: #E84C4C; }
|
|||
}
|
||||
/* Filesize column */
|
||||
.tr-filelist td:nth-child(2) {
|
||||
width: 30%;
|
||||
text-align: center;
|
||||
}
|
||||
/* Input that show/hides each folder */
|
||||
input.filelist-checkbox {
|
||||
display: none;
|
||||
}
|
||||
input.filelist-checkbox:checked + table.table-filelist {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tr-folder {
|
||||
.tr-folder label {
|
||||
cursor: pointer;
|
||||
}
|
||||
/* The folder or file icon */
|
||||
|
@ -594,6 +617,7 @@ td.tr-le { color: #E84C4C; }
|
|||
background-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.tr-file td:first-child::before {
|
||||
content: " ";
|
||||
|
@ -602,6 +626,7 @@ td.tr-le { color: #E84C4C; }
|
|||
background-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#torrent-name {
|
||||
|
|
|
@ -60,5 +60,3 @@ td.tr-le { color: #cc6666; }
|
|||
margin-bottom: -.5em;
|
||||
}
|
||||
|
||||
.tr-folder td:first-child::before { filter: invert(100%); -webkit-filter: invert(100%); }
|
||||
.tr-file td:first-child::before { filter: invert(100%); -webkit-filter: invert(100%); }
|
||||
|
|
|
@ -20,14 +20,6 @@ type captchaData struct {
|
|||
T languages.TemplateTfunc
|
||||
}
|
||||
|
||||
// Will be reused later.
|
||||
func fileSizeFunc(filesize int64, T languages.TemplateTfunc) template.HTML {
|
||||
if filesize == 0 {
|
||||
return T("unknown")
|
||||
}
|
||||
return template.HTML(util.FormatFilesize(filesize))
|
||||
}
|
||||
|
||||
// FuncMap : Functions accessible in templates by {{ $.Function }}
|
||||
var FuncMap = template.FuncMap{
|
||||
"inc": func(i int) int {
|
||||
|
@ -189,21 +181,24 @@ var FuncMap = template.FuncMap{
|
|||
}
|
||||
return ""
|
||||
},
|
||||
"fileSize": fileSizeFunc,
|
||||
"fileSize": func(filesize int64, T languages.TemplateTfunc) template.HTML {
|
||||
if filesize == 0 {
|
||||
return T("unknown")
|
||||
}
|
||||
return template.HTML(util.FormatFilesize(filesize))
|
||||
},
|
||||
"makeCaptchaData": func(captchaID string, T languages.TemplateTfunc) captchaData {
|
||||
return captchaData{captchaID, T}
|
||||
},
|
||||
"DefaultUserSettings": func(s string) bool {
|
||||
return config.DefaultUserSettings[s]
|
||||
},
|
||||
"MakeFolderTreeView": func(f *filelist.FileListFolder, folderFmt string, fileFmt string, data interface{}) template.HTML {
|
||||
out, err := f.MakeFolderTreeView(folderFmt, fileFmt, map[string]interface{}{
|
||||
// Add the functions needed for the tree view here.
|
||||
"fileSize": fileSizeFunc,
|
||||
}, data)
|
||||
if err != nil {
|
||||
return template.HTML("Error while making tree view")
|
||||
}
|
||||
return out
|
||||
"makeTreeViewData": func(f *filelist.FileListFolder, nestLevel int, T languages.TemplateTfunc, identifierChain string) interface{} {
|
||||
return struct{
|
||||
Folder *filelist.FileListFolder
|
||||
NestLevel int
|
||||
T languages.TemplateTfunc
|
||||
IdentifierChain string
|
||||
}{ f, nestLevel, T, identifierChain }
|
||||
},
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ func ViewHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
b := torrent.ToJSON()
|
||||
folder := filelist.FileListToFolder(torrent.FileList)
|
||||
folder := filelist.FileListToFolder(torrent.FileList, "root")
|
||||
captchaID := ""
|
||||
if userPermission.NeedsCaptcha(user) {
|
||||
captchaID = captcha.GetID()
|
||||
|
|
|
@ -1,5 +1,30 @@
|
|||
{{define "title"}}{{.Torrent.Name}}{{end}}
|
||||
{{define "contclass"}}cont-view {{if eq .Torrent.Status 2}}remake{{end}} {{if eq .Torrent.Status 3}}trusted{{end}} {{if eq .Torrent.Status 4}}aplus{{end}}{{end}}
|
||||
{{ define "make_treeview" }}
|
||||
{{ range $index, $folder := .Folder.Folders }}
|
||||
{{ $folderId := (print $.IdentifierChain "_" $index) }}
|
||||
<tr class="tr-filelist tr-folder" style="--nest-level: {{ $.NestLevel }}">
|
||||
<td><label for="contents_{{$folderId}}">{{$folder.FolderName}}</label></td>
|
||||
<td>{{ fileSize $folder.TotalSize $.T }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<input id="contents_{{$folderId}}" type="checkbox" class="filelist-checkbox">
|
||||
<table class="table-filelist">
|
||||
{{ template "make_treeview" (makeTreeViewData $folder (inc $.NestLevel) $.T $folderId) }}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
|
||||
{{ range .Folder.Files }}
|
||||
<tr class="tr-filelist tr-file" style="--nest-level: {{ $.NestLevel }}">
|
||||
<td>{{.Filename}}</td>
|
||||
<td>{{fileSize .Filesize $.T}}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{define "content"}}
|
||||
<div style="text-align: left;" class="box">
|
||||
{{with .Torrent}}
|
||||
|
@ -67,31 +92,10 @@
|
|||
{{else}}
|
||||
<p>No description provided!</p>
|
||||
{{end}}
|
||||
<p class="torrent-hr" id="filelist-control" onclick="javascript:toggleFilelist()" data-filelist-open="true">{{call $.T "files"}}</p>
|
||||
<input type="checkbox" id="show-filelist">
|
||||
<label class="torrent-hr filelist-control" for="show-filelist">{{call $.T "files"}}</label>
|
||||
{{ if gt (len .FileList) 0 }}
|
||||
{{/* how do i concat lol */}}
|
||||
{{ $folderFormat := `<tr id="{{.Identifier}}" class='childs-of-{{.ParentIdentifier}} tr-filelist tr-folder' onclick="javascript:toggleFolder(this)" data-filelist-open="true" style="--nest-level: {{.NestLevel}}"><td>{{.FolderName}}</td><td>{{ fileSize .TotalSize .Data.T }}</td></tr>` }}
|
||||
{{ $fileFormat := `<tr class='childs-of-{{.ParentIdentifier}} tr-filelist tr-file' style="--nest-level: {{.NestLevel}}"><td>{{.Filename}}</td><td>{{ fileSize .Filesize .Data.T }}</td>` }}
|
||||
<script>
|
||||
function toggleFilelist() {
|
||||
var control = document.getElementById("filelist-control")
|
||||
var filelist = document.getElementById("filelist")
|
||||
|
||||
filelist.hidden = !filelist.hidden
|
||||
control.setAttribute("data-filelist-open", filelist.hidden ? "false" : "true")
|
||||
}
|
||||
|
||||
function toggleFolder(folderNode) {
|
||||
var isOpen = folderNode.getAttribute("data-filelist-open") == "true" ? true : false
|
||||
var rows = document.querySelectorAll("*[class^='childs-of-" + folderNode.id + "']")
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
// If it's open (true), will hide, if not, will show.
|
||||
rows[i].hidden = isOpen
|
||||
}
|
||||
|
||||
folderNode.setAttribute("data-filelist-open", !isOpen ? "true" : "false")
|
||||
}
|
||||
</script>
|
||||
<div class="torrent-info-box" id="filelist">
|
||||
<table>
|
||||
<thead>
|
||||
|
@ -99,12 +103,10 @@
|
|||
<th>{{call $.T "size"}}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ MakeFolderTreeView $.RootFolder $folderFormat $fileFormat $ }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{/* Make filelist hidden by default with JS, but still visible without it */}}
|
||||
<script>toggleFilelist()</script>
|
||||
{{ template "make_treeview" (makeTreeViewData $.RootFolder 0 $.T "root") }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ else }}
|
||||
<p>No files found? That doesn't even make sense!</p>
|
||||
{{end}}
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
package filelist
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/NyaaPantsu/nyaa/model"
|
||||
"github.com/bradfitz/slice"
|
||||
"html/template"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type FileListFolder struct {
|
||||
Folders map[string]*FileListFolder
|
||||
Files []model.File
|
||||
Folders []*FileListFolder
|
||||
Files []model.File
|
||||
FolderName string
|
||||
}
|
||||
|
||||
func FileListToFolder(fileList []model.File) (out *FileListFolder) {
|
||||
func FileListToFolder(fileList []model.File, folderName string) (out *FileListFolder) {
|
||||
out = &FileListFolder{
|
||||
Folders: make(map[string]*FileListFolder),
|
||||
Files: make([]model.File, 0),
|
||||
Folders: make([]*FileListFolder, 0),
|
||||
Files: make([]model.File, 0),
|
||||
FolderName: folderName,
|
||||
}
|
||||
|
||||
pathsToFolders := make(map[string][]model.File)
|
||||
|
@ -40,9 +39,16 @@ func FileListToFolder(fileList []model.File) (out *FileListFolder) {
|
|||
}
|
||||
|
||||
for folderName, folderFiles := range pathsToFolders {
|
||||
out.Folders[folderName] = FileListToFolder(folderFiles)
|
||||
out.Folders = append(out.Folders, FileListToFolder(folderFiles, folderName))
|
||||
}
|
||||
|
||||
// Do some sorting
|
||||
slice.Sort(out.Folders, func(i, j int) bool {
|
||||
return strings.ToLower(out.Folders[i].FolderName) < strings.ToLower(out.Folders[j].FolderName)
|
||||
})
|
||||
slice.Sort(out.Files, func(i, j int) bool {
|
||||
return strings.ToLower(out.Files[i].Filename()) < strings.ToLower(out.Files[i].Filename())
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -58,86 +64,3 @@ func (f *FileListFolder) TotalSize() (out int64) {
|
|||
return
|
||||
}
|
||||
|
||||
type folderFormatData struct {
|
||||
Data interface{}
|
||||
FolderName string
|
||||
TotalSize int64
|
||||
NestLevel uint
|
||||
ParentIdentifier string
|
||||
Identifier string
|
||||
}
|
||||
|
||||
type fileFormatData struct {
|
||||
Data interface{}
|
||||
Filename string
|
||||
Filesize int64
|
||||
NestLevel uint
|
||||
ParentIdentifier string
|
||||
}
|
||||
|
||||
func execTemplateToHTML(tmpl *template.Template, data interface{}) (out template.HTML, err error) {
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.Execute(&buf, data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
out = template.HTML(buf.String())
|
||||
return
|
||||
}
|
||||
|
||||
func (f *FileListFolder) makeFolderTreeView(folderTmpl *template.Template, fileTmpl *template.Template, nestLevel uint, identifier string, data interface{}) (output template.HTML, err error) {
|
||||
output = template.HTML("")
|
||||
var tmp template.HTML
|
||||
var folderNames []string // need this for sorting
|
||||
for folderName, _ := range f.Folders {
|
||||
folderNames = append(folderNames, folderName)
|
||||
}
|
||||
|
||||
slice.Sort(folderNames, func(i, j int) bool {
|
||||
return strings.ToLower(folderNames[i]) < strings.ToLower(folderNames[j])
|
||||
})
|
||||
|
||||
for i, folderName := range folderNames {
|
||||
folder := f.Folders[folderName]
|
||||
childIdentifier := identifier + "_d" + strconv.Itoa(i)
|
||||
// To the folder, our identifier is their parent identifier, and our child identifier is their own identifier.
|
||||
tmp, err = execTemplateToHTML(folderTmpl, folderFormatData{data, folderName, folder.TotalSize(), nestLevel, identifier, childIdentifier})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output += tmp
|
||||
|
||||
tmp, err = folder.makeFolderTreeView(folderTmpl, fileTmpl, nestLevel+1, childIdentifier, data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output += tmp
|
||||
}
|
||||
|
||||
slice.Sort(f.Files, func(i, j int) bool {
|
||||
return strings.ToLower(f.Files[i].Filename()) < strings.ToLower(f.Files[j].Filename())
|
||||
})
|
||||
for _, file := range f.Files {
|
||||
tmp, err = execTemplateToHTML(fileTmpl, fileFormatData{data, file.Filename(), file.Filesize, nestLevel, identifier})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output += tmp
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (f *FileListFolder) MakeFolderTreeView(folderFormat string, fileFormat string, funcMap template.FuncMap, data interface{}) (output template.HTML, err error) {
|
||||
folderTmpl, err := template.New("folderTemplate").Funcs(funcMap).Parse(folderFormat)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fileTmpl, err := template.New("fileTemplate").Funcs(funcMap).Parse(fileFormat)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
output, err = f.makeFolderTreeView(folderTmpl, fileTmpl, 0, "root", data)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
package filelist
|
||||
|
||||
import (
|
||||
"github.com/NyaaPantsu/nyaa/model"
|
||||
"html/template"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func makeDummyFile(path ...string) (file model.File) {
|
||||
file.SetPath(path)
|
||||
return
|
||||
}
|
||||
|
||||
func dashes(n uint) (out string) {
|
||||
var i uint
|
||||
for i = 0; i < n; i++ {
|
||||
out += "-"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestFilelist(T *testing.T) {
|
||||
files := []model.File{
|
||||
makeDummyFile("A", "B", "C.txt"),
|
||||
makeDummyFile("A", "C", "C.txt"),
|
||||
makeDummyFile("B.txt"),
|
||||
}
|
||||
expected := "A\n" +
|
||||
"-B\n" +
|
||||
"--C.txt\n" +
|
||||
"-C\n" +
|
||||
"--C.txt\n" +
|
||||
"B.txt\n"
|
||||
|
||||
filelist := FileListToFolder(files)
|
||||
|
||||
out, err := filelist.MakeFolderTreeView("{{dashes .NestLevel}}{{.FolderName}}\n", "{{dashes .NestLevel}}{{.Filename}}\n", map[string]interface{}{
|
||||
"dashes": dashes,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
T.Fatalf("%v", err)
|
||||
return
|
||||
}
|
||||
if out != template.HTML(expected) {
|
||||
T.Fatalf("Error: expected %s, got %s", expected, out)
|
||||
return
|
||||
}
|
||||
}
|
Référencer dans un nouveau ticket