add inline edit / fix copy button
This commit is contained in:
parent
db3322c3d8
commit
7666186f83
10 changed files with 304 additions and 45 deletions
|
|
@ -14,6 +14,15 @@ func Unique[T any](slice []T, key func(item T) string) []T {
|
|||
return result
|
||||
}
|
||||
|
||||
func Find[T any](slice []T, predicate func(item *T) bool) *T {
|
||||
for _, v := range slice {
|
||||
if predicate(&v) {
|
||||
return &v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter returns a new slice with only items that match the predicate.
|
||||
func Filter[T any](slice []T, predicate func(item T) bool) []T {
|
||||
var result []T
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ func RenderMarkdown(reader io.Reader) bytes.Buffer {
|
|||
highlighting.WithFormatOptions(
|
||||
chromahtml.WithLineNumbers(true),
|
||||
chromahtml.WithCustomCSS(map[chroma.TokenType]string{
|
||||
chroma.PreWrapper: "padding: 12px; overflow: auto; background-color: rgb(245, 245, 245) !important;",
|
||||
chroma.PreWrapper: "font-size: 14px; padding: 12px; overflow: auto; background-color: rgb(245, 245, 245) !important;",
|
||||
}),
|
||||
),
|
||||
highlighting.WithStyle("github"),
|
||||
|
|
|
|||
10
htmgo-site/pages/examples/click-to-edit.go
Normal file
10
htmgo-site/pages/examples/click-to-edit.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package examples
|
||||
|
||||
import (
|
||||
"github.com/maddalax/htmgo/framework/h"
|
||||
)
|
||||
|
||||
func ClickToEditExample(ctx *h.RequestContext) *h.Page {
|
||||
SetSnippet(ctx, &ClickToEditSnippet)
|
||||
return Index(ctx)
|
||||
}
|
||||
|
|
@ -8,6 +8,9 @@ import (
|
|||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
|
@ -29,6 +32,21 @@ var RenderCodeToStringCached = h.CachedPerKeyT(time.Minute*30, func(snippet *Sni
|
|||
})
|
||||
|
||||
func renderCodeToString(snippet *Snippet) *h.Element {
|
||||
source := ""
|
||||
// in development, use the local file
|
||||
if h.IsDevelopment() {
|
||||
ptr := reflect.ValueOf(snippet.partial).Pointer()
|
||||
fnInfo := runtime.FuncForPC(ptr)
|
||||
if fnInfo == nil {
|
||||
return h.Empty()
|
||||
}
|
||||
file, _ := fnInfo.FileLine(ptr)
|
||||
b, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return h.Empty()
|
||||
}
|
||||
source = string(b)
|
||||
} else {
|
||||
url := GetGithubRawPath(snippet.path)
|
||||
slog.Info("getting snippet source code", slog.String("url", url))
|
||||
resp, err := http.Get(url)
|
||||
|
|
@ -44,5 +62,8 @@ func renderCodeToString(snippet *Snippet) *h.Element {
|
|||
if err != nil {
|
||||
return h.Empty()
|
||||
}
|
||||
return ui.CodeSnippet(out.String(), "border-radius: 0.5rem;")
|
||||
source = out.String()
|
||||
}
|
||||
|
||||
return ui.CodeSnippet(source, "border-radius: 0.5rem;")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import "htmgo-site/partials/snippets"
|
|||
var FormWithLoadingStateSnippet = Snippet{
|
||||
name: "Form",
|
||||
description: "A simple form submission example with a loading state",
|
||||
sidebarName: "Form with loading state",
|
||||
sidebarName: "Form With Loading State",
|
||||
path: "/examples/form",
|
||||
partial: snippets.FormExample,
|
||||
}
|
||||
|
|
@ -22,16 +22,16 @@ var UserAuthSnippet = Snippet{
|
|||
var ChatSnippet = Snippet{
|
||||
name: "Chat App",
|
||||
description: "A simple chat application built with htmgo using SSE for real-time updates",
|
||||
sidebarName: "Chat App",
|
||||
sidebarName: "Chat App Using SSE",
|
||||
path: "/examples/chat",
|
||||
externalRoute: "https://chat-example.htmgo.dev",
|
||||
sourceCodePath: "https://github.com/maddalax/htmgo/tree/master/examples/chat",
|
||||
}
|
||||
|
||||
var HackerNewsSnippet = Snippet{
|
||||
name: "Hacker News Clone",
|
||||
name: "HackerNews Clone",
|
||||
description: "A hacker news reader clone built with htmgo",
|
||||
sidebarName: "Hacker News Clone",
|
||||
sidebarName: "HackerNews Clone",
|
||||
path: "/examples/hackernews",
|
||||
externalRoute: "https://hn.htmgo.dev",
|
||||
sourceCodePath: "https://github.com/maddalax/htmgo/tree/master/examples/hackernews",
|
||||
|
|
@ -55,8 +55,17 @@ var TodoListSnippet = Snippet{
|
|||
sourceCodePath: "https://github.com/maddalax/htmgo/tree/master/examples/todo-list",
|
||||
}
|
||||
|
||||
var ClickToEditSnippet = Snippet{
|
||||
name: "Inline Click To Edit",
|
||||
description: "List view of items with a click to edit button and persistence",
|
||||
sidebarName: "Inline click to edit",
|
||||
path: "/examples/click-to-edit",
|
||||
partial: snippets.ClickToEdit,
|
||||
}
|
||||
|
||||
var examples = []Snippet{
|
||||
FormWithLoadingStateSnippet,
|
||||
ClickToEditSnippet,
|
||||
UserAuthSnippet,
|
||||
ChatSnippet,
|
||||
HackerNewsSnippet,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ func HtmlToGoPage(ctx *h.RequestContext) *h.Page {
|
|||
h.Div(
|
||||
h.Class("h-full w-full flex gap-4 p-8"),
|
||||
partials.HtmlInput(),
|
||||
partials.HiddenCopyOutput(""),
|
||||
partials.GoOutput(""),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,18 +2,20 @@ package partials
|
|||
|
||||
import (
|
||||
"github.com/maddalax/htmgo/framework/h"
|
||||
"github.com/maddalax/htmgo/framework/js"
|
||||
"github.com/maddalax/htmgo/tools/html-to-htmgo/htmltogo"
|
||||
"htmgo-site/ui"
|
||||
)
|
||||
|
||||
func ConvertHtmlToGo(ctx *h.RequestContext) *h.Partial {
|
||||
value := ctx.FormValue("html-input")
|
||||
parsed := htmltogo.Parse([]byte(value))
|
||||
parsed := string(htmltogo.Parse([]byte(value)))
|
||||
|
||||
formatted := ui.FormatCode(string(parsed), "height: 100%;")
|
||||
formatted := ui.FormatCode(parsed, "height: 100%;")
|
||||
|
||||
return h.SwapPartial(ctx, GoOutput(formatted))
|
||||
return h.SwapManyPartial(ctx,
|
||||
GoOutput(formatted),
|
||||
HiddenCopyOutput(parsed),
|
||||
)
|
||||
}
|
||||
|
||||
func HtmlInput() *h.Element {
|
||||
|
|
@ -30,6 +32,14 @@ func HtmlInput() *h.Element {
|
|||
)
|
||||
}
|
||||
|
||||
func HiddenCopyOutput(content string) *h.Element {
|
||||
return h.Div(
|
||||
h.Class("hidden"),
|
||||
h.Id("go-output-raw"),
|
||||
h.UnsafeRaw(content),
|
||||
)
|
||||
}
|
||||
|
||||
func GoOutput(content string) *h.Element {
|
||||
return h.Div(
|
||||
h.Class("h-full w-1/2 min-w-1/2"),
|
||||
|
|
@ -43,25 +53,7 @@ func GoOutput(content string) *h.Element {
|
|||
),
|
||||
h.If(
|
||||
content != "",
|
||||
h.Div(
|
||||
h.Class("absolute top-0 right-0 p-2 bg-slate-800 text-white rounded-bl-md cursor-pointer"),
|
||||
h.Text("Copy"),
|
||||
h.OnClick(
|
||||
// language=JavaScript
|
||||
js.EvalJs(`
|
||||
if(!navigator.clipboard) {
|
||||
alert("Clipboard API not supported");
|
||||
return;
|
||||
}
|
||||
let text = self.parentElement.querySelector("#go-output-content").innerText;
|
||||
navigator.clipboard.writeText(text);
|
||||
self.innerText = "Copied!";
|
||||
setTimeout(() => {
|
||||
self.innerText = "Copy";
|
||||
}, 1000);
|
||||
`),
|
||||
),
|
||||
),
|
||||
ui.CopyButton("#go-output-raw"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
178
htmgo-site/partials/snippets/click-to-edit.go
Normal file
178
htmgo-site/partials/snippets/click-to-edit.go
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
package snippets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/maddalax/htmgo/framework/h"
|
||||
)
|
||||
|
||||
// RowClasses defined here for simplicity of the example
|
||||
var RowClasses = "whitespace-nowrap px-4 py-4 font-medium text-gray-900 text-left"
|
||||
var ButtonClasses = "inline-block rounded bg-slate-900 px-4 py-2 text-xs font-medium text-white hover:bg-slate-800"
|
||||
var InputClasses = "-ml-2 max-w-[125px] border p-2 rounded focus:outline-none focus:ring focus:ring-slate-800"
|
||||
|
||||
type Record struct {
|
||||
Id string
|
||||
Name string
|
||||
Birthday string
|
||||
Role string
|
||||
Salary string
|
||||
}
|
||||
|
||||
type TableProps struct {
|
||||
EditingId string
|
||||
}
|
||||
|
||||
var records = []Record{
|
||||
{
|
||||
Id: "1",
|
||||
Name: "John Doe",
|
||||
Birthday: "24/05/1995",
|
||||
Role: "htmgo developer",
|
||||
Salary: "$250,000",
|
||||
},
|
||||
{
|
||||
Id: "2",
|
||||
Name: "Jake Smith",
|
||||
Birthday: "24/05/1995",
|
||||
Role: "htmx developer",
|
||||
Salary: "$100,000",
|
||||
},
|
||||
}
|
||||
|
||||
func ClickToEdit(ctx *h.RequestContext) *h.Partial {
|
||||
return h.NewPartial(
|
||||
h.Div(
|
||||
h.Class("flex gap-2 items-center w-full"),
|
||||
Table(TableProps{
|
||||
// no record is being edited initially
|
||||
EditingId: "",
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// StartEditing is a partial that is called when the user clicks on the edit button,
|
||||
// it will swap in the form for editing for the given record
|
||||
func StartEditing(ctx *h.RequestContext) *h.Partial {
|
||||
id := ctx.QueryParam("id")
|
||||
|
||||
record := h.Find(records, func(record *Record) bool {
|
||||
return record.Id == id
|
||||
})
|
||||
|
||||
if record == nil {
|
||||
return h.EmptyPartial()
|
||||
}
|
||||
|
||||
return h.SwapManyPartial(
|
||||
ctx,
|
||||
TableRow(record, true),
|
||||
)
|
||||
}
|
||||
|
||||
// SaveEditing is a partial that is called when the user clicks on the save button while editing,
|
||||
// it will update the record with the new values and swap it back out
|
||||
// note: in the example, we are just creating a new record in memory instead of updating the existing one,
|
||||
// normally you would update a persistent record in a database
|
||||
func SaveEditing(ctx *h.RequestContext) *h.Partial {
|
||||
id := ctx.QueryParam("id")
|
||||
|
||||
// just for the example, create a new record so it doesn't affect the global original
|
||||
record := Record{
|
||||
Id: id,
|
||||
Name: ctx.FormValue("name"),
|
||||
Birthday: ctx.FormValue("birthday"),
|
||||
Role: ctx.FormValue("role"),
|
||||
Salary: ctx.FormValue("salary"),
|
||||
}
|
||||
|
||||
return h.SwapPartial(ctx, TableRow(&record, false))
|
||||
}
|
||||
|
||||
func Table(props TableProps) *h.Element {
|
||||
return h.Div(
|
||||
h.Class("overflow-x-auto w-full"),
|
||||
h.Table(
|
||||
h.Class("divide-y divide-gray-200 bg-white table-fixed"),
|
||||
h.THead(
|
||||
h.Tr(
|
||||
h.Th(
|
||||
h.Class(RowClasses),
|
||||
h.Text("Name"),
|
||||
),
|
||||
h.Th(
|
||||
h.Class(RowClasses),
|
||||
h.Text("Date of Birth"),
|
||||
),
|
||||
h.Th(
|
||||
h.Class(RowClasses),
|
||||
h.Text("Role"),
|
||||
),
|
||||
h.Th(
|
||||
h.Class(RowClasses),
|
||||
h.Text("Salary"),
|
||||
),
|
||||
h.Th(
|
||||
h.Class("px-4 py-2"),
|
||||
),
|
||||
),
|
||||
),
|
||||
h.TBody(
|
||||
h.Class("divide-y divide-gray-200"),
|
||||
h.List(records, func(record Record, index int) *h.Element {
|
||||
editing := props.EditingId == record.Id
|
||||
return TableRow(&record, editing)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func TableRow(record *Record, editing bool) *h.Element {
|
||||
recordId := fmt.Sprintf("record-%s", record.Id)
|
||||
|
||||
var Cell = func(name string, value string) *h.Element {
|
||||
return h.Td(
|
||||
h.Class(RowClasses, "h-[75px]"),
|
||||
h.IfElse(
|
||||
!editing,
|
||||
h.Pf(
|
||||
value,
|
||||
h.Class("w-[125px]"),
|
||||
),
|
||||
h.Input(
|
||||
"text",
|
||||
h.Class(InputClasses),
|
||||
h.Value(value),
|
||||
h.Name(name),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return h.Tr(
|
||||
h.If(
|
||||
editing,
|
||||
// this is important to make sure the inputs are included in the form submission
|
||||
h.HxInclude("input"),
|
||||
),
|
||||
h.Id(recordId),
|
||||
Cell("name", record.Name),
|
||||
Cell("birthday", record.Birthday),
|
||||
Cell("role", record.Role),
|
||||
Cell("salary", record.Salary),
|
||||
// Edit button
|
||||
h.Td(
|
||||
h.Button(
|
||||
h.Class(ButtonClasses),
|
||||
h.PostPartialWithQs(
|
||||
h.Ternary(!editing, StartEditing, SaveEditing),
|
||||
h.NewQs("id", record.Id),
|
||||
),
|
||||
h.Text(
|
||||
h.Ternary(!editing, "Edit", "Save"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
28
htmgo-site/ui/copy.go
Normal file
28
htmgo-site/ui/copy.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/maddalax/htmgo/framework/h"
|
||||
"github.com/maddalax/htmgo/framework/js"
|
||||
)
|
||||
|
||||
func CopyButton(selector string) *h.Element {
|
||||
return h.Div(
|
||||
h.Class("absolute top-0 right-0 p-2 bg-slate-800 text-white rounded-bl-md cursor-pointer"),
|
||||
h.Text("Copy"),
|
||||
h.OnClick(
|
||||
// language=JavaScript
|
||||
js.EvalJs(fmt.Sprintf(`
|
||||
if(!navigator.clipboard) {
|
||||
return;
|
||||
}
|
||||
let text = document.querySelector("%s").innerText;
|
||||
navigator.clipboard.writeText(text);
|
||||
self.innerText = "Copied!";
|
||||
setTimeout(() => {
|
||||
self.innerText = "Copy";
|
||||
}, 1000);
|
||||
`, selector)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/google/uuid"
|
||||
"github.com/maddalax/htmgo/framework/h"
|
||||
"strings"
|
||||
)
|
||||
|
|
@ -19,7 +20,7 @@ func FormatCode(code string, customStyles ...string) string {
|
|||
html.WrapLongLines(true),
|
||||
html.WithLineNumbers(true),
|
||||
html.WithCustomCSS(map[chroma.TokenType]string{
|
||||
chroma.PreWrapper: fmt.Sprintf("padding: 12px; overflow: auto; background-color: rgb(245, 245, 245) !important; %s", strings.Join(customStyles, ";")),
|
||||
chroma.PreWrapper: fmt.Sprintf("font-size: 14px; padding: 12px; overflow: auto; background-color: rgb(245, 245, 245) !important; %s", strings.Join(customStyles, ";")),
|
||||
}))
|
||||
iterator, err := lexer.Tokenise(nil, code)
|
||||
if err != nil {
|
||||
|
|
@ -30,7 +31,17 @@ func FormatCode(code string, customStyles ...string) string {
|
|||
}
|
||||
|
||||
func CodeSnippet(code string, customStyles ...string) *h.Element {
|
||||
id := fmt.Sprintf("code-snippet-%s", uuid.NewString())
|
||||
return h.Div(
|
||||
h.UnsafeRaw(FormatCode(code, customStyles...)),
|
||||
h.Class("relative"),
|
||||
h.Div(
|
||||
h.UnsafeRaw(code),
|
||||
h.Class("hidden"),
|
||||
h.Id(id),
|
||||
),
|
||||
CopyButton("#"+id),
|
||||
h.UnsafeRaw(
|
||||
FormatCode(code, customStyles...),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue