Commit Graph

21 Commits

Author SHA1 Message Date
jordi fita mas 992bbf32a9 Toggle filters forms
I tried this already when i started adding filters, but i tried to use
AlpineJS for that, and could not because it would reset the context each
time i submitted the filters, due to HTMx replacing the whole content.

I realized that the only thing i need is some “flag” to show and hide
the form with CSS.  I do not even need AlpineJS for that, but i used it
anyway because then i can use the x-cloak thing to hidde the toggle
button for users with JavaScript disabled.

Similarly, the body by default has that “flag” set in the markup, and is
removed when AlpineJS is initialized, thus if JavaScript is disabled the
filters form is shown nevertheless.
2023-05-24 12:13:09 +02:00
jordi fita mas d941adcdfe Trigger a recompute when price, quantity, discount, or vat changes
I had to add the correct change event to the select in order for this to
work, too; in tags it was already done, i and did something very
similar.
2023-04-28 00:22:28 +02:00
jordi fita mas 86ccbbe830 Add keyboard controls for product search
They are almost the same as for the multiselect, except that it “clicks”
the option to “select” it, as this will trigger the replacement of the
<fieldset> with the whole product.
2023-04-28 00:06:48 +02:00
jordi fita mas 7d895fe5f9 Use HTMx to add product rows “inline” in the invoice form
I actually find more comfortable to select the product from the list
presented up until now, but this is mostly because i have very few
products and the list is not too long, so the idea is that with
JavaScript we will dynamically add an empty product row to the invoice
and then use the name field to search the product by name.

I have the feeling that i am doing something wrong because i ended up
with a lot of HTMx attribute for what i feel is not that much work,
but for now it will work.

I have added the `Is` field to `InputField` in order to include the `id`
attribute to the HTML element, because the HTMLAttributes are attached
to the `input`, not the `div`, and i felt like this one should also be
a custom element based on div, like all the others.

These is not yet any keyboard control to select the search results.

I am not happy with having the search of products in a different URL
than the index, specially since they use the exact same SQL query and
ProductFilter struct, but i did not know how else ask for a different
representation without resorting to the more complicated MIME types.
2023-04-24 02:00:38 +02:00
jordi fita mas 884c6dc2db Make sure the tag’s condition menu is within the limits of <body>
Otherwise, when the tag input is too close to the right side of the
screen, it may be unreadable without scrolling.
2023-04-17 11:51:10 +02:00
jordi fita mas 149557e42e “Integrate” the tags’ condition into the input field
We have reconsidered the toggle thing and instead moved the selection
into a little menu on top of the input, like the input’s label does à
la Material Design.

I just moved the checkboxes into a new details, that works as a menu,
but i had to add the type="search" to the existing input in the tags
field, or the CSS would style the checkboxes as well.

I do not do anything when the checkbox selection changes because that
already triggers a POST to the server that returns the new HTML with
the checkbox changed, and the JavaScript only has to retrieve that new
structure, exactly as it does in the initial rendering.

Since we want to add a little description to the options, i no longer
can use the same SelectOption in ToggleField, even though i could have
reused the Group element, but that felt wrong.
2023-04-16 19:01:11 +02:00
jordi fita mas 8c592cfe5e Execute “focus out” handler in tag input when clicking any other element
Apparently i was only testing that control with tab, because clicking
on any other non-focusable element (e.g., a table row) it did not add
the new tag and would not dispatch the “numerus-tag-out” custom element,
which is why i have seen it now.

This is equivalent to AlpineJS’s @click.outside, and i was already using
it for the multiselect dropdown.  The isConnected check is because i
probably found some cases in the dropdown’s handler, but i can not
remeber now, but since AlpineJS does it too, i guess it is important.
2023-04-12 11:59:45 +02:00
jordi fita mas d20573aa99 Allow editing invoice tags inline from the index table
I use the same pattern as HTMx’s “Click to Edit” example[0], except that
my edit form is triggered by submit and by focus out of the tags input.

I could not, however, use the standard focus out event because it would
also trigger when removing a tag with the mouse, as for a moment the
remove button has the focus and the search input dispatches a bubbling
focusout.  I had to resort to a custom event for that, but i am not
happy with it.

The autofocus attribute seems to do nothing in this case, so i need to
manually change the focus to the new input with JavaScript.  However,
this means that i can not use the same input ID for all the forms
because getElementById would always return the first in document order,
changing the focus to that same element and automatically submit the
form due to focus out.  That’s why in this form i append the invoice’s
slug to the input’s ID.

Finally, this is the first time i am using an HTMx-only solution and i
needed a way to return back just the HTML for the <td>, without <title>,
breadcrumbs, or <dialog>.  In principle, the template would be the
“layout”, but then i would need to modify everything to check whether
the template file is empty, or something to that effect, so instead i
created a “standalone” template for these cases.

[0]: https://htmx.org/examples/click-to-edit/
2023-04-11 10:46:27 +02:00
jordi fita mas 1290fc7283 Set the focus back to the search input when removing a tag
The idea is that if they removed a tag it is more that possible that
they want to continue editing tags.
2023-04-11 10:24:40 +02:00
jordi fita mas 3b568b013f Fix adding empty tag on focus out from element
For some reason, i was looking at the value of the focus’ **target**,
which is not my search field at all, but whatever control the focus
changes **to**.  It that new control is an input with value, then it
created a new tag with whatever my search field had, which could be the
empty string.
2023-04-11 10:23:32 +02:00
jordi fita mas 33277454fa Try to remove as many leaky references from event listeners as possible 2023-04-10 23:04:16 +02:00
jordi fita mas f945051f4a Remove document and window event handlers when removing custom elements
I realized that the event handlers that i was setting when creating the
tags input and the multi-select controls were not removed just because
these elements are no longer in the document, and kept firing again and
again.

I no longer can use an anonymous function, because removeEventListener
would not match it with the one passed to addEventListener.  I also have
to bind the handler to `this` in order to keep having access to the
object, and, again, can not do it in the call to addEventListener, or
i would get a different function each time.

I added the check to see if the element is connected inside the
connectedCallback because the documentation warns that this callback
“may be called once your element is no longer connected”[0], and i
understood it to mean that the connected and disconnected callbacks
could be called our of order, thus it would be possible to add event
listeners that would not be removed—again.

I am not actually sure where i have to do the same for the rest of the
“internal” events.

[0]: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#using_the_lifecycle_callbacks
2023-04-10 00:05:29 +02:00
jordi fita mas ba880c6560 Create the tag when focusing out of the input
This is mainly because i sometimes think that the tag is accepted just
because it is there in the input, but actually it is not being used at
all.  I fear more people would do the same mistake.
2023-04-08 21:27:40 +02:00
jordi fita mas b6668e72ef Trigger filter form on change and search, as well as submit as before
Changed the invoice number field’s type to search to add the delete icon
on Chromium.  Firefox does not add that icon, but i do not care; it is
still better that type="text".

Had to emit the change event to the numerus-tag field, otherwise the
form would not detect the change.

I also can not use keyup as a trigger because the changed modifier can
not be used in the <form>, as nothing ever changes, i do not know how to
trigger the form from children (i.e., data-hx-trigger on the <input>
does nothing), and i can not trigger for just any keyup, or i would
make the request even if they only moved the cursor with the arrow keys,
which is very confusing as Firefox resets the position (this may be due
the fact that i reload the whole <main>, but still).
2023-04-03 12:45:15 +02:00
jordi fita mas 5717a5b9ed Put new invoice and edit invoice forms into a dialog
In this case i have to use the same id for the dialog content in all
pages because, for now, there are a couple of forms that need to replace
it on submit—the new/edit form and the product selection form.

Unfortunately, HTMx does not have support for `formaction` attribute at
this point, so i had to use the workaround described in [0].

[0] https://github.com/bigskysoftware/htmx/issues/623
2023-03-31 13:01:26 +02:00
jordi fita mas 5d82597d14 Manually update and restore the <title> when showing dialog with HTMx
Mostly, the same problem as before: if the document title does not
reflect the application’s state, it becomes useless when there are
multiple open tabs.

In this case, however, i do not know how to tell HTMx to restore the
title to how it was before opening the dialog without a new request to
the server, that makes no sense when the dialog was closed without any
change whatsoever.  Thus, i do it with JavaScript on the client side.
2023-03-26 14:06:26 +02:00
jordi fita mas 7e8ec539ff Add a SnackBar to show HTMx errors
We do not have any design yet for errors and other notifications, so i
followed material design, for now, since we already kind of use their
input fields design.

This time i decided to use AlpineJS because there is not that much HTML
code, and the transitioning is way easier to do in AlpineJS than it
would be with plain JavaScript—not to mention the bugs i would
introduce.
2023-03-25 01:56:26 +01:00
jordi fita mas 9e757cb9f4 Show the profile form in a dialog using HTMx
Had to split the actual page content and the breadcrumbs because they
do not belong in a dialog.  However, i had to change all templates to
do that.
2023-03-20 13:09:52 +01:00
jordi fita mas 82eb8a2733 Start the tag input custom element
This is more or less the same as a multiselect, except that now it
adds a list of string element that you write into the search element.

It is supposed to fetch a list of tag suggestions from the server, but i
have not implemented it yet.
2023-03-19 23:11:40 +01:00
jordi fita mas 041017adc3 Add missing ARIA attributes and keyboard controls to multiselect
I use MDN’s documentation[0] as guid for both.

[0]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role
2023-03-18 07:17:28 +01:00
jordi fita mas 2dde25c862 Reimplement the multiselect as a custom element
What i really set off on was to refactor the multiselect’s x-data
context to a separate JavaScript file.

I did not see the need at first, thinking that it would not matter
because it was used only in a template and i was not duplicating the
code in my files.  However, i then realized that having the context
in the template means the visitor has to download it each and every time
it accesses a form with a multiselect, even if nothing changed, and,
worse, it would download it multiple times if there were many
multiselect controls.

It makes more sense to put all that into a file that the browser would
only download and parse once, if the proper caching is set.

Once i realized that, it was a shame that AlpineJS has no way to do
the same for the HTML structure[0], for the exact same reasons: not
wanting to download many times the same extra <template> and other
markup required to build the control for JavaScript users.  And then i
remembered that this is supposed to be custom element’s main selling
point.

At first i tried to create a shadow DOW to replace the <select> with
the same <div> and <ul> that i used with Alpine, but it turns out that
<select> is not one of the allowed elements that can have a shadow root
attached[0].

Therefore, i changed the custom element to extend the <div> for the
<select> and <label> instead—the same element that had the x-init
context—, but i would have to define or include all the styles inside
the shadow DOM, and bring the lang attribute, for it to look like it
did before.   Out with the shadow DOM, and modify the <div>’s contents
instead.

At this point the code was so far removed from the declarative way that
AlpineJS promotes that i did not see much value on using it, except for
its reactivity.   But, given that this is such a small component, at the
end decided to write it all in plain JavaScript.

It is more code, at least looking only at the code i had to write, but
i love how i only have to add an is="numerus-multiselect" attribute to
HTML for it to work.

[0]: https://github.com/alpinejs/alpine/discussions/1205
[1]: https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow
2023-03-17 14:55:12 +01:00