简单实现Web页面元素拖放功能(Web Drag & Drop)

之前,想在浏览器中实现拖放似乎很困难,现在有了HTML5就简单了。下面实现一个例子:简单的拖拽排序。

拖放测试页面(DragDrop.html)

页面使用bootstrap展示几张图片(缩略图)
注:省略号部分表示你可以自己在div.row下复制更多个div.thumbnail

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drag & Drop sample</title>
<link rel="stylesheet" href="http://cdn.staticfile.org/twitter-bootstrap/3.1.1/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="thumbnail col-sm-6 col-md-4">
<img class="img-responsive" src="img1.png">
<div class="caption text-center"><h5>Image1</h5></div>
</div>
<div class="thumbnail col-sm-6 col-md-4">
<img class="img-responsive" src="img2.png">
<div class="caption text-center"><h5>Image2</h5></div>
</div>
<!-- ...... -->
</div>
</div>
<script src="http://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<script src="DragDrop.js"></script>
</body>
</html>

拖放测试脚本(DragDrop.js)

注:脚本依赖的jquery已经在页面里引入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
$(function(){
//拖拽的对象是这些缩略图
var dragable = '.thumbnail';
var $dragable = $(dragable);
//设置可拖拽属性
$dragable.attr('draggable', true);
//响应开始拖拽事件
$dragable.on('dragstart', setDragData);
//拖放的目标也是这些缩略图
var $dropable = $dragable;
//让目标允许拖放
$dropable.on('dragover', allowDrop);
//响应拖放事件
$dropable.on('drop', dropDragData);

//开始拖拽事件处理
function setDragData(e){
var $target = getDragable(e.target);
//传递被拖拽对象的位置索引
e.originalEvent.dataTransfer.setData("text",$target.index());
}

function allowDrop(e){
e.preventDefault();//这样就能允许拖放
}

//拖放事件处理:把被拖拽对象放到目标对象所在的位置,即实现拖拽排序。
function dropDragData(e){
e.preventDefault();//阻止默认的放置行为
var $target = getDragable(e.target);
var dragIdx = e.originalEvent.dataTransfer.getData("text");
if (dragIdx !== $target.index()){
var $draged = $(dragable).eq(dragIdx);
if ($draged.index() > $target.index()){
$target.before($draged);//向前拖:放在目标对象的前面
}else{
$target.after($draged);//向后拖:放在目标对象的后面
}
}
}
//获取可拖拽对象
function getDragable(dom){
var $dragable = $(dom);
if (!$dragable.is(dragable)){
//拖拽子元素时定位到父元素
$dragable = $dragable.closest(dragable);
}
return $dragable;
}
});

总结

上例的拖放效果在最新的ChromeIEQQ浏览器下测试了都很正常,但在FireFox下拖放图片会激活一个新的页签(有人说使用e.preventDefault()可以解决,但我试了没有效果)。

让table固定表头并冻结列的一种简单实现方法

标题已经说明需求,就不说废话了,直接上code;)

.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--把table放到div容器里。-->
<div class="table-container">
<table class="table-fixed">
<thead>
<tr>
<th class="col-fixed">fixed0</th>
<th>column0</th>
...省略其它th...
</tr>
</thead>
<tbody>
<tr>
<td class="col-fixed">fixed1</td>
<td>column1</td>
....省略其它td...
</tr>
....省略其它tr...
</tbody>
</table>
<div>

.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<style>
/*限制容器宽高并允许滚动*/
.table-container{
width:100%;
max-height:200px;
overflow:auto;
}
/*表头cell设为相对定位*/
.table-fixed>thead>tr>th{
position:relative;
z-index:100;
background-color:#eee;
}
/*冻结列设为相对定位*/
.table-fixed>tbody>tr>td.col-fixed{
position:relative;
background-color:#eee;
}
/*设置z-index避免被覆盖*/
.table-fixed>thead>tr>th.col-fixed{
z-index:110;
}
</style>

.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
//需先引入jquery
$(function(){
var $table = $('.table-fixed');
var $topFixes = $table.find('thead>tr>th');
var $colFixes = $table.find('.col-fixed');
//容器滚动时,为实现fixed冻结效果:
//1、把表头cell的top设为scrollTop
//2、冻结列的left设为scrollLeft
$table.closest('.table-container').scroll(function(){
$topFixes.css('top',this.scrollTop);
$colFixes.css('left',this.scrollLeft);
});
});
</script>

##总结
相较于表头和内容分离的做法,这种实现方式不会出现对不齐的问题,而且比较简单(:不想写js的请忽略)

##已知问题

  1. 桌面Chrome下的效果最好,其他浏览器fixed内容有点闪烁。
  2. 移动端浏览器下的延迟比较严重,只能改为用多个table实现。
  3. 如果冻结列超出容器(.table-container)的范围,会导致水平滚动条滚不到头(无限滚动)。

Mongodb MapReduce 介绍、示例及特别说明

介绍

Map-Reduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结果合并成最终结果(REDUCE)。
MongoDB提供的Map-Reduce非常灵活,可以高效的进行大规模数据的统计分析。

语法

格式一

1
2
3
4
5
6
7
8
9
10
11
12
13
db.collection.mapReduce(
function() {emit(key,value)},//map 函数
function(key,values) {return reduced}, //reduce 函数
<collection>//out collection
)
//第三个参数也可以传入更多选项:
{
finalize: function(key, reduced){return finalized}
out: <collection>|{inline:true},
query: <document>,
sort: <document>,
limit: <number>,
}

格式二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
db.runCommand({
mapReduce: <collection>,
map: <function>,
reduce: <function>,
finalize: <function>,
out: <output>,
query: <document>,
sort: <document>,
limit: <number>,
scope: <document>,
jsMode: <boolean>,
verbose: <boolean>,
bypassDocumentValidation: <boolean>,
collation: <document>
})

示例

示例集合person存储各省市居民的姓名、性别等记录

1
2
3
4
5
6
7
8
9
10
11
12
13
1.{
"name" : "姓名1",
"gender" : "男",
"city" : "城市a",
"province" : "省份A"
}
2.{
"name" : "姓名2",
"gender" : "女",
"city" : "城市b",
"province" : "省份B"
}
3...

现用mapReduce统计各省市人口性别比例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
db.person.mapReduce(
function(){//map
var key = {province:this.province, city:this.city}
var value = {total: 1}
if (this.gender == '男'){
value.male = 1
}else if (this.gender == '女'){
value.female = 1
}else{
value.unknown = 1
}
emit(key, value)
},
function(key, values){//reduce
var value = {}//累计各性别的数量
values.forEach(function(item){
//item里可能是单个值,也可能已经是累计值
for (var k in item){
value[k] = (value[k]||0) + item[k]
}
})
return value//reduce返回值不支持数组,若需多值请用对象格式
},{
query:{},//指定过滤源数据的查询条件
sort:{province:1, city:1},//按key排序可减少reduce的次数,加快执行速度
finalize: function(key, rValue){
for (var k in rValue){
if (k !== 'total'){//数量转为比例
rValue[k] = rValue[k]/rValue.total
}
}
return rValue
},
out:{inline:true},
}
).find()

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[
{
"_id" : {
"province" : "省份A",
"city" : "城市a"
},
"value" : {
"total" : 40.0,
"male" : 0.425,
"female" : 0.575
}
},
{
"_id" : {
"province" : "省份A",
"city" : "城市b"
},
"value" : {
"total" : 1.0,
"male" : 1.0
}
},
{
"_id" : {
"province" : "省份A",
"city" : "城市c"
},
"value" : {
"total" : 150.0,
"male" : 0.526,
"female" : 0.473
}
},
...
]

特别说明

  1. 当key对应的value只有一组时不会触发reduce。所以,map函数emit的value值的格式最好和reduce函数return值一致,避免最终的结果不一致。
  2. 同一个key可能会触发多次reduce,即reduce接收的values中有些item元素可能是已经reduced的,累积时要考虑这种情况(即values包含直接emit的value,也包含reduce返回的value)。
  3. reduce返回值不支持数组,若需多值请用对象格式。
  4. 按key排序可以减少reduce的次数,提高处理效率。
  5. out可以指定输出到一个collection集合,但这样会导致写操作;指定为{inline:true}时mongodb会用runReadCommand代替runCommand——if inline output is specified, we need to apply readPreference on the command as it could be run on a secondary.

JS的“缺陷”

JS从诞生到现在已经二十几年了,在其演进中难免引入了一些bug,且积习难改。理解这些缺陷有助于避开那些新手陷阱。

typeof相关

Null和Undefined

他们都是基本类型,也都只对应一个常量值(分别是null和undefined)。

1
2
3
4
5
6
//已知:
typeof undefined === 'undefined'
//你可能会以为:
typeof null === 'null'//但,不是这样
//真相是:
typeof null === 'object'//null是指空对象,空对象也是对象!

Object、Array、Function、RegExp、Date、Error、…

这些本质上都是对象类型。

1
2
3
4
5
6
7
//已知:
typeof {} === 'object'
typeof function(){} === 'function'
//你可能会以为:
typeof [] === 'array'//但,不是这样的
//真相是:
typeof [] === 'object'//数组也是对象,基本类型外的除了函数都是对象。

typeof一览表

类型 Type typeof
布尔 Boolean true ‘boolean’
数值 Number 1 ‘number’
字符串 String ‘’ ‘string’
符号 Symbol Symbol() ‘symbol’
未定义 Undefined undefined ‘undefined’
空* Null null ‘object’
对象 Object {} ‘object’
下列都属于 对象的派生类型
数组 Array [] ‘object’
函数* Function function(){} ‘function’
Function class{} ‘function’

相等比较

宽松比较==

相等操作符比较两个值是否可能相等,即如果类型不同则进行可能的类型转换。

1
2
3
4
5
6
7
8
9
0==''//true
0=='0'//true
0==false//true
[1,2]=='1,2'//true
null==undefined//true
!null && !undefined//true
//注意:null和undefined既不等于true也不等于false
null==true || null==false//false
undefined==true || undefined==false//false

严格比较===

全等操作符比较两个值是否严格相等,即要求类型一样,也要求值相同。

1
2
3
4
5
6
null===null//true
undefined===undefined//true
null===undefined//false
0==='0'//false
0===-0//true
NaN===NaN//false?--这个比较有性格

同值相等Object.is()比较

Object.js()比较的结果同全等操作符,除了下面这两个相反:

1
2
Objecct.is(NaN,NaN)//true
Objecct.is(0,-0)//false

A链接打开的新窗口无法显示内容?

通常,我们想要一个A链接跳转到新窗口的话,会这样写:

1
<a href="http://www.zybuluo.com" target="_blank">link to a new window</a>

如果href的url需要动态的话,需要用到javascript,会改写成这样:

1
<a href="javascript:openUrl();" target="_blank">link to a new window</a>

当然还需要额外的script定义openUrl()函数:

1
2
3
4
function openUrl(){
var url = 'http://www.zybuluo.com';
window.open(url, '_blank');
}

似乎一切OK。——我是在chrome下测试的,确实没问题。
但发现,在IE和Firefox下试不行:新窗口无法显示内容。

原因是忽视了a标签里的target="_blank",去掉它就行了。

也就是说,借用A链接执行js的最好不要加target属性(虽然Chrome支持但其它浏览器不一定支持)。所以,还是在脚本里决定是否打开新窗口。

感想

  • 前端测试需顾及所有主流浏览器;
  • Chrome(google)的兼容性做得太好了:)

JS对象的克隆Clone

浅克隆[Plain Clone]:

1
2
3
4
5
6
var obj1 = {foo: "foo", bar: "bar"};
var obj2 = {foo: "foo2", bar: "bar2"};
var copy1 = {...obj1}; // Object {foo: "foo", bar: "bar"}
var copy2 = Object.assign({}, obj); //Object {foo: "foo2", bar: "bar2"}
var copySpread = {...obj1, ...obj2}; //Object {foo: "foo2", bar: "bar2"}
var copyAssign = Object.assign({}, obj2, obj1); //Object {foo: "foo", bar: "bar"}

JSON克隆[Json Clone]

1
2
var obj = { a: 0, b: { c: 0 } };
var copy = JSON.parse(JSON.stringify(obj));

深度克隆[Deep Clone]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function deepClone(obj) {
var copy;
// Handle the 3 simple types, and null or undefined
if (null == obj || "object" != typeof obj) return obj;
// Handle Date
if (obj instanceof Date) {
copy = new Date();
copy.setTime(obj.getTime());
return copy;
}
// Handle Array
if (obj instanceof Array) {
copy = [];
for (var i = 0, len = obj.length; i < len; i++) {
copy[i] = deepClone(obj[i]);
}
return copy;
}
// Handle Function
if (obj instanceof Function) {
copy = function() {
return obj.apply(this, arguments);
}
return copy;
}
// Handle Object
if (obj instanceof Object) {
copy = {};
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) copy[attr] = deepClone(obj[attr]);
}
return copy;
}
throw new Error("Unable to copy obj as type isn't supported " + obj.constructor.name);
}

使用键盘事件(keypress)对输入框(input)做输入限制

需求

为避免用户输入非法数据,希望能对某些输入框的输入做限制。这里用价格输入框(只允许输入最多2位小数的非负数值)来做例子:

1
<input type="text" id="price" name="price" placeholder="请输入价格(最多2位小数)" required>

键盘事件处理代码(javascript)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//用了jQuery
$(function(){
//事件绑定
$('#price').keypress(onPriceKeypress).keyup(onPriceKeyup);
//按键处理
function onPriceKeypress(e){
var allowKeys = '0123456789+.';//只允许输入这些字符
var keyCode = e.which;//键盘输入的字符码

if (code<32 || (code>126 && code<160)){
//在有些浏览器(如Firefox)下控制字符也会触发该事件
return true;//允许输入控制字符,如退格、删除、tab等键
}
if (allowKeys.indexOf(String.fromCharCode(code))<0){//若是不允许的字符
return false;//禁止输入
}
return true;
}
//起键处理
function onPriceKeyup(e){
var $ipt = $(this), val = $ipt.val();//取当前值
if (!/^[+\d]?\d*\.?\d{0,2}$/.test(val)){//若当前值不是合法价格
$ipt.val(/[+\d]?\d*\.?\d{0,2}/.exec(val));//去除不合法的内容
}
}
}

注:

  1. 只在input使用pattern属性(如:pattern="[+\d]?\d*\.?\d{0,2}")只能限制最后的结果,不能限制输入不当的字符。
  2. 把input的type设为number可限制只能输入数字,但无法输入小数。
  3. 在有些浏览器(如Firefox)下控制字符也会触发keypress事件,待textinput事件普及后改用该事件就不用做特殊处理了。

使用electron开发桌面应用(笔记)

开发环境

  • nodejs + npm/cnpm
    1
    安装node
  • electron
  • 开发工具
    • atom

      第三方(vendors)

  • jQuery
  • Bootstrap
  • Nedb

Q&A

  • 如何防止用户重复打开应用?
  • 执行Nedb操作有时无响应(callback不被调用)?
    • 上一次操作参数有误导致后面的操作都被挂起(参看BUG)
  • 路径问题:相对路径 v.s. 绝对路径?数据文件路径?
    • 界面(ui组件)载入使用相路径(相对于对于起始页index.html)
    • 业务(mdl&&dao)对象require使用绝对路径(appPath+对象.js)
    • 数据文件存放在userData路径下:app.getPath(‘userData’)