mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 02:25:30 +00:00
+Inhalte edit page
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -15,11 +16,14 @@ import (
|
|||||||
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
||||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/security"
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
"github.com/pocketbase/pocketbase/tools/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
URL_ALMANACH_CONTENTS_EDIT = "contents/edit"
|
URL_ALMANACH_CONTENTS_EDIT = "contents/edit"
|
||||||
|
URL_ALMANACH_CONTENTS_INSERT = "contents/insert"
|
||||||
|
URL_ALMANACH_CONTENTS_DELETE = "contents/delete"
|
||||||
TEMPLATE_ALMANACH_CONTENTS_EDIT = "/almanach/contents/edit/"
|
TEMPLATE_ALMANACH_CONTENTS_EDIT = "/almanach/contents/edit/"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,6 +49,8 @@ func (p *AlmanachContentsEditPage) Setup(router *router.Router[*core.RequestEven
|
|||||||
rg.BindFunc(middleware.IsAdminOrEditor())
|
rg.BindFunc(middleware.IsAdminOrEditor())
|
||||||
rg.GET(URL_ALMANACH_CONTENTS_EDIT, p.GET(engine, app))
|
rg.GET(URL_ALMANACH_CONTENTS_EDIT, p.GET(engine, app))
|
||||||
rg.POST(URL_ALMANACH_CONTENTS_EDIT, p.POSTSave(engine, app))
|
rg.POST(URL_ALMANACH_CONTENTS_EDIT, p.POSTSave(engine, app))
|
||||||
|
rg.POST(URL_ALMANACH_CONTENTS_INSERT, p.POSTInsert(engine, app))
|
||||||
|
rg.POST(URL_ALMANACH_CONTENTS_DELETE, p.POSTDelete(engine, app))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,11 +67,13 @@ func (p *AlmanachContentsEditPage) GET(engine *templating.Engine, app core.App)
|
|||||||
data["csrf_token"] = req.Session().Token
|
data["csrf_token"] = req.Session().Token
|
||||||
data["content_types"] = dbmodels.CONTENT_TYPE_VALUES
|
data["content_types"] = dbmodels.CONTENT_TYPE_VALUES
|
||||||
data["musenalm_types"] = dbmodels.MUSENALM_TYPE_VALUES
|
data["musenalm_types"] = dbmodels.MUSENALM_TYPE_VALUES
|
||||||
data["pagination_values"] = slices.Collect(maps.Values(dbmodels.MUSENALM_PAGINATION_VALUES))
|
data["pagination_values"] = paginationValuesSorted()
|
||||||
|
|
||||||
if msg := e.Request.URL.Query().Get("saved_message"); msg != "" {
|
if msg := e.Request.URL.Query().Get("saved_message"); msg != "" {
|
||||||
data["success"] = msg
|
data["success"] = msg
|
||||||
}
|
}
|
||||||
|
data["edit_content_id"] = strings.TrimSpace(e.Request.URL.Query().Get("edit_content"))
|
||||||
|
data["new_content"] = strings.TrimSpace(e.Request.URL.Query().Get("new_content"))
|
||||||
|
|
||||||
return engine.Response200(e, p.Template, data, p.Layout)
|
return engine.Response200(e, p.Template, data, p.Layout)
|
||||||
}
|
}
|
||||||
@@ -83,12 +91,145 @@ func (p *AlmanachContentsEditPage) renderError(engine *templating.Engine, app co
|
|||||||
data["csrf_token"] = req.Session().Token
|
data["csrf_token"] = req.Session().Token
|
||||||
data["content_types"] = dbmodels.CONTENT_TYPE_VALUES
|
data["content_types"] = dbmodels.CONTENT_TYPE_VALUES
|
||||||
data["musenalm_types"] = dbmodels.MUSENALM_TYPE_VALUES
|
data["musenalm_types"] = dbmodels.MUSENALM_TYPE_VALUES
|
||||||
data["pagination_values"] = slices.Collect(maps.Values(dbmodels.MUSENALM_PAGINATION_VALUES))
|
data["pagination_values"] = paginationValuesSorted()
|
||||||
data["error"] = message
|
data["error"] = message
|
||||||
|
data["edit_content_id"] = strings.TrimSpace(e.Request.URL.Query().Get("edit_content"))
|
||||||
|
data["new_content"] = strings.TrimSpace(e.Request.URL.Query().Get("new_content"))
|
||||||
return engine.Response200(e, p.Template, data, p.Layout)
|
return engine.Response200(e, p.Template, data, p.Layout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AlmanachContentsEditPage) POSTSave(engine *templating.Engine, app core.App) HandleFunc {
|
func (p *AlmanachContentsEditPage) POSTSave(engine *templating.Engine, app core.App) HandleFunc {
|
||||||
|
return func(e *core.RequestEvent) error {
|
||||||
|
id := e.Request.PathValue("id")
|
||||||
|
req := templating.NewRequest(e)
|
||||||
|
isHTMX := strings.EqualFold(e.Request.Header.Get("HX-Request"), "true")
|
||||||
|
|
||||||
|
if err := e.Request.ParseForm(); err != nil {
|
||||||
|
return p.renderSaveError(engine, app, e, req, nil, nil, err.Error(), isHTMX)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := req.CheckCSRF(e.Request.FormValue("csrf_token")); err != nil {
|
||||||
|
return p.renderSaveError(engine, app, e, req, nil, nil, err.Error(), isHTMX)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := dbmodels.Entries_MusenalmID(app, id)
|
||||||
|
if err != nil {
|
||||||
|
return engine.Response404(e, err, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
contents, err := dbmodels.Contents_Entry(app, entry.Id)
|
||||||
|
if err != nil {
|
||||||
|
return p.renderSaveError(engine, app, e, req, entry, nil, "Beiträge konnten nicht geladen werden.", isHTMX)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentInputs := parseContentsForm(e.Request.PostForm)
|
||||||
|
contentOrder := parseContentsOrder(e.Request.PostForm)
|
||||||
|
orderMap := buildContentOrderMap(contentOrder)
|
||||||
|
user := req.User()
|
||||||
|
existingByID := make(map[string]*dbmodels.Content, len(contents))
|
||||||
|
for _, content := range contents {
|
||||||
|
existingByID[content.Id] = content
|
||||||
|
}
|
||||||
|
newContentIDs := make([]string, 0)
|
||||||
|
for contentID := range contentInputs {
|
||||||
|
if _, exists := existingByID[contentID]; !exists {
|
||||||
|
newContentIDs = append(newContentIDs, contentID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(newContentIDs) > 1 {
|
||||||
|
sort.Slice(newContentIDs, func(i, j int) bool {
|
||||||
|
return orderMap[newContentIDs[i]] < orderMap[newContentIDs[j]]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedContents []*dbmodels.Content
|
||||||
|
if err := app.RunInTransaction(func(tx core.App) error {
|
||||||
|
if len(orderMap) > 0 {
|
||||||
|
for _, content := range contents {
|
||||||
|
numbering, ok := orderMap[content.Id]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if content.Numbering() == numbering {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
content.SetNumbering(numbering)
|
||||||
|
if err := tx.Save(content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextMusenalmID := 0
|
||||||
|
if len(newContentIDs) > 0 {
|
||||||
|
nextID, err := nextContentMusenalmID(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nextMusenalmID = nextID
|
||||||
|
}
|
||||||
|
created := make([]*dbmodels.Content, 0, len(newContentIDs))
|
||||||
|
for _, tempID := range newContentIDs {
|
||||||
|
fields, ok := contentInputs[tempID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
contentCollection, err := tx.FindCollectionByNameOrId(dbmodels.CONTENTS_TABLE)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
record := core.NewRecord(contentCollection)
|
||||||
|
content := dbmodels.NewContent(record)
|
||||||
|
content.SetMusenalmID(nextMusenalmID)
|
||||||
|
nextMusenalmID++
|
||||||
|
content.SetEditState("Edited")
|
||||||
|
numbering := orderMap[tempID]
|
||||||
|
if numbering <= 0 {
|
||||||
|
numbering = float64(len(contents) + len(created) + 1)
|
||||||
|
}
|
||||||
|
if err := applyContentForm(content, entry, fields, user, numbering); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Save(content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
created = append(created, content)
|
||||||
|
}
|
||||||
|
for _, content := range contents {
|
||||||
|
fields, ok := contentInputs[content.Id]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
numbering := orderMap[content.Id]
|
||||||
|
if err := applyContentForm(content, entry, fields, user, numbering); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Save(content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatedContents = append(updatedContents, contents...)
|
||||||
|
updatedContents = append(updatedContents, created...)
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
app.Logger().Error("Failed to save contents", "entry_id", entry.Id, "error", err)
|
||||||
|
return p.renderSaveError(engine, app, e, req, entry, contentInputs, err.Error(), isHTMX)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updatedContents) == 0 {
|
||||||
|
updatedContents = contents
|
||||||
|
}
|
||||||
|
go updateContentsFTS5(app, entry, updatedContents)
|
||||||
|
|
||||||
|
redirect := fmt.Sprintf("/almanach/%s/contents/edit?saved_message=%s", id, url.QueryEscape("Änderungen gespeichert."))
|
||||||
|
if isHTMX {
|
||||||
|
e.Response.Header().Set("HX-Redirect", redirect)
|
||||||
|
return e.String(http.StatusOK, "")
|
||||||
|
}
|
||||||
|
return e.Redirect(http.StatusSeeOther, redirect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AlmanachContentsEditPage) POSTInsert(engine *templating.Engine, app core.App) HandleFunc {
|
||||||
return func(e *core.RequestEvent) error {
|
return func(e *core.RequestEvent) error {
|
||||||
id := e.Request.PathValue("id")
|
id := e.Request.PathValue("id")
|
||||||
req := templating.NewRequest(e)
|
req := templating.NewRequest(e)
|
||||||
@@ -106,40 +247,213 @@ func (p *AlmanachContentsEditPage) POSTSave(engine *templating.Engine, app core.
|
|||||||
return engine.Response404(e, err, nil)
|
return engine.Response404(e, err, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
contents, err := dbmodels.Contents_Entry(app, entry.Id)
|
contentCollection, err := app.FindCollectionByNameOrId(dbmodels.CONTENTS_TABLE)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p.renderError(engine, app, e, "Beiträge konnten nicht geladen werden.")
|
return p.renderError(engine, app, e, "Beitrag konnte nicht vorbereitet werden.")
|
||||||
}
|
}
|
||||||
|
|
||||||
contentInputs := parseContentsForm(e.Request.PostForm)
|
record := core.NewRecord(contentCollection)
|
||||||
user := req.User()
|
record.Id = "tmp" + security.PseudorandomString(8)
|
||||||
|
record.MarkAsNew()
|
||||||
|
newContent := dbmodels.NewContent(record)
|
||||||
|
newContent.SetEntry(entry.Id)
|
||||||
|
newContent.SetYear(entry.Year())
|
||||||
|
newContent.SetEditState("Edited")
|
||||||
|
|
||||||
|
data := map[string]any{
|
||||||
|
"content": newContent,
|
||||||
|
"entry": entry,
|
||||||
|
"csrf_token": req.Session().Token,
|
||||||
|
"content_types": dbmodels.CONTENT_TYPE_VALUES,
|
||||||
|
"musenalm_types": dbmodels.MUSENALM_TYPE_VALUES,
|
||||||
|
"pagination_values": paginationValuesSorted(),
|
||||||
|
"open_edit": true,
|
||||||
|
"is_new": true,
|
||||||
|
"content_id": record.Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
if err := engine.Render(&builder, "/almanach/contents/insert/", data, "fragment"); err != nil {
|
||||||
|
app.Logger().Error("Failed to render content insert", "entry_id", entry.Id, "error", err)
|
||||||
|
return p.renderError(engine, app, e, "Beitrag konnte nicht vorbereitet werden.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.HTML(http.StatusOK, builder.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AlmanachContentsEditPage) POSTDelete(engine *templating.Engine, app core.App) HandleFunc {
|
||||||
|
return func(e *core.RequestEvent) error {
|
||||||
|
id := e.Request.PathValue("id")
|
||||||
|
req := templating.NewRequest(e)
|
||||||
|
|
||||||
|
if err := e.Request.ParseForm(); err != nil {
|
||||||
|
return p.renderError(engine, app, e, "Formulardaten ungültig.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := req.CheckCSRF(e.Request.FormValue("csrf_token")); err != nil {
|
||||||
|
return p.renderError(engine, app, e, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
contentID := strings.TrimSpace(e.Request.FormValue("content_id"))
|
||||||
|
if contentID == "" {
|
||||||
|
return p.renderError(engine, app, e, "Beitrag konnte nicht gelöscht werden.")
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := dbmodels.Entries_MusenalmID(app, id)
|
||||||
|
if err != nil {
|
||||||
|
return engine.Response404(e, err, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var remaining []*dbmodels.Content
|
||||||
if err := app.RunInTransaction(func(tx core.App) error {
|
if err := app.RunInTransaction(func(tx core.App) error {
|
||||||
for _, content := range contents {
|
record, err := tx.FindRecordById(dbmodels.CONTENTS_TABLE, contentID)
|
||||||
fields, ok := contentInputs[content.Id]
|
if err != nil {
|
||||||
if !ok {
|
return err
|
||||||
|
}
|
||||||
|
content := dbmodels.NewContent(record)
|
||||||
|
if content.Entry() != entry.Id {
|
||||||
|
return fmt.Errorf("Beitrag gehört zu einem anderen Band.")
|
||||||
|
}
|
||||||
|
|
||||||
|
relationsTable := dbmodels.RelationTableName(dbmodels.CONTENTS_TABLE, dbmodels.AGENTS_TABLE)
|
||||||
|
relations, err := dbmodels.RContentsAgents_Content(tx, contentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, rel := range relations {
|
||||||
|
relRecord, err := tx.FindRecordById(relationsTable, rel.Id)
|
||||||
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := applyContentForm(content, entry, fields, user); err != nil {
|
if err := tx.Delete(relRecord); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Delete(record); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining, err = dbmodels.Contents_Entry(tx, entry.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dbmodels.Sort_Contents_Numbering(remaining)
|
||||||
|
for idx, content := range remaining {
|
||||||
|
content.SetNumbering(float64(idx + 1))
|
||||||
if err := tx.Save(content); err != nil {
|
if err := tx.Save(content); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
app.Logger().Error("Failed to save contents", "entry_id", entry.Id, "error", err)
|
app.Logger().Error("Failed to delete content", "entry_id", entry.Id, "content_id", contentID, "error", err)
|
||||||
return p.renderError(engine, app, e, err.Error())
|
return p.renderError(engine, app, e, "Beitrag konnte nicht gelöscht werden.")
|
||||||
}
|
}
|
||||||
|
|
||||||
go updateContentsFTS5(app, entry, contents)
|
go func(contentID string) {
|
||||||
|
_ = dbmodels.DeleteFTS5Content(app, contentID)
|
||||||
|
}(contentID)
|
||||||
|
|
||||||
redirect := fmt.Sprintf("/almanach/%s/contents/edit?saved_message=%s", id, url.QueryEscape("Änderungen gespeichert."))
|
if len(remaining) > 0 {
|
||||||
|
go updateContentsFTS5(app, entry, remaining)
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect := fmt.Sprintf("/almanach/%s/contents/edit", id)
|
||||||
return e.Redirect(http.StatusSeeOther, redirect)
|
return e.Redirect(http.StatusSeeOther, redirect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *AlmanachContentsEditPage) renderSaveError(
|
||||||
|
engine *templating.Engine,
|
||||||
|
app core.App,
|
||||||
|
e *core.RequestEvent,
|
||||||
|
req *templating.Request,
|
||||||
|
entry *dbmodels.Entry,
|
||||||
|
contentInputs map[string]map[string][]string,
|
||||||
|
message string,
|
||||||
|
isHTMX bool,
|
||||||
|
) error {
|
||||||
|
if !isHTMX {
|
||||||
|
return p.renderError(engine, app, e, message)
|
||||||
|
}
|
||||||
|
if entry == nil {
|
||||||
|
id := e.Request.PathValue("id")
|
||||||
|
result, err := dbmodels.Entries_MusenalmID(app, id)
|
||||||
|
if err != nil {
|
||||||
|
return p.renderError(engine, app, e, message)
|
||||||
|
}
|
||||||
|
entry = result
|
||||||
|
}
|
||||||
|
|
||||||
|
contentID := ""
|
||||||
|
fields := map[string][]string{}
|
||||||
|
if contentInputs != nil {
|
||||||
|
for id, values := range contentInputs {
|
||||||
|
contentID = id
|
||||||
|
fields = values
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if contentID == "" {
|
||||||
|
return p.renderError(engine, app, e, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
content := (*dbmodels.Content)(nil)
|
||||||
|
contents, err := dbmodels.Contents_Entry(app, entry.Id)
|
||||||
|
if err == nil {
|
||||||
|
for _, existing := range contents {
|
||||||
|
if existing.Id == contentID {
|
||||||
|
content = existing
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isNew := false
|
||||||
|
if content == nil {
|
||||||
|
contentCollection, err := app.FindCollectionByNameOrId(dbmodels.CONTENTS_TABLE)
|
||||||
|
if err != nil {
|
||||||
|
return p.renderError(engine, app, e, message)
|
||||||
|
}
|
||||||
|
record := core.NewRecord(contentCollection)
|
||||||
|
record.Id = contentID
|
||||||
|
record.MarkAsNew()
|
||||||
|
content = dbmodels.NewContent(record)
|
||||||
|
content.SetEditState("Edited")
|
||||||
|
isNew = true
|
||||||
|
}
|
||||||
|
|
||||||
|
numbering := 0.0
|
||||||
|
if order := parseContentsOrder(e.Request.PostForm); len(order) > 0 {
|
||||||
|
if mapped, ok := buildContentOrderMap(order)[contentID]; ok {
|
||||||
|
numbering = mapped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyContentFormDraft(content, entry, fields, numbering)
|
||||||
|
|
||||||
|
data := map[string]any{
|
||||||
|
"content": content,
|
||||||
|
"content_id": contentID,
|
||||||
|
"entry": entry,
|
||||||
|
"csrf_token": req.Session().Token,
|
||||||
|
"content_types": dbmodels.CONTENT_TYPE_VALUES,
|
||||||
|
"musenalm_types": dbmodels.MUSENALM_TYPE_VALUES,
|
||||||
|
"pagination_values": paginationValuesSorted(),
|
||||||
|
"open_edit": true,
|
||||||
|
"is_new": isNew,
|
||||||
|
"error": message,
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
if err := engine.Render(&builder, "/almanach/contents/item/", data, "fragment"); err != nil {
|
||||||
|
app.Logger().Error("Failed to render content save error", "entry_id", entry.Id, "content_id", contentID, "error", err)
|
||||||
|
return p.renderError(engine, app, e, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.HTML(http.StatusOK, builder.String())
|
||||||
|
}
|
||||||
|
|
||||||
func parseContentsForm(form url.Values) map[string]map[string][]string {
|
func parseContentsForm(form url.Values) map[string]map[string][]string {
|
||||||
contentInputs := map[string]map[string][]string{}
|
contentInputs := map[string]map[string][]string{}
|
||||||
for key, values := range form {
|
for key, values := range form {
|
||||||
@@ -168,8 +482,8 @@ func parseContentsForm(form url.Values) map[string]map[string][]string {
|
|||||||
return contentInputs
|
return contentInputs
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyContentForm(content *dbmodels.Content, entry *dbmodels.Entry, fields map[string][]string, user *dbmodels.FixedUser) error {
|
func applyContentForm(content *dbmodels.Content, entry *dbmodels.Entry, fields map[string][]string, user *dbmodels.FixedUser, numbering float64) error {
|
||||||
preferredTitle := strings.TrimSpace(firstValue(fields["preferred_title"]))
|
preferredTitle := buildContentPreferredTitle(content, fields)
|
||||||
if preferredTitle == "" {
|
if preferredTitle == "" {
|
||||||
label := content.Id
|
label := content.Id
|
||||||
if content.Numbering() > 0 {
|
if content.Numbering() > 0 {
|
||||||
@@ -178,26 +492,6 @@ func applyContentForm(content *dbmodels.Content, entry *dbmodels.Entry, fields m
|
|||||||
return fmt.Errorf("Kurztitel ist erforderlich (Beitrag %s).", label)
|
return fmt.Errorf("Kurztitel ist erforderlich (Beitrag %s).", label)
|
||||||
}
|
}
|
||||||
|
|
||||||
yearValue := strings.TrimSpace(firstValue(fields["year"]))
|
|
||||||
year := 0
|
|
||||||
if yearValue != "" {
|
|
||||||
parsed, err := strconv.Atoi(yearValue)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Ungültiges Jahr (Beitrag %s).", content.Id)
|
|
||||||
}
|
|
||||||
year = parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
numberingValue := strings.TrimSpace(firstValue(fields["numbering"]))
|
|
||||||
numbering := 0.0
|
|
||||||
if numberingValue != "" {
|
|
||||||
parsed, err := strconv.ParseFloat(numberingValue, 64)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Ungültige Nummerierung (Beitrag %s).", content.Id)
|
|
||||||
}
|
|
||||||
numbering = parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
status := strings.TrimSpace(firstValue(fields["edit_state"]))
|
status := strings.TrimSpace(firstValue(fields["edit_state"]))
|
||||||
if status == "" {
|
if status == "" {
|
||||||
status = content.EditState()
|
status = content.EditState()
|
||||||
@@ -206,34 +500,180 @@ func applyContentForm(content *dbmodels.Content, entry *dbmodels.Entry, fields m
|
|||||||
return fmt.Errorf("Ungültiger Status (Beitrag %s).", content.Id)
|
return fmt.Errorf("Ungültiger Status (Beitrag %s).", content.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
musenalmTypes := sanitizeContentStrings(fields["musenalm_type"])
|
||||||
|
if len(musenalmTypes) == 0 {
|
||||||
|
return fmt.Errorf("Musenalm-Typ ist erforderlich (Beitrag %s).", content.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if numbering <= 0 {
|
||||||
|
numbering = content.Numbering()
|
||||||
|
}
|
||||||
|
|
||||||
content.SetPreferredTitle(preferredTitle)
|
content.SetPreferredTitle(preferredTitle)
|
||||||
content.SetVariantTitle(strings.TrimSpace(firstValue(fields["variant_title"])))
|
if value, ok := optionalFieldValue(fields, "variant_title"); ok {
|
||||||
content.SetParallelTitle(strings.TrimSpace(firstValue(fields["parallel_title"])))
|
content.SetVariantTitle(value)
|
||||||
content.SetTitleStmt(strings.TrimSpace(firstValue(fields["title_statement"])))
|
}
|
||||||
content.SetSubtitleStmt(strings.TrimSpace(firstValue(fields["subtitle_statement"])))
|
if value, ok := optionalFieldValue(fields, "parallel_title"); ok {
|
||||||
content.SetIncipitStmt(strings.TrimSpace(firstValue(fields["incipit_statement"])))
|
content.SetParallelTitle(value)
|
||||||
content.SetResponsibilityStmt(strings.TrimSpace(firstValue(fields["responsibility_statement"])))
|
}
|
||||||
content.SetPlaceStmt(strings.TrimSpace(firstValue(fields["place_statement"])))
|
if value, ok := optionalFieldValue(fields, "title_statement"); ok {
|
||||||
content.SetPublicationStmt(strings.TrimSpace(firstValue(fields["publication_statement"])))
|
content.SetTitleStmt(value)
|
||||||
content.SetYear(year)
|
}
|
||||||
|
if value, ok := optionalFieldValue(fields, "subtitle_statement"); ok {
|
||||||
|
content.SetSubtitleStmt(value)
|
||||||
|
}
|
||||||
|
if value, ok := optionalFieldValue(fields, "incipit_statement"); ok {
|
||||||
|
content.SetIncipitStmt(value)
|
||||||
|
}
|
||||||
|
if value, ok := optionalFieldValue(fields, "responsibility_statement"); ok {
|
||||||
|
content.SetResponsibilityStmt(value)
|
||||||
|
}
|
||||||
|
if value, ok := optionalFieldValue(fields, "place_statement"); ok {
|
||||||
|
content.SetPlaceStmt(value)
|
||||||
|
}
|
||||||
|
content.SetYear(entry.Year())
|
||||||
content.SetExtent(strings.TrimSpace(firstValue(fields["extent"])))
|
content.SetExtent(strings.TrimSpace(firstValue(fields["extent"])))
|
||||||
content.SetDimensions(strings.TrimSpace(firstValue(fields["dimensions"])))
|
content.SetLanguage(sanitizeContentStrings(fields["language"]))
|
||||||
content.SetLanguage(fields["language"])
|
if values, ok := fields["content_type"]; ok {
|
||||||
content.SetContentType(fields["content_type"])
|
content.SetContentType(sanitizeContentStrings(values))
|
||||||
content.SetMusenalmType(fields["musenalm_type"])
|
}
|
||||||
|
content.SetMusenalmType(musenalmTypes)
|
||||||
content.SetMusenalmPagination(strings.TrimSpace(firstValue(fields["musenalm_pagination"])))
|
content.SetMusenalmPagination(strings.TrimSpace(firstValue(fields["musenalm_pagination"])))
|
||||||
content.SetNumbering(numbering)
|
content.SetNumbering(numbering)
|
||||||
content.SetEntry(entry.Id)
|
content.SetEntry(entry.Id)
|
||||||
content.SetMusenalmID(entry.MusenalmID())
|
|
||||||
content.SetEditState(status)
|
content.SetEditState(status)
|
||||||
content.SetComment(strings.TrimSpace(firstValue(fields["edit_comment"])))
|
if value, ok := optionalFieldValue(fields, "edit_comment"); ok {
|
||||||
content.SetAnnotation(strings.TrimSpace(firstValue(fields["annotation"])))
|
content.SetComment(value)
|
||||||
|
}
|
||||||
|
if value, ok := optionalFieldValue(fields, "annotation"); ok {
|
||||||
|
content.SetAnnotation(value)
|
||||||
|
}
|
||||||
if user != nil {
|
if user != nil {
|
||||||
content.SetEditor(user.Id)
|
content.SetEditor(user.Id)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyContentFormDraft(content *dbmodels.Content, entry *dbmodels.Entry, fields map[string][]string, numbering float64) {
|
||||||
|
if value, ok := optionalFieldValue(fields, "variant_title"); ok {
|
||||||
|
content.SetVariantTitle(value)
|
||||||
|
}
|
||||||
|
if value, ok := optionalFieldValue(fields, "parallel_title"); ok {
|
||||||
|
content.SetParallelTitle(value)
|
||||||
|
}
|
||||||
|
if value, ok := optionalFieldValue(fields, "title_statement"); ok {
|
||||||
|
content.SetTitleStmt(value)
|
||||||
|
}
|
||||||
|
if value, ok := optionalFieldValue(fields, "subtitle_statement"); ok {
|
||||||
|
content.SetSubtitleStmt(value)
|
||||||
|
}
|
||||||
|
if value, ok := optionalFieldValue(fields, "incipit_statement"); ok {
|
||||||
|
content.SetIncipitStmt(value)
|
||||||
|
}
|
||||||
|
if value, ok := optionalFieldValue(fields, "responsibility_statement"); ok {
|
||||||
|
content.SetResponsibilityStmt(value)
|
||||||
|
}
|
||||||
|
if value, ok := optionalFieldValue(fields, "place_statement"); ok {
|
||||||
|
content.SetPlaceStmt(value)
|
||||||
|
}
|
||||||
|
if value, ok := optionalFieldValue(fields, "edit_comment"); ok {
|
||||||
|
content.SetComment(value)
|
||||||
|
}
|
||||||
|
if value, ok := optionalFieldValue(fields, "annotation"); ok {
|
||||||
|
content.SetAnnotation(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
content.SetExtent(strings.TrimSpace(firstValue(fields["extent"])))
|
||||||
|
content.SetLanguage(sanitizeContentStrings(fields["language"]))
|
||||||
|
if values, ok := fields["content_type"]; ok {
|
||||||
|
content.SetContentType(sanitizeContentStrings(values))
|
||||||
|
}
|
||||||
|
content.SetMusenalmType(sanitizeContentStrings(fields["musenalm_type"]))
|
||||||
|
content.SetMusenalmPagination(strings.TrimSpace(firstValue(fields["musenalm_pagination"])))
|
||||||
|
content.SetEntry(entry.Id)
|
||||||
|
content.SetYear(entry.Year())
|
||||||
|
|
||||||
|
if status := strings.TrimSpace(firstValue(fields["edit_state"])); status != "" {
|
||||||
|
content.SetEditState(status)
|
||||||
|
}
|
||||||
|
if numbering > 0 {
|
||||||
|
content.SetNumbering(numbering)
|
||||||
|
}
|
||||||
|
if preferredTitle := buildContentPreferredTitle(content, fields); preferredTitle != "" {
|
||||||
|
content.SetPreferredTitle(preferredTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeContentStrings(values []string) []string {
|
||||||
|
cleaned := make([]string, 0, len(values))
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
for _, value := range values {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[value]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = struct{}{}
|
||||||
|
cleaned = append(cleaned, value)
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildContentPreferredTitle(content *dbmodels.Content, fields map[string][]string) string {
|
||||||
|
title := fieldOrCurrent(fields, "title_statement", content.TitleStmt())
|
||||||
|
if title != "" {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
subtitle := fieldOrCurrent(fields, "subtitle_statement", content.SubtitleStmt())
|
||||||
|
if subtitle != "" {
|
||||||
|
return subtitle
|
||||||
|
}
|
||||||
|
incipit := fieldOrCurrent(fields, "incipit_statement", content.IncipitStmt())
|
||||||
|
if incipit != "" {
|
||||||
|
return incipit
|
||||||
|
}
|
||||||
|
|
||||||
|
types := fields["musenalm_type"]
|
||||||
|
if _, ok := fields["musenalm_type"]; !ok {
|
||||||
|
types = content.MusenalmType()
|
||||||
|
}
|
||||||
|
types = sanitizeContentStrings(types)
|
||||||
|
typeLabel := strings.Join(types, ", ")
|
||||||
|
responsibility := fieldOrCurrent(fields, "responsibility_statement", content.ResponsibilityStmt())
|
||||||
|
if responsibility != "" && !strings.EqualFold(responsibility, "unbezeichnet") {
|
||||||
|
if typeLabel != "" {
|
||||||
|
return fmt.Sprintf("[%s] Unterzeichnet: %s", typeLabel, responsibility)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Unterzeichnet: %s", responsibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
extent := fieldOrCurrent(fields, "extent", content.Extent())
|
||||||
|
if typeLabel == "" {
|
||||||
|
typeLabel = "Beitrag"
|
||||||
|
}
|
||||||
|
if extent != "" {
|
||||||
|
return fmt.Sprintf("[%s %s]", typeLabel, extent)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[%s]", typeLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func optionalFieldValue(fields map[string][]string, key string) (string, bool) {
|
||||||
|
values, ok := fields[key]
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(firstValue(values)), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldOrCurrent(fields map[string][]string, key, current string) string {
|
||||||
|
if value, ok := optionalFieldValue(fields, key); ok {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(current)
|
||||||
|
}
|
||||||
|
|
||||||
func firstValue(values []string) string {
|
func firstValue(values []string) string {
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
return ""
|
return ""
|
||||||
@@ -241,6 +681,30 @@ func firstValue(values []string) string {
|
|||||||
return values[0]
|
return values[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseContentsOrder(form url.Values) []string {
|
||||||
|
raw := form["content_order[]"]
|
||||||
|
if len(raw) == 0 {
|
||||||
|
raw = form["content_order"]
|
||||||
|
}
|
||||||
|
order := make([]string, 0, len(raw))
|
||||||
|
for _, value := range raw {
|
||||||
|
trimmed := strings.TrimSpace(value)
|
||||||
|
if trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
order = append(order, trimmed)
|
||||||
|
}
|
||||||
|
return order
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildContentOrderMap(order []string) map[string]float64 {
|
||||||
|
orderMap := make(map[string]float64, len(order))
|
||||||
|
for index, id := range order {
|
||||||
|
orderMap[id] = float64(index + 1)
|
||||||
|
}
|
||||||
|
return orderMap
|
||||||
|
}
|
||||||
|
|
||||||
func updateContentsFTS5(app core.App, entry *dbmodels.Entry, contents []*dbmodels.Content) {
|
func updateContentsFTS5(app core.App, entry *dbmodels.Entry, contents []*dbmodels.Content) {
|
||||||
if len(contents) == 0 {
|
if len(contents) == 0 {
|
||||||
return
|
return
|
||||||
@@ -262,3 +726,40 @@ func updateContentsFTS5(app core.App, entry *dbmodels.Entry, contents []*dbmodel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func paginationValuesSorted() []string {
|
||||||
|
orderedKeys := []string{"", "ar", "röm", "alph", "sonst"}
|
||||||
|
for i := 1; i <= 8; i++ {
|
||||||
|
orderedKeys = append(orderedKeys, fmt.Sprintf("ar%d", i))
|
||||||
|
}
|
||||||
|
for i := 1; i <= 8; i++ {
|
||||||
|
orderedKeys = append(orderedKeys, fmt.Sprintf("röm%d", i))
|
||||||
|
}
|
||||||
|
|
||||||
|
values := make([]string, 0, len(dbmodels.MUSENALM_PAGINATION_VALUES))
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
for _, key := range orderedKeys {
|
||||||
|
value, ok := dbmodels.MUSENALM_PAGINATION_VALUES[key]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[value]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = struct{}{}
|
||||||
|
values = append(values, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingKeys := slices.Collect(maps.Keys(dbmodels.MUSENALM_PAGINATION_VALUES))
|
||||||
|
sort.Strings(remainingKeys)
|
||||||
|
for _, key := range remainingKeys {
|
||||||
|
value := dbmodels.MUSENALM_PAGINATION_VALUES[key]
|
||||||
|
if _, exists := seen[value]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = struct{}{}
|
||||||
|
values = append(values, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,3 +67,18 @@ func nextPlaceMusenalmID(app core.App) (int, error) {
|
|||||||
}
|
}
|
||||||
return place.MusenalmID() + 1, nil
|
return place.MusenalmID() + 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nextContentMusenalmID(app core.App) (int, error) {
|
||||||
|
var content dbmodels.Content
|
||||||
|
err := app.RecordQuery(dbmodels.CONTENTS_TABLE).
|
||||||
|
OrderBy(dbmodels.MUSENALMID_FIELD + " DESC").
|
||||||
|
Limit(1).
|
||||||
|
One(&content)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return content.MusenalmID() + 1, nil
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1 @@
|
|||||||
{{ block "body" . }}
|
{{ block "body" . }}{{ end }}
|
||||||
{{ end }}
|
|
||||||
|
|||||||
@@ -117,44 +117,25 @@
|
|||||||
|
|
||||||
<div class="container-normal mx-auto mt-4 !px-0">
|
<div class="container-normal mx-auto mt-4 !px-0">
|
||||||
{{ template "_usermessage" $model }}
|
{{ template "_usermessage" $model }}
|
||||||
<form
|
<div id="contents-sync-indicator" class="fixed right-6 bottom-6 z-50 hidden rounded-full bg-stone-200 px-3 py-2 text-sm text-stone-700 shadow-md">
|
||||||
autocomplete="off"
|
<i class="ri-loader-4-line spinning mr-2"></i>
|
||||||
class="w-full dbform"
|
Reihenfolge wird gespeichert
|
||||||
id="changecontentsform"
|
</div>
|
||||||
method="POST"
|
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" data-role="csrf-token" />
|
||||||
action="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/edit">
|
<div class="flex flex-col gap-1" data-role="contents-list" data-insert-endpoint="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/insert">
|
||||||
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
|
{{- range $_, $content := $model.result.Contents -}}
|
||||||
|
{{- template "_content_item" (Dict
|
||||||
<div class="flex flex-col gap-3">
|
"content" $content
|
||||||
{{- range $_, $content := $model.result.Contents -}}
|
"entry" $model.result.Entry
|
||||||
{{- template "_content_edit" (Dict
|
"csrf_token" $model.csrf_token
|
||||||
"content" $content
|
"content_types" $model.content_types
|
||||||
"entry" $model.result.Entry
|
"musenalm_types" $model.musenalm_types
|
||||||
"content_types" $model.content_types
|
"pagination_values" $model.pagination_values
|
||||||
"musenalm_types" $model.musenalm_types
|
"open_edit" false
|
||||||
"pagination_values" $model.pagination_values
|
"is_new" false
|
||||||
) -}}
|
) -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex items-end justify-between gap-4 mt-6 flex-wrap">
|
|
||||||
<p class="text-sm text-gray-600" aria-live="polite"></p>
|
|
||||||
<div class="flex items-center gap-3 self-end flex-wrap">
|
|
||||||
<a href="/almanach/{{ $model.result.Entry.MusenalmID }}" class="resetbutton w-48 flex items-center gap-2 justify-center">
|
|
||||||
<i class="ri-close-line"></i>
|
|
||||||
<span>Abbrechen</span>
|
|
||||||
</a>
|
|
||||||
<a href="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/edit" class="resetbutton w-48 flex items-center gap-2 justify-center">
|
|
||||||
<i class="ri-loop-left-line"></i>
|
|
||||||
<span>Reset</span>
|
|
||||||
</a>
|
|
||||||
<button type="submit" class="submitbutton w-48 flex items-center gap-2 justify-center">
|
|
||||||
<i class="ri-save-line"></i>
|
|
||||||
<span>Speichern</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</edit-page>
|
</edit-page>
|
||||||
|
|
||||||
@@ -183,20 +164,417 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initPage = () => {
|
const initPage = () => {
|
||||||
document.querySelectorAll(".content-numbering").forEach((input, index) => {
|
const list = document.querySelector("[data-role='contents-list']");
|
||||||
input.value = index + 1;
|
if (!list) {
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
document
|
const getItems = () => Array.from(list.querySelectorAll("[data-role='content-item']"));
|
||||||
.querySelectorAll("multi-select-simple[data-initial-options], multi-select-simple[data-initial-values]")
|
const removeGaps = () => {
|
||||||
.forEach((el) => applyMultiSelectInit(el));
|
list.querySelectorAll("[data-role='content-gap']").forEach((gap) => gap.remove());
|
||||||
|
};
|
||||||
|
const renderInsertGaps = () => {
|
||||||
|
removeGaps();
|
||||||
|
const insertEndpoint = list.dataset.insertEndpoint || "";
|
||||||
|
const items = getItems();
|
||||||
|
if (!insertEndpoint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (list.querySelector("[data-role='content-item'].data-editing")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const createGap = (position, contentId, label) => {
|
||||||
|
const gap = document.createElement("div");
|
||||||
|
gap.className = "relative group h-6 -my-2.5";
|
||||||
|
gap.dataset.role = "content-gap";
|
||||||
|
const line = document.createElement("div");
|
||||||
|
line.className = "pointer-events-none absolute left-0 right-0 top-1/2 h-0.5 -translate-y-1/2 bg-slate-300 opacity-0 transition-opacity duration-150 group-hover:opacity-100";
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "absolute left-1/2 top-1/2 z-[10000] -translate-x-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-150 rounded-full border border-slate-300 bg-stone-100 text-slate-700 px-3 py-2 text-base shadow-sm";
|
||||||
|
button.setAttribute("aria-label", "Beitrag einfügen");
|
||||||
|
button.setAttribute("hx-post", insertEndpoint);
|
||||||
|
button.setAttribute("hx-target", "closest [data-role='content-gap']");
|
||||||
|
button.setAttribute("hx-swap", "beforebegin");
|
||||||
|
button.setAttribute("hx-include", "[data-role='csrf-token']");
|
||||||
|
button.setAttribute("hx-vals", JSON.stringify({ position, content_id: contentId }));
|
||||||
|
button.innerHTML = label ? '<i class="ri-add-line"></i><span>Neuer Beitrag</span>' : '<i class="ri-add-line"></i>';
|
||||||
|
gap.appendChild(line);
|
||||||
|
gap.appendChild(button);
|
||||||
|
gap.appendChild(document.createElement("div")).className = "h-1";
|
||||||
|
return gap;
|
||||||
|
};
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
const contentId = item.querySelector("[data-role='content-card']")?.dataset.contentId || "";
|
||||||
|
list.insertBefore(createGap("before", contentId, false), item);
|
||||||
|
});
|
||||||
|
list.appendChild(createGap("after", "", true));
|
||||||
|
if (window.htmx?.process) {
|
||||||
|
list.querySelectorAll("[data-role='content-gap']").forEach((gap) => {
|
||||||
|
window.htmx.process(gap);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const setEditSpacing = (active) => {
|
||||||
|
list.style.rowGap = active ? "0.75rem" : "";
|
||||||
|
list.style.paddingTop = active ? "0.25rem" : "";
|
||||||
|
list.style.paddingBottom = active ? "0.25rem" : "";
|
||||||
|
};
|
||||||
|
const syncEditSpacing = () => {
|
||||||
|
setEditSpacing(!!list.querySelector("[data-role='content-item'].data-editing"));
|
||||||
|
};
|
||||||
|
if (getItems().length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTempContentId = (contentId) => contentId && contentId.startsWith("tmp");
|
||||||
|
const orderEndpoint = getItems()[0]?.querySelector("form")?.getAttribute("action") || "";
|
||||||
|
const deleteEndpoint = window.location.pathname.replace(/\/contents\/edit\/?$/, "/contents/delete");
|
||||||
|
const csrfToken = document.querySelector("input[name='csrf_token']")?.value || "";
|
||||||
|
const syncIndicator = document.querySelector("#contents-sync-indicator");
|
||||||
|
let orderSyncTimer = null;
|
||||||
|
let isOrderSyncing = false;
|
||||||
|
let pendingOrderSync = false;
|
||||||
|
|
||||||
|
const setSyncIndicator = (active) => {
|
||||||
|
if (!syncIndicator) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
syncIndicator.classList.toggle("hidden", !active);
|
||||||
|
};
|
||||||
|
|
||||||
|
const performOrderSync = () => {
|
||||||
|
if (!list || !orderEndpoint || !csrfToken || isOrderSyncing) {
|
||||||
|
pendingOrderSync = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isOrderSyncing = true;
|
||||||
|
pendingOrderSync = false;
|
||||||
|
setSyncIndicator(true);
|
||||||
|
const payload = new URLSearchParams();
|
||||||
|
payload.set("csrf_token", csrfToken);
|
||||||
|
list.querySelectorAll("[data-role='content-item']").forEach((card) => {
|
||||||
|
const contentId = card.querySelector("[data-role='content-card']")?.dataset.contentId;
|
||||||
|
if (!contentId || isTempContentId(contentId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.append("content_order[]", contentId);
|
||||||
|
});
|
||||||
|
fetch(orderEndpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: payload.toString(),
|
||||||
|
})
|
||||||
|
.catch(() => null)
|
||||||
|
.finally(() => {
|
||||||
|
isOrderSyncing = false;
|
||||||
|
setSyncIndicator(false);
|
||||||
|
if (pendingOrderSync) {
|
||||||
|
performOrderSync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncOrder = () => {
|
||||||
|
if (orderSyncTimer) {
|
||||||
|
clearTimeout(orderSyncTimer);
|
||||||
|
}
|
||||||
|
orderSyncTimer = setTimeout(() => {
|
||||||
|
performOrderSync();
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeAll = () => {
|
||||||
|
getItems().forEach((item) => {
|
||||||
|
const view = item.querySelector("[data-role='content-view']");
|
||||||
|
const edit = item.querySelector("[data-role='content-edit']");
|
||||||
|
const editButton = item.querySelector("[data-role='content-edit-button']");
|
||||||
|
item.classList.remove("data-editing");
|
||||||
|
if (view) {
|
||||||
|
view.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
if (edit) {
|
||||||
|
edit.classList.add("hidden");
|
||||||
|
}
|
||||||
|
if (editButton) {
|
||||||
|
editButton.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openItem = (item) => {
|
||||||
|
closeAll();
|
||||||
|
const view = item.querySelector("[data-role='content-view']");
|
||||||
|
const edit = item.querySelector("[data-role='content-edit']");
|
||||||
|
if (view && edit) {
|
||||||
|
view.classList.add("hidden");
|
||||||
|
edit.classList.remove("hidden");
|
||||||
|
item.classList.add("data-editing");
|
||||||
|
}
|
||||||
|
setEditSpacing(true);
|
||||||
|
removeGaps();
|
||||||
|
getItems().forEach((other) => {
|
||||||
|
const otherButton = other.querySelector("[data-role='content-edit-button']");
|
||||||
|
if (!otherButton || other === item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
otherButton.classList.add("hidden");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeItem = (item) => {
|
||||||
|
const gap = item.previousElementSibling;
|
||||||
|
if (gap && gap.matches("[data-role='content-gap']")) {
|
||||||
|
gap.remove();
|
||||||
|
}
|
||||||
|
item.remove();
|
||||||
|
renderInsertGaps();
|
||||||
|
getItems().forEach((other) => {
|
||||||
|
const otherButton = other.querySelector("[data-role='content-edit-button']");
|
||||||
|
if (otherButton) {
|
||||||
|
otherButton.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupItem = (item) => {
|
||||||
|
if (!item || item.dataset.init === "true") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
item.dataset.init = "true";
|
||||||
|
|
||||||
|
const editButton = item.querySelector("[data-role='content-edit-button']");
|
||||||
|
const cancelButton = item.querySelector("[data-role='content-edit-cancel']");
|
||||||
|
const deleteButton = item.querySelector("[data-role='content-delete']");
|
||||||
|
const deleteDialog = item.querySelector("[data-role='content-delete-dialog']");
|
||||||
|
const deleteConfirm = item.querySelector("[data-role='content-delete-confirm']");
|
||||||
|
const deleteCancel = item.querySelector("[data-role='content-delete-cancel']");
|
||||||
|
const view = item.querySelector("[data-role='content-view']");
|
||||||
|
const edit = item.querySelector("[data-role='content-edit']");
|
||||||
|
const form = item.querySelector("form");
|
||||||
|
|
||||||
|
if (editButton && view && edit) {
|
||||||
|
editButton.addEventListener("click", () => {
|
||||||
|
openItem(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelButton && view && edit) {
|
||||||
|
cancelButton.addEventListener("click", () => {
|
||||||
|
if (item.dataset.contentTemp === "true") {
|
||||||
|
removeItem(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
edit.classList.add("hidden");
|
||||||
|
view.classList.remove("hidden");
|
||||||
|
item.classList.remove("data-editing");
|
||||||
|
getItems().forEach((other) => {
|
||||||
|
const otherButton = other.querySelector("[data-role='content-edit-button']");
|
||||||
|
if (otherButton) {
|
||||||
|
otherButton.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
syncEditSpacing();
|
||||||
|
renderInsertGaps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteButton && deleteDialog) {
|
||||||
|
deleteButton.addEventListener("click", () => {
|
||||||
|
if (deleteDialog.showModal) {
|
||||||
|
deleteDialog.showModal();
|
||||||
|
} else {
|
||||||
|
deleteDialog.setAttribute("open", "true");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteCancel && deleteDialog) {
|
||||||
|
deleteCancel.addEventListener("click", () => {
|
||||||
|
deleteDialog.close();
|
||||||
|
});
|
||||||
|
deleteDialog.addEventListener("cancel", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
deleteDialog.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteConfirm) {
|
||||||
|
deleteConfirm.addEventListener("click", () => {
|
||||||
|
if (item.dataset.contentTemp === "true") {
|
||||||
|
deleteDialog?.close();
|
||||||
|
removeItem(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = new URLSearchParams();
|
||||||
|
payload.set("csrf_token", csrfToken);
|
||||||
|
payload.set("content_id", item.querySelector("[data-role='content-card']")?.dataset.contentId || "");
|
||||||
|
fetch(deleteEndpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: payload.toString(),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.redirected) {
|
||||||
|
window.location.assign(response.url);
|
||||||
|
} else {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener("submit", () => {
|
||||||
|
form.querySelectorAll("input[name='content_order[]']").forEach((input) => input.remove());
|
||||||
|
getItems().forEach((card) => {
|
||||||
|
const contentId = card.querySelector("[data-role='content-card']")?.dataset.contentId;
|
||||||
|
if (!contentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "hidden";
|
||||||
|
input.name = "content_order[]";
|
||||||
|
input.value = contentId;
|
||||||
|
form.appendChild(input);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
item.querySelectorAll("multi-select-simple[data-initial-options], multi-select-simple[data-initial-values]").forEach((el) => {
|
||||||
|
applyMultiSelectInit(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (item.dataset.openEdit === "true") {
|
||||||
|
item.dataset.openEdit = "";
|
||||||
|
openItem(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getItems().forEach((item) => setupItem(item));
|
||||||
|
renderInsertGaps();
|
||||||
|
syncEditSpacing();
|
||||||
|
|
||||||
|
if (list.dataset.pageInit !== "true") {
|
||||||
|
list.dataset.pageInit = "true";
|
||||||
|
let draggedItem = null;
|
||||||
|
list.addEventListener("click", (event) => {
|
||||||
|
const moveUp = event.target.closest("[data-role='content-move-up']");
|
||||||
|
const moveDown = event.target.closest("[data-role='content-move-down']");
|
||||||
|
if (!moveUp && !moveDown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
const item = event.target.closest("[data-role='content-item']");
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (moveUp) {
|
||||||
|
const prev = item.previousElementSibling;
|
||||||
|
if (prev) {
|
||||||
|
prev.before(item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const next = item.nextElementSibling;
|
||||||
|
if (next) {
|
||||||
|
next.after(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syncOrder();
|
||||||
|
renderInsertGaps();
|
||||||
|
});
|
||||||
|
|
||||||
|
list.addEventListener("dragstart", (event) => {
|
||||||
|
if (event.target.closest("[data-role='content-edit-button']")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.target.closest("[data-role='content-move-up']") || event.target.closest("[data-role='content-move-down']")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.target.closest(".status-badge") || event.target.closest("multi-select-simple") || event.target.closest("select")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const handle = event.target.closest("[data-role='content-drag-handle']");
|
||||||
|
if (!handle) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const item = handle.closest("[data-role='content-item']");
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
draggedItem = item;
|
||||||
|
draggedItem.classList.add("opacity-60");
|
||||||
|
list.style.rowGap = "0.75rem";
|
||||||
|
list.style.paddingTop = "0.25rem";
|
||||||
|
list.style.paddingBottom = "0.25rem";
|
||||||
|
removeGaps();
|
||||||
|
event.dataTransfer.effectAllowed = "move";
|
||||||
|
event.dataTransfer.setData("text/plain", "move");
|
||||||
|
});
|
||||||
|
|
||||||
|
list.addEventListener("dragover", (event) => {
|
||||||
|
if (!draggedItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
const targetItem = event.target.closest("[data-role='content-item']");
|
||||||
|
if (!targetItem || targetItem === draggedItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rect = targetItem.getBoundingClientRect();
|
||||||
|
const before = event.clientY - rect.top < rect.height / 2;
|
||||||
|
if (before) {
|
||||||
|
targetItem.before(draggedItem);
|
||||||
|
} else {
|
||||||
|
targetItem.after(draggedItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
list.addEventListener("dragend", () => {
|
||||||
|
if (draggedItem) {
|
||||||
|
draggedItem.classList.remove("opacity-60");
|
||||||
|
}
|
||||||
|
draggedItem = null;
|
||||||
|
list.style.rowGap = "";
|
||||||
|
list.style.paddingTop = "";
|
||||||
|
list.style.paddingBottom = "";
|
||||||
|
syncOrder();
|
||||||
|
renderInsertGaps();
|
||||||
|
});
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const editContentId = params.get("edit_content");
|
||||||
|
if (editContentId) {
|
||||||
|
const targetItem = getItems().find((item) => {
|
||||||
|
return item.querySelector(`[data-role='content-card'][data-content-id='${editContentId}']`);
|
||||||
|
});
|
||||||
|
if (targetItem) {
|
||||||
|
openItem(targetItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (window.customElements?.whenDefined) {
|
const initWhenReady = () => {
|
||||||
window.customElements.whenDefined("multi-select-simple").then(() => {
|
if (window.customElements?.whenDefined) {
|
||||||
requestAnimationFrame(initPage);
|
window.customElements.whenDefined("multi-select-simple").then(() => {
|
||||||
});
|
requestAnimationFrame(initPage);
|
||||||
} else {
|
});
|
||||||
initPage();
|
} else {
|
||||||
}
|
initPage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initWhenReady();
|
||||||
|
|
||||||
|
document.body.addEventListener("htmx:afterSwap", () => {
|
||||||
|
initWhenReady();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
19
views/routes/almanach/contents/insert/body.gohtml
Normal file
19
views/routes/almanach/contents/insert/body.gohtml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{{- $content := index . "content" -}}
|
||||||
|
{{- $entry := index . "entry" -}}
|
||||||
|
{{- $csrf := index . "csrf_token" -}}
|
||||||
|
{{- $contentTypes := index . "content_types" -}}
|
||||||
|
{{- $musenalmTypes := index . "musenalm_types" -}}
|
||||||
|
{{- $paginationValues := index . "pagination_values" -}}
|
||||||
|
{{- $contentID := index . "content_id" -}}
|
||||||
|
|
||||||
|
{{- template "_content_item" (Dict
|
||||||
|
"content" $content
|
||||||
|
"content_id" $contentID
|
||||||
|
"entry" $entry
|
||||||
|
"csrf_token" $csrf
|
||||||
|
"content_types" $contentTypes
|
||||||
|
"musenalm_types" $musenalmTypes
|
||||||
|
"pagination_values" $paginationValues
|
||||||
|
"open_edit" true
|
||||||
|
"is_new" true
|
||||||
|
) -}}
|
||||||
23
views/routes/almanach/contents/item/body.gohtml
Normal file
23
views/routes/almanach/contents/item/body.gohtml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{{- $content := index . "content" -}}
|
||||||
|
{{- $entry := index . "entry" -}}
|
||||||
|
{{- $csrf := index . "csrf_token" -}}
|
||||||
|
{{- $contentTypes := index . "content_types" -}}
|
||||||
|
{{- $musenalmTypes := index . "musenalm_types" -}}
|
||||||
|
{{- $paginationValues := index . "pagination_values" -}}
|
||||||
|
{{- $contentID := index . "content_id" -}}
|
||||||
|
{{- $openEdit := index . "open_edit" -}}
|
||||||
|
{{- $isNew := index . "is_new" -}}
|
||||||
|
{{- $error := index . "error" -}}
|
||||||
|
|
||||||
|
{{- template "_content_item" (Dict
|
||||||
|
"content" $content
|
||||||
|
"content_id" $contentID
|
||||||
|
"entry" $entry
|
||||||
|
"csrf_token" $csrf
|
||||||
|
"content_types" $contentTypes
|
||||||
|
"musenalm_types" $musenalmTypes
|
||||||
|
"pagination_values" $paginationValues
|
||||||
|
"open_edit" $openEdit
|
||||||
|
"is_new" $isNew
|
||||||
|
"error" $error
|
||||||
|
) -}}
|
||||||
@@ -72,6 +72,13 @@ type AlmanachResult struct {
|
|||||||
<i class="ri-loop-left-line"></i> Reset
|
<i class="ri-loop-left-line"></i> Reset
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
·
|
||||||
|
<div>
|
||||||
|
<a href="/almanach/{{- $model.result.Entry.MusenalmID -}}/contents/edit" class="text-gray-700
|
||||||
|
no-underline hover:text-slate-950 block ">
|
||||||
|
<i class="ri-file-list-3-line"></i> Inhalte
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
{{- $annotation := index . 0 -}}
|
{{- $annotation := index . 0 -}}
|
||||||
{{- $label := "Annotation" -}}
|
{{- $label := "Annotation" -}}
|
||||||
|
{{- $fieldID := "annotation" -}}
|
||||||
{{- if gt (len .) 1 -}}
|
{{- if gt (len .) 1 -}}
|
||||||
{{- if index . 1 -}}
|
{{- if index . 1 -}}
|
||||||
{{- $label = index . 1 -}}
|
{{- $label = index . 1 -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
{{- if gt (len .) 2 -}}
|
||||||
|
{{- if index . 2 -}}
|
||||||
|
{{- $fieldID = index . 2 -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- $toolbarID := printf "%s-toolbar" $fieldID -}}
|
||||||
|
|
||||||
<div class="inputwrapper">
|
<div class="inputwrapper">
|
||||||
<div class="inputlabelrow">
|
<div class="inputlabelrow">
|
||||||
<label for="annotation" class="inputlabel">{{ $label }}</label>
|
<label for="{{ $fieldID }}" class="inputlabel">{{ $label }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<trix-toolbar id="annotation-toolbar">
|
<trix-toolbar id="{{ $toolbarID }}">
|
||||||
<div class="trix-toolbar-container">
|
<div class="trix-toolbar-container">
|
||||||
<!-- Text formatting group -->
|
<!-- Text formatting group -->
|
||||||
<span class="trix-toolbar-group">
|
<span class="trix-toolbar-group">
|
||||||
@@ -87,6 +94,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</trix-toolbar>
|
</trix-toolbar>
|
||||||
|
|
||||||
<textarea hidden id="annotation" name="annotation" autocomplete="off">{{- $annotation -}}</textarea>
|
<textarea hidden id="{{ $fieldID }}" name="{{ $fieldID }}" autocomplete="off">{{- $annotation -}}</textarea>
|
||||||
<trix-editor input="annotation" toolbar="annotation-toolbar"></trix-editor>
|
<trix-editor input="{{ $fieldID }}" toolbar="{{ $toolbarID }}"></trix-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,93 +1,32 @@
|
|||||||
{{- $content := index . "content" -}}
|
{{- $content := index . "content" -}}
|
||||||
{{- $entry := index . "entry" -}}
|
{{- $entry := index . "entry" -}}
|
||||||
{{- $contentTypes := index . "content_types" -}}
|
|
||||||
{{- $musenalmTypes := index . "musenalm_types" -}}
|
{{- $musenalmTypes := index . "musenalm_types" -}}
|
||||||
{{- $paginationValues := index . "pagination_values" -}}
|
{{- $paginationValues := index . "pagination_values" -}}
|
||||||
{{- $prefix := printf "content_%s_" $content.Id -}}
|
{{- $overrideID := index . "content_id" -}}
|
||||||
{{- $baseID := printf "content-%s" $content.Id -}}
|
{{- $contentID := $content.Id -}}
|
||||||
|
{{- if and $overrideID (ne $overrideID "") -}}
|
||||||
|
{{- $contentID = $overrideID -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- $prefix := printf "content_%s_" $contentID -}}
|
||||||
|
{{- $baseID := printf "content-%s" $contentID -}}
|
||||||
|
{{- $annotationID := printf "%sannotation" $prefix -}}
|
||||||
|
{{- $annotationToolbar := printf "%sannotation-toolbar" $prefix -}}
|
||||||
|
|
||||||
<div class="border border-slate-200 bg-white rounded-xs p-3" data-content-id="{{ $content.Id }}" data-content-order="{{ $content.Numbering }}">
|
<div class="border border-slate-200 bg-white rounded-xs" data-role="content-card" data-content-id="{{ $contentID }}" data-content-order="{{ $content.Numbering }}">
|
||||||
<input type="hidden" name="{{ $prefix }}numbering" class="content-numbering" value="{{- $content.Numbering -}}" />
|
<input type="hidden" name="{{ $prefix }}numbering" class="content-numbering" value="{{- $content.Numbering -}}" />
|
||||||
<input type="hidden" name="{{ $prefix }}musenalm_id" value="{{ $entry.MusenalmID }}" />
|
|
||||||
<input type="hidden" name="{{ $prefix }}entries" value="{{ $entry.Id }}" />
|
<input type="hidden" name="{{ $prefix }}entries" value="{{ $entry.Id }}" />
|
||||||
|
<div class="border border-slate-200 bg-stone-100 rounded-xs">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-4 border-b border-slate-200 bg-stone-200 px-3 py-2">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<i class="ri-drag-move-2-line text-slate-400 cursor-grab"></i>
|
<button type="button" class="text-slate-600 cursor-grab text-sm" data-role="content-drag-handle" draggable="true" aria-label="Beitrag verschieben">
|
||||||
<div class="text-xs text-slate-500">Alm-ID {{ $entry.MusenalmID }}</div>
|
<i class="ri-drag-move-2-line"></i>
|
||||||
</div>
|
</button>
|
||||||
<div class="border border-dashed border-slate-300 bg-slate-50 px-2 py-1 text-xs text-slate-600 rounded-xs">
|
<button type="button" class="text-slate-600 px-1.5 py-1 text-sm rounded-xs hover:bg-stone-200" data-role="content-move-up" aria-label="Beitrag nach oben">
|
||||||
<i class="ri-image-add-line"></i> Scans folgen
|
<i class="ri-arrow-up-line"></i>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
<button type="button" class="text-slate-600 px-1.5 py-1 text-sm rounded-xs hover:bg-stone-200" data-role="content-move-down" aria-label="Beitrag nach unten">
|
||||||
|
<i class="ri-arrow-down-line"></i>
|
||||||
<div class="mt-3 grid gap-2 xl:grid-cols-[1.3fr_1fr_1fr]">
|
</button>
|
||||||
<div class="grid gap-2">
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-preferred-title" class="inputlabel">Kurztitel</label>
|
|
||||||
</div>
|
|
||||||
<textarea name="{{ $prefix }}preferred_title" id="{{ $baseID }}-preferred-title" class="inputinput no-enter" autocomplete="off" rows="1" required>{{- $content.PreferredTitle -}}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-title" class="inputlabel">Titel</label>
|
|
||||||
</div>
|
|
||||||
<textarea name="{{ $prefix }}title_statement" id="{{ $baseID }}-title" class="inputinput no-enter" autocomplete="off" rows="1">{{- $content.TitleStmt -}}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-incipit" class="inputlabel">Incipit</label>
|
|
||||||
</div>
|
|
||||||
<textarea name="{{ $prefix }}incipit_statement" id="{{ $baseID }}-incipit" class="inputinput no-enter" autocomplete="off" rows="1">{{- $content.IncipitStmt -}}</textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-responsibility" class="inputlabel">Autorangabe</label>
|
|
||||||
</div>
|
|
||||||
<textarea name="{{ $prefix }}responsibility_statement" id="{{ $baseID }}-responsibility" class="inputinput no-enter" autocomplete="off" rows="1">{{- $content.ResponsibilityStmt -}}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-extent" class="inputlabel">Seite</label>
|
|
||||||
</div>
|
|
||||||
<textarea name="{{ $prefix }}extent" id="{{ $baseID }}-extent" class="inputinput no-enter" autocomplete="off" rows="1">{{- $content.Extent -}}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-pagination" class="inputlabel">Paginierung</label>
|
|
||||||
</div>
|
|
||||||
<select name="{{ $prefix }}musenalm_pagination" id="{{ $baseID }}-pagination" class="inputselect">
|
|
||||||
{{- range $_, $p := $paginationValues -}}
|
|
||||||
<option value="{{- $p -}}" {{ if eq $content.MusenalmPagination $p }}selected{{ end }}>{{- $p -}}</option>
|
|
||||||
{{- end -}}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-content-type" class="inputlabel">Inhaltstyp</label>
|
|
||||||
</div>
|
|
||||||
<multi-select-simple
|
|
||||||
id="{{ $baseID }}-content-type"
|
|
||||||
name="{{ $prefix }}content_type[]"
|
|
||||||
show-create-button="false"
|
|
||||||
placeholder="Inhaltstypen suchen..."
|
|
||||||
data-empty-text="Keine Typen verknüpft"
|
|
||||||
value='[{{- range $i, $t := $content.ContentType -}}{{- if $i }},{{ end -}}"{{ $t }}"{{- end -}}]'
|
|
||||||
data-initial-options='[{{- range $i, $t := $contentTypes -}}{{- if $i }},{{ end -}}{{ printf "{\"id\":%q,\"name\":%q}" $t $t }}{{- end -}}]'
|
|
||||||
data-initial-values='[{{- range $i, $t := $content.ContentType -}}{{- if $i }},{{ end -}}{{ printf "%q" $t }}{{- end -}}]'>
|
|
||||||
</multi-select-simple>
|
|
||||||
</div>
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-musenalm-type" class="inputlabel">Musenalm-Typ</label>
|
|
||||||
</div>
|
|
||||||
<multi-select-simple
|
<multi-select-simple
|
||||||
id="{{ $baseID }}-musenalm-type"
|
id="{{ $baseID }}-musenalm-type"
|
||||||
name="{{ $prefix }}musenalm_type[]"
|
name="{{ $prefix }}musenalm_type[]"
|
||||||
@@ -99,106 +38,171 @@
|
|||||||
data-initial-values='[{{- range $i, $t := $content.MusenalmType -}}{{- if $i }},{{ end -}}{{ printf "%q" $t }}{{- end -}}]'>
|
data-initial-values='[{{- range $i, $t := $content.MusenalmType -}}{{- if $i }},{{ end -}}{{ printf "%q" $t }}{{- end -}}]'>
|
||||||
</multi-select-simple>
|
</multi-select-simple>
|
||||||
</div>
|
</div>
|
||||||
<div class="inputwrapper">
|
<div class="flex items-center gap-2">
|
||||||
<div class="inputlabelrow">
|
<select name="{{ $prefix }}edit_state" id="{{ $baseID }}-edit-state" class="inputselect font-bold status-select px-2 py-1" data-status="{{ $content.EditState }}">
|
||||||
<label for="{{ $baseID }}-language" class="inputlabel">Sprache</label>
|
<option value="Unknown" {{ if eq $content.EditState "Unknown" }}selected{{ end }}>Unbekannt</option>
|
||||||
</div>
|
<option value="ToDo" {{ if eq $content.EditState "ToDo" }}selected{{ end }}>Zu erledigen</option>
|
||||||
<multi-select-simple
|
<option value="Review" {{ if eq $content.EditState "Review" }}selected{{ end }}>Überprüfen</option>
|
||||||
id="{{ $baseID }}-language"
|
<option value="Edited" {{ if eq $content.EditState "Edited" }}selected{{ end }}>Erfasst</option>
|
||||||
name="{{ $prefix }}language[]"
|
</select>
|
||||||
show-create-button="false"
|
|
||||||
placeholder="Sprachen suchen..."
|
|
||||||
data-empty-text="Keine Sprachen verknüpft"
|
|
||||||
value='[{{- range $i, $lang := $content.Language -}}{{- if $i }},{{ end -}}"{{ $lang }}"{{- end -}}]'
|
|
||||||
data-initial-values='[{{- range $i, $lang := $content.Language -}}{{- if $i }},{{ end -}}{{ printf "%q" $lang }}{{- end -}}]'>
|
|
||||||
</multi-select-simple>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="px-3 py-2">
|
||||||
|
<div class="grid grid-cols-[8rem_1fr_1fr] gap-x-3 gap-y-2 items-start">
|
||||||
|
<label for="{{ $baseID }}-extent" class="text-sm font-bold text-gray-700">Seite & Paginierung</label>
|
||||||
|
<textarea name="{{ $prefix }}extent" id="{{ $baseID }}-extent" class="inputinput no-enter whitespace-normal border border-slate-300 rounded-xs px-2 py-1 bg-white focus:outline-none focus:ring-2 focus:ring-slate-400/30" autocomplete="off" rows="1">{{- $content.Extent -}}</textarea>
|
||||||
|
<select name="{{ $prefix }}musenalm_pagination" id="{{ $baseID }}-pagination" class="inputselect border border-slate-300 rounded-xs px-2 py-1 bg-white focus:outline-none focus:ring-2 focus:ring-slate-400/30">
|
||||||
|
{{- range $_, $p := $paginationValues -}}
|
||||||
|
<option value="{{- $p -}}" {{ if eq $content.MusenalmPagination $p }}selected{{ end }}>{{- $p -}}</option>
|
||||||
|
{{- end -}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="{{ $baseID }}-edit-fields" class="mt-3 flex flex-col gap-3"></div>
|
||||||
|
<div-manager dm-target="{{ $baseID }}-edit-fields" class="flex items-center justify-end">
|
||||||
|
<button class="dm-menu-button text-right cursor-pointer whitespace-nowrap"><i class="ri-add-line"></i>
|
||||||
|
Felder hinzufügen</button>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[8rem_1fr_1.5rem] gap-x-3 gap-y-2 items-start {{ if eq (len $content.Language) 0 }}hidden{{ end }}">
|
||||||
|
<label for="{{ $baseID }}-language" class="text-sm font-bold text-gray-700">Sprache</label>
|
||||||
|
<multi-select-simple
|
||||||
|
id="{{ $baseID }}-language"
|
||||||
|
name="{{ $prefix }}language[]"
|
||||||
|
show-create-button="false"
|
||||||
|
placeholder="Sprachen suchen..."
|
||||||
|
data-empty-text="Keine Sprachen verknüpft"
|
||||||
|
value='[{{- range $i, $lang := $content.Language -}}{{- if $i }},{{ end -}}"{{ $lang }}"{{- end -}}]'
|
||||||
|
data-initial-values='[{{- range $i, $lang := $content.Language -}}{{- if $i }},{{ end -}}{{ printf "%q" $lang }}{{- end -}}]'>
|
||||||
|
</multi-select-simple>
|
||||||
|
<button class="dm-close-button text-gray-500">
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[8rem_1fr_1.5rem] gap-x-3 gap-y-2 items-start {{ if eq $content.TitleStmt "" }}hidden{{ end }}">
|
||||||
|
<label for="{{ $baseID }}-title" class="text-sm font-bold text-gray-700">Titel</label>
|
||||||
|
<textarea name="{{ $prefix }}title_statement" id="{{ $baseID }}-title" class="inputinput no-enter whitespace-normal border border-slate-300 rounded-xs px-2 py-1 bg-white focus:outline-none focus:ring-2 focus:ring-slate-400/30" autocomplete="off" rows="1">{{- $content.TitleStmt -}}</textarea>
|
||||||
|
<button class="dm-close-button text-gray-500">
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[8rem_1fr_1.5rem] gap-x-3 gap-y-2 items-start {{ if eq $content.SubtitleStmt "" }}hidden{{ end }}">
|
||||||
|
<label for="{{ $baseID }}-subtitle" class="text-sm font-bold text-gray-700">Untertitel</label>
|
||||||
|
<textarea name="{{ $prefix }}subtitle_statement" id="{{ $baseID }}-subtitle" class="inputinput no-enter whitespace-normal border border-slate-300 rounded-xs px-2 py-1 bg-white focus:outline-none focus:ring-2 focus:ring-slate-400/30" autocomplete="off" rows="1">{{- $content.SubtitleStmt -}}</textarea>
|
||||||
|
<button class="dm-close-button text-gray-500">
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[8rem_1fr_1.5rem] gap-x-3 gap-y-2 items-start {{ if eq $content.IncipitStmt "" }}hidden{{ end }}">
|
||||||
|
<label for="{{ $baseID }}-incipit" class="text-sm font-bold text-gray-700">Incipit</label>
|
||||||
|
<textarea name="{{ $prefix }}incipit_statement" id="{{ $baseID }}-incipit" class="inputinput no-enter whitespace-normal border border-slate-300 rounded-xs px-2 py-1 bg-white focus:outline-none focus:ring-2 focus:ring-slate-400/30" autocomplete="off" rows="1">{{- $content.IncipitStmt -}}</textarea>
|
||||||
|
<button class="dm-close-button text-gray-500">
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[8rem_1fr_1.5rem] gap-x-3 gap-y-2 items-start {{ if eq $content.ResponsibilityStmt "" }}hidden{{ end }}">
|
||||||
|
<label for="{{ $baseID }}-responsibility" class="text-sm font-bold text-gray-700">Autorangabe</label>
|
||||||
|
<textarea name="{{ $prefix }}responsibility_statement" id="{{ $baseID }}-responsibility" class="inputinput no-enter whitespace-normal border border-slate-300 rounded-xs px-2 py-1 bg-white focus:outline-none focus:ring-2 focus:ring-slate-400/30" autocomplete="off" rows="1">{{- $content.ResponsibilityStmt -}}</textarea>
|
||||||
|
<button class="dm-close-button text-gray-500">
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[8rem_1fr_1.5rem] gap-x-3 gap-y-2 items-start {{ if eq $content.ParallelTitle "" }}hidden{{ end }}">
|
||||||
|
<label for="{{ $baseID }}-parallel-title" class="text-sm font-bold text-gray-700">Paralleltitel</label>
|
||||||
|
<textarea name="{{ $prefix }}parallel_title" id="{{ $baseID }}-parallel-title" class="inputinput no-enter whitespace-normal border border-slate-300 rounded-xs px-2 py-1 bg-white focus:outline-none focus:ring-2 focus:ring-slate-400/30" autocomplete="off" rows="1">{{- $content.ParallelTitle -}}</textarea>
|
||||||
|
<button class="dm-close-button text-gray-500">
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[8rem_1fr_1.5rem] gap-x-3 gap-y-2 items-start {{ if eq $content.VariantTitle "" }}hidden{{ end }}">
|
||||||
|
<label for="{{ $baseID }}-variant-title" class="text-sm font-bold text-gray-700">Titelvarianten</label>
|
||||||
|
<textarea name="{{ $prefix }}variant_title" id="{{ $baseID }}-variant-title" class="inputinput no-enter whitespace-normal border border-slate-300 rounded-xs px-2 py-1 bg-white focus:outline-none focus:ring-2 focus:ring-slate-400/30" autocomplete="off" rows="1">{{- $content.VariantTitle -}}</textarea>
|
||||||
|
<button class="dm-close-button text-gray-500">
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[8rem_1fr_1.5rem] gap-x-3 gap-y-2 items-start {{ if eq $content.PlaceStmt "" }}hidden{{ end }}">
|
||||||
|
<label for="{{ $baseID }}-place" class="text-sm font-bold text-gray-700">Ortsangabe</label>
|
||||||
|
<textarea name="{{ $prefix }}place_statement" id="{{ $baseID }}-place" class="inputinput no-enter whitespace-normal border border-slate-300 rounded-xs px-2 py-1 bg-white focus:outline-none focus:ring-2 focus:ring-slate-400/30" autocomplete="off" rows="1">{{- $content.PlaceStmt -}}</textarea>
|
||||||
|
<button class="dm-close-button text-gray-500">
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[8rem_1fr_1.5rem] gap-x-3 gap-y-2 items-start {{ if eq $content.Annotation "" }}hidden{{ end }}">
|
||||||
|
<label for="{{ $annotationID }}" class="text-sm font-bold text-gray-700">Anmerkung</label>
|
||||||
|
<div class="border border-slate-300 rounded-xs bg-white px-2 py-1 focus-within:ring-2 focus-within:ring-slate-400/30">
|
||||||
|
<trix-toolbar id="{{ $annotationToolbar }}">
|
||||||
|
<div class="trix-toolbar-container">
|
||||||
|
<span class="trix-toolbar-group">
|
||||||
|
<button type="button" class="trix-toolbar-button" data-trix-attribute="bold" data-trix-key="b" title="Fett">
|
||||||
|
<i class="ri-bold"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="trix-toolbar-button" data-trix-attribute="italic" data-trix-key="i" title="Kursiv">
|
||||||
|
<i class="ri-italic"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="trix-toolbar-button" data-trix-attribute="strike" title="Gestrichen">
|
||||||
|
<i class="ri-strikethrough"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="trix-toolbar-button" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="Link">
|
||||||
|
<i class="ri-links-line"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span class="trix-toolbar-group">
|
||||||
|
<button type="button" class="trix-toolbar-button" data-trix-attribute="heading1" title="Überschrift">
|
||||||
|
<i class="ri-h-1"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="trix-toolbar-button" data-trix-attribute="quote" title="Zitat">
|
||||||
|
<i class="ri-double-quotes-l"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="trix-toolbar-button" data-trix-attribute="bullet" title="Liste">
|
||||||
|
<i class="ri-list-unordered"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="trix-toolbar-button" data-trix-attribute="number" title="Aufzählung">
|
||||||
|
<i class="ri-list-ordered"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="trix-toolbar-button" data-trix-action="decreaseNestingLevel" title="Einzug verkleinern">
|
||||||
|
<i class="ri-indent-decrease"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="trix-toolbar-button" data-trix-action="increaseNestingLevel" title="Einzug vergrößern">
|
||||||
|
<i class="ri-indent-increase"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span class="trix-toolbar-group">
|
||||||
|
<button type="button" class="trix-toolbar-button" data-trix-action="undo" data-trix-key="z" title="Rückgängig">
|
||||||
|
<i class="ri-arrow-go-back-line"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="trix-toolbar-button" data-trix-action="redo" data-trix-key="shift+z" title="Wiederholen">
|
||||||
|
<i class="ri-arrow-go-forward-line"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="trix-dialogs" data-trix-dialogs>
|
||||||
|
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
|
||||||
|
<div class="trix-dialog__link-fields flex flex-row">
|
||||||
|
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="URL eingeben…" aria-label="URL" required data-trix-input>
|
||||||
|
<div class="trix-button-group flex-row">
|
||||||
|
<input type="button" class="trix-button trix-button--dialog" value="Link" data-trix-method="setAttribute">
|
||||||
|
<input type="button" class="trix-button trix-button--dialog" value="Unlink" data-trix-method="removeAttribute">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</trix-toolbar>
|
||||||
|
<textarea hidden id="{{ $annotationID }}" name="{{ $annotationID }}" autocomplete="off">{{- $content.Annotation -}}</textarea>
|
||||||
|
<trix-editor input="{{ $annotationID }}" toolbar="{{ $annotationToolbar }}" class="min-h-[6rem]"></trix-editor>
|
||||||
|
</div>
|
||||||
|
<button class="dm-close-button text-gray-500">
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div-manager>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details class="mt-3">
|
|
||||||
<summary class="text-sm font-bold text-slate-700 cursor-pointer select-none">
|
|
||||||
Weitere Felder & Anmerkungen
|
|
||||||
</summary>
|
|
||||||
<div class="mt-3 grid gap-2">
|
|
||||||
<div class="grid gap-2 md:grid-cols-2">
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-subtitle" class="inputlabel">Untertitel</label>
|
|
||||||
</div>
|
|
||||||
<textarea name="{{ $prefix }}subtitle_statement" id="{{ $baseID }}-subtitle" class="inputinput no-enter" autocomplete="off" rows="1">{{- $content.SubtitleStmt -}}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-parallel-title" class="inputlabel">Paralleltitel</label>
|
|
||||||
</div>
|
|
||||||
<textarea name="{{ $prefix }}parallel_title" id="{{ $baseID }}-parallel-title" class="inputinput no-enter" autocomplete="off" rows="1">{{- $content.ParallelTitle -}}</textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-2 md:grid-cols-2">
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-variant-title" class="inputlabel">Titelvarianten</label>
|
|
||||||
</div>
|
|
||||||
<textarea name="{{ $prefix }}variant_title" id="{{ $baseID }}-variant-title" class="inputinput no-enter" autocomplete="off" rows="1">{{- $content.VariantTitle -}}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-year" class="inputlabel">Jahr</label>
|
|
||||||
</div>
|
|
||||||
<input name="{{ $prefix }}year" id="{{ $baseID }}-year" class="inputinput" autocomplete="off" value="{{ if $content.Year }}{{ $content.Year }}{{ end }}" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-2 md:grid-cols-2">
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-publication" class="inputlabel">Publikationsangabe</label>
|
|
||||||
</div>
|
|
||||||
<textarea name="{{ $prefix }}publication_statement" id="{{ $baseID }}-publication" class="inputinput no-enter" autocomplete="off" rows="1">{{- $content.PublicationStmt -}}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-place" class="inputlabel">Ortsangabe</label>
|
|
||||||
</div>
|
|
||||||
<textarea name="{{ $prefix }}place_statement" id="{{ $baseID }}-place" class="inputinput no-enter" autocomplete="off" rows="1">{{- $content.PlaceStmt -}}</textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-2 md:grid-cols-2">
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-dimensions" class="inputlabel">Maße</label>
|
|
||||||
</div>
|
|
||||||
<textarea name="{{ $prefix }}dimensions" id="{{ $baseID }}-dimensions" class="inputinput no-enter" autocomplete="off" rows="1">{{- $content.Dimensions -}}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-edit-state" class="inputlabel">Status</label>
|
|
||||||
</div>
|
|
||||||
<select name="{{ $prefix }}edit_state" id="{{ $baseID }}-edit-state" class="inputselect font-bold">
|
|
||||||
<option value="Unknown" {{ if eq $content.EditState "Unknown" }}selected{{ end }}>Unbekannt</option>
|
|
||||||
<option value="ToDo" {{ if eq $content.EditState "ToDo" }}selected{{ end }}>Zu erledigen</option>
|
|
||||||
<option value="Review" {{ if eq $content.EditState "Review" }}selected{{ end }}>Überprüfen</option>
|
|
||||||
<option value="Edited" {{ if eq $content.EditState "Edited" }}selected{{ end }}>Erfasst</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-edit-comment" class="inputlabel">Bearbeitungsvermerk</label>
|
|
||||||
</div>
|
|
||||||
<textarea name="{{ $prefix }}edit_comment" id="{{ $baseID }}-edit-comment" class="inputinput no-enter" autocomplete="off" rows="1">{{- $content.Comment -}}</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="inputwrapper">
|
|
||||||
<div class="inputlabelrow">
|
|
||||||
<label for="{{ $baseID }}-annotation" class="inputlabel">Anmerkung</label>
|
|
||||||
</div>
|
|
||||||
<textarea name="{{ $prefix }}annotation" id="{{ $baseID }}-annotation" class="inputinput" autocomplete="off" rows="2">{{- $content.Annotation -}}</textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
26
views/routes/components/_content_gap.gohtml
Normal file
26
views/routes/components/_content_gap.gohtml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{{- $entry := index . "entry" -}}
|
||||||
|
{{- $contentID := index . "content_id" -}}
|
||||||
|
{{- $position := index . "position" -}}
|
||||||
|
{{- $label := index . "label" -}}
|
||||||
|
|
||||||
|
<div class="relative group h-6 -my-2.5" data-role="content-gap">
|
||||||
|
<div class="pointer-events-none absolute left-0 right-0 top-1/2 h-0.5 -translate-y-1/2 bg-slate-300 opacity-0 transition-opacity duration-150 group-hover:opacity-100"></div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute left-1/2 top-1/2 z-[10000] -translate-x-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-150 rounded-full border border-slate-300 bg-stone-100 text-slate-700 px-3 py-2 text-base shadow-sm"
|
||||||
|
data-role="content-insert"
|
||||||
|
data-position="{{ $position }}"
|
||||||
|
data-content-id="{{ $contentID }}"
|
||||||
|
aria-label="Beitrag einfügen"
|
||||||
|
hx-post="/almanach/{{ $entry.MusenalmID }}/contents/insert"
|
||||||
|
hx-target="closest [data-role='content-gap']"
|
||||||
|
hx-swap="beforebegin"
|
||||||
|
hx-include="[data-role='csrf-token']"
|
||||||
|
hx-vals='{{ printf "{\"position\":%q,\"content_id\":%q}" $position $contentID }}'>
|
||||||
|
<i class="ri-add-line"></i>
|
||||||
|
{{- if $label -}}
|
||||||
|
<span>Neuer Beitrag</span>
|
||||||
|
{{- end -}}
|
||||||
|
</button>
|
||||||
|
<div class="h-1"></div>
|
||||||
|
</div>
|
||||||
133
views/routes/components/_content_item.gohtml
Normal file
133
views/routes/components/_content_item.gohtml
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
{{- $content := index . "content" -}}
|
||||||
|
{{- $entry := index . "entry" -}}
|
||||||
|
{{- $csrf := index . "csrf_token" -}}
|
||||||
|
{{- $contentTypes := index . "content_types" -}}
|
||||||
|
{{- $musenalmTypes := index . "musenalm_types" -}}
|
||||||
|
{{- $paginationValues := index . "pagination_values" -}}
|
||||||
|
{{- $overrideID := index . "content_id" -}}
|
||||||
|
{{- $openEdit := index . "open_edit" -}}
|
||||||
|
{{- $isNew := index . "is_new" -}}
|
||||||
|
{{- $error := index . "error" -}}
|
||||||
|
|
||||||
|
{{- $contentID := $content.Id -}}
|
||||||
|
{{- if and $overrideID (ne $overrideID "") -}}
|
||||||
|
{{- $contentID = $overrideID -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
<div data-role="content-item" class="relative {{ if $isNew }}data-new-content{{ end }} {{ if $openEdit }}data-editing{{ end }}" data-open-edit="{{ if $openEdit }}true{{ end }}" data-content-temp="{{ if $isNew }}true{{ end }}">
|
||||||
|
<div data-role="content-view" class="{{ if $openEdit }}hidden{{ end }} mt-2">
|
||||||
|
<div class="border border-slate-200 bg-stone-100 rounded-xs overflow-hidden">
|
||||||
|
<div class="flex items-center justify-between gap-4 border-b border-slate-200 bg-stone-200 px-3 py-2 cursor-grab" data-role="content-drag-handle" draggable="true" aria-label="Beitrag verschieben">
|
||||||
|
<div class="flex items-center gap-3 text-sm font-bold text-gray-800">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button type="button" class="text-slate-600 text-sm" data-role="content-move-up" aria-label="Beitrag nach oben">
|
||||||
|
<i class="ri-arrow-up-line"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="text-slate-600 text-sm" data-role="content-move-down" aria-label="Beitrag nach unten">
|
||||||
|
<i class="ri-arrow-down-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{{- if $content.MusenalmType -}}
|
||||||
|
<span class="flex flex-wrap gap-1 text-gray-700 font-normal">
|
||||||
|
{{- range $i, $t := $content.MusenalmType -}}
|
||||||
|
<span class="bg-stone-100 px-1.5 py-0.5 rounded text-sm font-semibold">{{- $t -}}</span>
|
||||||
|
{{- end -}}
|
||||||
|
</span>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="status-badge text-xs" data-status="{{ $content.EditState }}">
|
||||||
|
<i class="status-icon {{- if eq $content.EditState "Edited" }} ri-checkbox-circle-line{{- else if eq $content.EditState "Seen" }} ri-information-line{{- else if eq $content.EditState "Review" }} ri-search-line{{- else if eq $content.EditState "ToDo" }} ri-list-check{{- else }} ri-forbid-2-line{{- end }}"></i>
|
||||||
|
{{- if eq $content.EditState "Edited" -}}Erfasst{{- else if eq $content.EditState "Review" -}}Überprüfen{{- else if eq $content.EditState "ToDo" -}}Zu erledigen{{- else if eq $content.EditState "Seen" -}}Autopsiert{{- else -}}Unbekannt{{- end -}}
|
||||||
|
</span>
|
||||||
|
<button type="button" class="resetbutton w-32 flex items-center gap-2 justify-center" data-role="content-edit-button">
|
||||||
|
<i class="ri-edit-2-line"></i>
|
||||||
|
<span>Bearbeiten</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2 grid-cols-[8rem_1fr] items-start px-3 py-2">
|
||||||
|
{{- if $content.Extent -}}
|
||||||
|
<div class="text-sm font-bold text-gray-700">Seite</div>
|
||||||
|
<div class="text-base">{{- $content.Extent -}}</div>
|
||||||
|
{{- end -}}
|
||||||
|
{{- if $content.TitleStmt -}}
|
||||||
|
<div class="text-sm font-bold text-gray-700">Titel</div>
|
||||||
|
<div class="text-base italic">{{- $content.TitleStmt -}}</div>
|
||||||
|
{{- end -}}
|
||||||
|
{{- if $content.IncipitStmt -}}
|
||||||
|
<div class="text-sm font-bold text-gray-700">Incipit</div>
|
||||||
|
<div class="text-base italic">{{ $content.IncipitStmt }}…</div>
|
||||||
|
{{- end -}}
|
||||||
|
{{- if $content.ResponsibilityStmt -}}
|
||||||
|
<div class="text-sm font-bold text-gray-700">Autorangabe</div>
|
||||||
|
<div class="text-base italic">{{- $content.ResponsibilityStmt -}}</div>
|
||||||
|
{{- end -}}
|
||||||
|
{{- if $content.Annotation -}}
|
||||||
|
{{- $link := printf "%s%s" "/almanach/" $entry.MusenalmIDString -}}
|
||||||
|
<div class="text-sm font-bold text-gray-700">Anmerkung</div>
|
||||||
|
<div class="text-base">
|
||||||
|
{{- Safe (LinksAnnotation (ReplaceSlashParen $content.Annotation) $link) -}}
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-role="content-edit" class="{{ if not $openEdit }}hidden{{ end }} mt-2">
|
||||||
|
<form
|
||||||
|
autocomplete="off"
|
||||||
|
class="w-full dbform"
|
||||||
|
method="POST"
|
||||||
|
hx-boost="false"
|
||||||
|
hx-post="/almanach/{{ $entry.MusenalmID }}/contents/edit"
|
||||||
|
hx-target="closest [data-role='content-item']"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
action="/almanach/{{ $entry.MusenalmID }}/contents/edit">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ $csrf }}" />
|
||||||
|
{{- if $error -}}
|
||||||
|
<div class="mb-3 rounded-xs border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800">
|
||||||
|
{{ $error }}
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
|
{{- template "_content_edit" (Dict
|
||||||
|
"content" $content
|
||||||
|
"content_id" $contentID
|
||||||
|
"entry" $entry
|
||||||
|
"content_types" $contentTypes
|
||||||
|
"musenalm_types" $musenalmTypes
|
||||||
|
"pagination_values" $paginationValues
|
||||||
|
) -}}
|
||||||
|
<div class="w-full flex items-center justify-end gap-3 mt-4 flex-wrap">
|
||||||
|
<button type="button" class="resetbutton w-40 flex items-center gap-2 justify-center" data-role="content-edit-cancel">
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
<span>Verwerfen</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="resetbutton w-40 flex items-center gap-2 justify-center bg-red-50 text-red-800 hover:bg-red-100 hover:text-red-900" data-role="content-delete">
|
||||||
|
<i class="ri-delete-bin-line"></i>
|
||||||
|
<span>Eintrag löschen</span>
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="submitbutton w-40 flex items-center gap-2 justify-center">
|
||||||
|
<i class="ri-save-line"></i>
|
||||||
|
<span>Speichern</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<dialog data-role="content-delete-dialog" class="fixed inset-0 m-auto rounded-md border border-slate-200 p-0 shadow-xl backdrop:bg-black/40">
|
||||||
|
<div class="p-5 w-[22rem]">
|
||||||
|
<div class="text-base font-bold text-gray-900">Eintrag löschen?</div>
|
||||||
|
{{- if $content.TitleStmt -}}
|
||||||
|
<div class="text-sm font-bold text-gray-900 mt-1">{{ $content.TitleStmt }}</div>
|
||||||
|
{{- end -}}
|
||||||
|
<p class="text-sm text-gray-700 mt-2">
|
||||||
|
Der Eintrag wird dauerhaft gelöscht. Verknüpfungen, Exemplare und Inhalte werden entfernt.
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center justify-end gap-3 mt-4">
|
||||||
|
<button type="button" class="resetbutton w-auto px-3 py-1 text-sm" data-role="content-delete-cancel">Abbrechen</button>
|
||||||
|
<button type="button" class="submitbutton w-auto bg-red-700 hover:bg-red-800 px-3 py-1 text-sm" data-role="content-delete-confirm">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -62,10 +62,20 @@ export class DivManager extends HTMLElement {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const existingButton = this._button;
|
||||||
this._button = this.querySelector(`.${DM_MENU_BUTTON_CLASS}`);
|
this._button = this.querySelector(`.${DM_MENU_BUTTON_CLASS}`);
|
||||||
|
if (!this._button && existingButton) {
|
||||||
|
this._button = existingButton;
|
||||||
|
if (!this._button.parentElement) {
|
||||||
|
this.appendChild(this._button);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!this._button) {
|
if (!this._button) {
|
||||||
console.error("DivManagerMenu needs a button element.");
|
this._button = document.createElement("button");
|
||||||
return;
|
this._button.type = "button";
|
||||||
|
this._button.classList.add(DM_MENU_BUTTON_CLASS, TAILWIND_HIDDEN_CLASS);
|
||||||
|
this._button.innerHTML = '<i class="ri-add-line"></i> Felder hinzufügen';
|
||||||
|
this.appendChild(this._button);
|
||||||
}
|
}
|
||||||
if (!this._originalButtonText) {
|
if (!this._originalButtonText) {
|
||||||
this._originalButtonText = this._button.innerHTML;
|
this._originalButtonText = this._button.innerHTML;
|
||||||
@@ -214,11 +224,15 @@ export class DivManager extends HTMLElement {
|
|||||||
|
|
||||||
// Always ensure the hidden class is added
|
// Always ensure the hidden class is added
|
||||||
child.node.classList.add(TAILWIND_HIDDEN_CLASS);
|
child.node.classList.add(TAILWIND_HIDDEN_CLASS);
|
||||||
|
this._clearFields(child.node);
|
||||||
|
|
||||||
const target = child.target();
|
const target = child.target();
|
||||||
if (target && target.contains(child.node)) {
|
if (target && target.contains(child.node)) {
|
||||||
target.removeChild(child.node);
|
target.removeChild(child.node);
|
||||||
}
|
}
|
||||||
|
if (!child.node.parentElement) {
|
||||||
|
this.appendChild(child.node);
|
||||||
|
}
|
||||||
// INFO: the order of these matter, dont fuck it up
|
// INFO: the order of these matter, dont fuck it up
|
||||||
this.renderButton();
|
this.renderButton();
|
||||||
this.renderMenu();
|
this.renderMenu();
|
||||||
@@ -337,4 +351,26 @@ export class DivManager extends HTMLElement {
|
|||||||
target.classList.toggle(TAILWIND_HIDDEN_CLASS, !hasVisibleChild);
|
target.classList.toggle(TAILWIND_HIDDEN_CLASS, !hasVisibleChild);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_clearFields(container) {
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.querySelectorAll("input, textarea, select").forEach((field) => {
|
||||||
|
if (field.matches("input[type='checkbox'], input[type='radio']")) {
|
||||||
|
field.checked = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (field.matches("select")) {
|
||||||
|
field.selectedIndex = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
field.value = "";
|
||||||
|
});
|
||||||
|
container.querySelectorAll("trix-editor").forEach((editor) => {
|
||||||
|
if (typeof editor.editor?.loadHTML === "function") {
|
||||||
|
editor.editor.loadHTML("");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,17 +11,19 @@ export class EditPage extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_setupStatusSelect() {
|
_setupStatusSelect() {
|
||||||
const statusSelect = this.querySelector(".status-select");
|
const statusSelects = Array.from(this.querySelectorAll(".status-select"));
|
||||||
if (!statusSelect) {
|
if (statusSelects.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const statusIcon = this.querySelector(".status-icon");
|
statusSelects.forEach((statusSelect) => {
|
||||||
statusSelect.addEventListener("change", (event) => {
|
const statusIcon = statusSelect.parentElement?.querySelector(".status-icon");
|
||||||
const newStatus = event.target.value;
|
statusSelect.addEventListener("change", (event) => {
|
||||||
statusSelect.setAttribute("data-status", newStatus);
|
const newStatus = event.target.value;
|
||||||
if (statusIcon) {
|
statusSelect.setAttribute("data-status", newStatus);
|
||||||
this._updateStatusIcon(statusIcon, newStatus);
|
if (statusIcon) {
|
||||||
}
|
this._updateStatusIcon(statusIcon, newStatus);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user