FTS5-Suche

This commit is contained in:
Simon Martens
2025-02-22 19:08:36 +01:00
parent 29576ec7a0
commit 3d54725283
28 changed files with 795 additions and 77 deletions

View File

@@ -8,6 +8,8 @@ import (
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/datatypes"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
const (
@@ -94,6 +96,52 @@ var CONTENTS_FTS5_FIELDS = []string{
COMMENT_FIELD,
}
var ErrInvalidQuery = errors.New("invalid input into the search function")
func NormalizeQuery(query string) []string {
query = datatypes.NormalizeString(query)
query = datatypes.DeleteTags(query)
query = datatypes.RemovePunctuation(query)
query = cases.Lower(language.German).String(query)
// TODO: how to normalize, which unicode normalization to use?
split := strings.Split(query, " ")
res := []string{}
for _, s := range split {
if len(s) > 2 {
res = append(res, s)
}
}
return res
}
func FTS5Search(app core.App, table string, mapfq ...FTS5QueryRequest) ([]*FTS5IDQueryResult, error) {
if mapfq == nil || len(mapfq) == 0 || table == "" {
return nil, ErrInvalidQuery
}
q := NewFTS5Query().From(table).SelectID()
for _, v := range mapfq {
for _, que := range v.Query {
q.AndMatch(v.Fields, que)
}
}
querystring := q.Query()
if querystring == "" {
return nil, ErrInvalidQuery
}
res := []*FTS5IDQueryResult{}
err := app.DB().NewQuery(querystring).All(&res)
if err != nil {
return nil, err
}
return res, nil
}
func CreateFTS5TableQuery(tablename string, fields ...string) string {
if len(fields) == 0 {
return ""

View File

@@ -6,6 +6,11 @@ import (
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/datatypes"
)
type FTS5QueryRequest struct {
Fields []string
Query []string
}
type FTS5IDQueryResult struct {
ID string `db:"id"`
}

View File

@@ -51,7 +51,15 @@ func BasicSearchSeries(app core.App, query string) ([]*Series, []*Series, error)
}
// INFO: Needing to differentiate matches
altids, err := FTS5SearchSeries(app, query)
querysplit := NormalizeQuery(query)
if len(querysplit) == 0 {
return series, []*Series{}, nil
}
altids, err := FTS5Search(app, SERIES_TABLE, FTS5QueryRequest{
Fields: []string{SERIES_TITLE_FIELD, ANNOTATION_FIELD, REFERENCES_FIELD},
Query: querysplit,
})
if err != nil {
return nil, nil, err
}

View File

@@ -0,0 +1,5 @@
package functions
func Length(arr []any) int {
return len(arr)
}

View File

@@ -1,58 +1,43 @@
package pagemodels
import (
"database/sql"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
type DefaultPage struct {
core.BaseRecordProxy
type DefaultPage[T IPageCollection] struct {
Record T
Name string
Template string
Layout string
URL string
}
func NewDefaultPage(record *core.Record) *DefaultPage {
i := &DefaultPage{}
i.SetProxyRecord(record)
return i
func (r *DefaultPage[T]) Up(app core.App, engine *templating.Engine) error {
_, err := app.FindCollectionByNameOrId(GeneratePageTableName(r.Name))
if err == sql.ErrNoRows {
collection := r.Record.Collection(r.Name)
err = app.Save(collection)
if err != nil {
app.Logger().Error("Error saving collection", "Name", GeneratePageTableName(r.Name), "Error", err, "Collection", collection)
return err
}
} else if err != nil {
app.Logger().Error("Error finding collection %s: %s", GeneratePageTableName(r.Name), err)
return err
}
func (r *DefaultPage) Title() string {
return r.GetString(F_TITLE)
}
func (r *DefaultPage) SetTitle(titel string) {
r.Set(F_TITLE, titel)
}
func (r *DefaultPage) Description() string {
return r.GetString(F_DESCRIPTION)
}
func (r *DefaultPage) SetDescription(beschreibung string) {
r.Set(F_DESCRIPTION, beschreibung)
}
func (r *DefaultPage) Keywords() string {
return r.GetString(F_TAGS)
}
func (r *DefaultPage) SetKeywords(keywords string) {
r.Set(F_TAGS, keywords)
}
func (r *DefaultPage) Up(app core.App, engine *templating.Engine) error {
return nil
}
func (r *DefaultPage) Down(app core.App, engine *templating.Engine) error {
func (r *DefaultPage[T]) Down(app core.App, engine *templating.Engine) error {
return nil
}
func (p *DefaultPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
func (p *DefaultPage[T]) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET(p.URL, func(e *core.RequestEvent) error {
data := make(map[string]interface{})
@@ -64,14 +49,14 @@ func (p *DefaultPage) Setup(router *router.Router[*core.RequestEvent], app core.
return engine.Response404(e, err, data)
}
p.SetProxyRecord(record)
p.Record.SetProxyRecord(record)
data["record"] = p
return engine.Response200(e, p.Template, data, p.Layout)
})
return nil
}
func (p *DefaultPage) Get(e *core.RequestEvent, engine *templating.Engine, data map[string]interface{}) error {
func (p *DefaultPage[T]) Get(e *core.RequestEvent, engine *templating.Engine, data map[string]interface{}) error {
err := p.SetCommonData(e.App, data)
if err != nil {
return engine.Response404(e, err, data)
@@ -80,17 +65,17 @@ func (p *DefaultPage) Get(e *core.RequestEvent, engine *templating.Engine, data
return engine.Response200(e, p.Template, data, p.Layout)
}
func (p *DefaultPage) SetCommonData(app core.App, data map[string]interface{}) error {
func (p *DefaultPage[T]) SetCommonData(app core.App, data map[string]interface{}) error {
record, err := p.GetLatestData(app)
if err != nil {
return err
}
p.SetProxyRecord(record)
data["page"] = p
p.Record.SetProxyRecord(record)
data["page"] = p.Record
return nil
}
func (p *DefaultPage) GetLatestData(app core.App) (*core.Record, error) {
func (p *DefaultPage[T]) GetLatestData(app core.App) (*core.Record, error) {
record := &core.Record{}
tn := GeneratePageTableName(p.Name)
err := app.RecordQuery(tn).OrderBy("created").Limit(1).One(record)

View File

@@ -0,0 +1,44 @@
package pagemodels
import (
"github.com/pocketbase/pocketbase/core"
)
type DefaultPageRecord struct {
core.BaseRecordProxy
}
func NewDefaultPageRecord(record *core.Record) *DefaultPageRecord {
i := &DefaultPageRecord{}
i.SetProxyRecord(record)
return i
}
func (r *DefaultPageRecord) Title() string {
return r.GetString(F_TITLE)
}
func (r *DefaultPageRecord) SetTitle(titel string) {
r.Set(F_TITLE, titel)
}
func (r *DefaultPageRecord) Description() string {
return r.GetString(F_DESCRIPTION)
}
func (r *DefaultPageRecord) SetDescription(beschreibung string) {
r.Set(F_DESCRIPTION, beschreibung)
}
func (r *DefaultPageRecord) Keywords() string {
return r.GetString(F_TAGS)
}
func (r *DefaultPageRecord) SetKeywords(keywords string) {
r.Set(F_TAGS, keywords)
}
func (r *DefaultPageRecord) Collection(pagename string) *core.Collection {
coll := BasePageCollection(pagename)
return coll
}

View File

@@ -88,3 +88,13 @@ func (t *IndexTexte) Abs2() string {
func (t *IndexTexte) SetAbs2(abs2 string) {
t.Set(F_INDEX_TEXTE_ABS2, abs2)
}
func (t *IndexTexte) Collection(pagename string) *core.Collection {
coll := BasePageCollection(pagename)
coll.Fields = append(coll.Fields, StandardPageFields()...)
coll.Fields = append(coll.Fields, core.NewFieldsList(
EditorField(F_INDEX_TEXTE_ABS1),
EditorField(F_INDEX_TEXTE_ABS2),
)...)
return coll
}

View File

@@ -0,0 +1,8 @@
package pagemodels
import "github.com/pocketbase/pocketbase/core"
type IPageCollection interface {
core.RecordProxy
Collection(pagename string) *core.Collection
}

View File

@@ -3,6 +3,8 @@ package pagemodels
const (
P_DATENSCHUTZ_NAME = "datenschutz"
P_SUCHE_NAME = "suche"
P_INDEX_NAME = "index"
T_INDEX_BILDER = "bilder"
T_INDEX_TEXTE = "texte"

View File

@@ -13,7 +13,7 @@ import (
func init() {
ip := &IndexPage{
DefaultPage: pagemodels.DefaultPage{
DefaultPage: pagemodels.DefaultPage[*pagemodels.IndexTexte]{
Name: pagemodels.P_INDEX_NAME,
},
}
@@ -21,7 +21,7 @@ func init() {
}
type IndexPage struct {
pagemodels.DefaultPage
pagemodels.DefaultPage[*pagemodels.IndexTexte]
}
// TODO:

View File

@@ -22,15 +22,19 @@ const (
func init() {
rp := &ReihenPage{
DefaultPage: pagemodels.DefaultPage{
DefaultPage: pagemodels.DefaultPage[*pagemodels.DefaultPageRecord]{
Name: pagemodels.P_REIHEN_NAME,
URL: URL_REIHEN,
Template: URL_REIHEN,
Layout: templating.DEFAULT_LAYOUT_NAME,
Record: &pagemodels.DefaultPageRecord{},
},
}
app.Register(rp)
}
type ReihenPage struct {
pagemodels.DefaultPage
pagemodels.DefaultPage[*pagemodels.DefaultPageRecord]
}
func (p *ReihenPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {

View File

@@ -23,6 +23,7 @@ func RegisterStaticPage(url, name string) {
})
}
// TODO: mocve textpage to defaultpage with T = TextPageRecord
func RegisterTextPage(url, name string) {
app.Register(&pagemodels.TextPage{
Name: name,
@@ -33,10 +34,11 @@ func RegisterTextPage(url, name string) {
}
func RegisterDefaultPage(url string, name string) {
app.Register(&pagemodels.DefaultPage{
app.Register(&pagemodels.DefaultPage[*pagemodels.DefaultPageRecord]{
Name: name,
Layout: templating.DEFAULT_LAYOUT_NAME,
Template: url,
URL: url,
Record: &pagemodels.DefaultPageRecord{},
})
}

162
pages/suche.go Normal file
View File

@@ -0,0 +1,162 @@
package pages
import (
"net/http"
"slices"
"strings"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
)
type SuchePage struct {
pagemodels.DefaultPage[*pagemodels.DefaultPageRecord]
}
const (
URL_SUCHE = "/suche/{type}"
URL_SUCHE_ALT = "/suche/{$}"
DEFAULT_SUCHE = "/suche/reihen"
PARAM_QUERY = "q"
PARAM_EXTENDED = "extended"
TEMPLATE_SUCHE = "/suche/"
)
var availableTypes = []string{"reihen", "baende", "beitraege", "personen"}
func init() {
rp := &SuchePage{
DefaultPage: pagemodels.DefaultPage[*pagemodels.DefaultPageRecord]{
Record: &pagemodels.DefaultPageRecord{},
Name: pagemodels.P_SUCHE_NAME,
Template: TEMPLATE_SUCHE,
Layout: templating.DEFAULT_LAYOUT_NAME,
URL: URL_SUCHE,
},
}
app.Register(rp)
}
func (p *SuchePage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET(URL_SUCHE_ALT, func(e *core.RequestEvent) error {
return e.Redirect(http.StatusPermanentRedirect, DEFAULT_SUCHE)
})
router.GET(URL_SUCHE, func(e *core.RequestEvent) error {
if !slices.Contains(availableTypes, e.Request.PathValue("type")) {
return engine.Response404(e, nil, nil)
}
q := e.Request.URL.Query().Get(PARAM_QUERY)
q = strings.TrimSpace(q)
if q != "" {
return p.SimpleSearchRequest(app, engine, e)
}
// if e.Request.URL.Query().Get(PARAM_QUERY) != "" {
// return p.SearchRequest(app, engine, e)
// }
return p.DefaultRequest(app, engine, e)
})
return nil
}
func (p *SuchePage) SimpleSearchRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
t := e.Request.PathValue("type")
if t == "reihen" {
return p.SimpleSearchReihenRequest(app, engine, e)
}
// if t == "baende" {
// return p.SimpleSearchBaendeRequest(app, engine, e)
// }
// if t == "beitraege" {
// return p.SimpleSearchBeitraegeRequest(app, engine, e)
// }
// if t == "personen" {
// return p.SimpleSearchPersonenRequest(app, engine, e)
// }
return engine.Response404(e, nil, nil)
}
func (p *SuchePage) SimpleSearchReihenRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
q := e.Request.URL.Query().Get(PARAM_QUERY)
data := p.CommonData(app, engine, e)
data["q"] = q
hasTitle := e.Request.URL.Query().Get("title") == "on"
hasAnnotations := e.Request.URL.Query().Get("annotations") == "on"
hasReferences := e.Request.URL.Query().Get("references") == "on"
if !hasTitle && !hasAnnotations && !hasReferences {
engine.Response404(e, nil, nil)
}
fields := []string{}
options := map[string]bool{}
if hasTitle {
fields = append(fields, dbmodels.SERIES_TITLE_FIELD)
options["title"] = true
}
if hasAnnotations {
fields = append(fields, dbmodels.ANNOTATION_FIELD)
options["annotations"] = true
}
if hasReferences {
fields = append(fields, dbmodels.REFERENCES_FIELD)
options["references"] = true
}
data["options"] = options
query := dbmodels.NormalizeQuery(q)
if len(q) == 0 {
return engine.Response404(e, nil, nil)
}
ids, err := dbmodels.FTS5Search(app, dbmodels.SERIES_TABLE, dbmodels.FTS5QueryRequest{
Fields: fields,
Query: query,
})
if err != nil {
return engine.Response500(e, err, nil)
}
idsany := []any{}
for _, id := range ids {
idsany = append(idsany, id.ID)
}
series, err := dbmodels.SeriessesForIds(app, idsany)
rmap, bmap, err := dbmodels.EntriesForSeriesses(app, series)
if err != nil {
return engine.Response500(e, err, nil)
}
dbmodels.SortSeriessesByTitle(series)
data["series"] = series
data["relations"] = rmap
data["entries"] = bmap
data["count"] = len(series)
// TODO: get relavant agents, years and places
return engine.Response200(e, p.Template, data, p.Layout)
}
func (p *SuchePage) DefaultRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
data := p.CommonData(app, engine, e)
return engine.Response200(e, p.Template, data, p.Layout)
}
func (p *SuchePage) CommonData(app core.App, engine *templating.Engine, e *core.RequestEvent) map[string]interface{} {
data := map[string]interface{}{}
data["type"] = e.Request.PathValue("type")
data[PARAM_EXTENDED] = false
if e.Request.URL.Query().Get(PARAM_EXTENDED) == "true" {
data[PARAM_EXTENDED] = true
}
return data
}

View File

@@ -28,19 +28,36 @@ Modelle umwandeln (zzt RecordProxy)
- Ersellen & Abfragen FTS5-Tabellen
- Erstellen Textseiten
- Technologie-Stack auf Server-Rendering / Go Templates umgestellt
- Die Seiten sollten jetzt insgesamt schneller laden
- Man kann auf der Startseit nach Almanach-Nummern suchen
- Man kann auf der Startseite nach Almanach-Nummern suchen
- Überall werden die Almanachnummer und Inhaltsnummer angezeigt
- Die URL referenziert die Almanachnummern, nicht mher die DB-IDs
- In der Almanach-Ansicht werden die Abkürzungen erklärt
- In der Almanach-Ansicht können die Beiträge nach Sammlungen gruppiert werden
- In der Almanach-Ansicht kann nach Inhalten frei gefiltert werden, oder nach Typ
- In der Almanach- und Suchansicht werden Sammlungen abgehoben
- In der Almanach- und Suchansicht werden auch mehrere Bilder zu einem Eintrag angezeigt
- In der Almanach- und Suchansicht kann nach Inhalten frei gefiltert werden, oder nach Typ
- Es gibt neue URLs sowohl für die einzelne Reihe, als auch für den einzelnen Beitrag
- Es gibt neue URLs, die fest verlinkt werden können für einzelne:
- Personen
- Reihen
- Bände
- Beiträge
- Die Suche ist klar nach Typ unterteilt
- Die Suche ist klar nach Typ unterteilt und insgesamt zuverlässiger
- Zusätzlich zur jetzigen Suchfunktion gibt es für jeden Typ noch eine Detailsuche
- Suchergebnisse können nach Typ, Person, Jahr gefiltert werden
- Suchergebnisse könnnen nach Jahr und Band, nach Band und Jahr (nach Personen) sortiert werden
- Jede Suche hat eine eindeutige URL
TODO danach:
- Google-Suchoptimierung
- Error Pages prüfen & error-Verhalten von HTMX
- Stimmigere Page-Abstraktion
- Weißraum in den Antworten
- Antworten komprimieren
- Cache?
- Footer

View File

@@ -234,7 +234,14 @@ class C extends HTMLElement {
super(), this._tooltipBox = null;
}
connectedCallback() {
this.style.position = "relative";
this.classList.add(
"w-full",
"h-full",
"relative",
"block",
"leading-none",
"[&>*]:leading-normal"
);
const i = this.querySelector(".data-tip"), t = i ? i.innerHTML : "Tooltip";
i && i.remove(), this._tooltipBox = document.createElement("div"), this._tooltipBox.innerHTML = t, this._tooltipBox.className = [
"opacity-0",

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,6 @@
{{ $model := . }}
<div
id="mainmenu"
class="pb-1.5 border-b border-zinc-300"
x-data="{ openeditionmenu: window.location.pathname.startsWith('/edition/')}">
<div class="flex flex-row justify-between">
@@ -13,7 +14,7 @@
<nav
class="self-end font-serif font-bold flex flex-row gap-x-4 [&>a]:no-underline
[&>*]:px-1.5 [&>*]:pt-1 [&>*]:-mb-1.5">
[&>*]:px-2 [&>*]:pt-1 [&>*]:-mb-1.5">
<a
href="/reihen/"
{{ if and $model.page (HasPrefix $model.page.Path "/reihe") -}}

View File

@@ -52,7 +52,7 @@
hx-boost="false">
<i class="ri-links-line"></i
></a>
<div class="data-tip">Permalink zu dieser Suchanfrage</div>
<div class="data-tip">Link zu dieser Suchanfrage</div>
</tool-tip>
</div>
</div>

View File

@@ -2,7 +2,8 @@
{{ $entries := index . 1 }}
{{ $relations := index . 2 }}
{{ $showidseries := index . 3 }}
{{ $markar := index . 4 }}
{{ $marka := index . 4 }}
{{ $markr := index . 5 }}
{{ $bds := index $relations $r.Id }}
@@ -10,7 +11,7 @@
<div class="flex flex-col lg:flex-row mb-1.5">
<div class="grow-0 shrink-0 w-[12rem] flex flex-col">
{{ if $r.References }}
<div class="text-sm font-sans px-2 py-1 bg-stone-100 {{ if $markar }}reihen-text{{ end }}">
<div class="text-sm font-sans px-2 py-1 bg-stone-100 {{ if $markr }}reihen-text{{ end }}">
{{ $r.References }}
</div>
{{ end }}
@@ -37,7 +38,7 @@
<span class="font-bold reihen-text">{{ $r.Title }}</span>
{{ if $r.Annotation }}
<span>&ensp;&middot;&ensp;</span>
<span class="{{ if $markar }}reihen-text{{ end }}">{{ Safe $r.Annotation }}</span>
<span class="{{ if $marka }}reihen-text{{ end }}">{{ Safe $r.Annotation }}</span>
{{ end }}
</div>
<div></div>

View File

@@ -1,4 +1,6 @@
<div class="container-normal">
<h1>Die Seite konnte nicht gefunden werden!</h1>
{{ if .Error }}
<p>{{ .Error }}</p>
{{ end }}
</div>

View File

@@ -13,16 +13,16 @@
<img src="{{ .record.ImagePath }}" />
</div>
</div>
<div class="absolute top-0 right-0 p-4">
<div class="absolute top-0 right-0 m-4 mr-8">
<tool-tip position="left">
<div class="data-tip">Hinweis schließen</div>
<button
@click="open = false"
class="text-xl p-2 text-stone-500 opacity-85 hover:opacity-100 transition-opacity
duration-200 hover:text-stone-900
class="text-3xl text-stone-500 opacity-85 hover:opacity-100 transition-opacity
duration-200 hover:text-stone-900 leading-none
hover:cursor-pointer">
<i class="ri-close-circle-fill"></i>
</button>
<div class="data-tip">Hinweis schließen</div>
</tool-tip>
</div>
</div>
@@ -138,7 +138,7 @@
{{ if and .search .idseries }}
<div class="mb-1 max-w-[60rem] hyphens-auto">
{{ range $id, $r := .idseries }}
{{ template "_reihe" (Arr $r $model.entries $model.relations true false) }}
{{ template "_reihe" (Arr $r $model.entries $model.relations true false false) }}
{{ end }}
</div>
{{ end }}
@@ -146,7 +146,9 @@
{{ if .series }}
<div class="mb-1 max-w-[60rem] hyphens-auto">
{{ range $id, $r := .series }}
{{ template "_reihe" (Arr $r $model.entries $model.relations false false) }}
{{ template "_reihe" (Arr $r $model.entries $model.relations false false
false)
}}
{{ end }}
</div>
{{ end }}
@@ -171,11 +173,11 @@
</div>
{{ end }}
<div class="border-t mb-1.5 text-sm font-sans text-right pt-0.5">
Treffer in allen Feldern (Anmerkungen, Verweisen etc.) &darr;
Treffer in allen Feldern (inkl. Anmerkungen &amp; Verweise) &darr;
</div>
<div class="mb-1 max-w-[60rem] hyphens-auto">
{{ range $id, $r := .altseries }}
{{ template "_reihe" (Arr $r $model.entries $model.relations false true) }}
{{ template "_reihe" (Arr $r $model.entries $model.relations false true true) }}
{{ end }}
</div>
{{ end }}

View File

@@ -0,0 +1,88 @@
{{ $model := . }}
<div id="searchcontrol" class="container-normal">
<div id="searchheading" class="flex flex-row justify-between min-h-14 items-end relative">
<nav id="searchnav" class="flex flex-row items-end">
<div class="align-bottom h-min self-end pb-0.5 hidden">Durchsuchen:</div>
<a
href="/suche/reihen"
class="block no-underline"
{{ if eq $model.type "reihen" }}aria-current="page"{{- end -}}
>Reihen</a
>
<a
href="/suche/baende"
class="block no-underline"
{{ if eq $model.type "baende" }}aria-current="page"{{- end -}}
>Bände</a
>
<a
href="/suche/beitraege"
class="block no-underline"
{{ if eq $model.type "beitraege" }}aria-current="page"{{- end -}}
>Beiträge</a
>
<a
href="/suche/personen"
class="block no-underline"
{{ if eq $model.type "personen" }}aria-current="page"{{- end -}}
>Personen</a
>
</nav>
<h1
class="text-3xl font-bold px-3 relative translate-y-[45%] w-min whitespace-nowrap
bg-stone-50 mr-24 z-20">
Suche&nbsp;&middot;&nbsp;<span class="">
{{- if eq $model.type "reihen" -}}
Reihen
{{- else if eq $model.type "personen" -}}
Personen &amp; Körperschaften
{{- else if eq $model.type "baende" -}}
Bände
{{- else if eq $model.type "beitraege" -}}
Beiträge
{{- end -}}
</span>
</h1>
</div>
<div id="" class="border-l border-zinc-300 px-8 py-10 relative">
{{ template "searchform" $model }}
</div>
</div>
{{- if $model.q -}}
<div id="searchresults">
{{- if eq $model.type "reihen" -}}
<!-- INFO: Resultate Reihen -->
<div id="" class="container-normal mt-4">
{{- if $model.series -}}
{{- $includeReferences := index $model.options "references" -}}
{{- $includeAnnotations := index $model.options "annotations" -}}
<div class="mb-1 max-w-[60rem] hyphens-auto">
{{- range $id, $r := $model.series -}}
{{- template "_reihe" (Arr $r $model.entries $model.relations false
$includeAnnotations $includeReferences)
-}}
{{- end -}}
</div>
{{ else }}
{{- end -}}
</div>
<script type="module">
let elements = document.querySelectorAll('.reihen-text');
let mark_instance = new Mark(elements);
// INFO: we wait a little bit before marking, to settle everything
setTimeout(() => {
let word = '{{ $model.q }}';
word = word.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,"");
mark_instance.mark(word, {
"seperateWordSearch": true,
"ignorePunctuation": [""],
});
}, 200);
</script>
<!-- INFO: Resultate Reihen Ende -->
{{- end -}}
</div>
{{- end -}}

View File

@@ -0,0 +1,17 @@
{{ $extendable := . }}
<div class="col-span-6 flex flex-row text-stone-700 gap-x-2">
<div>
<i class="ri-information-2-fill"></i>
</div>
<div class="font-sans hyphens-auto text-sm pt-1">
Die Suche durchsucht ganze Datensätze nach dem Vorkommen aller eingegebenen Suchbegriffe. Felder
können oben einzeln aus der Suche ausgeschlossen werden. Auch partielle Treffer in Worten werden
angezeigt. Wörter mit weniger als drei Zeichen, Sonderzeichen &ndash; auch Satzzeichen &ndash;
sowie die Groß- und Kleinschreibung werden dabei ignoriert
{{- if $extendable }}
(für mehr Optionen s. die &rarr; <a href="?extended=true">erweiterte Suche</a>)
{{- end -}}.
</div>
</div>

View File

@@ -0,0 +1,45 @@
{{ $model := index . 0 }}
{{ $extendable := index . 1 }}
<label for="q" class="hidden">Suchbegriffe</label>
<input
{{ if $model.q }}value="{{ $model.q }}"{{- end -}}
type="search"
name="q"
autofocus="true"
minlength="3"
required
placeholder="Suchbegriff (min. 3 Zeichen)"
class="w-full col-span-8
placeholder:italic" />
<button id="submitbutton" type="submit" class="col-span-2">Suchen</button>
{{ if $extendable }}
<a
href="/suche/{{ $model.type }}?extended=true"
class="whitespace-nowrap self-end block col-span-2">
<i class="ri-arrow-right-long-line"></i> Erweiterte Suche
</a>
{{ end }}
<script type="module">
const form = document.getElementById("searchform");
const submitBtn = document.getElementById("submitbutton");
function checkValidity() {
if (form.checkValidity()) {
submitBtn.disabled = false;
} else {
submitBtn.disabled = true;
}
}
checkValidity();
if (form && submitBtn) {
form.addEventListener("input", (event) => {
checkValidity();
});
}
</script>

View File

@@ -0,0 +1,193 @@
{{ $model := . }}
{{- $includeReferences := true -}}
{{- $includeAnnotations := true -}}
{{- $includeTitle := true -}}
{{- if eq $model.type "reihen" -}}
{{- if $model.options -}}
{{- $includeReferences = not (and $model.q (not (index $model.options "references"))) -}}
{{- $includeAnnotations = not (and $model.q (not (index $model.options "annotations"))) -}}
{{- $includeTitle = not (and $model.q (not (index $model.options "title"))) -}}
{{- end -}}
{{- end -}}
<form
id="searchform"
class="w-full font-serif"
method="get"
action="/suche/{{- $model.type -}}"
autocomplete="off">
{{- if eq $model.type "reihen" -}}
<!-- INFO: Reihen -->
{{- if not $model.extended -}}
<div class="grid grid-cols-12 gap-y-3 w-full gap-x-4">
{{ template "searchboxsimple" Arr . false }}
<fieldset class="selectgroup">
<div class="selectgroup-option">
<input
type="checkbox"
name="title"
id="title"
{{ if $includeTitle -}}
checked
{{- end -}} />
<label for="title">Titel</label>
</div>
<div class="selectgroup-option">
<input
type="checkbox"
name="annotations"
id="annotations"
{{ if $includeAnnotations -}}
checked
{{- end -}} />
<label for="annotations">Anmerkungen</label>
</div>
<div class="selectgroup-option">
<input
type="checkbox"
name="references"
id="references"
{{ if $includeReferences -}}
checked
{{- end -}} />
<label for="references">Nachweise</label>
</div>
</fieldset>
{{ template "infotextsimple" false }}
</div>
{{- else -}}
Extended search Reihen
{{- end -}}
<!-- INFO: Ende Reihen -->
{{- else if eq $model.type "personen" -}}
<!-- INFO: Personen -->
{{- if not $model.extended -}}
<div class="grid grid-cols-12 gap-y-3 w-full gap-x-4">
{{ template "searchboxsimple" Arr . false }}
<fieldset class="selectgroup">
<div class="selectgroup-option">
<input type="checkbox" name="names" id="names" checked />
<label for="names">Namen &amp; Pseudonyme</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="biographical" id="biographical" checked />
<label for="biographical">Lebensdaten</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="profession" id="profession" checked />
<label for="profession">Beruf(e)</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="annotations" id="annotations" checked />
<label for="annotations">Anmerkungen</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="references" id="references" checked />
<label for="references">Nachweise</label>
</div>
</fieldset>
{{ template "infotextsimple" false }}
</div>
{{- else -}}
Extended search Personen
{{- end -}}
<!-- INFO: Ende Personen -->
{{- else if eq $model.type "baende" -}}
<!-- INFO: Bände -->
{{- if not $model.extended -}}
<div class="grid grid-cols-12 gap-y-3 w-full gap-x-4">
{{ template "searchboxsimple" Arr . true }}
<fieldset class="selectgroup">
<div class="selectgroup-option">
<input type="checkbox" name="number" id="number" checked />
<label for="number">Almanach-Nr.</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="title" id="title" checked />
<label for="title">Titel</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="series" id="series" checked />
<label for="series">Reihentitel</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="person" id="person" checked />
<label for="person">Personen &amp; Verlage</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="pubdata" id="pubdata" checked />
<label for="pubdata">Orte</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="year" id="year" checked />
<label for="year">Jahr</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="references" id="references" checked />
<label for="references">Nachweise</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="annotations" id="annotations" checked />
<label for="annotations">Anmerkungen</label>
</div>
</fieldset>
{{ template "infotextsimple" true }}
</div>
{{- else -}}
Extended search Bände
{{- end -}}
<!-- INFO: Ende Bände -->
{{- else if eq $model.type "beitraege" -}}
<!-- INFO: Beiträge -->
{{- if not $model.extended -}}
<div class="grid grid-cols-12 gap-y-3 w-full gap-x-4">
{{ template "searchboxsimple" Arr . true }}
<fieldset class="selectgroup">
<div class="selectgroup-option">
<input type="checkbox" name="number" id="number" checked />
<label for="number">Almanach-Nr.</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="title" id="title" checked />
<label for="title">Titelinformationen</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="entry" id="entry" checked />
<label for="entry">Bandtitel</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="person" id="person" checked />
<label for="person">Personen &amp; Pseudonyme</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="annotations" id="annotations" checked />
<label for="annotations">Anmerkungen</label>
</div>
</fieldset>
{{ template "infotextsimple" true }}
</div>
{{- else -}}
Extended search Beiträge
{{- end -}}
{{- end -}}
</form>
<script type="module">
let fieldset = document.querySelector("fieldset.selectgroup");
let checkboxes = Array.from(fieldset.querySelectorAll('input[type="checkbox"]'));
fieldset.addEventListener("change", (event) => {
let target = event.target;
if (target.type === "checkbox") {
let name = target.name;
let checked = target.checked;
if (!checked) {
let allchecked = checkboxes.filter((checkbox) => checkbox.checked);
if (allchecked.length === 0) {
target.checked = true;
}
}
}
});
</script>

View File

@@ -0,0 +1 @@
<title>{{ .site.title }}: Suche</title>

View File

@@ -430,7 +430,14 @@ class ToolTip extends HTMLElement {
}
connectedCallback() {
this.style.position = "relative";
this.classList.add(
"w-full",
"h-full",
"relative",
"block",
"leading-none",
"[&>*]:leading-normal",
);
const dataTipElem = this.querySelector(".data-tip");
const tipContent = dataTipElem ? dataTipElem.innerHTML : "Tooltip";

View File

@@ -73,23 +73,23 @@
@apply ml-14 list-disc;
}
nav > a {
#mainmenu nav > a {
@apply hover:!border-zinc-200;
}
nav > * {
#mainmenu nav > * {
@apply border-b-4 border-transparent;
}
nav > button[aria-current="true"] {
#mainmenu nav > button[aria-current="true"] {
@apply !bg-slate-200;
}
nav a[aria-current="page"] {
#mainmenu nav a[aria-current="page"] {
@apply text-slate-800;
}
nav a[aria-current="page"] {
#mainmenu nav a[aria-current="page"] {
@apply !border-zinc-300;
}
@@ -197,4 +197,58 @@
.indented p {
@apply -indent-3.5 ml-3.5;
}
#searchnav > a:nth-of-type(1) {
@apply ml-12;
}
#searchnav > a {
@apply odd:bg-stone-100 even:bg-zinc-100 mx-1.5 border-zinc-300 border-x border-t px-2.5 no-underline transition-all duration-75 py-0.5;
}
#searchnav > a[aria-current="page"]:not(.inactive) {
@apply font-bold italic !bg-stone-50 relative -bottom-3 border-b z-20;
}
#searchnav > a:hover:not([aria-current="page"]:not(.inactive)) {
@apply pb-2 !bg-stone-50 relative;
}
#searchheading:before {
content: "";
@apply bg-zinc-300 w-[80%] absolute bottom-0 right-[20%] h-[1px] z-10;
}
#searchform:before {
content: "";
@apply bg-zinc-300 w-[30%] absolute bottom-0 right-[70%] h-[1px] z-10;
}
#searchform input {
@apply w-full mx-auto px-2 py-1 border-zinc-600 border;
}
#searchform button {
@apply bg-stone-100 text-base px-2.5 py-1 rounded font-sans transition-all duration-75;
}
#searchform button:hover:not(:disabled) {
@apply cursor-pointer bg-stone-200;
}
#searchform button:disabled {
@apply bg-stone-400 cursor-not-allowed;
}
#searchform .selectgroup {
@apply col-span-12 w-full flex flex-row gap-x-6;
}
#searchform .selectgroup .selectgroup-option {
@apply flex flex-row select-none gap-x-1.5;
}
#searchform .selectgroup .selectgroup-option label {
@apply whitespace-nowrap;
}
}