diff --git a/rocky/components/modal/README.md b/rocky/components/modal/README.md
index 9dad3585dbe..b6f996e6dc4 100644
--- a/rocky/components/modal/README.md
+++ b/rocky/components/modal/README.md
@@ -20,22 +20,25 @@ First you need to add `{% load component_tags %}` at the top of your template. N
{% endblock html_at_end_body %}
```
-After that, `{% component "modal" size="xx" %}` is enough to instantiate the dialog modal component where size should contain the appropriate class name to achieve the correct sizing. This can be either `dialog-small`, `dialog-medium` or `dialog-large`.
+After that, `{% component "modal" modal_id="xx" size="xx" %}` is enough to instantiate the dialog modal component where `modal_id` should contain a unique identifier that must be also used in the triggers `data-modal-id` attribute, and `size` should contain the appropriate class name to achieve the correct sizing. This can be either `dialog-small`, `dialog-medium` or `dialog-large`.
-### Slots and fills
+### The trigger element
-Each named `fill` corresponds with a placeholder/target `slot` in the component template. The contents between the `fill` tag will be passed to the corresponding `slot`. As shown in the below example it's possible to utilise Django template tags and `HTML` tags with these `fill` tags. This enables us to entirely build the contents of the modal in the template where we implement it. Because we can use `HTML` tags here, we can also use `forms` and leave the handling of said form up to the Django template that knows about the context and applicable data, where we implement the modal. The defaults are used when no `fill` tags are implemented for this slot at all.
+The trigger element needs some explaining. This will be the element that `on click` will show the modal dialog,
+the `element` gets assigned the click handler by JavaScript.
+It's essential to include the data attribute `data-modal-id="xx"`, where "xx" is the same as the `id` attribute on the intended modal. This is what defines the modal dialog that this trigger is meant to target. The only exception to this is when an `` is used, with an `href` containing an anchor `"#xx"`, which points to `the modal-id` of the target modal.
+While it might seem obvious to use a `button` or a `a` as a trigger, the modal is set up in a way that allows for any HTML element to be used as a trigger.
+The trigger doesn't need to be part of the `{% component %}` itself and can be placed anywhere in the HTML template, though it makes sense to place them adjacent or as close as possible to each other.
-There's a total of four slots you can fill:
+### Slots and fills
-1. `trigger`: call to action `button` by default, with the caption "Open modal".
-2. `header`: empty by default
-3. `content`: empty by default
-4. `footer_buttons`: cancel `button` by default. To have _no buttons_ show at all, it's needed to implement empty `fill` tags for this `slot`.
+Each named `fill` corresponds with a placeholder/target `slot` in the component template. The contents between the `fill` tag will be passed to the corresponding `slot`. As shown in the below example it's possible to utilise Django template tags and `HTML` tags with these `fill` tags. This enables us to entirely build the contents of the modal in the template where we implement it. Because we can use `HTML` tags here, we can also use `forms` and leave the handling of said form up to the Django template that knows about the context and applicable data, where we implement the modal. The defaults are used when no `fill` tags are implemented for this slot at all.
-### The trigger element
+There's a total of three slots you can fill:
-The trigger `slot` is a special one. This needs to contain the HTML `element` that gets assigned the click handler by JavaScript. It's essential to include the `class="modal-trigger"` attribute, because this is what we target to assign the click handler, using JS. While it might seem obvious to use a `button` as a trigger, the modal is setup in a way that allows for any HTML element to be used as a trigger.
+1. `header`: empty by default
+2. `content`: empty by default
+3. `footer_buttons`: cancel `button` by default. To have _no buttons_ show at all, it's needed to implement empty `fill` tags for this `slot`.
### CSS dependencies
@@ -44,10 +47,11 @@ Including `{% component_css_dependencies %}` is needed to inject the reference t
### Example implementation
```
-{% component "modal" size="dialog-small" %}
- {% fill "trigger" %}
-
- {% endfill %}
+
+```
+
+```
+{% component "modal" modal_id="rename-modal" size="dialog-small" %}
{% fill "header" %}
{% translate "This is an example header." %}
{% endfill %}
diff --git a/rocky/components/modal/modal.py b/rocky/components/modal/modal.py
index f3ad0d6dd6d..c2c55f668cb 100644
--- a/rocky/components/modal/modal.py
+++ b/rocky/components/modal/modal.py
@@ -8,6 +8,7 @@ class Modal(component.Component):
def get_context_data(self, **kwargs):
return {
"size": kwargs["size"],
+ "modal_id": kwargs["modal_id"],
}
class Media:
diff --git a/rocky/components/modal/script.js b/rocky/components/modal/script.js
index 006c8cc8ccb..24e077f7b0a 100644
--- a/rocky/components/modal/script.js
+++ b/rocky/components/modal/script.js
@@ -1,6 +1,17 @@
import { onDomReady } from "../js/imports/utils.js";
-onDomReady(initDialogs);
+onDomReady(function () {
+ initDialogs();
+ openDialogFromUrl();
+});
+
+export function openDialogFromUrl() {
+ // If ID is present in the URL on DomReady, open the dialog immediately.
+ let id = window.location.hash.slice(1);
+ if (id) {
+ showModalBasedOnAnchor(id);
+ }
+}
export function initDialogs(element) {
let root = element || document;
@@ -10,67 +21,52 @@ export function initDialogs(element) {
}
export function initDialog(modal) {
- let input_elements = [];
+ let dialog_element = modal.querySelector("dialog");
+ if (!dialog_element) return;
- modal.querySelector(".modal-trigger").addEventListener("click", (event) => {
- // Get and clone input elements to be able to "reset" them on "cancel".
- input_elements = modal.querySelectorAll("input, textarea");
+ let trigger = document.querySelector(
+ "[data-modal-id='" + dialog_element.id + "']:not(a)",
+ );
- // Store the initial value in a data attribute
- input_elements.forEach((element) => {
- element.defaultvalue = element.value;
+ // Check if trigger element is , if not, on click,
+ // alter the URL to open the dialog using the onhaschange event.
+ if (trigger) {
+ trigger.addEventListener("click", (event) => {
+ window.location.hash = "#" + dialog_element.id;
});
+ }
- // Used ".closest" instead of ".parentNode" to make sure we stay flexible in terms of
- // HTML-structure when implementing the trigger.
- event.target.closest(".modal-wrapper").querySelector("dialog").showModal();
- });
-
- modal.querySelector("dialog").addEventListener("click", (event) => {
+ dialog_element.addEventListener("click", (event) => {
// The actual handling (like posting) of the input values should be done when implementing the component.
- if (event.target.classList.contains("confirm-modal-button")) {
- if (input_elements) {
- // Closing is only allowed when the inputs are 'valid'.
- if (checkValidity(input_elements)) {
- event.target
- .closest(".modal-wrapper")
- .querySelector("dialog")
- .close();
- }
- return;
- }
- }
// event.target.nodeName === 'DIALOG' is needed to check if the ::backdrop is clicked.
if (
+ event.target.classList.contains("confirm-modal-button") ||
event.target.classList.contains("close-modal-button") ||
event.target.nodeName === "DIALOG"
) {
- // When canceling or closing using the 'x' remove the "error" styles.
- input_elements.forEach((element) => {
- element.classList.remove("error");
- });
-
- // When canceling or closing the modal, the inputs get reset to their initial value.
- input_elements.forEach((element) => {
- element.value = element.defaultvalue;
- });
-
event.target.closest(".modal-wrapper").querySelector("dialog").close();
}
});
-}
-
-export function checkValidity(elements) {
- let valid = true;
- elements.forEach((element) => {
- if (!element.checkValidity()) {
- valid = false;
- element.classList.add("error");
- } else {
- element.classList.remove("error");
- }
+ dialog_element.addEventListener("close", (event) => {
+ removeDialogAnchor();
});
+}
- return valid;
+export function removeDialogAnchor() {
+ // Remove the anchor from the URL when closing the modal
+ let baseUrl = window.location.toString().split("#")[0];
+ window.history.pushState("", "Base URL", baseUrl);
}
+
+export function showModalBasedOnAnchor(id) {
+ if (id && document.querySelector("dialog#" + id + ".modal")) {
+ // Show modal, selected by ID
+ document.querySelector("#" + id).showModal();
+ }
+}
+
+addEventListener("hashchange", function () {
+ let id = window.location.toString().split("#")[1];
+ showModalBasedOnAnchor(id);
+});
diff --git a/rocky/components/modal/template.html b/rocky/components/modal/template.html
index 7e1235532f2..f0dc8be4356 100644
--- a/rocky/components/modal/template.html
+++ b/rocky/components/modal/template.html
@@ -3,13 +3,10 @@
{% load compress %}
- {% slot "trigger" %}
-
- {% endslot %}
-