2 Commits

13 changed files with 56 additions and 119 deletions

View File

@@ -1,7 +1,11 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link, select_all_checkbox %} {% from 'core/macros.jinja' import user_profile_link, select_all_checkbox %}
{% block additional_js %}
<script type="module" src="{{ static("bundled/core/components/ajax-select-index.ts") }}"></script>
{% endblock %}
{% block additional_css %} {% block additional_css %}
<link rel="stylesheet" href="{{ static("bundled/core/components/ajax-select-index.css") }}">
<link rel="stylesheet" href="{{ static("club/members.scss") }}"> <link rel="stylesheet" href="{{ static("club/members.scss") }}">
{% endblock %} {% endblock %}

View File

@@ -1,106 +1,18 @@
import { inheritHtmlElement, registerComponent } from "#core:utils/web-components.ts"; import { inheritHtmlElement, registerComponent } from "#core:utils/web-components.ts";
/**
* Create an abstract class for ElementOnce types Web Components
*
* Those class aren't really abstract because that would be complicated with the
* multiple inheritance involved
* Instead, we just raise an unimplemented error
**/
function elementOnce<K extends keyof HTMLElementTagNameMap>(tagName: K) {
return class ElementOnce extends inheritHtmlElement(tagName) {
getElementQuerySelector(): string {
throw new Error("Unimplemented");
}
clearNode() {
while (this.firstChild) {
this.removeChild(this.lastChild);
}
}
refresh() {
this.clearNode();
if (document.querySelectorAll(this.getElementQuerySelector()).length === 0) {
this.appendChild(this.node);
}
}
connectedCallback() {
super.connectedCallback(false);
this.refresh();
}
disconnectedCallback() {
// The MutationObserver can't see web components being removed
// It also can't see if something is removed inside after the component gets deleted
// We need to manually clear the containing node to trigger the observer
this.clearNode();
}
};
}
// Set of ElementOnce type components to refresh with the observer
const registeredComponents: Set<string> = new Set();
/**
* Helper to register ElementOnce types Web Components
* It's a wrapper around registerComponent that registers that component on
* a MutationObserver that activates a refresh on them when elements are removed
**/
function registerElementOnce(name: string, options?: ElementDefinitionOptions) {
registeredComponents.add(name);
return registerComponent(name, options);
}
const startObserver = (observer: MutationObserver) => {
observer.observe(document, {
// We want to also listen for elements contained in the header (eg: link)
subtree: true,
childList: true,
});
};
// Refresh *-once components when changes happens
const observer = new MutationObserver((mutations: MutationRecord[]) => {
observer.disconnect();
for (const mutation of mutations) {
for (const node of mutation.removedNodes) {
if (node.nodeType !== node.ELEMENT_NODE) {
continue;
}
const refreshElement = (componentName: string, tagName: string) => {
for (const element of document.getElementsByTagName(componentName)) {
// We can't guess if an element is compatible before we get one
// We exit the function completely if it's not compatible
if (
(element as any).inheritedTagName.toUpperCase() !== tagName.toUpperCase()
) {
return;
}
(element as any).refresh();
}
};
for (const registered of registeredComponents) {
refreshElement(registered, (node as HTMLElement).tagName);
}
}
}
startObserver(observer);
});
startObserver(observer);
/** /**
* Web component used to import css files only once * Web component used to import css files only once
* If called multiple times or the file was already imported, it does nothing * If called multiple times or the file was already imported, it does nothing
**/ **/
@registerElementOnce("link-once") @registerComponent("link-once")
export class LinkOnce extends elementOnce("link") { export class LinkOnce extends inheritHtmlElement("link") {
getElementQuerySelector(): string { connectedCallback() {
super.connectedCallback(false);
// We get href from node.attributes instead of node.href to avoid getting the domain part // We get href from node.attributes instead of node.href to avoid getting the domain part
return `link[href='${this.node.attributes.getNamedItem("href").nodeValue}']`; const href = this.node.attributes.getNamedItem("href").nodeValue;
if (document.querySelectorAll(`link[href='${href}']`).length === 0) {
this.appendChild(this.node);
}
} }
} }
@@ -108,10 +20,14 @@ export class LinkOnce extends elementOnce("link") {
* Web component used to import javascript files only once * Web component used to import javascript files only once
* If called multiple times or the file was already imported, it does nothing * If called multiple times or the file was already imported, it does nothing
**/ **/
@registerElementOnce("script-once") @registerComponent("script-once")
export class ScriptOnce extends inheritHtmlElement("script") { export class ScriptOnce extends inheritHtmlElement("script") {
getElementQuerySelector(): string { connectedCallback() {
// We get href from node.attributes instead of node.src to avoid getting the domain part super.connectedCallback(false);
return `script[src='${this.node.attributes.getNamedItem("src").nodeValue}']`; // We get src from node.attributes instead of node.src to avoid getting the domain part
const src = this.node.attributes.getNamedItem("src").nodeValue;
if (document.querySelectorAll(`script[src='${src}']`).length === 0) {
this.appendChild(this.node);
}
} }
} }

View File

@@ -36,7 +36,6 @@ export function registerComponent(name: string, options?: ElementDefinitionOptio
**/ **/
export function inheritHtmlElement<K extends keyof HTMLElementTagNameMap>(tagName: K) { export function inheritHtmlElement<K extends keyof HTMLElementTagNameMap>(tagName: K) {
return class Inherited extends HTMLElement { return class Inherited extends HTMLElement {
readonly inheritedTagName = tagName;
protected node: HTMLElementTagNameMap[K]; protected node: HTMLElementTagNameMap[K];
connectedCallback(autoAddNode?: boolean) { connectedCallback(autoAddNode?: boolean) {

View File

@@ -10,7 +10,7 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% endif %} {% endif %}
{% from "core/macros.jinja" import paginate_htmx %} {% from "core/macros.jinja" import paginate_htmx with context %}
{% block title %} {% block title %}
{% trans %}File moderation{% endtrans %} {% trans %}File moderation{% endtrans %}

View File

@@ -124,6 +124,9 @@
This must be coupled with a view that handles pagination This must be coupled with a view that handles pagination
with the Django Paginator object. with the Django Paginator object.
Warning:
You must include this macro `with context` as it uses the `querystring` macro
Parameters: Parameters:
current_page (django.core.paginator.Page): the current page object current_page (django.core.paginator.Page): the current page object
paginator (django.core.paginator.Paginator): the paginator object paginator (django.core.paginator.Paginator): the paginator object
@@ -139,6 +142,9 @@
The replaced fragment will be #content so make sure you are calling this macro inside your content block. The replaced fragment will be #content so make sure you are calling this macro inside your content block.
Warning:
You must include this macro `with context` as it uses the `querystring` macro
Parameters: Parameters:
current_page (django.core.paginator.Page): the current page object current_page (django.core.paginator.Page): the current page object
paginator (django.core.paginator.Paginator): the paginator object paginator (django.core.paginator.Paginator): the paginator object
@@ -248,14 +254,5 @@
{% macro querystring() %} {% macro querystring() %}
{%- for key, values in request.GET.lists() -%} {{- urlencode(dict(dict(request.GET.lists()) | items | list + kwargs | items | list), doseq=True) -}}
{%- if key not in kwargs -%}
{%- for value in values -%}
{{ key }}={{ value }}&amp;
{%- endfor -%}
{%- endif -%}
{%- endfor -%}
{%- for key, value in kwargs.items() -%}
{{ key }}={{ value }}&amp;
{%- endfor -%}
{% endmacro %} {% endmacro %}

View File

@@ -1,7 +1,14 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{%- block additional_js -%}
<script type="module" src="{{ static("bundled/core/components/ajax-select-index.ts") }}"></script>
{%- endblock -%}
{%- block additional_css -%} {%- block additional_css -%}
<link rel="stylesheet" href="{{ static('user/user_preferences.scss') }}"> <link rel="stylesheet" href="{{ static('user/user_preferences.scss') }}">
{# importing ajax-select-index is necessary for it to be applied after HTMX reload #}
<link rel="stylesheet" href="{{ static("bundled/core/components/ajax-select-index.css") }}">
<link rel="stylesheet" href="{{ static("core/components/ajax-select.scss") }}">
{%- endblock -%} {%- endblock -%}
{% block title %} {% block title %}

View File

@@ -1,5 +1,6 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link, paginate_jinja %} {% from 'core/macros.jinja' import user_profile_link %}
{% from 'core/macros.jinja' import paginate_jinja with context %}
{% block title %} {% block title %}
{% trans %}Cash register summary list{% endtrans %} {% trans %}Cash register summary list{% endtrans %}

View File

@@ -1,5 +1,5 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from "core/macros.jinja" import paginate_jinja %} {% from "core/macros.jinja" import paginate_jinja with context %}
{% block title %} {% block title %}
{%- trans %}Reloads list{% endtrans %} -- {{ counter.name }} {%- trans %}Reloads list{% endtrans %} -- {{ counter.name }}

View File

@@ -1,5 +1,5 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from "core/macros.jinja" import paginate_jinja %} {% from "core/macros.jinja" import paginate_jinja with context %}
{% block title %} {% block title %}
{%- trans %}Election list{% endtrans %} {%- trans %}Election list{% endtrans %}

View File

@@ -1,7 +1,7 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link %} {% from 'core/macros.jinja' import user_profile_link %}
{% from 'forum/macros.jinja' import display_message, display_breadcrumb, display_search_bar %} {% from 'forum/macros.jinja' import display_message, display_breadcrumb, display_search_bar %}
{% from 'core/macros.jinja' import paginate_jinja %} {% from 'core/macros.jinja' import paginate_jinja with context %}
{% block title %} {% block title %}
{{ topic }} {{ topic }}
@@ -44,7 +44,9 @@
<p><a class="ib button" href="{{ url('forum:new_message', topic_id=topic.id) }}">{% trans %}Reply{% endtrans %}</a></p> <p><a class="ib button" href="{{ url('forum:new_message', topic_id=topic.id) }}">{% trans %}Reply{% endtrans %}</a></p>
{{ paginate_jinja(msgs, msgs.paginator) }} {% if is_paginated %}
{{ paginate_jinja(msgs, msgs.paginator) }}
{% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,5 +1,5 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from "core/macros.jinja" import paginate_jinja %} {% from "core/macros.jinja" import paginate_jinja with context %}
{% block title %} {% block title %}
{% trans %}Operation logs{% endtrans %} {% trans %}Operation logs{% endtrans %}
@@ -28,5 +28,7 @@
</table> </table>
<br> <br>
{{ paginate_jinja(page_obj, paginator) }} {% if is_paginated %}
{{ paginate_jinja(page_obj, paginator) }}
{% endif %}
{% endblock content %} {% endblock content %}

View File

@@ -190,6 +190,7 @@ TEMPLATES = [
"get_sith": "com.views.sith", "get_sith": "com.views.sith",
"get_language": "django.utils.translation.get_language", "get_language": "django.utils.translation.get_language",
"timedelta": "datetime.timedelta", "timedelta": "datetime.timedelta",
"urlencode": "urllib.parse.urlencode",
}, },
"bytecode_cache": { "bytecode_cache": {
"name": "default", "name": "default",

View File

@@ -6,8 +6,14 @@
{% trans %}New subscription{% endtrans %} {% trans %}New subscription{% endtrans %}
{% endblock %} {% endblock %}
{# The following statics are bundled with our autocomplete select.
However, if one tries to swap a form by another, then the urls in script-once
and link-once disappear.
So we give them here.
If the aforementioned bug is resolved, you can remove this. #}
{% block additional_js %} {% block additional_js %}
<script type="module" src="{{ static('bundled/core/components/tabs-index.ts') }}"></script> <script type="module" src="{{ static('bundled/core/components/tabs-index.ts') }}"></script>
<script type="module" src="{{ static("bundled/core/components/ajax-select-index.ts") }}"></script>
<script <script
type="module" type="module"
src="{{ static("bundled/subscription/creation-form-existing-user-index.ts") }}" src="{{ static("bundled/subscription/creation-form-existing-user-index.ts") }}"
@@ -15,6 +21,8 @@
{% endblock %} {% endblock %}
{% block additional_css %} {% block additional_css %}
<link rel="stylesheet" href="{{ static("core/components/tabs.scss") }}"> <link rel="stylesheet" href="{{ static("core/components/tabs.scss") }}">
<link rel="stylesheet" href="{{ static("bundled/core/components/ajax-select-index.css") }}">
<link rel="stylesheet" href="{{ static("core/components/ajax-select.scss") }}">
<link rel="stylesheet" href="{{ static("subscription/css/subscription.scss") }}"> <link rel="stylesheet" href="{{ static("subscription/css/subscription.scss") }}">
{% endblock %} {% endblock %}