Refactoring of calendar widget
This commit is contained in:
@@ -3,16 +3,15 @@
|
|||||||
data-display-month="{{.DisplayMonth}}">
|
data-display-month="{{.DisplayMonth}}">
|
||||||
<div class="panel-header"><a href="{{.DiaryURL}}">Chronological</a></div>
|
<div class="panel-header"><a href="{{.DiaryURL}}">Chronological</a></div>
|
||||||
<div class="diary-cal-nav">
|
<div class="diary-cal-nav">
|
||||||
<a href="{{.DisplayMonthURL}}" class="diary-cal-heading" data-cal-month-link>{{.DisplayMonthName}}</a>
|
|
||||||
<div class="dropdown diary-cal-drop">
|
<div class="dropdown diary-cal-drop">
|
||||||
<button type="button" class="btn btn-small" data-action="cal-month-drop" aria-expanded="false" title="Monat wählen">▾</button>
|
<button type="button" class="btn" data-cal-month-link data-action="cal-month-drop" aria-expanded="false" title="Monat wählen"> {{.DisplayMonthName}} </button>
|
||||||
<div class="dropdown-menu scrollable">
|
<div class="dropdown-menu scrollable">
|
||||||
{{range .Months}}<a class="btn btn-block" data-cal-month-jump="{{.Num}}" href="{{.AnchorURL}}">{{.Name}}</a>{{end}}
|
{{range .Months}}<a class="btn btn-block" data-cal-month-jump="{{.Num}}" href="{{.AnchorURL}}">{{.Name}}</a>{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{.YearURL}}" class="diary-cal-heading">{{.DisplayYear}}</a>
|
<a href="{{.YearURL}}" class="diary-cal-heading">{{.DisplayYear}}</a>
|
||||||
<div class="dropdown diary-cal-drop">
|
<div class="dropdown diary-cal-drop">
|
||||||
<button type="button" class="btn btn-small" data-action="cal-year-drop" aria-expanded="false" title="Jahr wählen">▾</button>
|
<button type="button" class="btn" data-action="cal-year-drop" aria-expanded="false" title="Jahr wählen">▾</button>
|
||||||
<div class="dropdown-menu align-right scrollable">
|
<div class="dropdown-menu align-right scrollable">
|
||||||
{{range .Years}}<a class="btn btn-block{{if .IsCurrent}} cal-current{{end}}" href="{{.URL}}">{{.Num}}</a>{{end}}
|
{{range .Years}}<a class="btn btn-block{{if .IsCurrent}} cal-current{{end}}" href="{{.URL}}">{{.Num}}</a>{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
var displayYear = parseInt(cal.dataset.displayYear, 10);
|
var displayYear = parseInt(cal.dataset.displayYear, 10);
|
||||||
var current = parseInt(cal.dataset.displayMonth, 10);
|
var current = parseInt(cal.dataset.displayMonth, 10);
|
||||||
var monthLink = cal.querySelector("[data-cal-month-link]");
|
var monthLabel = cal.querySelector("[data-cal-month-link]");
|
||||||
var months = {};
|
var months = {};
|
||||||
cal.querySelectorAll("[data-cal-month]").forEach(function (t) {
|
cal.querySelectorAll("[data-cal-month]").forEach(function (t) {
|
||||||
months[parseInt(t.dataset.calMonth, 10)] = t;
|
months[parseInt(t.dataset.calMonth, 10)] = t;
|
||||||
@@ -15,18 +15,15 @@
|
|||||||
jumpLinks[parseInt(a.dataset.calMonthJump, 10)] = a;
|
jumpLinks[parseInt(a.dataset.calMonthJump, 10)] = a;
|
||||||
});
|
});
|
||||||
|
|
||||||
function pad(n) { return n < 10 ? "0" + n : "" + n; }
|
|
||||||
|
|
||||||
function show(m) {
|
function show(m) {
|
||||||
if (m === current) return;
|
if (m === current) return;
|
||||||
if (!months[m]) return;
|
if (!months[m]) return;
|
||||||
months[current].hidden = true;
|
months[current].hidden = true;
|
||||||
months[m].hidden = false;
|
months[m].hidden = false;
|
||||||
current = m;
|
current = m;
|
||||||
if (monthLink) {
|
if (monthLabel) {
|
||||||
var label = jumpLinks[m];
|
var label = jumpLinks[m];
|
||||||
if (label) monthLink.textContent = label.textContent;
|
if (label) monthLabel.textContent = " " + label.textContent + " ";
|
||||||
monthLink.setAttribute("href", "#" + displayYear + "-" + pad(m));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -369,7 +369,6 @@ type calendarData struct {
|
|||||||
DisplayYear int
|
DisplayYear int
|
||||||
DisplayMonth int
|
DisplayMonth int
|
||||||
DisplayMonthName string // pre-resolved so the template doesn't need arithmetic
|
DisplayMonthName string // pre-resolved so the template doesn't need arithmetic
|
||||||
DisplayMonthURL string
|
|
||||||
DiaryURL string
|
DiaryURL string
|
||||||
YearURL string
|
YearURL string
|
||||||
Months []calMonthGrid
|
Months []calMonthGrid
|
||||||
@@ -470,7 +469,7 @@ func computeCalendarWidget(diaryRootFS, diaryRootURL, fsPath string, depth int)
|
|||||||
Num: m,
|
Num: m,
|
||||||
Name: germanMonths[time.Month(m)],
|
Name: germanMonths[time.Month(m)],
|
||||||
AnchorURL: monthAnchor(displayYear, m),
|
AnchorURL: monthAnchor(displayYear, m),
|
||||||
Weeks: buildMonthGrid(displayYear, m, today, cd, hasDayEntryByMonth[m], diaryRootURL, dayAnchor),
|
Weeks: buildMonthGrid(displayYear, m, today, cd, hasDayEntryByMonth[m], dayAnchor),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,7 +505,6 @@ func computeCalendarWidget(diaryRootFS, diaryRootURL, fsPath string, depth int)
|
|||||||
DisplayYear: displayYear,
|
DisplayYear: displayYear,
|
||||||
DisplayMonth: displayMonth,
|
DisplayMonth: displayMonth,
|
||||||
DisplayMonthName: months[displayMonth-1].Name,
|
DisplayMonthName: months[displayMonth-1].Name,
|
||||||
DisplayMonthURL: months[displayMonth-1].AnchorURL,
|
|
||||||
DiaryURL: diaryRootURL,
|
DiaryURL: diaryRootURL,
|
||||||
YearURL: yearURL,
|
YearURL: yearURL,
|
||||||
Months: months,
|
Months: months,
|
||||||
@@ -523,10 +521,11 @@ func computeCalendarWidget(diaryRootFS, diaryRootURL, fsPath string, depth int)
|
|||||||
|
|
||||||
// buildMonthGrid renders one month's day cells as a Monday-first week grid.
|
// buildMonthGrid renders one month's day cells as a Monday-first week grid.
|
||||||
// hasDayEntry maps day-of-month → has a diary entry. dayAnchor produces the
|
// hasDayEntry maps day-of-month → has a diary entry. dayAnchor produces the
|
||||||
// in-page anchor (or full URL when crossing pages). Empty days always link
|
// in-page anchor (or full URL when crossing pages); empty days link to the
|
||||||
// to the full month/day URL with ?edit so the diary handler can route into
|
// same anchor — every day exists on the year page as either a real or
|
||||||
// the "insert new day section" editor.
|
// virtual section, so navigation is enough. Page creation happens via the
|
||||||
func buildMonthGrid(year, month int, today time.Time, currentDay int, hasDayEntry map[int]bool, diaryRootURL string, dayAnchor func(int, int, int) string) [][]calDay {
|
// [edit] button on the heading itself.
|
||||||
|
func buildMonthGrid(year, month int, today time.Time, currentDay int, hasDayEntry map[int]bool, dayAnchor func(int, int, int) string) [][]calDay {
|
||||||
firstDay := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
|
firstDay := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
|
||||||
startOffset := int(firstDay.Weekday()+6) % 7
|
startOffset := int(firstDay.Weekday()+6) % 7
|
||||||
daysInMonth := time.Date(year, time.Month(month)+1, 0, 0, 0, 0, 0, time.UTC).Day()
|
daysInMonth := time.Date(year, time.Month(month)+1, 0, 0, 0, 0, 0, time.UTC).Day()
|
||||||
@@ -535,14 +534,10 @@ func buildMonthGrid(year, month int, today time.Time, currentDay int, hasDayEntr
|
|||||||
week := make([]calDay, 7)
|
week := make([]calDay, 7)
|
||||||
col := startOffset
|
col := startOffset
|
||||||
for d := 1; d <= daysInMonth; d++ {
|
for d := 1; d <= daysInMonth; d++ {
|
||||||
cell := calDay{Num: d, HasEntry: hasDayEntry[d]}
|
cell := calDay{
|
||||||
if cell.HasEntry {
|
Num: d,
|
||||||
cell.URL = dayAnchor(year, month, d)
|
HasEntry: hasDayEntry[d],
|
||||||
} else {
|
URL: dayAnchor(year, month, d),
|
||||||
cell.URL = path.Join(diaryRootURL,
|
|
||||||
fmt.Sprintf("%d", year),
|
|
||||||
fmt.Sprintf("%02d", month),
|
|
||||||
fmt.Sprintf("%02d", d)) + "/?edit"
|
|
||||||
}
|
}
|
||||||
cell.IsCurrent = currentDay > 0 && d == currentDay
|
cell.IsCurrent = currentDay > 0 && d == currentDay
|
||||||
cell.IsToday = d == today.Day() &&
|
cell.IsToday = d == today.Day() &&
|
||||||
@@ -669,7 +664,7 @@ func renderDiaryYear(yearFS, yearURL string) template.HTML {
|
|||||||
photos := yearPhotos(yearFS, yearURL)
|
photos := yearPhotos(yearFS, yearURL)
|
||||||
|
|
||||||
out := buildSectionsForRange(sections, photos, 1, len(sections), yearURL)
|
out := buildSectionsForRange(sections, photos, 1, len(sections), yearURL)
|
||||||
out = appendOrphanPhotoDays(out, photos, year)
|
out = appendVirtualEntries(out, sections, photos, year, yearURL)
|
||||||
return renderDiaryContent(out)
|
return renderDiaryContent(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,79 +694,112 @@ func buildSectionsForRange(sections [][]byte, photos []diaryPhoto, start, end in
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendOrphanPhotoDays inserts synthetic day sections (no edit button, no
|
// appendVirtualEntries inserts virtual month and day sections for every
|
||||||
// body) for photo dates not already covered by an explicit day section.
|
// `## YYYY-MM` / `### YYYY-MM-DD` slot in `year` that lacks a real section.
|
||||||
// Orphans are interleaved by date with existing *date* day sections only —
|
// Virtual day sections carry photos when present. Each virtual entry's
|
||||||
// non-date level-3 headings (e.g. `### Movies` in a year intro) keep their
|
// EditURL routes through the insert-before flow so clicking [edit] splices
|
||||||
// original document position. Remaining orphans are appended at the end so
|
// the section into the year file at the right chronological position.
|
||||||
// the year file's intro material always stays above the diary entries.
|
//
|
||||||
func appendOrphanPhotoDays(existing []diarySection, photos []diaryPhoto, year int) []diarySection {
|
// Scope: past years get all 12 months / 365(6) days; the current year stops
|
||||||
covered := map[string]bool{}
|
// at today; future years are returned unchanged.
|
||||||
|
//
|
||||||
|
// Interleave: real date sections (`## YYYY-MM`, `### YYYY-MM-DD`) keep their
|
||||||
|
// document position; virtual entries are spliced in lexicographic ID order
|
||||||
|
// before the next real date section. Non-date headings (e.g. `## Events` →
|
||||||
|
// `### Festival` in a year intro) are left where the user wrote them.
|
||||||
|
func appendVirtualEntries(existing []diarySection, sections [][]byte, photos []diaryPhoto, year int, yearURL string) []diarySection {
|
||||||
|
today := time.Now()
|
||||||
|
if year > today.Year() {
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
|
||||||
|
coveredMonth := map[string]bool{}
|
||||||
|
coveredDay := map[string]bool{}
|
||||||
for _, s := range existing {
|
for _, s := range existing {
|
||||||
if s.Level == 3 {
|
switch {
|
||||||
covered[s.ID] = true
|
case s.Level == 2 && len(s.ID) == 7 && isDateHeading(s.ID):
|
||||||
|
coveredMonth[s.ID] = true
|
||||||
|
case s.Level == 3 && len(s.ID) == 10 && isDateHeading(s.ID):
|
||||||
|
coveredDay[s.ID] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type orphan struct {
|
|
||||||
date time.Time
|
photoByDay := map[string][]diaryPhoto{}
|
||||||
header string
|
|
||||||
photos []diaryPhoto
|
|
||||||
}
|
|
||||||
orphMap := map[string]*orphan{}
|
|
||||||
for _, p := range photos {
|
for _, p := range photos {
|
||||||
if p.Date.Year() != year {
|
if p.Date.Year() != year {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
key := p.Date.Format("2006-01-02")
|
photoByDay[p.Date.Format("2006-01-02")] = append(photoByDay[p.Date.Format("2006-01-02")], p)
|
||||||
if covered[key] {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
o, ok := orphMap[key]
|
|
||||||
if !ok {
|
lastDay := time.Date(year, time.December, 31, 0, 0, 0, 0, time.UTC)
|
||||||
o = &orphan{date: p.Date, header: key}
|
if year == today.Year() {
|
||||||
orphMap[key] = o
|
lastDay = time.Date(year, today.Month(), today.Day(), 0, 0, 0, 0, time.UTC)
|
||||||
}
|
}
|
||||||
o.photos = append(o.photos, p)
|
|
||||||
|
var virtual []diarySection
|
||||||
|
for d := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC); !d.After(lastDay); d = d.AddDate(0, 0, 1) {
|
||||||
|
if d.Day() == 1 {
|
||||||
|
monthID := d.Format("2006-01")
|
||||||
|
if !coveredMonth[monthID] {
|
||||||
|
idx := computeInsertIndex(sections, monthID)
|
||||||
|
virtual = append(virtual, diarySection{
|
||||||
|
Level: 2,
|
||||||
|
ID: monthID,
|
||||||
|
Heading: monthID,
|
||||||
|
EditURL: fmt.Sprintf("%s?edit&insert_before=%d&heading=%s&level=%s",
|
||||||
|
yearURL, idx, url.QueryEscape(monthID), url.QueryEscape("##")),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if len(orphMap) == 0 {
|
}
|
||||||
|
dayID := d.Format("2006-01-02")
|
||||||
|
if !coveredDay[dayID] {
|
||||||
|
idx := computeInsertIndex(sections, dayID)
|
||||||
|
virtual = append(virtual, diarySection{
|
||||||
|
Level: 3,
|
||||||
|
ID: dayID,
|
||||||
|
Heading: dayID,
|
||||||
|
EditURL: fmt.Sprintf("%s?edit&insert_before=%d&heading=%s",
|
||||||
|
yearURL, idx, url.QueryEscape(dayID)),
|
||||||
|
Photos: photoByDay[dayID],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(virtual) == 0 {
|
||||||
return existing
|
return existing
|
||||||
}
|
}
|
||||||
orphans := make([]*orphan, 0, len(orphMap))
|
|
||||||
for _, o := range orphMap {
|
|
||||||
orphans = append(orphans, o)
|
|
||||||
}
|
|
||||||
sort.Slice(orphans, func(i, j int) bool { return orphans[i].date.Before(orphans[j].date) })
|
|
||||||
|
|
||||||
out := make([]diarySection, 0, len(existing)+len(orphans))
|
out := make([]diarySection, 0, len(existing)+len(virtual))
|
||||||
oi := 0
|
vi := 0
|
||||||
for _, s := range existing {
|
for _, s := range existing {
|
||||||
if s.Level == 3 {
|
if isRealDateSection(s) {
|
||||||
if _, _, _, ok := parseISODate(s.ID); ok {
|
for vi < len(virtual) && virtual[vi].ID < s.ID {
|
||||||
for oi < len(orphans) && orphans[oi].header < s.ID {
|
out = append(out, virtual[vi])
|
||||||
out = append(out, diarySection{
|
vi++
|
||||||
Level: 3,
|
|
||||||
ID: orphans[oi].header,
|
|
||||||
Heading: orphans[oi].header,
|
|
||||||
Photos: orphans[oi].photos,
|
|
||||||
})
|
|
||||||
oi++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out = append(out, s)
|
out = append(out, s)
|
||||||
}
|
}
|
||||||
for oi < len(orphans) {
|
for vi < len(virtual) {
|
||||||
out = append(out, diarySection{
|
out = append(out, virtual[vi])
|
||||||
Level: 3,
|
vi++
|
||||||
ID: orphans[oi].header,
|
|
||||||
Heading: orphans[oi].header,
|
|
||||||
Photos: orphans[oi].photos,
|
|
||||||
})
|
|
||||||
oi++
|
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isRealDateSection reports whether a rendered diarySection is one of the
|
||||||
|
// date-headed slots (`## YYYY-MM` or `### YYYY-MM-DD`) the virtual-entry
|
||||||
|
// interleave sorts against.
|
||||||
|
func isRealDateSection(s diarySection) bool {
|
||||||
|
switch s.Level {
|
||||||
|
case 2:
|
||||||
|
return len(s.ID) == 7 && isDateHeading(s.ID)
|
||||||
|
case 3:
|
||||||
|
return len(s.ID) == 10 && isDateHeading(s.ID)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// parseISODate parses "YYYY-MM-DD" leading characters of s. Returns ok=false
|
// parseISODate parses "YYYY-MM-DD" leading characters of s. Returns ok=false
|
||||||
// if the prefix does not match.
|
// if the prefix does not match.
|
||||||
func parseISODate(s string) (year, month, day int, ok bool) {
|
func parseISODate(s string) (year, month, day int, ok bool) {
|
||||||
|
|||||||
Reference in New Issue
Block a user