【问题标题】:How do I attach a listener to a date selection in the Django admin?如何将侦听器附加到 Django 管理员中的日期选择?
【发布时间】:2024-05-25 12:50:02
【问题描述】:

我有一个模型的 Django 管理页面,模型的 DateField 称为“日期”:

# models.py
class Article(models.Model):
    date = models.DateField(
        help_text="Article publication date"
    )
# admin.py
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    change_form_template = "article_extension.html"

我已扩展此模型的管理员添加/更改模板,以包括选择日期时执行的 JavaScript。

在管理员添加/更改页面中,此字段的默认小部件是带有日历选择器的文本元素。页面源代码没有显示选择器的任何代码,所以我唯一可以附加 JavaScript 的就是输入框:

<input type="text" ... id="id_date">

在模板的 JavaScript 中,我为这个输入框上的“输入”事件附加了一个 EventListener:

# article_extension.html
{% extends "admin/change_form.html" %}
{% block extrahead %}
 block.super 
<script type="text/javascript">
window.onload = function() {
    var dateInput = document.getElementById("id_date");
    dateInput.addEventListener('input', dateListener);

    function dateListener() {
        console.log("date event recognized");
    }
}
</script>
{% endblock %}

然后,当我从日历选择器中选择日期时,输入框的内容会更改以反映所选日期,但 EventListener 不会触发。但是,如果我在输入框中手动输入内容,EventListener 会触发,并且控制台中会显示“日期事件识别”。显然,当使用日历将内容放入输入框中时,这并不能被识别为“输入”。

如何将 EvenListener 附加到输入框或日历以在从日历中选择日期时触发?

【问题讨论】:

    标签: javascript django


    【解决方案1】:

    为什么您当前的解决方案不起作用?

    这不能像您预期的那样工作的原因是 django-admin 日期时间字段已经由整个负载的 javascript 运行。它不是“本机”日期时间输入。

    只要元素的值被用户交互更改,input 事件就会被触发。如果该值被另一个 javascript 函数以编程方式更改,则输入事件被触发。

    当您从日历中选择一个日期时,这就是正在发生的事情。输入的值会更新,但不会触发输入事件。

    我该如何解决?

    想到了 3 个解决方案:

    选项 1

    使用不同的小部件。那里有一大堆日期选择器,我相信其中一些会有一个简单的 API 来添加值更改时的回调。我不知道一个好主意,但谷歌将成为你的朋友。

    选项 2 和 3 涉及使用当前小部件。如果我们深入研究一下代码,我们会看到加载了 2 个对这个小部件很重要的 js 文件:

    • django/contrib/admin/static/admin/js/calendar.js
    • django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js

    经过一番挖掘,您会发现DateTimeShortcuts 中有一个名为handleCalendarCallback 的函数。这是设置值的函数。

    handleCalendarCallback: function(num) {
        var format = get_format('DATE_INPUT_FORMATS')[0];
        // the format needs to be escaped a little
        format = format.replace('\\', '\\\\')
            .replace('\r', '\\r')
            .replace('\n', '\\n')
            .replace('\t', '\\t')
            .replace("'", "\\'");
        return function(y, m, d) {
            DateTimeShortcuts.calendarInputs[num].value = new Date(y, m - 1, d).strftime(format);
            DateTimeShortcuts.calendarInputs[num].focus();
            document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none';
        };
    },
    

    选项 2

    制作您自己的小部件,该小部件使用上述文件的新版本,其中一个功能稍作编辑。它看起来像这样:

    class AdminDateWidget(forms.DateInput):
        class Media:
            js = [
                'admin/js/calendar.js',
                'admin/js/admin/MyVersionOfDateTimeShortcuts.js',
            ]
    
        def __init__(self, attrs=None, format=None):
            attrs = {'class': 'vDateField', 'size': '10', **(attrs or {})}
            super().__init__(attrs=attrs, format=format)
    

    然后,您可以将该小部件用于相关字段,并且您已编辑的代码将运行。不幸的是,由于该文件是静态文件而不是模板,因此您不能使用继承,因此您必须复制并粘贴整个文件,然后编辑该函数。

    选项 3

    您会注意到,在值更新后,.focus() 被调用,因此我们可以利用它并将事件侦听器附加到焦点事件。当然,我们只希望在值实际发生变化时发生一些事情,所以我们需要做这样的事情:

    var dateValue;
    window.onload = function() {
        var dateInput = document.getElementById("id_date");
        dateInput.addEventListener('focus', dateListener);
    
        function dateListener(event) {
            if (dateValue === event.target.value) {
                return
            }
            dateValue = event.target.value;
            // put the stuff you actually want to happen here
        }
    }
    

    【讨论】:

    • 感谢另一个惊人的答案。如果没有您的帮助,我永远不会识别出相关的 JS 文件或 handleCalendarCallback 函数。我选择了选项 2;这是相当具有挑战性的,但它的工作原理!
    • 第一次单击日期选择器时似乎会触发“焦点”,但在选择新日期后似乎不会再次触发焦点,因为日期值直到下一次才会有所不同单击日期选择器。我在包含表单表单字段的 div 上设置了 id。
      {{ form.time_filter|as_crispy_field }}
      让这个解决方案发挥作用有什么想法吗?
    最近更新 更多