From c6eec9364fb5533dc72fecdd6c52f793698ded94 Mon Sep 17 00:00:00 2001 From: MrLetsplay Date: Sat, 18 Jan 2025 20:43:31 +0100 Subject: [PATCH] initial commit --- .classpath | 40 ++++++ .gitignore | 1 + .project | 23 ++++ .settings/org.eclipse.core.resources.prefs | 3 + .settings/org.eclipse.jdt.core.prefs | 8 ++ .settings/org.eclipse.m2e.core.prefs | 4 + pom.xml | 67 ++++++++++ src/main/java/me/mrletsplay/nojs/NoJS.java | 109 ++++++++++++++++ .../me/mrletsplay/nojs/_test/DynPage.java | 37 ++++++ .../me/mrletsplay/nojs/_test/LinksPage.java | 76 +++++++++++ .../me/mrletsplay/nojs/_test/TestPage.java | 30 +++++ .../me/mrletsplay/nojs/action/Action.java | 54 ++++++++ .../me/mrletsplay/nojs/action/ActionData.java | 49 ++++++++ .../mrletsplay/nojs/action/ActionEvent.java | 21 ++++ .../mrletsplay/nojs/action/ActionHandler.java | 14 +++ .../nojs/action/ActionHandlers.java | 29 +++++ .../nojs/component/ActionableComponent.java | 9 ++ .../mrletsplay/nojs/component/Component.java | 9 ++ .../nojs/component/ContainerComponent.java | 11 ++ .../nojs/component/impl/Button.java | 76 +++++++++++ .../mrletsplay/nojs/component/impl/Form.java | 73 +++++++++++ .../mrletsplay/nojs/component/impl/Group.java | 47 +++++++ .../mrletsplay/nojs/component/impl/Link.java | 35 ++++++ .../nojs/component/impl/Message.java | 49 ++++++++ .../mrletsplay/nojs/component/impl/Text.java | 42 +++++++ .../nojs/component/impl/TextInput.java | 60 +++++++++ .../java/me/mrletsplay/nojs/page/Page.java | 79 ++++++++++++ .../me/mrletsplay/nojs/page/StaticPage.java | 56 +++++++++ .../nojs/page/layout/GridLayout.java | 115 +++++++++++++++++ .../nojs/page/layout/InsetLayout.java | 20 +++ .../mrletsplay/nojs/page/layout/Layout.java | 9 ++ .../me/mrletsplay/nojs/rest/TestEndpoint.java | 28 +++++ .../nojs/util/ValidationException.java | 23 ++++ src/main/resources/icon/x.svg | 41 ++++++ src/main/resources/style/base.css | 119 ++++++++++++++++++ 35 files changed, 1466 insertions(+) create mode 100644 .classpath create mode 100644 .gitignore create mode 100644 .project create mode 100644 .settings/org.eclipse.core.resources.prefs create mode 100644 .settings/org.eclipse.jdt.core.prefs create mode 100644 .settings/org.eclipse.m2e.core.prefs create mode 100644 pom.xml create mode 100644 src/main/java/me/mrletsplay/nojs/NoJS.java create mode 100644 src/main/java/me/mrletsplay/nojs/_test/DynPage.java create mode 100644 src/main/java/me/mrletsplay/nojs/_test/LinksPage.java create mode 100644 src/main/java/me/mrletsplay/nojs/_test/TestPage.java create mode 100644 src/main/java/me/mrletsplay/nojs/action/Action.java create mode 100644 src/main/java/me/mrletsplay/nojs/action/ActionData.java create mode 100644 src/main/java/me/mrletsplay/nojs/action/ActionEvent.java create mode 100644 src/main/java/me/mrletsplay/nojs/action/ActionHandler.java create mode 100644 src/main/java/me/mrletsplay/nojs/action/ActionHandlers.java create mode 100644 src/main/java/me/mrletsplay/nojs/component/ActionableComponent.java create mode 100644 src/main/java/me/mrletsplay/nojs/component/Component.java create mode 100644 src/main/java/me/mrletsplay/nojs/component/ContainerComponent.java create mode 100644 src/main/java/me/mrletsplay/nojs/component/impl/Button.java create mode 100644 src/main/java/me/mrletsplay/nojs/component/impl/Form.java create mode 100644 src/main/java/me/mrletsplay/nojs/component/impl/Group.java create mode 100644 src/main/java/me/mrletsplay/nojs/component/impl/Link.java create mode 100644 src/main/java/me/mrletsplay/nojs/component/impl/Message.java create mode 100644 src/main/java/me/mrletsplay/nojs/component/impl/Text.java create mode 100644 src/main/java/me/mrletsplay/nojs/component/impl/TextInput.java create mode 100644 src/main/java/me/mrletsplay/nojs/page/Page.java create mode 100644 src/main/java/me/mrletsplay/nojs/page/StaticPage.java create mode 100644 src/main/java/me/mrletsplay/nojs/page/layout/GridLayout.java create mode 100644 src/main/java/me/mrletsplay/nojs/page/layout/InsetLayout.java create mode 100644 src/main/java/me/mrletsplay/nojs/page/layout/Layout.java create mode 100644 src/main/java/me/mrletsplay/nojs/rest/TestEndpoint.java create mode 100644 src/main/java/me/mrletsplay/nojs/util/ValidationException.java create mode 100644 src/main/resources/icon/x.svg create mode 100644 src/main/resources/style/base.css diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..f0e9804 --- /dev/null +++ b/.classpath @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/.project b/.project new file mode 100644 index 0000000..46e8f4c --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + NoJS + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..5b781ec --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/test/java=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..b5490a0 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 +org.eclipse.jdt.core.compiler.compliance=11 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=11 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d083047 --- /dev/null +++ b/pom.xml @@ -0,0 +1,67 @@ + + 4.0.0 + me.mrletsplay + NoJS + 0.0.1-SNAPSHOT + + src/main/java + + + maven-compiler-plugin + 3.10.1 + + 11 + UTF-8 + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.4.0 + + 11 + UTF-8 + src/main/java + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.0 + + + attach-sources + + jar + + + + + + + + + + Graphite-Official + https://maven.graphite-official.com/releases + + + + + + me.mrletsplay + SimpleHTTPServer + 2.1-SNAPSHOT + + + \ No newline at end of file diff --git a/src/main/java/me/mrletsplay/nojs/NoJS.java b/src/main/java/me/mrletsplay/nojs/NoJS.java new file mode 100644 index 0000000..9538077 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/NoJS.java @@ -0,0 +1,109 @@ +package me.mrletsplay.nojs; + +import java.io.IOException; +import java.io.InputStream; + +import org.slf4j.helpers.NOPLogger; + +import me.mrletsplay.nojs._test.DynPage; +import me.mrletsplay.nojs._test.LinksPage; +import me.mrletsplay.nojs._test.TestPage; +import me.mrletsplay.nojs.action.Action; +import me.mrletsplay.nojs.action.ActionData; +import me.mrletsplay.nojs.component.impl.Button; +import me.mrletsplay.nojs.component.impl.Button.Style; +import me.mrletsplay.nojs.component.impl.Form; +import me.mrletsplay.nojs.component.impl.Link; +import me.mrletsplay.nojs.component.impl.TextInput; +import me.mrletsplay.nojs.page.StaticPage; +import me.mrletsplay.simplehttpserver.dom.html.HtmlDocument; +import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; +import me.mrletsplay.simplehttpserver.dom.html.element.HtmlButton; +import me.mrletsplay.simplehttpserver.http.HttpRequestMethod; +import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; +import me.mrletsplay.simplehttpserver.http.header.DefaultClientContentTypes; +import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; +import me.mrletsplay.simplehttpserver.http.request.urlencoded.UrlEncoded; +import me.mrletsplay.simplehttpserver.http.response.HtmlResponse; +import me.mrletsplay.simplehttpserver.http.response.TextResponse; +import me.mrletsplay.simplehttpserver.http.server.HttpServer; +import me.mrletsplay.simplehttpserver.http.util.MimeType; + +public class NoJS { + + public static void main(String[] args) { + HttpServer httpServer = new HttpServer(HttpServer.newConfigurationBuilder() + .hostBindAll() + .port(6969) + .poolSize(10) + .ioWorkers(2) + .debugMode(true) + .logger(NOPLogger.NOP_LOGGER) + .create()); + + httpServer.getDocumentProvider().register(HttpRequestMethod.GET, "/", () -> { + HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); + HtmlDocument doc = new HtmlDocument(); + + doc.setTitle("Test"); + doc.setIcon("/amogus.png"); + doc.setDescription("Just testing"); + + HtmlElement form = HtmlElement.of("form"); + form.setAttribute("method", "post"); + + HtmlElement input = HtmlElement.of("input"); + input.setAttribute("name", "texttest"); + form.appendChild(input); + + HtmlButton button = HtmlElement.button(); + button.setText("Click me!"); + form.appendChild(button); + + doc.getBodyNode().appendChild(form); + + ctx.respond(HttpStatusCodes.OK_200, new HtmlResponse(doc)); + }); + +// new TestEndpoint().register(httpServer.getDocumentProvider()); + + StaticPage test = new StaticPage() + .add(new Button().text("Click me!").action(new Action("poopy").extraData(new ActionData().put("test", "abc"))).style(Style.LINK)) + .add(new Link().text("Click me!").href("#")) + .add(new Form() + .add(new TextInput("username").placeholder("Username")) + .add(new TextInput("password").placeholder("Password")) + .add(new Button().text("Login")) + .add(new Button().text("Can't click here").enabled(false)) + .action(new Action("beans")) + ); + test.register(httpServer.getDocumentProvider(), "/test"); + + new TestPage().register(httpServer.getDocumentProvider(), "/test2"); + new DynPage().register(httpServer.getDocumentProvider(), "/dyn"); + new LinksPage().register(httpServer.getDocumentProvider(), "/links"); + + httpServer.getDocumentProvider().register(HttpRequestMethod.POST, "/", () -> { + HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); + UrlEncoded data = ctx.getClientHeader().getPostData().getParsedAs(DefaultClientContentTypes.URLENCODED); + ctx.respond(HttpStatusCodes.OK_200, new TextResponse(data.toString())); + }); + + httpServer.getDocumentProvider().register(HttpRequestMethod.GET, "/base.css", () -> { + try(InputStream in = NoJS.class.getResourceAsStream("/style/base.css")) { + HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); + ctx.getServerHeader().setContent(MimeType.CSS, in.readAllBytes()); + } catch(IOException ex) { + throw new RuntimeException(ex); + } + }); + +// httpServer.getDocumentProvider().registerPattern(HttpRequestMethod.GET, "/{path...?}", () -> { +// HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); +// ctx.respond(HttpStatusCodes.OK_200, new TextResponse(ctx.getPathParameters().get("path"))); +// }); + + httpServer.start(); + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/_test/DynPage.java b/src/main/java/me/mrletsplay/nojs/_test/DynPage.java new file mode 100644 index 0000000..2c419f8 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/_test/DynPage.java @@ -0,0 +1,37 @@ +package me.mrletsplay.nojs._test; + +import java.util.Date; +import java.util.List; + +import me.mrletsplay.nojs.action.Action; +import me.mrletsplay.nojs.component.Component; +import me.mrletsplay.nojs.component.impl.Button; +import me.mrletsplay.nojs.component.impl.Form; +import me.mrletsplay.nojs.component.impl.Group; +import me.mrletsplay.nojs.component.impl.Text; +import me.mrletsplay.nojs.page.Page; +import me.mrletsplay.nojs.page.layout.GridLayout; +import me.mrletsplay.nojs.page.layout.InsetLayout; +import me.mrletsplay.nojs.page.layout.Layout; + +public class DynPage implements Page { + + @Override + public List getComponents() { + return List.of( + new Text().text(new Date().toString()), + new Form() + .action(Action.empty()) + .layout(new InsetLayout(GridLayout.ofColumns(1), "100px")) + .add(new Button().text("Refresh")), + new Group() + .add(new Text().text("Your name is")) + ); + } + + @Override + public Layout getLayout() { + return GridLayout.ofColumns(1); + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/_test/LinksPage.java b/src/main/java/me/mrletsplay/nojs/_test/LinksPage.java new file mode 100644 index 0000000..9eb5023 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/_test/LinksPage.java @@ -0,0 +1,76 @@ +package me.mrletsplay.nojs._test; + +import java.util.ArrayList; +import java.util.List; + +import me.mrletsplay.nojs.action.Action; +import me.mrletsplay.nojs.action.ActionEvent; +import me.mrletsplay.nojs.action.ActionHandler; +import me.mrletsplay.nojs.component.Component; +import me.mrletsplay.nojs.component.impl.Button; +import me.mrletsplay.nojs.component.impl.Form; +import me.mrletsplay.nojs.component.impl.Group; +import me.mrletsplay.nojs.component.impl.Link; +import me.mrletsplay.nojs.component.impl.Message; +import me.mrletsplay.nojs.component.impl.Text; +import me.mrletsplay.nojs.component.impl.TextInput; +import me.mrletsplay.nojs.page.Page; +import me.mrletsplay.nojs.page.layout.GridLayout; +import me.mrletsplay.nojs.page.layout.Layout; + +public class LinksPage implements Page { + + private List links = new ArrayList<>(); + private String error; + + @Override + public List getComponents() { + Group linksGroup = new Group() + .layout(GridLayout.ofColumns(1) + .gap("2px")); + + for(String s : links) { + linksGroup.add(new Link().href(s).text(s)); + } + + if(links.isEmpty()) { + linksGroup.add(new Text().text("No links added yet")); + } + + if(error != null) { + linksGroup.add(new Message().text(error)); + error = null; + } + + return List.of( + linksGroup, + new Form() + .add(new Text().text("Add a new link:")) + .add(new Group() + .add(new TextInput("link").placeholder("Insert link here")) + .add(new Button().text("Add Link")) + .layout(new GridLayout().columns("1fr", "auto").gap("4px")) + ) + .layout(GridLayout.ofColumns(1).gap("4px")) + .action(new Action("addLink")) + ); + } + + @Override + public Layout getLayout() { + return GridLayout.ofColumns(1) + .gap("10px"); + } + + @ActionHandler("addLink") + public void addLink(ActionEvent event) { + String link = event.getData("link"); + if(link == null || link.isBlank()) { + error = "Invalid link"; + return; + } + + links.add(event.getData("link")); + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/_test/TestPage.java b/src/main/java/me/mrletsplay/nojs/_test/TestPage.java new file mode 100644 index 0000000..9e67848 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/_test/TestPage.java @@ -0,0 +1,30 @@ +package me.mrletsplay.nojs._test; + +import me.mrletsplay.nojs.action.Action; +import me.mrletsplay.nojs.action.ActionEvent; +import me.mrletsplay.nojs.action.ActionHandler; +import me.mrletsplay.nojs.component.impl.Button; +import me.mrletsplay.nojs.component.impl.Form; +import me.mrletsplay.nojs.component.impl.Link; +import me.mrletsplay.nojs.component.impl.TextInput; +import me.mrletsplay.nojs.page.StaticPage; +import me.mrletsplay.nojs.page.layout.GridLayout; + +public class TestPage extends StaticPage { + + public TestPage() { + add(new Form() + .add(new TextInput("beans")) + .add(new Button()) + .add(new Link().href("/test").text("Link to other page")) + .action(new Action("submit")) + .layout(GridLayout.ofSize(2, 2)) + ); + } + + @ActionHandler("submit") + public void onSubmit(ActionEvent event) { + System.out.println("Got event " + event.getAction()); + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/action/Action.java b/src/main/java/me/mrletsplay/nojs/action/Action.java new file mode 100644 index 0000000..f9a72d2 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/action/Action.java @@ -0,0 +1,54 @@ +package me.mrletsplay.nojs.action; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; + +public class Action { + + public static final String ACTION_NAME = "_action"; + + private String name; + private ActionData extraData; + + public Action(String name) { + Objects.requireNonNull(name, "name"); + + this.name = name; + this.extraData = ActionData.EMPTY_DATA; + } + + public String getName() { + return name; + } + + public Action extraData(ActionData extraData) { + this.extraData = extraData != null ? extraData : ActionData.EMPTY_DATA; + return this; + } + + public ActionData getExtraData() { + return extraData; + } + + public List createFormElements() { + List elements = new ArrayList<>(); + + HtmlElement action = HtmlElement.of("input"); + action.setAttribute("name", ACTION_NAME); + action.setAttribute("value", name); + action.setAttribute("hidden"); + elements.add(action); + + elements.addAll(extraData.createFormElements()); + + return elements; + } + + public static Action empty() { + return new Action(""); + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/action/ActionData.java b/src/main/java/me/mrletsplay/nojs/action/ActionData.java new file mode 100644 index 0000000..bd5cb47 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/action/ActionData.java @@ -0,0 +1,49 @@ +package me.mrletsplay.nojs.action; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; + +public class ActionData { + + public static final ActionData EMPTY_DATA = new ActionData(Collections.emptyMap()); + + private Map data; + + private ActionData(Map data) { + this.data = data; + } + + public ActionData() { + this.data = new HashMap<>(); + } + + public ActionData put(String key, String value) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(value, "value"); + + data.put(key, value); + return this; + } + + public List createFormElements() { + if(data.isEmpty()) return Collections.emptyList(); + + List elements = new ArrayList<>(); + for(Map.Entry d : data.entrySet()) { + HtmlElement e = HtmlElement.of("input"); + e.setAttribute("name", d.getKey()); + e.setAttribute("value", d.getValue()); + e.setAttribute("hidden"); + elements.add(e); + } + + return elements; + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/action/ActionEvent.java b/src/main/java/me/mrletsplay/nojs/action/ActionEvent.java new file mode 100644 index 0000000..eb985f0 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/action/ActionEvent.java @@ -0,0 +1,21 @@ +package me.mrletsplay.nojs.action; + +import me.mrletsplay.simplehttpserver.http.request.urlencoded.UrlEncoded; + +public class ActionEvent { + + private UrlEncoded data; + + public ActionEvent(UrlEncoded data) { + this.data = data; + } + + public String getAction() { + return data.getFirst(Action.ACTION_NAME); + } + + public String getData(String name) { + return data.getFirst(name); + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/action/ActionHandler.java b/src/main/java/me/mrletsplay/nojs/action/ActionHandler.java new file mode 100644 index 0000000..6021197 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/action/ActionHandler.java @@ -0,0 +1,14 @@ +package me.mrletsplay.nojs.action; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ActionHandler { + + public String value(); + +} diff --git a/src/main/java/me/mrletsplay/nojs/action/ActionHandlers.java b/src/main/java/me/mrletsplay/nojs/action/ActionHandlers.java new file mode 100644 index 0000000..f423a35 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/action/ActionHandlers.java @@ -0,0 +1,29 @@ +package me.mrletsplay.nojs.action; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public interface ActionHandlers { + + public default void onAction(ActionEvent event) { + if(event.getAction() == null) return; + + for(Method m : getClass().getDeclaredMethods()) { + ActionHandler handler = m.getAnnotation(ActionHandler.class); + if(handler == null) continue; + if(!handler.value().equals(event.getAction())) continue; + + if(m.getParameterCount() != 1 || m.getParameters()[0].getType() != ActionEvent.class) { + System.err.println("Invalid action handler found: " + m.getName() + ". Must have one parameter of type ActionEvent"); + continue; + } + + try { + m.invoke(this, event); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new RuntimeException(e); // TODO: exception + } + } + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/component/ActionableComponent.java b/src/main/java/me/mrletsplay/nojs/component/ActionableComponent.java new file mode 100644 index 0000000..bfa1421 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/component/ActionableComponent.java @@ -0,0 +1,9 @@ +package me.mrletsplay.nojs.component; + +import me.mrletsplay.nojs.action.Action; + +public interface ActionableComponent extends Component { + + public ActionableComponent action(Action action); + +} diff --git a/src/main/java/me/mrletsplay/nojs/component/Component.java b/src/main/java/me/mrletsplay/nojs/component/Component.java new file mode 100644 index 0000000..76f6c48 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/component/Component.java @@ -0,0 +1,9 @@ +package me.mrletsplay.nojs.component; + +import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; + +public interface Component { + + public HtmlElement toHtml(); + +} diff --git a/src/main/java/me/mrletsplay/nojs/component/ContainerComponent.java b/src/main/java/me/mrletsplay/nojs/component/ContainerComponent.java new file mode 100644 index 0000000..29dcd3b --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/component/ContainerComponent.java @@ -0,0 +1,11 @@ +package me.mrletsplay.nojs.component; + +import me.mrletsplay.nojs.page.layout.Layout; + +public interface ContainerComponent extends Component { + + public ContainerComponent add(Component component); + + public ContainerComponent layout(Layout layout); + +} diff --git a/src/main/java/me/mrletsplay/nojs/component/impl/Button.java b/src/main/java/me/mrletsplay/nojs/component/impl/Button.java new file mode 100644 index 0000000..2882640 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/component/impl/Button.java @@ -0,0 +1,76 @@ +package me.mrletsplay.nojs.component.impl; + +import me.mrletsplay.nojs.action.Action; +import me.mrletsplay.nojs.component.ActionableComponent; +import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; +import me.mrletsplay.simplehttpserver.dom.html.element.HtmlButton; + +public class Button implements ActionableComponent { + + private String text; + private Style style; + private boolean enabled; + private Action action; + + public Button() { + this.text = "Button"; + this.style = Style.NORMAL; + this.enabled = true; + } + + public Button text(String text) { + this.text = text != null ? text : "Button"; + return this; + } + + public Button style(Style style) { + this.style = style != null ? style : Style.NORMAL; + return this; + } + + public Button enabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + @Override + public Button action(Action action) { + this.action = action; + return this; + } + + @Override + public HtmlElement toHtml() { + HtmlButton button = HtmlElement.button(); + button.addClass("button"); + + if(style != null) { + button.setAttribute("data-style", style.name().toLowerCase()); + } + + if(!enabled) { + button.setAttribute("disabled"); + } + + button.setText(text); + + if(action != null) { + button.setAttribute("data-action"); + + HtmlElement form = Form.createElement(action, null); + form.appendChild(button); + return form; + } + + return button; + } + + public static enum Style { // TODO + + NORMAL, + LINK, + ; + + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/component/impl/Form.java b/src/main/java/me/mrletsplay/nojs/component/impl/Form.java new file mode 100644 index 0000000..b9528c5 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/component/impl/Form.java @@ -0,0 +1,73 @@ +package me.mrletsplay.nojs.component.impl; + +import java.util.ArrayList; +import java.util.List; + +import me.mrletsplay.nojs.action.Action; +import me.mrletsplay.nojs.component.ActionableComponent; +import me.mrletsplay.nojs.component.Component; +import me.mrletsplay.nojs.component.ContainerComponent; +import me.mrletsplay.nojs.page.layout.Layout; +import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; + +public class Form implements ActionableComponent, ContainerComponent { + + private Action action; + private List components; + private Layout layout; + + public Form() { + this.components = new ArrayList<>(); + } + + @Override + public Form add(Component component) { + components.add(component); + return this; + } + + @Override + public Form layout(Layout layout) { + this.layout = layout; + return this; + } + + @Override + public Form action(Action action) { + this.action = action; + return this; + } + + @Override + public HtmlElement toHtml() { + HtmlElement form = createElement(action, layout); + + for(Component c : components) { + // TODO: check for nested forms + + form.appendChild(c.toHtml()); + } + + return form; + } + + public static HtmlElement createElement(Action action, Layout layout) { + HtmlElement form = HtmlElement.of("form"); + form.addClass("form"); + form.setAttribute("action", ""); + form.setAttribute("method", "post"); + + if(layout != null) { + layout.apply(form); + } + + if(action != null) { + for(HtmlElement e : action.createFormElements()) { + form.appendChild(e); + } + } + + return form; + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/component/impl/Group.java b/src/main/java/me/mrletsplay/nojs/component/impl/Group.java new file mode 100644 index 0000000..b9eebf6 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/component/impl/Group.java @@ -0,0 +1,47 @@ +package me.mrletsplay.nojs.component.impl; + +import java.util.ArrayList; +import java.util.List; + +import me.mrletsplay.nojs.component.Component; +import me.mrletsplay.nojs.component.ContainerComponent; +import me.mrletsplay.nojs.page.layout.Layout; +import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; + +public class Group implements ContainerComponent { + + private List components; + private Layout layout; + + public Group() { + this.components = new ArrayList<>(); + } + + @Override + public Group add(Component component) { + components.add(component); + return this; + } + + @Override + public Group layout(Layout layout) { + this.layout = layout; + return this; + } + + @Override + public HtmlElement toHtml() { + HtmlElement div = HtmlElement.of("div"); + div.addClass("group"); + + if(layout != null) { + layout.apply(div); + } + + for(Component c : components) { + div.appendChild(c.toHtml()); + } + + return div; + } +} diff --git a/src/main/java/me/mrletsplay/nojs/component/impl/Link.java b/src/main/java/me/mrletsplay/nojs/component/impl/Link.java new file mode 100644 index 0000000..a262dd7 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/component/impl/Link.java @@ -0,0 +1,35 @@ +package me.mrletsplay.nojs.component.impl; + +import me.mrletsplay.nojs.component.Component; +import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; + +public class Link implements Component { + + private String text; + private String href; + + public Link() { + this.text = "Link"; + this.href = "#"; + } + + public Link text(String text) { + this.text = text != null ? text : "Link"; + return this; + } + + public Link href(String href) { + this.href = href != null ? href : "#"; + return this; + } + + @Override + public HtmlElement toHtml() { + HtmlElement link = HtmlElement.of("a"); + link.addClass("link"); + link.setText(text); + link.setAttribute("href", href); + return link; + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/component/impl/Message.java b/src/main/java/me/mrletsplay/nojs/component/impl/Message.java new file mode 100644 index 0000000..f429654 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/component/impl/Message.java @@ -0,0 +1,49 @@ +package me.mrletsplay.nojs.component.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import me.mrletsplay.nojs.NoJS; +import me.mrletsplay.nojs.component.Component; +import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; +import me.mrletsplay.simplehttpserver.dom.html.element.HtmlRaw; + +public class Message implements Component { + + private String text; + + public Message() { + this.text = "Message"; + } + + public Message text(String text) { + this.text = text != null ? text : "Message"; + return this; + } + + @Override + public HtmlElement toHtml() { + HtmlElement message = HtmlElement.of("div"); + message.addClass("message"); + + HtmlElement textSpan = HtmlElement.of("span"); + textSpan.setText(text); + message.appendChild(textSpan); + + HtmlElement checkLabel = HtmlElement.of("label"); + message.appendChild(checkLabel); + + HtmlElement check = HtmlElement.of("input"); + check.setAttribute("type", "checkbox"); + check.setAttribute("autocomplete", "off"); // Clear on reload + checkLabel.appendChild(check); + + try(InputStream in = NoJS.class.getResourceAsStream("/icon/x.svg")) { + checkLabel.appendChild(new HtmlRaw(new String(in.readAllBytes(), StandardCharsets.UTF_8))); + }catch(IOException ex) {} + + return message; + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/component/impl/Text.java b/src/main/java/me/mrletsplay/nojs/component/impl/Text.java new file mode 100644 index 0000000..2284564 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/component/impl/Text.java @@ -0,0 +1,42 @@ +package me.mrletsplay.nojs.component.impl; + +import me.mrletsplay.nojs.component.Component; +import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; + +public class Text implements Component { + + private Type type; + private String text; + + public Text() { + this.type = Type.PLAIN; + this.text = "Text"; + } + + public Text type(Type type) { + this.type = type == null ? Type.PLAIN : type; + return this; + } + + public Text text(String text) { + this.text = text != null ? text : "Text"; + return this; + } + + @Override + public HtmlElement toHtml() { + HtmlElement text = HtmlElement.of(type == Type.PLAIN ? "span" : "p"); + text.addClass("text"); + text.setText(this.text); + return text; + } + + public static enum Type { + + PLAIN, + PARAGRAPH, + ; + + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/component/impl/TextInput.java b/src/main/java/me/mrletsplay/nojs/component/impl/TextInput.java new file mode 100644 index 0000000..d46de63 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/component/impl/TextInput.java @@ -0,0 +1,60 @@ +package me.mrletsplay.nojs.component.impl; + +import java.util.Objects; + +import me.mrletsplay.nojs.component.Component; +import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; + +public class TextInput implements Component { + + private String name; + private String placeholder; + private String value; + private boolean hidden; + + public TextInput(String name) { + Objects.requireNonNull(name, "name"); + + this.name = name; + this.placeholder = ""; + this.value = ""; + this.hidden = false; + } + + public TextInput placeholder(String placeholder) { + this.placeholder = placeholder != null ? placeholder : ""; + return this; + } + + public TextInput value(String value) { + this.value = value != null ? value : ""; + return this; + } + + public TextInput hidden(boolean hidden) { + this.hidden = hidden; + return this; + } + + @Override + public HtmlElement toHtml() { + HtmlElement input = HtmlElement.of("input"); + input.addClass("input"); + input.setAttribute("name", name); + + if(!placeholder.isEmpty()) { + input.setAttribute("placeholder", placeholder); + } + + if(!value.isEmpty()) { + input.setAttribute("value", value); + } + + if(hidden) { + input.setAttribute("hidden"); + } + + return input; + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/page/Page.java b/src/main/java/me/mrletsplay/nojs/page/Page.java new file mode 100644 index 0000000..67be9e6 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/page/Page.java @@ -0,0 +1,79 @@ +package me.mrletsplay.nojs.page; + +import java.util.List; + +import me.mrletsplay.nojs.action.ActionEvent; +import me.mrletsplay.nojs.action.ActionHandlers; +import me.mrletsplay.nojs.component.Component; +import me.mrletsplay.nojs.page.layout.Layout; +import me.mrletsplay.simplehttpserver.dom.css.CssElement; +import me.mrletsplay.simplehttpserver.dom.css.CssSelector; +import me.mrletsplay.simplehttpserver.dom.css.StyleSheet; +import me.mrletsplay.simplehttpserver.dom.html.HtmlDocument; +import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; +import me.mrletsplay.simplehttpserver.http.HttpRequestMethod; +import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; +import me.mrletsplay.simplehttpserver.http.document.DocumentProvider; +import me.mrletsplay.simplehttpserver.http.document.HttpDocument; +import me.mrletsplay.simplehttpserver.http.header.DefaultClientContentTypes; +import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; +import me.mrletsplay.simplehttpserver.http.request.urlencoded.UrlEncoded; +import me.mrletsplay.simplehttpserver.http.response.HtmlResponse; + +public interface Page extends HttpDocument, ActionHandlers { + + public List getComponents(); + + public default Layout getLayout() { + return null; + } + + public default HtmlDocument toHtml() { + HtmlDocument doc = new HtmlDocument(); + doc.addStyleSheet("/base.css"); + + StyleSheet sheet = new StyleSheet(); + + CssElement cssE = new CssElement(CssSelector.selectClass("ee")); + cssE.setProperty("text", "red"); + sheet.addElement(cssE); + + doc.getHeadNode().appendChild(HtmlElement.style(sheet)); + + Layout layout = getLayout(); + if(layout != null) { + layout.apply(doc.getBodyNode()); + } + + for(Component c : getComponents()) { + doc.getBodyNode().appendChild(c.toHtml()); + } + + return doc; + } + + @Override + public default void createContent() { + HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); + + if(ctx.getClientHeader().getMethod() == HttpRequestMethod.POST) { + // TODO: error handling + UrlEncoded data = ctx.getClientHeader().getPostData().getParsedAs(DefaultClientContentTypes.URLENCODED); + System.out.println(data); + + ActionEvent e = new ActionEvent(data); + onAction(e); + + ctx.redirect(HttpStatusCodes.SEE_OTHER_303, ctx.getClientHeader().getPath().toString()); + return; + } + + ctx.respond(HttpStatusCodes.OK_200, new HtmlResponse(toHtml())); + } + + public default void register(DocumentProvider provider, String path) { + provider.register(HttpRequestMethod.GET, path, this); + provider.register(HttpRequestMethod.POST, path, this); + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/page/StaticPage.java b/src/main/java/me/mrletsplay/nojs/page/StaticPage.java new file mode 100644 index 0000000..7992ca3 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/page/StaticPage.java @@ -0,0 +1,56 @@ +package me.mrletsplay.nojs.page; + +import java.util.ArrayList; +import java.util.List; + +import me.mrletsplay.nojs.action.ActionEvent; +import me.mrletsplay.nojs.action.ActionHandlers; +import me.mrletsplay.nojs.component.Component; +import me.mrletsplay.nojs.page.layout.Layout; + +public class StaticPage implements Page { + + private List components; + private Layout layout; + private List actionHandlers; + + public StaticPage() { + this.components = new ArrayList<>(); + this.actionHandlers = new ArrayList<>(); + } + + public StaticPage add(Component component) { + components.add(component); + return this; + } + + public StaticPage layout(Layout layout) { + this.layout = layout; + return this; + } + + public StaticPage addActionHandler(ActionHandlers handler) { + actionHandlers.add(handler); + return this; + } + + @Override + public List getComponents() { + return components; + } + + @Override + public Layout getLayout() { + return layout; + } + + @Override + public void onAction(ActionEvent event) { + Page.super.onAction(event); + + for(ActionHandlers handler : actionHandlers) { + handler.onAction(event); + } + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/page/layout/GridLayout.java b/src/main/java/me/mrletsplay/nojs/page/layout/GridLayout.java new file mode 100644 index 0000000..6525b63 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/page/layout/GridLayout.java @@ -0,0 +1,115 @@ +package me.mrletsplay.nojs.page.layout; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; + +public class GridLayout implements Layout { + + private List columns; + private List rows; + private String gap; + private Flow flow; + + public GridLayout() { + this.columns = new ArrayList<>(); + this.rows = new ArrayList<>(); + this.flow = Flow.ROW; + } + + public GridLayout columns(Collection columns) { + this.columns = new ArrayList<>(columns == null ? Collections.emptyList() : columns); + return this; + } + + public GridLayout rows(Collection rows) { + this.rows = new ArrayList<>(rows == null ? Collections.emptyList() : rows); + return this; + } + + public GridLayout columns(String... columns) { + return columns(columns == null ? null : Arrays.asList(columns)); + } + + public GridLayout rows(String... rows) { + return rows(rows == null ? null : Arrays.asList(rows)); + } + + public GridLayout gap(String gap) { + this.gap = gap; + return this; + } + + public GridLayout flow(Flow flow) { + this.flow = flow; + return this; + } + + @Override + public void apply(HtmlElement html) { + String css = "display:grid;"; + + if(!columns.isEmpty()) { + css += "grid-template-columns:" + String.join(" ", columns) + ";"; + } + + if(!rows.isEmpty()) { + css += "grid-template-rows:" + String.join(" ", rows) + ";"; + } + + if(gap != null) { + css += "gap:" + gap + ";"; + } + + if(flow != Flow.ROW) { + css += "grid-auto-flow:" + flow.name().toLowerCase() + ";"; + } + + html.setAttribute("style", css); + } + + public static GridLayout ofColumns(int columns) { + List colSizes = new ArrayList<>(); + for(int i = 0; i < columns; i++) { + colSizes.add("auto"); + } + + return new GridLayout() + .columns(colSizes); + } + + public static GridLayout ofRows(int rows) { + List rowSizes = new ArrayList<>(); + for(int i = 0; i < rows; i++) { + rowSizes.add("auto"); + } + + return new GridLayout() + .flow(Flow.COLUMN) + .rows(rowSizes); + } + + public static GridLayout ofSize(int columns, int rows) { + List colsAndRows = new ArrayList<>(); + for(int i = 0; i < Math.max(columns, rows); i++) { + colsAndRows.add("auto"); + } + + return new GridLayout() + .columns(colsAndRows.subList(0, columns)) + .rows(colsAndRows.subList(0, rows)); + } + + public static enum Flow { + + ROW, + COLUMN, + ; + + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/page/layout/InsetLayout.java b/src/main/java/me/mrletsplay/nojs/page/layout/InsetLayout.java new file mode 100644 index 0000000..4a922f2 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/page/layout/InsetLayout.java @@ -0,0 +1,20 @@ +package me.mrletsplay.nojs.page.layout; + +import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; + +public class InsetLayout implements Layout { + + private Layout layout; + private String inset; + + public InsetLayout(Layout layout, String inset) { + this.layout = layout; + this.inset = inset == null ? "0" : inset; + } + + public void apply(HtmlElement html) { + if(layout != null) layout.apply(html); + html.appendAttribute("style", "margin:" + inset + ";"); + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/page/layout/Layout.java b/src/main/java/me/mrletsplay/nojs/page/layout/Layout.java new file mode 100644 index 0000000..3da5a6c --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/page/layout/Layout.java @@ -0,0 +1,9 @@ +package me.mrletsplay.nojs.page.layout; + +import me.mrletsplay.simplehttpserver.dom.html.HtmlElement; + +public interface Layout { + + public void apply(HtmlElement html); + +} diff --git a/src/main/java/me/mrletsplay/nojs/rest/TestEndpoint.java b/src/main/java/me/mrletsplay/nojs/rest/TestEndpoint.java new file mode 100644 index 0000000..5cbf391 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/rest/TestEndpoint.java @@ -0,0 +1,28 @@ +package me.mrletsplay.nojs.rest; + +import me.mrletsplay.simplehttpserver.http.HttpRequestMethod; +import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; +import me.mrletsplay.simplehttpserver.http.endpoint.Endpoint; +import me.mrletsplay.simplehttpserver.http.endpoint.EndpointCollection; +import me.mrletsplay.simplehttpserver.http.header.DefaultClientContentTypes; +import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; +import me.mrletsplay.simplehttpserver.http.request.urlencoded.UrlEncoded; +import me.mrletsplay.simplehttpserver.http.response.TextResponse; + +public class TestEndpoint implements EndpointCollection { + + @Endpoint(method = HttpRequestMethod.POST, path = "/test") + public void test() { + HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); + + UrlEncoded encoded = ctx.getClientHeader().getPostData().getParsedAs(DefaultClientContentTypes.URLENCODED); + + ctx.respond(HttpStatusCodes.OK_200, new TextResponse(encoded.getFirst("test"))); + } + + @Override + public String getBasePath() { + return "/api"; + } + +} diff --git a/src/main/java/me/mrletsplay/nojs/util/ValidationException.java b/src/main/java/me/mrletsplay/nojs/util/ValidationException.java new file mode 100644 index 0000000..749e578 --- /dev/null +++ b/src/main/java/me/mrletsplay/nojs/util/ValidationException.java @@ -0,0 +1,23 @@ +package me.mrletsplay.nojs.util; + +public class ValidationException extends Exception { + + private static final long serialVersionUID = 5956388648233218085L; + + public ValidationException() { + super(); + } + + public ValidationException(String message, Throwable cause) { + super(message, cause); + } + + public ValidationException(String message) { + super(message); + } + + public ValidationException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/resources/icon/x.svg b/src/main/resources/icon/x.svg new file mode 100644 index 0000000..5cbd046 --- /dev/null +++ b/src/main/resources/icon/x.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + diff --git a/src/main/resources/style/base.css b/src/main/resources/style/base.css new file mode 100644 index 0000000..0c1a44a --- /dev/null +++ b/src/main/resources/style/base.css @@ -0,0 +1,119 @@ +html { + --background: #222; + --foreground: #444; + --accent: #3584e4; + --text: #fff; + --link: var(--accent); +} + +* { + font-family: system-ui; + color: var(--text); +} + +body { + background-color: var(--background); +} + +/*.button, .link, .input { + margin: 2px; +}*/ + +.button { + background: var(--accent); + color: var(--text); + border: none; + border-radius: 5px; + padding: 8px; + cursor: pointer; + font-weight: bold; +} + +.button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.input { + background: #2c3444; + border: none; + color: var(--text); + border-radius: 5px; + padding: 8px; +} + +.input:focus, .button:focus, .link:focus { + outline: 2px solid var(--accent); +} + +.link, .button[data-style="link"] { + background: unset; + color: var(--link); + text-decoration: none; + padding: 0; + font-weight: normal; + font-size: 1em; + border-radius: 0; +} + +.link:hover, .button[data-style="link"]:hover { + text-decoration: underline; +} + +.message { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + position: fixed; + top: 0; + left: 0; + min-height: 30px; + background-color: orangered; + width: 100vw; +} + +.message>label>input { + display: none; +} + +.message { + padding-top: 5px; + padding-bottom: 5px; +} + +.message>*:first-child { + margin-left: 10px; +} + +.message>*:last-child { + margin-right: 10px; +} + +.message>label { + position: relative; + /*background: var(--accent);*/ + cursor: pointer; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; +} + +.message>label>svg { + fill: white; + width: 20px; + height: 20px; +} + +/*.message>label:after { + display: block; + text-align: center; + content: "x"; + width: 100%; + height: 100%; +}*/ + +.message:has(:checked) { + display: none; +}