Compare commits

...

3 Commits

Author SHA1 Message Date
BordedDev
9089589d36 Made input styling shared 2025-03-09 14:07:50 +00:00
BordedDev
fd07001983
Returned a semi 2025-03-08 21:18:20 +01:00
BordedDev
0266b2a559 Added form preloading, and autofocus on the first input element
Also adds the preloading functionality to login & register pages
2025-03-08 20:06:11 +00:00
9 changed files with 182 additions and 114 deletions

View File

@ -1,3 +1,5 @@
@import "shared.css";
* { * {
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;
@ -342,4 +344,3 @@ a {
.sidebar ul li a:hover { .sidebar ul li a:hover {
color: #fff; color: #fff;
} }

View File

@ -21,6 +21,24 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE. // THE SOFTWARE.
const getAdoptionStyles = () => {
return Array.from(document.styleSheets)
.filter((styleSheet) => styleSheet.href?.endsWith("shared.css"))
.map(x => {
const sheet = new CSSStyleSheet();
sheet.replaceSync(Array.from(x.cssRules).flatMap(rule => {
if (rule instanceof CSSImportRule) {
return Array.from(rule.styleSheet.cssRules).map(rule => rule.cssText);
} else {
return rule.cssText;
}
}).join(' '));
return sheet;
});
}
class GenericField extends HTMLElement { class GenericField extends HTMLElement {
form = null; form = null;
field = null; field = null;
@ -83,78 +101,69 @@ class GenericField extends HTMLElement {
background-color: #1a1a1a; background-color: #1a1a1a;
color: #e6e6e6; color: #e6e6e6;
font-size: 1em; font-size: 1em;
&:focus {
outline: 2px solid #f05a28 !important;
}
&::placeholder {
transition: opacity 0.3s;
}
&:focus::placeholder {
opacity: 0.4;
}
} }
button { button {
width: 50%; width: 50%;
padding: 10px; padding: 10px;
background-color: #f05a28; background-color: #f05a28;
border: none; border: none;
float: right; float: right;
margin-top: 10px; margin-top: 10px;
margin-left: 10px; margin-left: 10px;
margin-right: 10px; margin-right: 10px;
border-radius: 5px; border-radius: 5px;
color: white; color: white;
font-size: 1em; font-size: 1em;
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s; transition: background-color 0.3s;
clear: both; clear: both;
} }
button:hover { button:hover {
background-color: #e04924; background-color: #e04924;
} }
a { a {
color: #f05a28; color: #f05a28;
text-decoration: none; text-decoration: none;
display: block; display: block;
margin-top: 15px; margin-top: 15px;
font-size: 0.9em; font-size: 0.9em;
transition: color 0.3s; transition: color 0.3s;
} }
a:hover { a:hover {
color: #e04924; color: #e04924;
} }
.valid { .valid {
border: 1px solid green; border: 1px solid green;
color: green; color: green;
font-size: 0.9em; font-size: 0.9em;
margin-top: 5px; margin-top: 5px;
} }
.error { .error {
border: 3px solid red; border: 3px solid red;
color: #d8000c; color: #d8000c;
font-size: 0.9em; font-size: 0.9em;
margin-top: 5px; margin-top: 5px;
} }
@media (max-width: 500px) { @media (max-width: 500px) {
input { input {
width: 90%; width: 90%;
} }
} }
`; `;
this.container.appendChild(this.styleElement); this.container.appendChild(this.styleElement);
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
this.shadowRoot.adoptedStyleSheets = getAdoptionStyles();
} }
connectedCallback() { connectedCallback() {
@ -165,7 +174,13 @@ class GenericField extends HTMLElement {
this[name] = value; this[name] = value;
} }
focus(options) {
this.inputElement?.focus(options);
}
updateAttributes() { updateAttributes() {
const inputUpdate = this.inputElement != null;
if (this.inputElement == null && this.field) { if (this.inputElement == null && this.field) {
this.inputElement = document.createElement(this.field.tag); this.inputElement = document.createElement(this.field.tag);
if (this.field.tag === 'button' && this.field.value === "submit") { if (this.field.tag === 'button' && this.field.value === "submit") {
@ -218,7 +233,9 @@ class GenericField extends HTMLElement {
} }
this.inputElement.setAttribute("tabindex", this.field.index); this.inputElement.setAttribute("tabindex", this.field.index);
this.inputElement.classList.add(this.field.name); this.inputElement.classList.add(this.field.name);
this.value = this.field.value; if (this.field.value != null || !inputUpdate) {
this.value = this.field.value;
}
let place_holder = this.field.place_holder ?? null; let place_holder = this.field.place_holder ?? null;
if (this.field.required && place_holder) { if (this.field.required && place_holder) {
@ -281,6 +298,15 @@ class GenericForm extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
const preloadedForm = this.getAttribute('preloaded-structure');
if (preloadedForm) {
try {
const form = JSON.parse(preloadedForm);
this.constructForm(form)
} catch (error) {
console.error(error, preloadedForm);
}
}
const url = this.getAttribute('url'); const url = this.getAttribute('url');
if (url) { if (url) {
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get"); const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get");
@ -293,31 +319,52 @@ class GenericForm extends HTMLElement {
} }
} }
async loadForm(url) { async constructForm(formPayload) {
try { try {
const response = await fetch(url); this.form = formPayload;
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
}
this.form = await response.json();
let fields = Object.values(this.form.fields); let fields = Object.values(this.form.fields);
let hasAutoFocus = Object.keys(this.fields).length !== 0;
fields.sort((a, b) => a.index - b.index); fields.sort((a, b) => a.index - b.index);
fields.forEach(field => { fields.forEach(field => {
const fieldElement = document.createElement('generic-field'); const updatingField = field.name in this.fields
this.fields[field.name] = fieldElement;
this.fields[field.name] ??= document.createElement('generic-field');
const fieldElement = this.fields[field.name];
fieldElement.setAttribute("form", this); fieldElement.setAttribute("form", this);
fieldElement.setAttribute("field", field); fieldElement.setAttribute("field", field);
this.container.appendChild(fieldElement);
fieldElement.updateAttributes(); fieldElement.updateAttributes();
fieldElement.addEventListener("change", (e) => { if (!updatingField) {
this.form.fields[e.detail.name].value = e.detail.value; this.container.appendChild(fieldElement);
});
fieldElement.addEventListener("click", async (e) => { if (!hasAutoFocus && field.tag === "input") {
if (e.detail.type === "button" && e.detail.value === "submit") { fieldElement.focus();
hasAutoFocus = true;
}
fieldElement.addEventListener("change", (e) => {
this.form.fields[e.detail.name].value = e.detail.value;
});
fieldElement.addEventListener("click", async (e) => {
if (e.detail.type === "button" && e.detail.value === "submit") {
const isValid = await this.validate();
if (isValid) {
const saveResult = await this.submit();
if (saveResult.redirect_url) {
window.location.pathname = saveResult.redirect_url;
}
}
}
});
fieldElement.addEventListener("submit", async (e) => {
const isValid = await this.validate(); const isValid = await this.validate();
if (isValid) { if (isValid) {
const saveResult = await this.submit(); const saveResult = await this.submit();
@ -325,20 +372,22 @@ class GenericForm extends HTMLElement {
window.location.pathname = saveResult.redirect_url; window.location.pathname = saveResult.redirect_url;
} }
} }
} })
}); }
fieldElement.addEventListener("submit", async (e) => {
const isValid = await this.validate();
if (isValid) {
const saveResult = await this.submit();
if (saveResult.redirect_url) {
window.location.pathname = saveResult.redirect_url;
}
}
})
}); });
} catch (error) {
this.container.textContent = `Error: ${error.message}`;
}
}
async loadForm(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
}
await this.constructForm(await response.json());
} catch (error) { } catch (error) {
this.container.textContent = `Error: ${error.message}`; this.container.textContent = `Error: ${error.message}`;
} }

View File

@ -0,0 +1,15 @@
input, textarea {
&:focus {
outline: 2px solid #f05a28 !important;
}
&::placeholder {
transition: opacity 0.3s;
}
&:focus::placeholder {
opacity: 0.4;
}
}

View File

@ -1,55 +1,57 @@
@import "shared.css";
* { * {
box-sizing: border-box;
box-sizing: border-box; }
.dialog {
background-color: #0f0f0f;
border-radius: 10px;
padding: 30px;
width: 800px;
margin: 30px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
}
@media screen and (max-width: 500px) {
.center {
width: 100%;
left: 0px;
} }
.dialog { .dialog {
width: 100%;
background-color: #0f0f0f; left: 0px;
border-radius: 10px;
padding: 30px;
width: 800px;
margin: 30px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
}
@media screen and (max-width: 500px) {
.center {
width: 100%;
left: 0px;
}
.dialog {
width: 100%;
left: 0px;
}
} }
}
h1 { h1 {
font-size: 2em; font-size: 2em;
color: #f05a28; color: #f05a28;
margin-bottom: 20px; margin-bottom: 20px;
} }
h2 { h2 {
font-size: 1.4em; font-size: 1.4em;
color: #f05a28; color: #f05a28;
margin-bottom: 20px; margin-bottom: 20px;
} }
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
background-color: #1a1a1a; background-color: #1a1a1a;
color: #e6e6e6; color: #e6e6e6;
line-height: 1.5; line-height: 1.5;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-height: 100vh; min-height: 100vh;
} }
div { div {
text-align: left; text-align: left;
} }

View File

@ -20,6 +20,7 @@
<script src="/html-frame.js"></script> <script src="/html-frame.js"></script>
<script src="/generic-form.js"></script> <script src="/generic-form.js"></script>
<link rel="stylesheet" href="/html-frame.css"> <link rel="stylesheet" href="/html-frame.css">
<link rel="stylesheet" href="/shared.css">
{% block head %} {% block head %}
{% endblock %} {% endblock %}

View File

@ -11,6 +11,6 @@
{% block main %} {% block main %}
<div class="back-form"> <div class="back-form">
<fancy-button url="/back" text="Back" size="auto"></fancy-button> <fancy-button url="/back" text="Back" size="auto"></fancy-button>
<generic-form class="center" url="/login.json"></generic-form> <generic-form class="center" url="/login.json" preloaded-structure='{{ form|tojson|safe }}'></generic-form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -12,6 +12,6 @@
<div class="back-form"> <div class="back-form">
<fancy-button url="/back" text="Back" size="auto"></fancy-button> <fancy-button url="/back" text="Back" size="auto"></fancy-button>
<generic-form class="center" url="/register.json"></generic-form> <generic-form class="center" url="/register.json" preloaded-structure='{{ form|tojson|safe }}'></generic-form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -18,7 +18,7 @@ class LoginView(BaseFormView):
return web.HTTPFound("/web.html") return web.HTTPFound("/web.html")
if self.request.path.endswith(".json"): if self.request.path.endswith(".json"):
return await super().get() return await super().get()
return await self.render_template("login.html") return await self.render_template("login.html", {"form": await self.form(app=self.app).to_json()})
async def submit(self, form): async def submit(self, form):
if await form.is_valid: if await form.is_valid:

View File

@ -18,7 +18,7 @@ class RegisterView(BaseFormView):
return web.HTTPFound("/web.html") return web.HTTPFound("/web.html")
if self.request.path.endswith(".json"): if self.request.path.endswith(".json"):
return await super().get() return await super().get()
return await self.render_template("register.html") return await self.render_template("register.html", {"form": await self.form(app=self.app).to_json()})
async def submit(self, form): async def submit(self, form):
result = await self.app.services.user.register( result = await self.app.services.user.register(