mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 02:25:30 +00:00
FIX: pages load with indexed map[string]any, page edit page
This commit is contained in:
342
controllers/seiten_edit.go
Normal file
342
controllers/seiten_edit.go
Normal file
@@ -0,0 +1,342 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
const (
|
||||
URL_SEITEN_EDITOR = "/redaktion/seiten/"
|
||||
URL_SEITEN_EDITOR_FORM = "form/"
|
||||
URL_SEITEN_EDITOR_SAVE = "save/"
|
||||
TEMPLATE_SEITEN_EDITOR = "/redaktion/seiten/"
|
||||
TEMPLATE_SEITEN_FORM = "/redaktion/seiten/form/"
|
||||
LAYOUT_FRAGMENT = "fragment"
|
||||
pageHTMLPrefixSeparator = "."
|
||||
)
|
||||
|
||||
func init() {
|
||||
p := &SeitenEditPage{
|
||||
StaticPage: pagemodels.StaticPage{
|
||||
Name: pagemodels.P_SEITEN_EDIT_NAME,
|
||||
URL: URL_SEITEN_EDITOR,
|
||||
Template: TEMPLATE_SEITEN_EDITOR,
|
||||
Layout: pagemodels.LAYOUT_LOGIN_PAGES,
|
||||
},
|
||||
}
|
||||
app.Register(p)
|
||||
}
|
||||
|
||||
type SeitenEditPage struct {
|
||||
pagemodels.StaticPage
|
||||
}
|
||||
|
||||
type SeitenEditorPageItem struct {
|
||||
Key string
|
||||
Title string
|
||||
}
|
||||
|
||||
type SeitenEditorSection struct {
|
||||
Key string
|
||||
Label string
|
||||
HTML string
|
||||
}
|
||||
|
||||
type SeitenEditorSelected struct {
|
||||
Key string
|
||||
Title string
|
||||
Description string
|
||||
Keywords string
|
||||
Sections []SeitenEditorSection
|
||||
}
|
||||
|
||||
func (p *SeitenEditPage) Setup(router *router.Router[*core.RequestEvent], ia pagemodels.IApp, engine *templating.Engine) error {
|
||||
app := ia.Core()
|
||||
rg := router.Group(URL_SEITEN_EDITOR)
|
||||
rg.BindFunc(middleware.IsAdminOrEditor())
|
||||
rg.GET("", p.GET(engine, app))
|
||||
rg.GET(URL_SEITEN_EDITOR_FORM, p.GETForm(engine, app))
|
||||
rg.POST(URL_SEITEN_EDITOR_SAVE, p.POSTSave(engine, ia))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SeitenEditPage) GET(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
data, err := seitenEditorData(e, app)
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, nil)
|
||||
}
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SeitenEditPage) GETForm(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
data, err := seitenEditorData(e, app)
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, nil)
|
||||
}
|
||||
return engine.Response200(e, TEMPLATE_SEITEN_FORM, data, LAYOUT_FRAGMENT)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SeitenEditPage) POSTSave(engine *templating.Engine, ia pagemodels.IApp) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
app := ia.Core()
|
||||
req := templating.NewRequest(e)
|
||||
|
||||
if err := e.Request.ParseForm(); err != nil {
|
||||
return p.redirectError(e, "Formulardaten ungültig.")
|
||||
}
|
||||
|
||||
csrfToken := e.Request.FormValue("csrf_token")
|
||||
if err := req.CheckCSRF(csrfToken); err != nil {
|
||||
return p.redirectError(e, err.Error())
|
||||
}
|
||||
|
||||
pageKey := strings.TrimSpace(e.Request.FormValue("page_key"))
|
||||
if pageKey == "" {
|
||||
return p.redirectError(e, "Bitte eine Seite auswählen.")
|
||||
}
|
||||
|
||||
title := strings.TrimSpace(e.Request.FormValue("title"))
|
||||
description := strings.TrimSpace(e.Request.FormValue("description"))
|
||||
keywords := strings.TrimSpace(e.Request.FormValue("keywords"))
|
||||
|
||||
htmlUpdates := make(map[string]string)
|
||||
for key, values := range e.Request.Form {
|
||||
if !strings.HasPrefix(key, "html[") || !strings.HasSuffix(key, "]") {
|
||||
continue
|
||||
}
|
||||
htmlKey := strings.TrimSuffix(strings.TrimPrefix(key, "html["), "]")
|
||||
if htmlKey == "" || len(values) == 0 {
|
||||
continue
|
||||
}
|
||||
htmlUpdates[htmlKey] = values[0]
|
||||
}
|
||||
|
||||
pagesCollection, err := app.FindCollectionByNameOrId(dbmodels.PAGES_TABLE)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to load pages collection", "error", err)
|
||||
return p.redirectError(e, "Speichern fehlgeschlagen.")
|
||||
}
|
||||
|
||||
pageRecord, err := app.FindFirstRecordByData(pagesCollection.Id, dbmodels.KEY_FIELD, pageKey)
|
||||
if err != nil || pageRecord == nil {
|
||||
return p.redirectError(e, "Seite nicht gefunden.")
|
||||
}
|
||||
|
||||
data := map[string]any{}
|
||||
if existing := pageRecord.Get(dbmodels.DATA_FIELD); existing != nil {
|
||||
switch value := existing.(type) {
|
||||
case map[string]any:
|
||||
data = value
|
||||
case types.JSONRaw:
|
||||
if err := json.Unmarshal(value, &data); err != nil {
|
||||
app.Logger().Error("Failed to unmarshal page data", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
data["description"] = description
|
||||
data["keywords"] = keywords
|
||||
|
||||
pageRecord.Set(dbmodels.TITLE_FIELD, title)
|
||||
pageRecord.Set(dbmodels.DATA_FIELD, data)
|
||||
|
||||
htmlCollection, err := app.FindCollectionByNameOrId(dbmodels.HTML_TABLE)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to load html collection", "error", err)
|
||||
return p.redirectError(e, "Speichern fehlgeschlagen.")
|
||||
}
|
||||
|
||||
if err := app.RunInTransaction(func(tx core.App) error {
|
||||
if err := tx.Save(pageRecord); err != nil {
|
||||
return err
|
||||
}
|
||||
for key, value := range htmlUpdates {
|
||||
record, _ := tx.FindFirstRecordByData(htmlCollection.Id, dbmodels.KEY_FIELD, key)
|
||||
if record == nil {
|
||||
record = core.NewRecord(htmlCollection)
|
||||
record.Set(dbmodels.KEY_FIELD, key)
|
||||
}
|
||||
record.Set(dbmodels.HTML_FIELD, value)
|
||||
if err := tx.Save(record); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
app.Logger().Error("Failed to save page data", "page", pageKey, "error", err)
|
||||
return p.redirectError(e, "Speichern fehlgeschlagen.")
|
||||
}
|
||||
|
||||
go ia.ResetPagesCache()
|
||||
go ia.ResetHtmlCache()
|
||||
|
||||
redirect := fmt.Sprintf("%s?key=%s&success=%s", URL_SEITEN_EDITOR, url.QueryEscape(pageKey), url.QueryEscape("Änderungen gespeichert."))
|
||||
return e.Redirect(http.StatusSeeOther, redirect)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SeitenEditPage) redirectError(e *core.RequestEvent, message string) error {
|
||||
redirect := fmt.Sprintf("%s?error=%s", URL_SEITEN_EDITOR, url.QueryEscape(message))
|
||||
return e.Redirect(http.StatusSeeOther, redirect)
|
||||
}
|
||||
|
||||
func seitenEditorData(e *core.RequestEvent, app core.App) (map[string]any, error) {
|
||||
pages, err := seitenEditorPages(app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
selectedKey := strings.TrimSpace(e.Request.URL.Query().Get("key"))
|
||||
if selectedKey == "" && len(pages) > 0 {
|
||||
selectedKey = pages[0].Key
|
||||
}
|
||||
|
||||
var selected *SeitenEditorSelected
|
||||
if selectedKey != "" {
|
||||
selected, err = seitenEditorSelected(app, selectedKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"pages": pages,
|
||||
"selected": selected,
|
||||
"selected_key": selectedKey,
|
||||
}
|
||||
|
||||
req := templating.NewRequest(e)
|
||||
if req.Session() != nil {
|
||||
data["csrf_token"] = req.Session().Token
|
||||
} else {
|
||||
data["csrf_token"] = ""
|
||||
}
|
||||
|
||||
if msg := e.Request.URL.Query().Get("success"); msg != "" {
|
||||
data["success"] = msg
|
||||
}
|
||||
if msg := e.Request.URL.Query().Get("error"); msg != "" {
|
||||
data["error"] = msg
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func seitenEditorPages(app core.App) ([]SeitenEditorPageItem, error) {
|
||||
pages, err := dbmodels.Pages_All(app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]SeitenEditorPageItem, 0, len(pages))
|
||||
for _, page := range pages {
|
||||
title := strings.TrimSpace(page.Title())
|
||||
if title == "" {
|
||||
title = page.Key()
|
||||
}
|
||||
items = append(items, SeitenEditorPageItem{
|
||||
Key: page.Key(),
|
||||
Title: title,
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortFunc(items, func(a, b SeitenEditorPageItem) int {
|
||||
return strings.Compare(strings.ToLower(a.Title), strings.ToLower(b.Title))
|
||||
})
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func seitenEditorSelected(app core.App, key string) (*SeitenEditorSelected, error) {
|
||||
page, err := dbmodels.TableByField[dbmodels.Page](app, dbmodels.PAGES_TABLE, dbmodels.KEY_FIELD, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
description := ""
|
||||
keywords := ""
|
||||
if data := (&page).Data(); data != nil {
|
||||
if value, ok := data["description"]; ok && value != nil {
|
||||
description = fmt.Sprint(value)
|
||||
}
|
||||
if value, ok := data["keywords"]; ok && value != nil {
|
||||
keywords = fmt.Sprint(value)
|
||||
}
|
||||
}
|
||||
|
||||
sections, err := seitenEditorSections(app, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SeitenEditorSelected{
|
||||
Key: key,
|
||||
Title: page.Title(),
|
||||
Description: description,
|
||||
Keywords: keywords,
|
||||
Sections: sections,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func seitenEditorSections(app core.App, key string) ([]SeitenEditorSection, error) {
|
||||
prefix := "page." + key
|
||||
records := make([]*dbmodels.HTML, 0)
|
||||
err := app.RecordQuery(dbmodels.HTML_TABLE).
|
||||
Where(dbx.NewExp(dbmodels.KEY_FIELD+" LIKE {:prefix}", dbx.Params{"prefix": prefix + "%"})).
|
||||
All(&records)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sections := make([]SeitenEditorSection, 0, len(records))
|
||||
for _, record := range records {
|
||||
section := strings.TrimPrefix(record.Key(), prefix)
|
||||
section = strings.TrimPrefix(section, pageHTMLPrefixSeparator)
|
||||
label := "Inhalt"
|
||||
if section != "" {
|
||||
label = strings.ReplaceAll(section, "_", " ")
|
||||
}
|
||||
|
||||
sections = append(sections, SeitenEditorSection{
|
||||
Key: record.Key(),
|
||||
Label: label,
|
||||
HTML: seitenEditorHTMLValue(record.HTML()),
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortFunc(sections, func(a, b SeitenEditorSection) int {
|
||||
return strings.Compare(a.Key, b.Key)
|
||||
})
|
||||
|
||||
return sections, nil
|
||||
}
|
||||
|
||||
func seitenEditorHTMLValue(value any) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return v
|
||||
case []byte:
|
||||
return string(v)
|
||||
case types.JSONRaw:
|
||||
return string(v)
|
||||
default:
|
||||
return fmt.Sprint(value)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package dbmodels
|
||||
|
||||
import "github.com/pocketbase/pocketbase/core"
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
core.BaseRecordProxy
|
||||
@@ -59,7 +63,19 @@ func (p *Page) Data() map[string]interface{} {
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
return val.(map[string]interface{})
|
||||
if data, ok := val.(map[string]interface{}); ok {
|
||||
return data
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
if err := p.UnmarshalJSONField(DATA_FIELD, &data); err != nil {
|
||||
slog.Error("Error unmarshalling page data", "error", err)
|
||||
return nil
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (p *Page) SetData(data map[string]interface{}) {
|
||||
|
||||
@@ -11,6 +11,7 @@ type IApp interface {
|
||||
Core() core.App
|
||||
ResetDataCache()
|
||||
ResetHtmlCache()
|
||||
ResetPagesCache()
|
||||
Logger() *slog.Logger
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ const (
|
||||
|
||||
P_USER_MGMT_NAME = "user_management"
|
||||
|
||||
P_SEITEN_EDIT_NAME = "seiten_edit"
|
||||
|
||||
P_ALMANACH_EDIT_NAME = "almanach_edit"
|
||||
P_ALMANACH_NEW_NAME = "almanach_new"
|
||||
P_REIHE_EDIT_NAME = "reihe_edit"
|
||||
|
||||
@@ -8812,6 +8812,15 @@ class Qc extends HTMLElement {
|
||||
<i class="ri-external-link-line text-base"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="grid grid-cols-[1fr_auto] group">
|
||||
<a href="/redaktion/seiten/" class="flex items-center px-4 py-2 group-hover:bg-gray-100 transition-colors no-underline text-sm">
|
||||
<i class="ri-pages-line text-base text-gray-700 mr-2.5"></i>
|
||||
<span class="text-gray-900">Seiten</span>
|
||||
</a>
|
||||
<a href="/redaktion/seiten/" target="_blank" class="flex items-center justify-center px-3 py-2 group-hover:bg-gray-100 text-gray-700 hover:text-slate-900 transition-colors no-underline text-sm" title="In neuem Tab öffnen">
|
||||
<i class="ri-external-link-line text-base"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 my-1"></div>
|
||||
` : "", w = r ? `
|
||||
<div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
views/layouts/fragment/root.gohtml
Normal file
2
views/layouts/fragment/root.gohtml
Normal file
@@ -0,0 +1,2 @@
|
||||
{{ block "body" . }}
|
||||
{{ end }}
|
||||
43
views/routes/redaktion/seiten/body.gohtml
Normal file
43
views/routes/redaktion/seiten/body.gohtml
Normal file
@@ -0,0 +1,43 @@
|
||||
{{ $model := . }}
|
||||
|
||||
<div class="flex container-normal bg-slate-100 mx-auto px-8">
|
||||
<div class="flex flex-row w-full justify-between">
|
||||
<div class="flex flex-col justify-end-safe flex-2/5">
|
||||
<div class="mb-1">
|
||||
<i class="ri-pages-line"></i> Seiten
|
||||
</div>
|
||||
<h1 class="text-2xl w-full font-bold text-slate-900 mb-1">
|
||||
Seiteneditor
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-end gap-y-4 pr-4">
|
||||
<div class="inputwrapper !mb-0">
|
||||
<div class="inputlabelrow">
|
||||
<label for="page-editor-select" class="inputlabel">Seite auswählen</label>
|
||||
</div>
|
||||
<select
|
||||
id="page-editor-select"
|
||||
name="key"
|
||||
class="inputinput"
|
||||
hx-get="/redaktion/seiten/form/"
|
||||
hx-trigger="change"
|
||||
hx-target="#page-editor-form"
|
||||
hx-swap="outerHTML"
|
||||
hx-indicator="body">
|
||||
{{- if $model.pages -}}
|
||||
{{- range $page := $model.pages -}}
|
||||
<option value="{{ $page.Key }}" {{ if eq $page.Key $model.selected_key }}selected{{ end }}>{{ $page.Title }}</option>
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
<option value="">Keine Seiten gefunden</option>
|
||||
{{- end -}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-normal mx-auto mt-4 !px-0">
|
||||
{{ template "_page_form" $model }}
|
||||
</div>
|
||||
135
views/routes/redaktion/seiten/components/_page_form.gohtml
Normal file
135
views/routes/redaktion/seiten/components/_page_form.gohtml
Normal file
@@ -0,0 +1,135 @@
|
||||
{{ $model := . }}
|
||||
|
||||
<div id="page-editor-form">
|
||||
{{ template "_usermessage" $model }}
|
||||
|
||||
{{- if not $model.selected -}}
|
||||
<div class="text-gray-700 bg-slate-100 border border-slate-200 rounded-xs p-4">
|
||||
Keine Seite ausgewählt.
|
||||
</div>
|
||||
{{- else -}}
|
||||
<form
|
||||
class="w-full dbform"
|
||||
method="POST"
|
||||
action="/redaktion/seiten/save/"
|
||||
hx-boost="false">
|
||||
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
|
||||
<input type="hidden" name="page_key" value="{{ $model.selected.Key }}" />
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="inputwrapper">
|
||||
<div class="inputlabelrow">
|
||||
<label for="page-title" class="inputlabel">Titel</label>
|
||||
</div>
|
||||
<input type="text" id="page-title" name="title" class="inputinput" value="{{ $model.selected.Title }}" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="inputwrapper">
|
||||
<div class="inputlabelrow">
|
||||
<label for="page-description" class="inputlabel">Beschreibung</label>
|
||||
</div>
|
||||
<textarea id="page-description" name="description" class="inputinput" rows="3" autocomplete="off">{{ $model.selected.Description }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="inputwrapper">
|
||||
<div class="inputlabelrow">
|
||||
<label for="page-keywords" class="inputlabel">Stichworte</label>
|
||||
</div>
|
||||
<input type="text" id="page-keywords" name="keywords" class="inputinput" value="{{ $model.selected.Keywords }}" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 text-lg font-bold text-gray-700 mt-4">
|
||||
<i class="ri-file-edit-line"></i>
|
||||
<span>Seiteninhalte</span>
|
||||
</div>
|
||||
|
||||
{{- if not $model.selected.Sections -}}
|
||||
<div class="text-gray-700 bg-slate-100 border border-slate-200 rounded-xs p-4">
|
||||
Keine HTML-Bereiche gefunden.
|
||||
</div>
|
||||
{{- else -}}
|
||||
{{- range $index, $section := $model.selected.Sections -}}
|
||||
<div class="inputwrapper">
|
||||
<div class="inputlabelrow">
|
||||
<div class="flex flex-col">
|
||||
<label for="page-html-{{ $index }}" class="inputlabel">{{ $section.Label }}</label>
|
||||
<span class="text-xs text-gray-500">{{ $section.Key }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<trix-toolbar id="page-html-toolbar-{{ $index }}">
|
||||
<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="page-html-{{ $index }}" name="html[{{ $section.Key }}]" autocomplete="off">{{- $section.HTML -}}</textarea>
|
||||
<trix-editor input="page-html-{{ $index }}" toolbar="page-html-toolbar-{{ $index }}"></trix-editor>
|
||||
</div>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
<div class="flex justify-end mt-6">
|
||||
<button type="submit" class="btn bg-slate-800 text-white px-4 py-2 rounded-xs hover:bg-slate-900">
|
||||
<i class="ri-save-line"></i> Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{- end -}}
|
||||
</div>
|
||||
1
views/routes/redaktion/seiten/form/body.gohtml
Normal file
1
views/routes/redaktion/seiten/form/body.gohtml
Normal file
@@ -0,0 +1 @@
|
||||
{{ template "_page_form" . }}
|
||||
1
views/routes/redaktion/seiten/head.gohtml
Normal file
1
views/routes/redaktion/seiten/head.gohtml
Normal file
@@ -0,0 +1 @@
|
||||
<title>Seiteneditor</title>
|
||||
@@ -188,6 +188,15 @@ export class FabMenu extends HTMLElement {
|
||||
<i class="ri-external-link-line text-base"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="grid grid-cols-[1fr_auto] group">
|
||||
<a href="/redaktion/seiten/" class="flex items-center px-4 py-2 group-hover:bg-gray-100 transition-colors no-underline text-sm">
|
||||
<i class="ri-pages-line text-base text-gray-700 mr-2.5"></i>
|
||||
<span class="text-gray-900">Seiten</span>
|
||||
</a>
|
||||
<a href="/redaktion/seiten/" target="_blank" class="flex items-center justify-center px-3 py-2 group-hover:bg-gray-100 text-gray-700 hover:text-slate-900 transition-colors no-underline text-sm" title="In neuem Tab öffnen">
|
||||
<i class="ri-external-link-line text-base"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 my-1"></div>
|
||||
`
|
||||
: "";
|
||||
|
||||
Reference in New Issue
Block a user