本文翻译自The Flask Mega-Tutorial Part XX: Some JavaScript Magic

这是Flask Mega-Tutorial系列的第二十部分,我将添加一个功能,当你将鼠标悬停在用户的昵称上时,会弹出一个漂亮的窗口。

现在,构建一个Web应用而不使用JavaScript是不可能的。 你一定知道,JavaScript是Web浏览器中可本地运行的唯一语言。 在第十四章中,你看到我在Flask模板中添加了一个简单的JavaScript的启用链接,以提供实时的用户动态的语言翻译。 而在本章中,我将深入探讨该主题,并向你展示另一个有用的JavaScript技巧,给应用程序增添趣味来吸引用户。

社交网站的常见用户交互模式是,当你将鼠标悬停在用户的名称上时,可以在弹出窗口中显示用户的主要信息。 如果你从未注意到这一点,请访问Twitter,Facebook,LinkedIn或任何其他主要社交网站,当你看到用户名时,只需将鼠标指针放在上面几秒钟即可看到弹出窗口。 本章将致力于为Microblog实现该功能,你可以在下面看到预览效果:

本章的GitHub链接为:BrowseZipDiff.

服务端的支持

在深入研究客户端之前,让我们先了解一下支持这些用户弹窗所需的服务器端的工作。 用户弹窗的内容将由新路由返回,它是现有个人主页路由的简化版本。 视图函数如下:

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)

该路由将被附加到/user//popup URL,并且将简单地加载所请求的用户,然后渲染到模板中。 该模板是个人主页的简化版本:

app/templates/user_popup.html:用户弹窗模板。

 <table class="table">
<tr>
<td width="" 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内容显示在弹出窗口中。 当用户移开鼠标时,弹出窗口将被删除。 听起来很简单,对吧?

如果你想了解弹窗像什么样,现在可以运行应用,跳转到任何用户的个人主页,然后在地址栏的URL中追加/popup以查看全屏版本的弹出窗口内容。

Bootstrap Popover组件简介

第十一章中,我向你介绍了可便捷地创建精美网页的Bootstrap框架。 到目前为止,我只使用了这个框架的一小部分。 Bootstrap捆绑了许多常见的UI元素,所有这些元素都在地址为https://getbootstrap.com的Bootstrap文档中有demo和示例。 其中一个组件是Popover(弹窗),在文档中将其描述为“用于容纳辅助信息的小的覆盖窗口”。 这正是我需要的! 
  
大多数bootstrap组件都是通过HTML标记定义的,该标记引用Bootstrap CSS的定义内容来添加漂亮的样式。 一些高级的组件还需要JavaScript。 应用程序在网页中包含这些组件的标准方式是在适当的位置添加HTML,然后为需要脚本支持的组件调用JavaScript函数,以便初始化或激活它。 popover组件确实需要JavaScript的支持。

要做弹窗的HTML部分非常简单,你只需要定义将触发弹窗的元素。 就我而言,就是处理每条用户动态中出现的可点击的用户名。 
app/templates/_post.html子模板具有已定义的用户名:

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

现在根据popover文档,我需要调用每个链接上的popover() JavaScript函数,就像上面出现在页面上的链接一样,这才能初始化弹出窗口。 初始化调用接受许多配置弹出窗口的选项,包括传递想要在弹出窗口中显示的内容,以及使用什么方法触发弹出窗口出现或消失(单击,悬停在元素上等),如果内容是纯文本或HTML,那么在文档中可以找到更多的选项。不幸的是,在阅读完这些信息之后,我的疑惑更多了,因为这个组件看起来并没有按照我需要的方式工作。 以下是我实现此功能需要解决的问题列表:

  • 在页面中会有很多用户名链接,每条用户动态都会显示一个。我需要有一种方法可以在页面渲染后用JavaScript中找到所有这些链接,以便我可以将它们初始化为弹出窗口。
  • Bootstrap文档中的popover示例都将目标HTML元素的data-content属性设置为popover的内容,因此当触发悬停事件时,Bootstrap需要做的只是显示弹出窗口。这对我来说要做的就不止这些了,因为我想对服务器进行Ajax调用以获取内容,并且只有当收到服务器的响应时,我才希望弹出窗口出现。
  • 使用“悬停”模式时,只要你将鼠标指针放在目标元素中,弹出窗口就会保持可见状态。当你移开鼠标时,弹出窗口将消失。这具有糟糕的副作用,即如果用户想要将鼠标指针移动到弹出窗口中,弹出窗口将消失。我需要找出一种方法来将悬停行为扩展为包含弹出窗口,以便用户可以移动到弹出窗口中,例如,单击那里的链接。

在开发基于浏览器的应用程序时,事情变得越来越复杂的情况,实际上并不罕见。 你必须非常仔细地考虑DOM元素如何相互作用,并使其行为方式提供良好的用户体验。

在页面加载完成后执行函数

很明显,我将需要在每个页面加载后立即运行一些JavaScript代码。 我要运行的函数将搜索页面中用户名的所有链接,并使用Bootstrap中的弹出窗口组件配置它们。

jQuery JavaScript库作为Bootstrap的依赖项加载,因此我将利用它。 当使用jQuery时,你可以用$(...)封装来注册一个函数,函数将会在页面加载完毕后运行。 我可以将它添加到app/templates/base.html模板中,以便它可以在应用程序的每个页面上运行:

app/templates/base.html:页面加载完毕后运行函数。

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

正如你所看到的,我已经在<script>元素中添加了我的启动函数,而在第十四章中,我已在该元素中定义了中的translate()函数。

使用选择器查找DOM元素

第一个要解决的问题是创建一个JavaScript函数来查找页面中的所有用户链接。 这个函数将在页面加载完成时运行,并且当完成时,将为所有页面配置悬停和弹出行为。 现在我要集中精力来寻找链接。

回顾第十四章,在实时翻译中被调用的HTML元素具有唯一的ID。 例如,ID = 123的用户动态中具有id="post123"属性。 然后使用jQuery,在JavaScript中使用表达式$('#post123')在DOM中定位此元素。 $()函数功能非常强大,并且具有相当复杂的查询语言来搜索DOM元素,可以参考CSS Selectors

我用于翻译功能的选择器旨在使用id属性查找一个具有唯一标识符的特定元素。 识别元素的另一种方法是使用class属性,它可以分配给页面中的多个元素。 例如,我可以用class="user_popup"标记所有的用户链接,然后我可以通过$('.user_popup')获取这些元素的列表(CSS选择器中,#前缀代表查询id属性,’.’前缀代表查询class属性)。 在本处,返回值将是具有该类的所有元素的集合。

弹窗和DOM元素

通过使用Bootstrap文档中的弹出窗口示例并在浏览器的调试器中检查DOM,我确定Bootstrap将弹出窗口组件创建为DOM中目标元素的同级元素。 正如我上面提到的,这会影响悬停事件的行为,只要用户将鼠标从<a>链接移动到弹出窗口本身,就会触发“鼠标移出”事件。

我可以扩展悬停事件以包含弹出窗口,就是将弹出窗口作为目标元素的子元素,这样悬停事件就会继承。 通过查看文档中的弹出选项,可以通过在container选项中传递父元素来完成此操作。

将popover作为悬停元素的子元素可以很好地用于按钮或一般的<div><span>元素,但在我的情况下,popover的target将是显示用户名的可点击链接的<a>元素。 使popover成为<a>元素的子元素的问题是,弹出窗口将获得<a>父元素的链接行为。 最终的结果是这样的:

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

为了避免弹出窗口出现在<a>元素中,我要使用的是另一个技巧。 我要将<a>元素封装在<span>元素中,然后将悬停事件和弹出窗口与<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 %}
...

如果你想知道弹出式HTML元素在哪里,好消息是我不必操心这一点。 当我在刚刚创建的<span>元素上调用popover()初始化函数时,Bootstrap框架会为我动态地插入弹出组件。

悬停事件

正如我上面提到的,Bootstrap中的popover组件使用的悬停行为不够灵活,无法满足我的需求,但如果你查看trigger选项的文档,则hover只是其中一个可能的值。 一个引起我注意的是manual模式,在这种模式下,可以通过JavaScript调用手动显示或删除弹出窗口,这种模式可以让我自由地实现悬停逻辑,所以我将使用该选项并实现我自己的悬停事件处理程序,并以我需要的方式工作。

所以我的下一步是将一个“hover”事件附加到页面中的所有链接。 使用jQuery,可以通过调用element.hover(handlerIn, handlerOut)将悬停事件附加到任何HTML元素。 如果在元素集合上调用这个函数,jQuery方便地将事件附加到所有元素上。 这两个参数是两个函数,分别在用户将鼠标指针移入和移出目标元素时调用对应的函数。

app/templates/base.html:悬停事件。

     $(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.currentTarget来提取事件的目标元素。

浏览器在鼠标进入受影响的元素后立即调度悬停事件。 针对弹出行为,你只想鼠标停留在元素上一段时间才能激活,以防当鼠标指针短暂通过元素但不停留在元素上时出现弹出闪烁。 由于该事件不支持延迟,因此这是我需要自己实现的另一件事情。 所以我打算在“鼠标进入”事件处理程序中添加一秒计时器:

app/templates/base.html:悬停延迟。

     $(function() {
var timer = null;
$('.user_popup').hover(
function(event) {
// mouse in event handler
var elem = event.currentTarget;
timer = setTimeout(function() {
timer = null;
// popup logic goes here
}, 1000);
},
function(event) {
// mouse out event handler
var elem = event.currentTarget;
if (timer) {
clearTimeout(timer);
timer = null;
}
}
)
});

setTimeout()函数在浏览器环境中才可用。 它需要两个参数,函数和毫秒单位的时间。 setTimeout()的效果是函数在给定的延迟后被调用。 所以我添加了一个函数(现在是空的),将在悬停事件的一秒钟后被调用。 由于JavaScript语言中的闭包机制,此函数可以访问在外部作用域中定义的变量,例如elem

我将timer对象存储在hover()调用之外定义的timer变量中,以使timer对象也可以被“mouse out”处理程序访问。 我需要这么做的原因是为了获得良好的用户体验。 如果用户将鼠标指针移动到其中一个用户链接中,并在移动它之前停留了半秒钟,我不希望该timer继续运行并调用显示弹出窗口的函数。 所以我的鼠标移出事件处理程序检查是否有一个活动的timer对象,如果有,就取消它。

Ajax请求

Ajax请求不是一个新话题了,因为我已经在第十四章中已介绍过这个主题,来作为实时语言翻译功能。 当使用jQuery时,$.ajax()函数向服务器发送一个异步请求。

我要发送到服务器的请求将具有类似/user//popup模式的URL,在本章开始时我已经将该URL添加到应用程序中。 这个请求的响应将包含我需要在弹出窗口中插入的HTML。

关于这个请求的直接问题是我需要知道包含在URL中的“username”的值是什么。 鼠标进入的事件处理函数是通用的,它将在页面中找到的所有用户链接,所以该函数需要从其上下文中确定用户名。

elem变量包含悬停事件中的目标元素,它是包裹<a>元素的<span>元素。 为了提取用户名,我可以从<span>开始浏览DOM,移至第一个子元素,即<a>元素,然后从中提取文本,这就是在网址中要使用的用户名 。 使用jQuery的DOM遍历函数,可以很简单地做到:

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

应用于DOM节点的first()函数返回其第一个子节点。 text()函数返回节点的文本内容。 该函数不会对文本进行任何修剪,例如,如果在一行中有<a>,在下一行中有文本,在另一行中有</a>text()将返回文本周围的所有空白。 为了消除所有空白并只留下文本,我使用了名为trim()的JavaScript函数。

这就是我需要向服务器发出请求的所有信息:

app/templates/base.html:XHR请求。

     $(function() {
var timer = null;
var xhr = null;
$('.user_popup').hover(
function(event) {
// mouse in event handler
var elem = $(event.currentTarget);
timer = setTimeout(function() {
timer = null;
xhr = $.ajax(
'/user/' + elem.first().text().trim() + '/popup').done(
function(data) {
xhr = null
// create and display popup here
}
);
}, 1000);
},
function(event) {
// mouse out event handler
var elem = $(event.currentTarget);
if (timer) {
clearTimeout(timer);
timer = null;
}
else if (xhr) {
xhr.abort();
xhr = null;
}
else {
// destroy popup here
}
}
)
});

代码中,我在外部范围中定义了一个新变量xhr。 这个变量将保存我通过调用$ .ajax()来初始化的异步请求对象。 不幸的是,当直接在JavaScript端构建URL时,我无法使用Flask中的url_for(),所以在这种情况下,我必须显式连接URL的各个部分。

$ .ajax()调用返回一个promise,这是一个代表异步操作的特殊JavaScript对象。 我可以通过添加.done(function)来附加一个完成回调函数,所以一旦请求完成,我的回调函数就会被调用。 回调函数将接收到的响应作为参数,你可以在上面的代码中看到,我将其命名为data。 这将是我要放入popover的HTML内容。

但在我们获得弹窗之前,还有一个细节需要处理,以便给予用户一个良好的体验。 回想一下之前添加的逻辑,如果用户在触发鼠标进入事件之后的一秒内将鼠标指针移出<span>,将触发取消弹窗的逻辑。 同样的逻辑也需要应用于异步请求,所以我添加了第二个子句来放弃我的xhr请求对象(如果存在)。

弹窗的创建和销毁

最后我使用在Ajax回调函数中传递给我的data参数来创建我的弹窗组件:

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();
}

弹出窗口的实际创建非常简单,Bootstrap的popover()函数完成设置所需的所有工作。 弹出窗口的选项作为参数给出。 我已经用manual触发模式,HTML内容,没有淡入淡出的动画(这样它就会更快地出现和消失)配置了这个弹出窗口,并且我已经将父元素设置为<span>元素本身,所以悬停行为通过继承扩展到弹出窗口。 最后,我将Ajax回调函数的data参数作为content参数的值。

popover()调用创建了一个弹窗组件,该组件也具有一个名为popover()的方法来显示弹窗。因此我不得不添加第二个popover('show')调用来将弹窗显示到页面中。

弹出窗口的内容包括第十二章中通过Flask-Moment插件生成的“最后访问”日期。 文档中提到,当通过Ajax添加新的Flask-Moment元素时,需要调用flask_moment_render_all()函数来适当地渲染这些元素。

现在剩下的就是完善鼠标移出事件处理程序上的删除弹出窗口逻辑。 如果用户将鼠标移出目标元素,该处理程序已经具有中止弹出操作的逻辑。 如果这些条件都不适用,那么这意味着弹出窗口当前显示并且用户正在离开target区域,所以在这种情况下,对目标元素的popover('destroy')调用将正确地执行移除和清理。

app/templates/base.html:销毁弹窗。

                 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');
}
}
 

Flask 教程 第二十章:加点JavaScript魔法的更多相关文章

  1. Flask 教程 第二十一章:用户通知

    本文翻译自The Flask Mega-Tutorial Part XXI: User Notifications 这是Flask Mega-Tutorial系列的第二十一章,我将添加一个私有消息功能 ...

  2. python 教程 第二十章、 数据库编程

    第二十章. 数据库编程 环境设置 1).安装MySQL-python http://www.lfd.uci.edu/~gohlke/pythonlibs/ MySQL-python-1.2.3.win ...

  3. Flask 教程 第二十二章:后台作业

    本文翻译自The Flask Mega-Tutorial Part XXII: Background Jobs 这是Flask Mega-Tutorial系列的第二十二部分,我将告诉你如何创建独立于W ...

  4. Flask 教程 第二十三章:应用程序编程接口(API)

    本文翻译自The Flask Mega-Tutorial Part XXIII: Application Programming Interfaces (APIs) 我为此应用程序构建的所有功能都只适 ...

  5. Flask 教程 第十章:邮件支持

    本文翻译自The Flask Mega-Tutorial Part X: Email Support 这是Flask Mega-Tutorial系列的第十部分,在其中我将告诉你,应用如何向你的用户发送 ...

  6. Flask 教程 第二章:模板

    本文翻译自 The Flask Mega-Tutorial Part II: Templates 在Flask Mega-Tutorial系列的第二部分中,我将讨论如何使用模板. 学习完第一章之后,你 ...

  7. C#图解教程 第二十章 异步编程

    笔记 异步编程 什么是异步 示例 async/await特性的结构什么是异步方法 异步方法的控制流await表达式取消一个异步操作异常处理和await表达式在调用方法中同步地等待任务在异步方法中异步地 ...

  8. JavaScript高级程序设计:第二十章

    第二十章 一.语法 JSON的语法可以表示以下三种类型的值: (1)简单值 (2)对象 JSON的对象与javascript字面量有一些不同.例如,在javascript中,前面的对象字面量可以写成下 ...

  9. javascript进阶教程第二章对象案例实战

    javascript进阶教程第二章对象案例实战 一.学习任务 通过几个案例练习回顾学过的知识 通过案例练习补充几个之前没有见到或者虽然讲过单是讲的不仔细的知识点. 二.具体实例 温馨提示 面向对象的知 ...

随机推荐

  1. 插入jupyter notebook代码

    <iframe src="https://nbviewer.jupyter.org/gist/gaowenxin95/53408e0f1ce268430efaad2cb1f0ca4f& ...

  2. [Python]python去除两个txt文件的重复词汇 python 2020.2.10

    两个txt文件词汇,用换行符分隔.可以用代码将要处理的文件去掉另一个文件所包含的重复内容. 如: a.txt内容为: 衡山 泰山 西湖 紫禁城 b.txt内容为: 泰山 衡山 长白山 张三丰 将a.t ...

  3. 模块简介与matplotlib基础

    模块简介与matplotlib基础 1.基本概念 1.1数据分析 对已知的数据进行分析,提取出一些有价值的信息. 1.2数据挖掘 对大量的数据进行分析与挖掘,得到一些未知的,有价值的信息. 1.3数据 ...

  4. python requests [Errno 104] Connection reset by peer

    有个需求,数据库有个表有将近 几千条 url 记录,每条记录都是一个图片,我需要请求他们拿到每个图片存到本地.一开始我是这么写的(伪代码): import requests for url in ur ...

  5. 获取table中CheckBox选中行的id

    方式一 var selectList=''; jQuery(".table tbody input[type=checkbox]:checked").map(function () ...

  6. linux - mysql - 卸载:RPM包安装方式的MySQL卸载

    (1)检查是否安装了MySQL组件 [root@DB-Server init.d]# rpm -qa | grep -i mysql MySQL-devel-5.6.23-1.linux_glibc2 ...

  7. solr es调优化和问题排查

    (1)TOP 显示当前进程状态,结合 ps -aux 可以看是哪一个服务.mpstat 可以看是cpu的负载 (2)TOP -H -u 用户名 显示该用户下 所有的线程. 还有pstree (3)js ...

  8. spring aop使用,spring aop注解,Spring切面编程

    ================================ ©Copyright 蕃薯耀 2020-01-21 https://www.cnblogs.com/fanshuyao/ 一.第一步, ...

  9. ISCC2018_leftleftrightright-Writeup

    leftleftrightright(150) 这个题学到了不少东西,值得认真写一下 下载好文件后发现是upx的壳,upx -d直接脱掉后运行,发现是经典的check输入的题目(作为一个linuxer ...

  10. Mybatis面试问题集锦

    1.#{}和${}的区别是什么? 答:mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值: mybatis在处理 $ { } 时,就 ...