Pull to refresh

Мега-Учебник Flask, Часть XX: Магия JavaScript

Reading time 14 min
Views 27K

(издание 2018)


Miguel Grinberg




Туда Сюда


Это двадцатый выпуск серии Мега-учебника Flask, в котором я собираюсь добавить всплывающее окно, появляющееся при наведении указателя мыши на имя пользователя.


Под спойлером приведен список всех статей серии 2018 года.



Примечание 1: Если вы ищете старые версии данного курса, это здесь.


Примечание 2: Если вдруг Вы захотели бы выступить в поддержку моей(Мигеля) работы, или просто не имеете терпения дожидаться статьи неделю, я (Мигель Гринберг)предлагаю полную версию данного руководства(на английском языке) в виде электронной книги или видео. Для получения более подробной информации посетите learn.miguelgrinberg.com.


В настоящее время невозможно создать веб-приложение, которое не использовало бы JavaScript хотя бы немного. Я уверен, вам известно, что JavaScript является единственным языком, который изначально создан для работы в веб-браузерах. В главе 14 вы видели использование простенького JavaScript включения ссылки в шаблоне flask, чтобы обеспечить в режиме реального времени языковые переводы сообщений в блоге. В этой главе я собираюсь углубиться в тему и показать вам еще один полезный трюк для JavaScript, чтобы сделать приложение более интересным и привлекательным для пользователей.


Общая черта пользовательского интерфейса для сайтов социальных сетей, в котором пользователи могут взаимодействовать друг с другом, — это отображение краткой сводки пользователя во всплывающей панели при наведении указателя мыши на имя пользователя в любом месте страницы. Если вы никогда не обращали на это внимание, перейдите в Twitter, Facebook, LinkedIn или любую другую крупную социальную сеть, и когда вы видите имя пользователя, просто оставьте указатель мыши поверх него на пару секунд, чтобы увидеть всплывающее окно. Эта глава будет посвящена созданию этой функции для Microblog, пример работы которой вы можете увидеть на скриншоте ниже:



Ссылки GitHub для этой главы: Browse, Zip, Diff.


Поддержка на стороне сервера


Прежде чем мы углубимся в клиентскую часть, давайте немного поработаем с сервером, для понимания того, что необходимо для поддержки этих пользовательских всплывающих окон. Содержимое всплывающего окна пользователя будет возвращено новым маршрутом, который будет упрощенной версией уже существующего маршрута профиля пользователя. Вот функция, представления:


app/main/routes.py: Функция всплывающего окна пользователя.

@bp.route('/user/<username>/popup')
@login_required
def user_popup(username):
    user = User.query.filter_by(username=username).first_or_404()
    return render_template('user_popup.html', user=user)

Этот маршрут будет прикреплен к URL-адресу /user/<username>/popup и просто загрузит запрошенного пользователя, а затем отобразит шаблон с его данными, который является укороченной версией страницы профиля пользователя:


app/templates/user_popup.html: Шаблон всплывающего окна пользователя.

<table class="table">
    <tr>
        <td width="64" style="border: 0px;"><img src="{{ user.avatar(64) }}"></td>
        <td style="border: 0px;">
            <p>
                <a href="{{ url_for('main.user', username=user.username) }}">
                    {{ user.username }}
                </a>
            </p>
            <small>
                {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
                {% if user.last_seen %}
                <p>{{ _('Last seen on') }}: 
                   {{ moment(user.last_seen).format('lll') }}</p>
                {% endif %}
                <p>{{ _('%(count)d followers', count=user.followers.count()) }},
                   {{ _('%(count)d following', count=user.followed.count()) }}</p>
                {% if user != current_user %}
                    {% if not current_user.is_following(user) %}
                    <a href="{{ url_for('main.follow', username=user.username) }}">
                        {{ _('Follow') }}
                    </a>
                    {% else %}
                    <a href="{{ url_for('main.unfollow', username=user.username) }}">
                        {{ _('Unfollow') }}
                    </a>
                    {% endif %}
                {% endif %}
            </small>
        </td>
    </tr>
</table>

Код JavaScript, который я напишу в следующих разделах, будет ссылаться на этот маршрут, когда пользователь наведёт указатель мыши на имя пользователя. В ответ сервер вернет содержимое HTML для всплывающего окна, которое затем отобразит клиентскую часть. Когда пользователь переместит мышь, всплывающее окно будет удалено. Звучит просто, не так ли?


Если вы хотите увидеть, как будет выглядеть всплывающее окно, вы можете запустить приложение, перейти на страницу профиля любого пользователя, а затем добавить /popup к URL-адресу в адресной строке, чтобы просмотреть полноэкранную версию всплывающего содержимого.


Введение в Bootstrap Popover Component


В главе 11 я познакомил вас с Bootstrap framework как удобным способом создания красиво отформатированных веб-страниц. До сих пор я использовал только минимальную часть этого фреймворка. Bootstrap поставляется в комплекте со многими общими элементами пользовательского интерфейса, все из которых имеют демонстрации и примеры в документации Bootstrap на https://getbootstrap.com. Одним из таких компонентов является Popover, который описан в документации как "небольшой накладной контент, для размещения дополнительной информации". Именно то, что мне нужно!


Большинство компонентов bootstrap определяются с помощью разметки HTML, которая ссылается на определения CSS Bootstrap, которые добавляют стиль форматирования. Некоторые из самых продвинутых также требуют JavaScript. Стандартный способ, которым приложение включает эти компоненты на веб-странице, — это добавление HTML в нужном месте, а затем для компонентов, которые нуждаются в поддержке сценариев, вызов функции JavaScript, которая инициализирует или активирует ее. Компонент popover требует поддержки JavaScript.


Часть HTML для создания popover очень проста, вам просто нужно определить элемент, который будет вызывать появление popover. В моем случае это будет кликабельное имя пользователя, которое появляется в каждом сообщении блога. В sub-шаблоне app/templates/_post.html имя пользователя уже определено:


        <a href="{{ url_for('main.user', username=post.author.username) }}">
            {{ post.author.username }}
        </a>

Теперь в соответствии с документацией popover мне нужно вызвать JavaScript-функцию popover() для каждой ссылки, подобной приведенной выше на странице, и это приведет к инициализации всплывающего окна. Вызов инициализации принимает несколько параметров, которые настраивают всплывающее окно, включая параметры, которые передают содержимое для отображения во всплывающем окне, какой метод использовать, чтобы вызвать всплывающее окно для появления или исчезновения (щелчок, зависание над элементом и т.д. ), если содержимое представляет собой простой текст или HTML, и еще несколько параметров, которые вы можете увидеть на странице документации. К сожалению, после прочтения этой информации у меня появилось больше вопросов, чем ответов, потому, похоже, что этот компонент не предназначен для работы так, как мне это нужно. Ниже приведен список проблем, которые мне потребуется решить для реализации этой функции:


  • На странице будет много ссылок на имя пользователя, по одной для каждого сообщения в блоге. Мне нужен способ найти все эти ссылки из JavaScript после отображения страницы, чтобы я мог инициализировать их как всплывающие окна.
  • Примеры popover в документации Bootstrap предоставляют содержимое popover в качестве атрибута data-content, добавленного в целевой элемент HTML, поэтому, когда событие hover запускается, все, что требуется Bootstrap, это отобразить всплывающее окно. Это жутко неудобно для меня, потому что я хочу создать Ajax-вызов на сервер, чтобы получить контент, и только когда ответ сервера получен, всплывающее окно должно появиться.
  • При использовании режима" hover" всплывающее окно будет оставаться видимым до тех пор, пока вы удерживаете указатель мыши в целевом элементе. Когда вы переместите мышь, всплывающее окно исчезнет. Это имеет не приятный побочный эффект: если пользователь хочет переместить указатель мыши в само всплывающее окно, всплывающее окно исчезнет. Мне нужно будет как то расширить поведение у всплывающего окна при наведении, чтобы пользователь мог перейти в него и, например, щелкнуть там ссылку.

На самом деле это не так уж редко встречается при работе с приложениями на базе браузера, которые быстро усложняются. Вы должны достаточно глубоко продумывать зависимости взаимодействия друг с другом элементов DOM и заставить их вести себя таким образом, чтобы у пользователя остались только приятные впечатления.


Выполнение функции при загрузке страницы


Понятно, что мне нужно будет запустить код JavaScript сразу после загрузки каждой страницы. Функция, которую я собираюсь запустить, будет искать все ссылки на имена пользователей на странице и настроить те, у которых есть компонент popover из Bootstrap.


Библиотека JavaScript jQuery загружается как зависимость от Bootstrap, поэтому я собираюсь воспользоваться ею. При использовании jQuery вы можете зарегистрировать функцию, которая будет запускаться при загрузке страницы, обернув ее в $(...). Пожалуй, это следует добавить в шаблон app/templates/base.html, чтобы выполнялось на каждой странице приложения:


app/templates/base.html: Запуск функции после загрузки страницы.

...
<script>
    // ...

    $(function() {
        // write start up code here
    });
</script>

Как видите, я добавил функцию start up в элемент <script>, в котором я определил функцию translate() в главе 14.


Поиск DOM-элементов с помощью Селекторов


Моя первая задача — создать функцию JavaScript, которая будет находить все ссылки на странице. Эта функция будет срабатывать, когда страница завершит загрузку, и когда она будет завершена, настроит поведение зависания и всплывания окна для всех из них. Сейчас я сосредоточусь на поиске ссылок.


Если вспомнить из главе 14, HTML-элементы, которые были задействованы в живых переводах, имели уникальные идентификаторы. Например, у записи с ID=123 был добавлен атрибут id="post123". Затем с помощью jQuery выражение $('#post123') было использовано в JavaScript для нахождения этого элемента в DOM. Функция $() чрезвычайно мощна и имеет довольно сложный язык запросов для поиска элементов DOM, основанный на CSS Selectors.


Селектор, который я использовал для функции перевода, был разработан, чтобы найти один конкретный элемент, который имел уникальный идентификатор, установленный в качестве атрибута id. Другой вариант идентификации элементов — с помощью атрибута class, который может быть назначен нескольким элементам на странице. Например, я мог бы пометить все пользовательские ссылки class="user_popup", а затем получить список ссылок из JavaScript с помощью $('.user_popup') (в селекторах CSS префикс # ищет по ID, в то время как . поиск префиксов по классам). Возвращаемое значение в этом случае будет представлять собой коллекцию всех элементов, имеющих класс.


Popovers и DOM


Поиграв с примерами popover в документации Bootstrap и проверяя DOM в отладчике браузера, я определил, что Bootstrap создает компонент popover в качестве дочернего элемента целевого элемента в DOM. Как я уже упоминал выше, это влияет на поведение события hover, которое выведет «мышь», как только пользователь переместит указатель мышки от ссылки <a> и в само всплывающее окно.


Трюк, который я могу использовать для расширения события hover, чтобы включить popover, заключается в том, чтобы сделать popover дочерним элементом целевого элемента и тогда наведение hover наследуется. Просматривая опции popover в документации я обнаружил, что это можно сделать, передав родительский элемент в опции container.


Создание дочернего элемента popover из hover будет хорошо работать для кнопок или общих элементов <div> или <span>, но в моем случае целью для popover будет элемент <a>, который отображает ссылку на имя пользователя. Проблема с созданием дочернего элемента popover <a> состоит в том, что popover затем получит поведение ссылки родителя <a>. Конечный результат будет примерно таким:


    <a href="..." class="user_popup">
        username
        <div> ... popover elements here ... </div>
    </a>

Чтобы избежать появления popover внутри элемента <a>, будем использовать другой трюк. Я собираюсь обернуть элемент <a> внутри элемента <span>, а затем связать событие hover и popover с <span>. Результирующая структура будет такая:


    <span class="user_popup">
        <a href="...">
            username
        </a>
        <div> ... popover elements here ... </div>
    </span>

Элементы <div>и <span> невидимы, поэтому они являются отличными элементами, которые помогут вам организовать и структурировать DOM. Элемент <div> является блочным элементом, похожим на абзац в документе HTML, в то время как элемент <span> является строковым элементом, который сравним со словом. Для этого случая я решил использовать элемент <span>, так как элемент <a>, который я обертываю, также является строковым элементом.


Теперь надо реорганизовать мой подшаблон app/templates/_post.html, чтобы включить элемент <span>:


...
            {% set user_link %}
                <span class="user_popup">
                    <a href="url_for('main.user', username=post.author.username)">
                        {{ post.author.username }}
                    </a>
                </span>
            {% endset %}
...

Если вам интересно, где находятся элементы popover HTML, то хорошей новостью является то, что мне не нужно беспокоиться об этом. Когда я получаю вызов функции инициализации popover() в элементах <span>, которые я только что создал, среда Bootstrap будет динамически вставлять компонент popup.


События Hover (наведения)


Как я уже упоминал выше, поведение наведения, используемое компонентом popover из Bootstrap, недостаточно гибко для удовлетворения моих потребностей, но если вы посмотрите на документацию для опции trigger, «hover (зависание)» — это всего лишь одно из возможных значений. Мой взгляд привлек режим «manual», в котором popover может отображаться или удаляться вручную выполнеием вызова JavaScript. Этот режим даст мне свободу в реализации логики зависания, поэтому я собираюсь использовать этот параметр и реализовать мои собственные обработчики событий hover, которые работают так, как они мне нужны.


Поэтому мой следующий шаг — прикрепить событие «hover» ко всем ссылкам на странице. Используя jQuery, событие hover можно привязать к любому элементу HTML, вызвав element.hover (handlerIn, handlerOut). Если эта функция вызывается в наборе элементов, jQuery удобно прикрепляет событие ко всем из них. Два аргумента — это две функции, вызываемые, когда пользователь перемещает указатель мыши в и из целевого элемента соответственно.


app/templates/base.html: Hover event.

$(function() {
    $('.user_popup').hover(
        function(event) {
            // mouse in event handler
            var elem = event.currentTarget;
        },
        function(event) {
            // mouse out event handler
            var elem = event.currentTarget;
        }
    )
});

Аргумент event является объектом event, который содержит полезную информацию. В этом случае я извлекаю элемент, который был целью события, используя event.currentTarget.


Браузер отправляет событие hover сразу после того, как мышь войдет область влияющего элемента. В случае всплывающего окна, хотелось бы, чтобы событие активировалось только после ожидания небольшого периода времени, когда мышь задерживается на элементе, так что, когда указатель мыши ненадолго проходит над элементом, но не останавливается на нем нет мгновенно всплывающих мигающих окон. Поскольку событие не приходит с поддержкой задержки, это еще одна вещь, которую я собираюсь реализовать сам. Наверное надо добавить секундный Таймер в обработчик событий "mouse in":


app/templates/base.html: Задержка при наведении.

$(function() {
    var timer = null;
    $('.user_popup').hover(
        function(event) {
            // обработчик события mouse in
            var elem = event.currentTarget;
            timer = setTimeout(function() {
                timer = null;
                // логика Popup должна быть здесь
            }, 1000);
        },
        function(event) {
            //обработчик события mouse out
            var elem = event.currentTarget;
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }
        }
    )
});

Функция setTimeout() доступна в среде браузера. Она принимает два аргумента: функцию и время в миллисекундах. Эффект setTimeout() заключается в том, что функция вызывается после заданной задержки. Поэтому я добавил функцию, которая пока пуста, которая будет вызвана через секунду после отправки события hover. Благодаря замыканиям на языке JavaScript эта функция может обращаться к переменным, определенным во внешней области видимости, таким как elem.


Я сохраняю объект timer в переменной timer, которую я определил вне вызова hover(), чтобы сделать объект timer доступным также для обработчика "mouse out". Причина, по которой мне это нужно, еще раз, чтобы оставить приятное впечатление пользователю. Если пользователь перемещает указатель мыши в одну из этих пользовательских ссылок и остается на ней, но скажем, за полсекунды до ее срабатывания перемещает указатель мыши, в этом случае я не хочу, чтобы Таймер досчитал свою задержку и вызывал функцию, которая будет отображать всплывающее окно. Поэтому мой обработчик событий mouse out проверяет, есть ли активный объект timer, и если есть, то отменяет его.


Запрос AJAX


Запросы Ajax не являются новой темой, так как я уже говорил об этом в главе 14, как часть живого языка перевода. При использовании jQuery, функция $.ajax() отправит асинхронный запрос на сервер.


Запрос, который я собираюсь отправить на сервер, будет иметь URL /user/<username>/popup, который я добавил в приложение в начале этой главы. Ответ от этого запроса будет содержать HTML, который мне нужно вставить во всплывающее окно.


Моя непосредственная проблема относительно этого запроса состоит в том, чтобы знать, каково значение username, которое мне нужно включить в URL. Функция mouse in event handler является универсальной, она будет работать для всех пользовательских ссылок, которые находятся на странице, поэтому функция должна определить имя пользователя из своего контекста.


Переменная elem содержит целевой элемент из события hover, который является элементом <span>, который обертывает элемент <a>. Чтобы извлечь имя пользователя, я могу перемещаться по DOM, начиная с <span>, переходя к первому дочернему элементу, который является элементом <a>, а затем извлекая из него текст, который является именем пользователя, которое мне нужно использовать в моем URL. С функциями обхода DOM jQuery это сделать не сложно:


elem.first().text().trim()

Функция first(), примененная к узлу DOM, возвращает его первый дочерний элемент. Функция text() возвращает текстовое содержимое узла. Эта функция не выполняет обрезку текста, поэтому, например, если <a> находится в одной строке, текст в следующей строке и </a> в другой строке, функция text() вернет все пробелы, которые окружают текст. Чтобы устранить все пробелы и оставить только текст, я использую функцию JavaScript trim().


И это вся информация, которая мне нужна, чтобы иметь возможность выдать запрос на сервер:


app/templates/base.html: XHR запрос.

$(function() {
    var timer = null;
    var xhr = null;
    $('.user_popup').hover(
        function(event) {
            // обработчик события mouse in
            var elem = $(event.currentTarget);
            timer = setTimeout(function() {
                timer = null;
                xhr = $.ajax(
                    '/user/' + elem.first().text().trim() + '/popup').done(
                        function(data) {
                            xhr = null
                            //  здесь создаём и отображаем всплывающее окно
                        }
                    );
            }, 1000);
        },
        function(event) {
            // обработчик события mouse out
            var elem = $(event.currentTarget);
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }
            else if (xhr) {
                xhr.abort();
                xhr = null;
            }
            else {
                //  здесь разрушаем всплывающее окно
            }
        }
    )
});

Здесь я определил новую переменную во внешней области видимости, xhr. Эта переменная будет содержать асинхронный объект запроса, который я инициализирую из вызова $.ajax(). К сожалению, при построении URL непосредственно на стороне JavaScript я не могу использовать url_for() из Flask, поэтому в этом случае я должен явно объединить части URL.


Вызов $.ajax() возвращает promise (обязательство), который является специальным объектом JavaScript, представляющим асинхронную операцию. Я могу прикрепить обратный вызов завершения, добавив .done (function), поэтому моя функция обратного вызова будет вызвана после завершения запроса. Функция обратного вызова получит ответ в качестве аргумента, который я назвал data и Вы можете увидеть его в коде выше. Это будет HTML-контент, который я собираюсь поместить в popover.


Но прежде чем мы доберемся до popover, есть еще одна деталь, связанная с предоставлением пользователю удобного интерфейса и хорошего настроения, о которой нужно позаботиться. Напомним, что я добавил логику в функцию обработчика событий "mouse out", чтобы отменить таймаут в одну секунду, если пользователь переместил указатель мыши из <span>. Та же идея должна быть применена к асинхронному запросу, поэтому я добавил второе условие, чтобы прервать мой объект запроса xhr, если он существует.


Создание и Разрушение Popover


Настал значительный момент.


Могу создать popover компонент,


Используя лишь data-аргумент


Из функции callback Ajax мне возвращенный:


app/templates/base.html: Отображение всплывающего окна.

                            function(data) {
                                xhr = null;
                                elem.popover({
                                    trigger: 'manual',
                                    html: true,
                                    animation: false,
                                    container: elem,
                                    content: data
                                }).popover('show');
                                flask_moment_render_all();
                            }

Фактическое создание всплывающего окна не так уж и сложно, функция popover() из Bootstrap выполнит всю работу, необходимую для его настройки. Параметры для popover приведены в качестве аргумента. Я настроил этот popover с "ручным" режимом триггера, HTML-контентом, без анимации затухания (так что он появляется и исчезает быстрее), и я установил родительский элемент как сам элемент <span>, так что поведение при наведении распространяется на popover по наследованию. Наконец, я передаю аргумент data в обратный вызов Ajax в качестве аргумента content.


Возврат вызова popover() недавно появившийся компонент popover, который по странной причине имел другой метод, также называемый popover(), который используется для его отображения. Поэтому мне пришлось добавить второй вызов popover('show'), чтобы всплывающее окно появилось на странице.


Содержимое всплывающего окна включает в себя дату "last seen" (последнего посещения), которая генерируется через плагин Flask-Moment, как описано в главе 12. Как описано в документации расширения для добавления новых элементов Flask-Moment через Ajax, функция flask_moment_render_all() должна быть вызвана для соответствующего отображения этих элементов.


Остается разобраться с удалением всплывающего окна при обработке события mouse out. У этого обработчика уже есть логика для прерывания операции popover, если она прерывается пользователем, перемещающим курсор мыши из целевого элемента. Если ни одно из этих условий не применимо, это означает, что всплывающее окно в настоящее время отображается, и Пользователь покидает целевую область, поэтому в этом случае вызов popover('destroy') к целевому элементу выполнит надлежащее в этом случае удаление и очистку.


app/templates/base.html: Разрушение popover.

            function(event) {
                // mouse out event handler
                var elem = $(event.currentTarget);
                if (timer) {
                    clearTimeout(timer);
                    timer = null;
                }
                else if (xhr) {
                    xhr.abort();
                    xhr = null;
                }
                else {
                    elem.popover('destroy');
                }
            }

Туда Сюда

Tags:
Hubs:
+10
Comments 0
Comments Leave a comment

Articles