JavaScript实现文本编辑器 【table相关操作】

  我自己写的基本操作可以实现
  就是table结构改变了之后 在操作会出现一些问题
  目前没想通要怎么处理 
  查了下看到一个大佬用jQuery写的合并拆分
  基本把我的一些bug解决了 但是我没有看懂他的逻辑QAQ

页面结构

table的底部和侧边有一个 + 号标记可以添加行/列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<div>
<button onclick="delTable()">delete</button>
<button onclick="addCell()">addCell</button>
<button onclick="addRow()">addRow</button>
<button onclick="mergeTable()">合并单元格</button>
<button onclick="mergeDown()">向下合并单元格</button>
<button onclick="mergeRight()">向右合并单元格</button>
<button onclick="splitTable()">拆分单元格</button>
</div>
<br>
<div class="container">
<div class="table" contentEditable='true' >
<table border="1" id="table">
...
</table>
</div>
<div id="addCell" onclick="addCell()">+</div>
<div id='addRow' onclick="addRow()">+</div>
</div>
</body>

删除功能

1
2
3
  delTable: function () {
this.table.remove() //dom自带方法
},

添加列功能

trObject.insertCell( index )

insertCell() 方法用于在 HTML 表的一行的指定位置插入一个空的 <td> 元素。
新单元格将被插入当前位于 index 指定位置的表元之前。如果 index 等于行中的单元格数,则新单元格被附加在行的末尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
  /**
* insertCell( index ) table的原生方法,参数传的是添加的位置
* 需要注意的是添加之后要重新给列表宽度赋值
*/
addCell: function () {
for (let i = 0; i < table.rows.length; i++) {
var newCell = table.rows[i].insertCell(this.table.colspan)
newCell.innerHTML = 'new add'
for (let j = 0; j < table.rows[i].cells.length; j++) {
this.table.rows[i].cells[j].style.width = 900 / this.table.rows[0].cells.length + 1 + 'px'
}
}
},

添加行功能

tableObject.insertRow( index )

insertRow() 方法用于在表格中的指定位置插入一个新行。
index : 指定插入新行的位置 (以 0 开始)。

1
2
3
4
5
6
7
8
9
  /**
* 被注释的那一段的想法是 在哪点击了就以点击的那一行为模板 在那行下面添加该行内容
* 但是右侧的添加行功能 不能实现这一操作 就直接获取的第一行的内容来添加
* 应该是有bug的
*/
addRow: function () {
var newRow = this.table.insertRow(this.table.rowspan)
newRow.innerHTML = this.table.rows[0].innerHTML //document.getSelection().getRangeAt(0).startContainer.parentNode.innerHTML
},

合并功能

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
  /**
* 这个是我写的 直接获取选中的td 然后替换删除
* 没有考虑到已经合并的情况 是有问题的
* 我的想法是获取带有选中类的节点,创建一个新的节点 去替换
* 其余的选中的节点自己删除
*/
mergeTable: function () {
var nodes = this.table.getElementsByClassName('ui-selected')
var newNode = document.createElement('td')
var parent = nodes[0].parentNode
newNode.rowSpan = this.table.rowspan
newNode.colSpan = this.table.colspan
parent.replaceChild(newNode, nodes[0])
while (nodes[0]) {
nodes[0].remove()
}
}
/**
* 这个是CSDN某大佬借助jquery写的 我没看懂QAQ
* 原文地址 2楼: https://bbs.csdn.net/topics/391807641
*/
mergeTable: function () {
var $t = $("#" + this.tableId);
if ($("table", $t).length > 0) {
alert("不支持嵌套表格!");
return;
}

var sigDel = "sign4delete"; // 删除标记,用作类名
var sigSel = "ui-selected"; // 选中标记,用作类名

// 补充单元格以便后继正确计算坐标
$("th,td", $t).each(function () {
// 坐标要实时计算,因会实时补充
var ridx = $("tr", $t).index($(this).parent("tr"));
var cidx = $(this).parent().children("th,td").index(this);

var rowspan = Number($(this).attr("rowspan")) || 1;
var colspan = Number($(this).attr("colspan")) || 1;
var isSel = $(this).hasClass(sigSel);
// 非选单元格拆出的单元格要加删除标记
if (rowspan <= 1 && colspan <= 1)
return;
// 跨格开插
$("tr", $t).each(function () {
var idx = $("tr", $t).index(this);
var arr, $td = $("<td>").addClass(isSel ? sigSel : sigDel);

if (idx == ridx) {
// 本行在 [cidx] 后插入 colspan-1 个

arr = $(); // 准备待插单元格
for (var i = 0; i < colspan - 1; i++)
arr = arr.add($td.clone());
// 插入
$("th,td", this).eq(cidx).after(arr);

} else if (ridx < idx && idx < ridx + rowspan) {
// 以下行在 [cidx] 前插入 colspan 个

arr = $(); // 准备待插单元格
for (var i = 0; i < colspan; i++)
arr = arr.add($td.clone());
// 插入
if (cidx > 0 && $("th,td", this).eq(cidx - 1).length > 0)
$("th,td", this).eq(cidx - 1).after(arr);
else if ($("th,td", this).eq(cidx).length > 0)
$("th,td", this).eq(cidx).before(arr);
else
$(this).prepend(arr);
}
});
});

var rmin = 10000,
cmin = 10000;
var rmax = 0,
cmax = 0;
var rnum, cnum;
// 计算起始和跨距
$("th,td", $t).filter("." + sigSel).each(function () {
var ridx = $("tr", $t).index($(this).parent("tr"));
rmin = ridx < rmin ? ridx : rmin;
rmax = ridx > rmax ? ridx : rmax;
var cidx = $(this).parent().children("th,td").index(this);
cmin = cidx < cmin ? cidx : cmin;
cmax = cidx > cmax ? cidx : cmax;
});
rnum = rmax - rmin + 1;
cnum = cmax - cmin + 1;

// 合并单元格
$("th,td", $t).each(function () {
var ridx = $("tr", $t).index($(this).parent("tr"));
var cidx = $(this).parent().children("th,td").index(this);
// 标记单元格待删
if (rmin <= ridx && ridx <= rmax &&
cmin <= cidx && cidx <= cmax)
$(this).addClass(sigDel);
// 处理被选左上角单元格
if (ridx == rmin && cidx == cmin)
$(this).removeClass(sigDel).attr({
rowspan: rnum,
colspan: cnum
});
// 清理残余
if ($(this).attr("rowspan") == 1) $(this).removeAttr("rowspan");
if ($(this).attr("colspan") == 1) $(this).removeAttr("colspan");
}).remove("." + sigDel);
},

向下合并功能

1
2
3
4
5
6
7
8
9
  mergeDown: function () {
var row = document.getSelection().anchorNode.parentNode.rowIndex
var cell = document.getSelection().anchorNode.cellIndex
var node = document.getSelection().anchorNode
if (row != this.table.rows.length - 1 && node.rowSpan == 1) {
node.rowSpan = 2
this.table.rows[row + 1].cells[cell].remove()
}
},

向右合并功能

1
2
3
4
5
6
7
8
  mergeRight: function () {
var cell = document.getSelection().anchorNode.cellIndex
var node = document.getSelection().anchorNode
if (cell != 0 && node.colSpan == 1) {
node.previousElementSibling.remove()
node.colSpan = 2
}
},

拆分功能

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
  /**
* 我实现的 因为在ueditor里面是只可以拆分两格的
* 我就按照它的写的 只能拆分两格的
* 但是多格拆分应该也差不多
*/
splitTable: function () {
var node = document.getSelection().anchorNode
var td = document.createElement('td')
if (node.rowSpan == 2 && node.colSpan == 1) {
node.rowSpan = 1
node.parentNode.nextElementSibling.appendChild(td)
} else if (node.colSpan == 2 && node.rowSpan == 1) {
node.colSpan = 1
node.parentNode.appendChild(td)
}
},

/**
* 也是那个大佬的代码 可以拆分多个单元格
*/
splitTable: function(){
var $t = $("#"+this.tableId);

if ($("table", $t).length > 0) {
alert("不支持嵌套表格!");
return;
}

var sigDel = "sign4delete"; // 删除标记,类名,自定义
var sigSel = "ui-selected"; // 选中标记,类名,jQuery UI 定义

// 补充单元格以便后继正确计算坐标
$("th,td", $t).each(function(){
// 坐标要实时计算,因会实时补充
var ridx = $("tr", $t).index($(this).parent("tr"));
var cidx = $(this).parent().children("th,td").index(this);
var rowspan = Number($(this).attr("rowspan")) || 1;
var colspan = Number($(this).attr("colspan")) || 1;
var isSel = $(this).hasClass(sigSel);
// 非选单元格拆出的单元格要加删除标记

if (rowspan <= 1 && colspan <= 1)
return;

if (isSel)
$(this).removeAttr("colspan").removeAttr("rowspan");

// 跨格开插
$("tr", $t).each(function(){
var idx = $("tr", $t).index(this);
var arr, $td = $("<td>");

if (!isSel)
$td.addClass(sigDel);

if (idx == ridx) {
// 本行在 [cidx] 后插入 colspan-1 个

arr = $(); // 准备待插单元格
for (var i=0; i < colspan-1; i++)
arr = arr.add($td.clone());

$("th,td", this).eq(cidx).after(arr);

} else if (ridx < idx && idx < ridx + rowspan) {
// 以下行在 [cidx] 前插入 colspan 个

arr = $(); // 准备待插单元格
for (var i=0; i < colspan; i++)
arr = arr.add($td.clone());

if (cidx > 0 && $("th,td", this).eq(cidx - 1).length > 0)
$("th,td", this).eq(cidx - 1).after(arr);
else if ($("th,td", this).eq(cidx).length > 0)
$("th,td", this).eq(cidx).before(arr);
else
$(this).prepend(arr);
}
});
});

// 重新获取以取到删者并删之
$("th,td", $t).remove("." + sigDel);
},

鼠标长按选中事件

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
   /**
* 点击选取范围那还是有问题的不知道怎么解决
* 主要是针对已经合并的单元格 怎么遍历的问题
*/
this.table.onmousedown = function (e) {
// 鼠标按下的时候获取所在单元格的坐标,和鼠标按下的时间
this.startRow = e.target.parentNode.rowIndex
this.startCell = e.target.cellIndex
this.startDate = new Date()
}

this.table.onmouseup = function () {
// 通过和鼠标抬起事件所获取的时间进行对比 判断是否是长按鼠标事件
var isClick = new Date() - this.startDate < 500 ? true : false;

// 获取鼠标抬起时 鼠标所在单元格的坐标点
var endCell = event.target.cellIndex
var endRow = event.target.parentNode.rowIndex

if (!isClick) {
for (let i = Math.min(endRow, this.startRow); i <= Math.max(endRow, this.startRow); i++) {
for (let j = Math.min(endCell, this.startCell); j <= Math.max(endCell, this.startCell); j++) {
// 以获取对角坐标 填充选中的区域 只能针对结构未改变的table
//this.rows[i].cells[j].className = 'ui-selected'

if (this.rows[i].cells[j]) {
if (this.rows[i].cells[j].rowSpan > 1) {
if (j == Math.max(endCell, this.startCell)) {
for (let a = 1; a < this.rows[i].cells[j].rowSpan; a++) {
if(this.rows[i + a].cells[j]){
this.rows[i + a].cells[j].className = 'del'
}
}
}else if((j + table.rows[i].cells[j].colSpan - 1 )== Math.max(endCell, this.startCell)){
for (let a = 1; a < this.rows[i].cells[j].rowSpan; a++) {
for (let b = 0; b < this.rows[i].cells[j].colSpan; b++) {
if (this.rows[i + a].cells[j+b]) {
this.rows[i + a].cells[j+b].className = 'del'
}
}
}
} else {
var maxJ = Math.max(endCell, this.startCell)
for (let a = 0; a < this.rows[i].cells[j].rowSpan; a++) {
if (table.rows[i + a].cells[maxJ]) {
this.rows[i + a].cells[maxJ].className = 'del'
}
}
}
}

if (this.rows[i].cells[j].colSpan > 1) {
if (j !== endCell && j !== this.startCell) {
for (let b = 1; b < this.rows[i].cells[j].colSpan; b++) {
if(this.rows[i].cells[j + b]){
this.rows[i].cells[j + b].className = 'del'
}
}
}else if((j + table.rows[i].cells[j].colSpan - 1 )== Math.max(endCell, this.startCell)){
for (let a = 0; a < this.rows[i].cells[j].rowSpan; a++) {
for (let b = 1; b < this.rows[i].cells[j].colSpan; b++) {
if (this.rows[i + a].cells[j+b]) {
this.rows[i + a].cells[j+b].className = 'del'
}
}
}
}
else {
this.rows[i].cells[j].className = ''
}
}
this.rows[i].cells[j].className += 'ui-selected'
}
}
}
// 在table中增加两个属性 获取到跨的行和列 供合并单元格
this.rowspan = Math.abs(endRow - this.startRow) + 1
this.colspan = Math.abs(endCell - this.startCell) + 1

} else {
// 清空样式
this.rowspan = endRow
this.colspan = endCell
for (let i = 0; i < table.rows.length; i++) {
for (let j = 0; j < table.rows[i].cells.length; j++) {
this.rows[i].cells[j].className = ''
}
}
}
}

鼠标进入离开显示添加行列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
   /**
* offset值好像是按照鼠标离每个td的位置不是离tr的位置
* 所以需要利用client来处理下距离的判断
*/
this.table.onmouseleave = function () {
setTimeout(() => {
addCellBtn.style.display = 'none'
addRowBtn.style.display = 'none'
}, 3000);

}
this.table.onmousemove = function () {
if (event.clientX - this.offsetLeft >= 8 && event.clientX - this.offsetLeft <= 20) {
this.style.cursor = "url('img/right.png') , auto"
this.colspan = this.rows.length
} else if (event.clientY - this.offsetTop >= 52 && event.clientY - this.offsetTop <= 60) {
this.style.cursor = "url('img/down.png') , auto"
} else {
this.style.cursor = 'auto'
}
}

鼠标点击选中一行一列事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

/**
* 判断鼠标的指针状态然后执行点击事件 选中一行/列
*/
this.table.onclick = function (e) {
if (this.style.cursor !== 'auto') {
if (this.style.cursor.includes('down')) {
for (let i = 0; i < this.rows.length; i++) {
this.rows[i].cells[e.target.cellIndex].className = 'ui-selected'
}
this.rowspan = this.rows.length
} else {
for (let i = 0; i < this.rows[e.target.parentNode.rowIndex].cells.length; i++) {
this.rows[e.target.parentNode.rowIndex].cells[i].className = 'ui-selected'
}
this.colspan = this.rows[e.target.parentNode.rowIndex].cells.length
this.rowspan = 1
}
}
}
return this;
}

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
(function (win) {
var win_ = win;
var kclass = function (tableId) {
this._init.call(this, arguments);
}
kclass.fn = {

tableId: "table",
table: null,
delTable: function () {
this.table.remove()
},
addCell: function () {
for (let i = 0; i < table.rows.length; i++) {
var newCell = table.rows[i].insertCell(this.table.colspan)
newCell.innerHTML = 'new add'
for (let j = 0; j < table.rows[i].cells.length; j++) {
this.table.rows[i].cells[j].style.width = 900 / this.table.rows[0].cells.length + 1 + 'px'
}
}
},
addRow: function () {
var newRow = this.table.insertRow(this.table.rowspan)
newRow.innerHTML = this.table.rows[0].innerHTML //document.getSelection().getRangeAt(0).startContainer.parentNode.innerHTML
},
mergeTable: function () {
var $t = $("#" + this.tableId);
if ($("table", $t).length > 0) {
alert("不支持嵌套表格!");
return;
}

var sigDel = "sign4delete"; // 删除标记,用作类名
var sigSel = "ui-selected"; // 选中标记,用作类名

// 补充单元格以便后继正确计算坐标
$("th,td", $t).each(function () {
// 坐标要实时计算,因会实时补充
var ridx = $("tr", $t).index($(this).parent("tr"));
var cidx = $(this).parent().children("th,td").index(this);

var rowspan = Number($(this).attr("rowspan")) || 1;
var colspan = Number($(this).attr("colspan")) || 1;
var isSel = $(this).hasClass(sigSel);
// 非选单元格拆出的单元格要加删除标记
if (rowspan <= 1 && colspan <= 1)
return;
// 跨格开插
$("tr", $t).each(function () {
var idx = $("tr", $t).index(this);
var arr, $td = $("<td>").addClass(isSel ? sigSel : sigDel);

if (idx == ridx) {
// 本行在 [cidx] 后插入 colspan-1 个

arr = $(); // 准备待插单元格
for (var i = 0; i < colspan - 1; i++)
arr = arr.add($td.clone());
// 插入
$("th,td", this).eq(cidx).after(arr);

} else if (ridx < idx && idx < ridx + rowspan) {
// 以下行在 [cidx] 前插入 colspan 个

arr = $(); // 准备待插单元格
for (var i = 0; i < colspan; i++)
arr = arr.add($td.clone());
// 插入
if (cidx > 0 && $("th,td", this).eq(cidx - 1).length > 0)
$("th,td", this).eq(cidx - 1).after(arr);
else if ($("th,td", this).eq(cidx).length > 0)
$("th,td", this).eq(cidx).before(arr);
else
$(this).prepend(arr);
}
});
});

var rmin = 10000,
cmin = 10000;
var rmax = 0,
cmax = 0;
var rnum, cnum;
// 计算起始和跨距
$("th,td", $t).filter("." + sigSel).each(function () {
var ridx = $("tr", $t).index($(this).parent("tr"));
rmin = ridx < rmin ? ridx : rmin;
rmax = ridx > rmax ? ridx : rmax;
var cidx = $(this).parent().children("th,td").index(this);
cmin = cidx < cmin ? cidx : cmin;
cmax = cidx > cmax ? cidx : cmax;
});
rnum = rmax - rmin + 1;
cnum = cmax - cmin + 1;

// 合并单元格
$("th,td", $t).each(function () {
var ridx = $("tr", $t).index($(this).parent("tr"));
var cidx = $(this).parent().children("th,td").index(this);
// 标记单元格待删
if (rmin <= ridx && ridx <= rmax &&
cmin <= cidx && cidx <= cmax)
$(this).addClass(sigDel);
// 处理被选左上角单元格
if (ridx == rmin && cidx == cmin)
$(this).removeClass(sigDel).attr({
rowspan: rnum,
colspan: cnum
});
// 清理残余
if ($(this).attr("rowspan") == 1) $(this).removeAttr("rowspan");
if ($(this).attr("colspan") == 1) $(this).removeAttr("colspan");
}).remove("." + sigDel);
},
/*
mergeTable: function () {
var nodes = this.table.getElementsByClassName('ui-selected')
var newNode = document.createElement('td')
var parent = nodes[0].parentNode
newNode.rowSpan = this.table.rowspan
newNode.colSpan = this.table.colspan
parent.replaceChild(newNode, nodes[0])
while (nodes[0]) {
nodes[0].remove()
}
},*/
mergeDown: function () {
var row = document.getSelection().anchorNode.parentNode.rowIndex
var cell = document.getSelection().anchorNode.cellIndex
var node = document.getSelection().anchorNode
if (row != this.table.rows.length - 1 && node.rowSpan == 1) {
node.rowSpan = 2
this.table.rows[row + 1].cells[cell].remove()
}
},
mergeRight: function () {
var cell = document.getSelection().anchorNode.cellIndex
var node = document.getSelection().anchorNode
if (cell != 0 && node.colSpan == 1) {
node.previousElementSibling.remove()
node.colSpan = 2
}
},
splitTable: function () {
var $t = $("#" + this.tableId);

if ($("table", $t).length > 0) {
alert("不支持嵌套表格!");
return;
}

var sigDel = "sign4delete"; // 删除标记,类名,自定义
var sigSel = "ui-selected"; // 选中标记,类名,jQuery UI 定义

// 补充单元格以便后继正确计算坐标
$("th,td", $t).each(function () {
// 坐标要实时计算,因会实时补充
var ridx = $("tr", $t).index($(this).parent("tr"));
var cidx = $(this).parent().children("th,td").index(this);
var rowspan = Number($(this).attr("rowspan")) || 1;
var colspan = Number($(this).attr("colspan")) || 1;
var isSel = $(this).hasClass(sigSel);
// 非选单元格拆出的单元格要加删除标记

if (rowspan <= 1 && colspan <= 1)
return;

if (isSel)
$(this).removeAttr("colspan").removeAttr("rowspan");

// 跨格开插
$("tr", $t).each(function () {
var idx = $("tr", $t).index(this);
var arr, $td = $("<td>");

if (!isSel)
$td.addClass(sigDel);

if (idx == ridx) {
// 本行在 [cidx] 后插入 colspan-1 个

arr = $(); // 准备待插单元格
for (var i = 0; i < colspan - 1; i++)
arr = arr.add($td.clone());

$("th,td", this).eq(cidx).after(arr);

} else if (ridx < idx && idx < ridx + rowspan) {
// 以下行在 [cidx] 前插入 colspan 个

arr = $(); // 准备待插单元格
for (var i = 0; i < colspan; i++)
arr = arr.add($td.clone());

if (cidx > 0 && $("th,td", this).eq(cidx - 1).length > 0)
$("th,td", this).eq(cidx - 1).after(arr);
else if ($("th,td", this).eq(cidx).length > 0)
$("th,td", this).eq(cidx).before(arr);
else
$(this).prepend(arr);
}
});
});

// 重新获取以取到删者并删之
$("th,td", $t).remove("." + sigDel);
},
/*
splitTable: function () {
var node = document.getSelection().anchorNode
var td = document.createElement('td')
if (node.rowSpan == 2 && node.colSpan == 1) {
node.rowSpan = 1
node.parentNode.nextElementSibling.appendChild(td)
} else if (node.colSpan == 2 && node.rowSpan == 1) {
node.colSpan = 1
node.parentNode.appendChild(td)
}
},*/
addEvent: function () {
document.body.onmouseup = function () {}
},
_init: function () {
var tableId = arguments[0][0];
if (!tableId)
throw new Error("table id cannot be null");
var table = document.getElementById(tableId);
if (!table || table.nodeName.toUpperCase() != 'TABLE') {
throw new Error("canot find the table,please enter the right id");
}
// 侧边和底部的新增按钮
var addCellBtn = document.getElementById('addCell')
var addRowBtn = document.getElementById('addRow')
this.tableId = tableId;
this.table = table;

//使表格可选
this.table.onmousedown = function (e) {
// 鼠标按下的时候获取所在单元格的坐标,和鼠标按下的时间
this.startRow = e.target.parentNode.rowIndex
this.startCell = e.target.cellIndex
this.startDate = new Date()
}
this.table.onmouseup = function () {
// 通过和鼠标抬起事件所获取的时间进行对比 判断是否是长按鼠标事件
var isClick = new Date() - this.startDate < 500 ? true : false;

// 获取鼠标抬起时 鼠标所在单元格的坐标点
var endCell = event.target.cellIndex
var endRow = event.target.parentNode.rowIndex

if (!isClick) {
for (let i = Math.min(endRow, this.startRow); i <= Math.max(endRow, this.startRow); i++) {
for (let j = Math.min(endCell, this.startCell); j <= Math.max(endCell, this.startCell); j++) {
// 以获取对角坐标 填充选中的区域
//this.rows[i].cells[j].className = 'ui-selected'

if (this.rows[i].cells[j]) {
if (this.rows[i].cells[j].rowSpan > 1) {
if (j == Math.max(endCell, this.startCell)) {
for (let a = 1; a < this.rows[i].cells[j].rowSpan; a++) {
if(this.rows[i + a].cells[j]){
this.rows[i + a].cells[j].className = 'del'
}
}
}else if((j + table.rows[i].cells[j].colSpan - 1 )== Math.max(endCell, this.startCell)){
for (let a = 1; a < this.rows[i].cells[j].rowSpan; a++) {
for (let b = 0; b < this.rows[i].cells[j].colSpan; b++) {
if (this.rows[i + a].cells[j+b]) {
this.rows[i + a].cells[j+b].className = 'del'
}
}
}
} else {
var maxJ = Math.max(endCell, this.startCell)
for (let a = 0; a < this.rows[i].cells[j].rowSpan; a++) {
if (table.rows[i + a].cells[maxJ]) {
this.rows[i + a].cells[maxJ].className = 'del'
}
}

}
}

if (this.rows[i].cells[j].colSpan > 1) {
if (j !== endCell && j !== this.startCell) {
for (let b = 1; b < this.rows[i].cells[j].colSpan; b++) {
if(this.rows[i].cells[j + b]){
this.rows[i].cells[j + b].className = 'del'
}
}
}else if((j + table.rows[i].cells[j].colSpan - 1 )== Math.max(endCell, this.startCell)){
for (let a = 0; a < this.rows[i].cells[j].rowSpan; a++) {
for (let b = 1; b < this.rows[i].cells[j].colSpan; b++) {
if (this.rows[i + a].cells[j+b]) {
this.rows[i + a].cells[j+b].className = 'del'
}
}
}
}
else {
this.rows[i].cells[j].className = ''
}
}
this.rows[i].cells[j].className += 'ui-selected'
}
}
}
// 在table中增加两个属性 获取到跨的行和列 供合并单元格
this.rowspan = Math.abs(endRow - this.startRow) + 1
this.colspan = Math.abs(endCell - this.startCell) + 1

} else {
// 清空样式
this.rowspan = endRow
this.colspan = endCell
for (let i = 0; i < table.rows.length; i++) {
for (let j = 0; j < table.rows[i].cells.length; j++) {
this.rows[i].cells[j].className = ''
}
}
}
}

this.table.onmouseenter = function () {
addCellBtn.style.display = 'inline-block'
addRowBtn.style.display = 'block'
}
this.table.onmouseleave = function () {
setTimeout(() => {
addCellBtn.style.display = 'none'
addRowBtn.style.display = 'none'
}, 3000);

}
this.table.onmousemove = function () {
if (event.clientX - this.offsetLeft >= 8 && event.clientX - this.offsetLeft <= 20) {
this.style.cursor = "url('img/right.png') , auto"
this.colspan = this.rows.length
} else if (event.clientY - this.offsetTop >= 52 && event.clientY - this.offsetTop <= 60) {
this.style.cursor = "url('img/down.png') , auto"
} else {
this.style.cursor = 'auto'
}
}
this.table.onclick = function (e) {
if (this.style.cursor !== 'auto') {
if (this.style.cursor.includes('down')) {
for (let i = 0; i < this.rows.length; i++) {
this.rows[i].cells[e.target.cellIndex].className = 'ui-selected'
}
this.rowspan = this.rows.length
} else {
for (let i = 0; i < this.rows[e.target.parentNode.rowIndex].cells.length; i++) {
this.rows[e.target.parentNode.rowIndex].cells[i].className = 'ui-selected'
}
this.colspan = this.rows[e.target.parentNode.rowIndex].cells.length
this.rowspan = 1
}
}
}
return this;
}
}
kclass.prototype = kclass.fn;
win_.ytable = (win_.ytable || kclass);
})(window);

JavaScript实现简单文本编辑器

百度富文本编辑器的文件结构

核心属性 / 方法 / 接口

contentEditable

html属性 用来设置 或 返回元素的内容是否可以被编辑

语法:

1
2
3
4
5
6
<p contentEditable = 'true | false' >...</p>

<script>
HTMLElementObject.contentEditable = true | false
HTMLElementObject.isContentEditable() // 获取元素是否是可编辑状态
</script>

document.execCommand()

操作可编辑元素的语法糖 大多数文本编辑命令都可执行

语法:

1
2
3
4
5
6
7
bool = document.execCommand( aCommandName,ashowDefaultUI,aValueArgument )

/**
* aCommandName : 命令的名称,可用命令参考mdn
* aShowDefaultUI : 是否展示用户界面 | false
* aValueArgument : 一些命令需要的额外参数如字体颜色/大小 | null
*/

借用语法糖实现的编辑器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<button data-command='bold' onclick="changeStyle(this.dataset)">B</button>
<button data-command="italic" onclick="changeStyle(this.dataset)">I</button>
<button data-command="fontSize" data-value="7" onclick="changeStyle(this.dataset)">fontSize</button>
<button data-command="foreColor" data-value="red" onclick="changeStyle(this.dataset)">color</button>
<button data-command="justifyCenter" onclick="changeStyle(this.dataset)">居中对齐</button>
<button data-command="justifyLeft" onclick="changeStyle(this.dataset)">左对齐</button>
<button data-command="justifyRight" onclick="changeStyle(this.dataset)">右对齐</button>
<br />
<p contentEditable='true'>犹豫就会败北</p>
<p contentEditable='true'>果断就会白给</p>


<script>
function changeStyle(data) {
var attr = data.command
var value = data.value
value ? document.execCommand(attr, false, value) : document.execCommand(attr, false, null)
}
</script>
</body>

Selection

Selection对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var selObj = document.getSelection()  // window | document 获取Selection对象
var selStr = selObj.toString() // 获取选中区域的 ‘纯文本’

document.onselectionchange = function(){ // 监听鼠标锚点的变化
console.log( document.getSelection() )
/**
* Selection = {
* anchorNode: node 选取起点所在节点,
* anchorOffset: num 起点偏移量,
* focusNode: node 选取终点所在节点,
* focusOffset: num 终点偏移量,
* isCollapsed: bool 起始点是否在同一位置,
* rangeCount: 返回该选区所包含的连续范围的数量
*
* }
*/
}

Range

表示一个包含节点与文本节点的一部分的文档片段

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建一个文档范围
var range = document.createRange()
var range = new Range()

// 获取选中的文档范围
var range = document.getSelection().getRangeAt(0)

/**
* Range = {
* collapsed : 起始点位置是否相同,
* commonAncetorContainer : 选中区域所在的完整节点, eg: <b> hhh<i>5[5</i>6]6</b> -> <b> hhh<i>55</i>66</b>
* endContainer : 包含range的终点节点, -> 66
* endOffset : 返回一个表示 Range 终点在 endContainer 中的位置的数字。 -> 1 (选中的6在66中排位第几)
* }
*
* Range.cloneContents() 返回一个包含 Range 中所有节点的文档片段
* Range.deleteContents() 移除range包含的内容
* Range.extractContents() 把 Range 的内容从文档树移动到一个文档片段中
* Range.insertNode(Node) 在range起点处插入一个节点
* Range.surroundContents(newNode) 将range内容移动到一个新的节点中
*/

利用 range & selection 接口实现的简易编辑器

有bug

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
<body>
<button data-command="b" onclick="changeStyle(dataset)"> B </button>
<button data-command="i" onclick="changeStyle(dataset)"> I </button>
<button data-command="h1" onclick="changeStyle(dataset)"> h1 </button>
<button data-value="textAlign:center" onclick="changeStyle(dataset)"> 居中</button>
<button data-value="textAlign:right" onclick="changeStyle(dataset)"> 居右</button>
<button data-value="textAlign:left" onclick="changeStyle(dataset)"> 居左</button>
<button id='btn'>
测试按钮
</button>
<p contentEditable='true'>犹豫<b>就会败北<i>52</i>墨菲</b>定律</p>
<p contentEditable='true'>薛定谔的猫🐱</p>

<script>
// 1.surrounContents(Node) 在原有内容 的基础上包裹一层node节点
// 不能判断是否已经有该元素
function changeStyle(data) {
var tagName = data.command || null
var value = data.value || null
var selobj = document.getSelection()
var Node = document.createElement(tagName)
var range = selobj.getRangeAt(0)
if(value){
var attr = value.split(':')[0].trim(),
cssStyle = value.split(':')[1].trim()
range.commonAncestorContainer.parentElement.style[attr] = cssStyle
}else{
range.surroundContents(Node)
}
}

// 2.把选择的范围节点删除 在字符串外重新添加标签,只能一种标签存在
btn.onclick = function(){
var range = document.getSelection().getRangeAt(0)
var oB = document.createElement('b')
oB.innerHTML = range.toString()
range.deleteContents()
range.insertNode(oB)
}
</script>
</body>

CSS3 立体轮播

CSS3 立体轮播

最近比较迷css,可能是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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
<style>
.cubeCover{
width: 100px;
height: 300px;
margin-top: 150px;
margin-left: 200px;
}
.cubeCover .cube{
transform-style: preserve-3d;
width: 100%;
height: 100%;
position: relative;
transform: rotateY(20deg) rotateX(-20deg) ;
animation: move1 6s linear infinite .75s;
}
.cubeCover .cube div{
border: 1px solid #ccc;
width: 100%;
height: 100%;
position: absolute;
}
.cubeCover .cube div:nth-child(1){
transform:translateZ(-150px);
background: url('/images/bottom.jpg');
background-position: 0 0;
}
.cubeCover .cube div:nth-child(2){
transform: rotateX(90deg) translateZ(150px);
background: url('/images/side.jpg');
background-position: 0 0;
}
.cubeCover .cube div:nth-child(3){
transform:rotateX(90deg) translateZ(-150px) ;
background: url('/images/top.jpg');
}
.cubeCover .cube div:nth-child(4){
width: 300px;
transform: rotateY(90deg) translateZ(-150px);
background: lightblue;
}
.cubeCover .cube div:nth-child(5){
width: 300px;
transform: rotateY(90deg) translateZ(-50px);
background: lightblue;
}
.cubeCover .cube div:nth-child(6){
transform: translateZ(150px);
background: url('/images/avatar.jpg');
background-position: 0 0;
}
.cubeCover .cube2{
margin-top: -300px;
margin-left: 90px;
z-index: -1;
animation: move1 6s linear infinite .5s;
}
.cubeCover .cube2 div:nth-child(1),.cubeCover .cube2 div:nth-child(2),.cubeCover .cube2 div:nth-child(3),.cubeCover .cube2 div:nth-child(6){
background-position: -100px 0;
}
.cubeCover .cube3{
margin-top: -300px;
margin-left: 180px;
z-index: -2;
animation: move1 6s linear infinite .25s;
}
.cubeCover .cube3 div:nth-child(1),.cubeCover .cube3 div:nth-child(2),.cubeCover .cube3 div:nth-child(3),.cubeCover .cube3 div:nth-child(6){
background-position: -200px 0;
}
.cubeCover .cube4{
margin-top: -300px;
margin-left: 270px;
z-index: -3;
animation: move1 6s linear infinite ;
}
.cubeCover .cube4 div:nth-child(1),.cubeCover .cube4 div:nth-child(2),.cubeCover .cube4 div:nth-child(3),.cubeCover .cube4 div:nth-child(6){
background-position: -300px 0;
}
@keyframes move1 {
12.5%,25%{
transform:rotateY(20deg) rotateX(70deg);
}
42.5%,55%{
transform:rotateY(20deg) rotateX(160deg);
}
62.5%,75%{
transform:rotateY(20deg) rotateX(250deg);
}
92.5%,100%{
transform:rotateY(20deg) rotateX(340deg);
}
}
</style>
<body>
<div class="cubeCover">
<div class="cube">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="cube cube2">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="cube cube3">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="cube cube4">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>

【第九章】 命令模式

命令模式的用途

一个执行某些特定事情的指令
常见的应用场景 : 有时候需要向某些对象发送请求,但并不知道请求的接受者是谁,也不知道被请求的操作是什么。此时希望用一种耦合的方式来设计程序,使得请求发送者和请求接受者能够消除彼此之间的耦合关系

命令模式实例

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
51
52
53
54
// 请求发送者 和 请求接受者 解耦开
var oBtn1 = document.getElementById('btn1')
var oBtn2 = document.getElementById('btn2')
var oBtn3 = document.getElementById('btn3')

// 执行命令的函数
var setCommand = function (button, command) {
button.onclick = function () {
command.execute();
}
};
// 提供的功能菜单
var MenuBar = {
refresh: function () {
console.log('刷新菜单目录');
}
};
var SubMenu = {
add: function () {
console.log('增加子菜单');
},
del: function () {
console.log('删除子菜单');
}
};
// 封装命令类
var RefreshMenuBarCommand = function (receiver) {
this.receiver = receiver
}
RefreshMenuBarCommand.prototype.execute = function () {
this.receiver.refresh();
}

var AddSubMenuCommand = function (receiver) {
this.receiver = receiver;
}
AddSubMenuCommand.prototype.execute = function () {
this.receiver.add();
};

var DelSubMenuCommand = function (receiver) {
this.receiver = receiver;
};
DelSubMenuCommand.prototype.execute = function () {
this.receiver.del()
};

//最后就是把命令接收者传入到 command 对象中,并且把 command 对象安装到 button 上面
var refreshMenuBarCommand = new RefreshMenuBarCommand( MenuBar );
var addSubMenuCommand = new AddSubMenuCommand(SubMenu);
var delSubMenuCommand = new DelSubMenuCommand(SubMenu);
setCommand(btn1, refreshMenuBarCommand); // 刷新菜单目录
setCommand(btn2, addSubMenuCommand); // 增加子菜单
setCommand(btn3, delSubMenuCommand); // 删除子菜单

JavaScript中的命令模式

1
2
3
4
5
6
7
8
9
10
11
// ...
var RefreshMenuBarCommand = function (receiver) {
return {
execute: function(){
receiver.refresh()
}
}
}
// ....
var refreshMenuBarCommand = RefreshMenuBarCommand( MenuBar );
setCommand( btn1, refreshMenuBarCommand ); // 刷新菜单目录

宏命令

是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令

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
//下面我们看看如何逐步创建一个宏命令。首先,我们依然要创建好各种 Command:
var closeDoorCommand = {
execute: function(){
console.log( '关门' );
}
};
var openPcCommand = {
execute: function(){
console.log( '开电脑' );
}
};
/*接下来定义宏命令 MacroCommand,它的结构也很简单。macroCommand.add 方法表示把子命令添加进宏命令对象,当调用宏命令对象的 execute 方法时,会迭代这一组子命令对象,并且依次执行它们的 execute 方法:*/
var MacroCommand = function(){
return {
commandsList: [],
add: function( command ){
this.commandsList.push( command );
},
execute: function(){
for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
command.execute();
}
}
}
};
var macroCommand = MacroCommand();
macroCommand.add( closeDoorCommand );
macroCommand.add( openPcCommand );
macroCommand.add( openQQCommand );
macroCommand.execute();

智能命令与傻瓜命令

1
2
3
4
5
var closeDoorCommand = { 
execute: function(){
console.log( '关门' );
}
};

closeDoorCommand 中没有包含任何 receiver 的信息,它本身就包揽了执行请求的行为,这跟我们之前看到的命令对象都包含了一个 receiver 是矛盾的。 一般来说,命令模式都会在 command 对象中保存一个接收者来负责真正执行客户的请求,这种情况下命令对象是“傻瓜式”的,它只负责把客户的请求转交给接收者来执行,这种模式的好处是请求发起者和请求接收者之间尽可能地得到了解耦。

但是我们也可以定义一些更“聪明”的命令对象,“聪明”的命令对象可以直接实现请求,这样一来就不再需要接收者的存在,这种“聪明”的命令对象也叫作智能命令。没有接收者的智能命令,退化到和策略模式非常相近,从代码结构上已经无法分辨它们,能分辨的只有它们意图的不同。策略模式指向的问题域更小,所有策略对象的目标总是一致的,它们只是达到这个目标的不同手段,它们的内部实现是针对“算法”而言的。而智能命令模式指向的问题域更广,command对象解决的目标更具发散性。命令模式还可以完成撤销、排队等功能。

clip-path

发现了一个有意思的CSS属性

属性详情
clip-path生成器

clip-path : 创建一个只有元素的部分区域可以显示的剪切区域

1
2
3
4
5
/* 我没有接触过svg 所以只写了几个我觉得能常用的属性*/
clip-path: circle( 50% at 50% 50%); /* 剪裁成 【圆形】 第一个参数是 圆半径 at 圆心坐标*/
clip-path: ellipse( 50% 25% at 50% 50%); /*剪裁成 【椭圆】 第一个参数是 横向半径 第二个是 纵向半径 at 圆心坐标 当半径一致 相当于圆*/
clip-path: polygon( 50% 0,0 100%,100% 100%); /* 剪裁成任意形状 后跟每个点坐标 这是个三角形 注意点连接的顺序! 如下:*/

可以实现的效果

我感觉比伪类方便 可以代替圆角之类的 也可以结合动画之类的

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
<style>
.clipPath{
width:150px;
height: 150px;
background-image: url('/images/avatar.jpg');
background-size:100% 100%;
display:inline-block;
}
.clipPath.dv1{
clip-path: polygon(20% 0%, 0% 20%, 30% 50%, 0% 80%, 20% 100%, 50% 70%, 80% 100%, 100% 80%, 70% 50%, 100% 20%, 80% 0%, 50% 30%);
}
.clipPath.dv2{
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}
.clipPath.dv3{
clip-path: polygon(0% 0%, 100% 0%, 100% 75%, 75% 75%, 75% 100%, 50% 75%, 0% 75%);
}
.clipPath.dv4{
/* clip-path: ellipse(100% 100% at 0% 0%); 效果一致*/
clip-path: circle(100% at 0% 0%);
}
.clipPath.dv5{
clip-path: polygon(0% 0%, 0% 100%, 25% 100%, 25% 25%, 75% 25%, 75% 75%, 25% 75%, 25% 100%, 100% 100%, 100% 0%);
}
</style>
<div class="clipPath dv1"></div>
<div class="clipPath dv2"></div>
<div class="clipPath dv3"></div>
<div class="clipPath dv4"></div>
<div class="clipPath dv5"></div>

【第八章】 发布-订阅模式

优点:

  1. 时间上的解耦
  2. 对象之间的解耦

缺点: 消耗时间和内存

自定义事件

如何实现发布-订阅模式:

  1. 指定发布者
  2. 给发布者添加一个缓存列表,用于存放回调函数 以便通知 订阅者
  3. 发布消息的时候,发布者会遍历这个缓存列表,依次触发存放的订阅者回调函数
    (还可以在回调函数中填入一些参数,订阅者可以接收这些参数。eg:售楼处可以在给订阅者短信内加上房子的单价、面积等信息)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var salesOffices = {}   // 售楼处
salesOffices.clientList = [] // 缓存列表,存放订阅者的回调函数
salesOffices.listen = function(fn){ // 增加订阅者
this.clientList.push(fn)
}
salesOffices.trigger = function(){ // 发布消息
for(var i=0;i<this.clientList.length;i++){
var fn = this.clientList[i]
fn.apply(this,arguments)
}
}

salesOffices.listen(function(price,squareMeter){ // 小明订阅消息
console.log('m 价格= '+price)
console.log('m 平方= '+squareMeter)
})

salesOffices.listen(function(price,squareMeter){ // 小红订阅消息
console.log('h 价格= '+price)
console.log('h 平方= '+squareMeter)
})

salesOffices.trigger(20000,22) // m/h 价格= 20000 m/h 平方= 22

我们看到订阅者接收到了发布者发布的每个消息,虽然小明只想买 88 平方米的房子,但是发布者把 122 平方米的信息也推送给了小明,这对小明来说是不必要的困扰。所以我们有必要增加一个标示 key,让订阅者只订阅自己感兴趣的消息。改写后的代码如下:

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
var salesOffices = {}   // 售楼处
salesOffices.clientList = [] // 缓存列表,存放订阅者的回调函数
salesOffices.listen = function(key,fn){ // 增加订阅者
if(!this.clientList[key]){ // 如果没有订阅此类消息,给该类创建一个缓存列表
this.clientList[key] = []
}
this.clientList[key].push(fn)
}
salesOffices.trigger = function(){ // 发布消息
var key = Array.prototype.shift.call(arguments) // 取出消息类型 <=> arguments[0]
var fns = this.clientList[key] // 取出该消息对应的回调函数集合
for(var i=0;i<fns.length;i++){
var fn = fns[i]
fn.apply(this,arguments)
}
}

salesOffices.listen('squareMeter88',function(price){
console.log('m 价格= '+price)
})
salesOffices.listen('squareMeter88',function(price){
console.log('w 价格= '+price)
})
salesOffices.listen('squareMeter122',function(price){
console.log('h 价格= '+price)
})

salesOffices.trigger('squareMeter88',2200) // m/w 价格= 2200
salesOffices.trigger('squareMeter122',122000) // h 价格= 122000

发布-订阅模式的通用实现

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
// 把发布-订阅功能提取出来,放在一个单独的对象内
var event = {
clientList : [],
listen : function(key,fn){
if(!this.clientList[key]){
this.clientList[key] = []
}
this.clientList[key].push(fn)
},
trigger: function(){
var key = Array.prototype.shift.call(arguments)
var fns = this.clientList[key]
if(!fns || fns.length===0){
return false
}
for(var i=0;i<fns.length;i++){
var fn = fns[i]
fn.apply(this,arguments)
}
}
}
// installEvent 函数,给所有对象动态安装 发布-订阅功能:
// 浅拷贝 (我感觉 没什么用。。)
function installEvent(obj){
for(var i in event){
obj[i] = event[i]
}
}
var salesOffices = {}
installEvent(salesOffices)
salesOffices.listen('squareMeter88',function(price){
console.log('price= '+ price)
})
salesOffices.trigger('squareMeter88',122)

取消订阅事件

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
var event = {
...
remove: function(key,fn){
var fns = this.clientList[key]
if(!fns){
return false
}
if(!fn){ // 如果没有传入具体的回调函数,表示需要取消 key 对应消息的所有订阅
fns && (fns.length = 0)
}else{
for(var i=fns.length-1;i>=0;i--){ // 倒序删除
var _fn = fns[i]
if(_fn===fn){
fns.splice(i,1)
}
}
}
}
}
...
salesOffices.listen('squareMeter88',fn1 = function(price){
console.log('f1 price= '+ price)
})
salesOffices.listen('squareMeter88',fn2 = function(price){
console.log('f2 price= '+ price)
})
salesOffices.listen('squareMeter88',fn1 = function(price){
console.log('f3 price= '+ price)
})
salesOffices.remove('squareMeter88',fn1) // 删除的是最后一个叫f1的订阅者
salesOffices.trigger('squareMeter88',122) // f1/f2 price= 122

CSS实现3D效果

transform相关属性实现3D效果

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
51
52
53
54
55
56
57
58
59
<style>
._3Dbox{
width: 200px;
margin: 200px auto;
perspective: 2000px; /* 视距 */
}
._3Dbox .cube{
position: relative;
width: 200px;
height: 200px;
transform-style: preserve-3d; /* 以3d的形式展示 !!! */
animation: _3dmove 10s linear infinite;
}
._3Dbox .cube div{
position: absolute;
width: 200px;
height: 200px;
border: 2px solid lightblue;
line-height: 200px;
font-size: 30px;
opacity: 0.9;
text-align: center;
background: url('/images/avatar.jpg');
background-size: contain;
}
._3Dbox .cube div:nth-child(1){ /* 上 */
transform:rotateX(90deg) translateZ(102px);
}
._3Dbox .cube div:nth-child(2){ /* 下 */
transform:rotateX(-90deg) translateZ(102px);
}
._3Dbox .cube div:nth-child(3){ /* 前 */
transform:translateZ(102px);
}
._3Dbox .cube div:nth-child(4){ /* 后 */
transform: translateZ(-102px);
}
._3Dbox .cube div:nth-child(5){ /* 左 */
transform: translateX(-102px) rotateY(90deg);
}
._3Dbox .cube div:nth-child(6){ /* 右 */
transform: translateX(102px) rotateY(-90deg);
}
@keyframes _3dmove {
to{
transform: rotateX(360deg) rotateY(360deg) ;
}
}
</style>
<div class="_3Dbox">
<div class="cube">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>

CSS中的3D坐标轴

rotateX/Y/Z() 相当于绕X/Y/Z轴旋转 多少度
translateX/Y/Z() 相当于沿着X/Y/Z轴的方向移动多少像素

【第七章】 迭代器模式

迭代器模式

提供一种 “顺序访问” 一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
迭代模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序方位其中的每个元素。

自己实现一个迭代器:

1
2
3
4
5
6
7
8
var each = function(ary,callback){
for(var i=0;i<ary.length;i++){
callback.call(null,i,ary[i]) // 把下标和元素当作参数传给callback函数
}
}
each([1,2,3],function(i,n){
console.log([i,n])
})

内部迭代器

上面编写的 each 函数属于内部迭代器,each 函数的内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用。

内部迭代器在调用的时候非常方便,外界不用关心迭代器内部的实现,跟迭代器的交互也仅仅是一次初始调用,但这也刚好是内部迭代器的缺点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 内部迭代器实现 比较两个数组
function compare(ary1,ary2){
if(ary1.length != ary2.length){
console.log('不相等')
return
}
each(ary1,function(i,n){
if(ary2[i]!=n){
throw new Error('不相等') // console.log('不相等')
}
})
console.log('相等')
}
compare([1,2,3],[1,2,3]) // 相等

外部迭代器

外部迭代器必须显式地请求迭代下一个元素。
外部迭代器增加了一些调用的复杂度,但相对也增强了迭代器的灵活性,我们可以手工控制迭代的过程或者顺序。

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
var Iterator = function(obj){
var current = 0
var next = function(){
current +=1
}
var isDone = function(){
return current >= obj.length
}
var getCurrentItem = function(){
return obj[current]
}
return {
next: next,
isDone: isDone,
getCurrentItem: getCurrentItem
}
}

// 外部迭代器实现 比较两个数组
function compare(Iterator1,Iterator2){
while(!Iterator1.isDone()&&!Iterator2.isDone()){
if(Iterator1.getCurrentItem() !== Iterator2.getCurrentItem()){
throw new Error('不相等')
}
Iterator1.next()
Iterator2.next()
}
console.log('相等')
}
var Iterator1 = Iterator([4,5,6])
var Iterator2 = Iterator([4,5,6])
compare(Iterator1,Iterator2) // 相等

迭代类数组对象和字面量对象

类数组:arguments
字面量对象:{‘0’:’a’,’1’:’b’}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function each(obj,callback){
var value,
i=0,
length = obj.length,
isArray = isArraylike(obj); //假装封装好了一个判断是否是类数组的方法

if(isArray){ // 迭代类数组
for(;i<length;i++){
value = callback.call(null,i,obj[i])
if(value === false){
break;
}
}
}else{
for(i in obj){
value = callback.call(null,i,obj[i])
if(value === false){
break;
}
}
}
return obj
}

中止迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
var each = function(ary,callback){
for(var i=0;i<ary.length;i++){
if(callback(i,ary[i]) === false){
break
}
}
}
each([11,21,31,41,51],function(){
if(n>30){
return false
}
console.log(n)
}) // 11 21

vue-element-admin权限管理&动态路由配置

vue-element-admin模板地址

需求: 调用登陆接口之后 后台传递路由数据给前端 进行动态创建和显示

问题: 各种问题 主要是不清楚这个模板的默认权限动态传递的配置

用到的核心文件地址

1
2
3
4
src/router/index.js                     路由
src/store/modules/user.js 用户登录,用户信息、路由信息传递
src/permission.js 信息传递(user -> permission 的中间商) 之前就是一直没注意到这个 导致一直弄不好
src/store/modules/permission.js 权限获取&配置

src/router/index.js 路由配置

constantRoutes: 不需要动态判断权限的路由 eg:登陆、404等通用页面
asyncRoutes:需要动态判断权限并通过addRoutes动态添加的页面

需要把动态路由先配置好 ‘permission’ 权限要写好
到时候才能正确的获取和显示相应的权限

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
51
52
53
54
55
56
57
58
59
60
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: '首页', icon: 'dashboard', affix: true }
}]
}
]

export const asyncRoutes = [
// 待审核
{
path: '/toPend',
component: Layout,
redirect: '/toPend',
permission: 'toPend',
title: '待审核',
children: [
{
permission: 'toPend',
path: '/toPendIndex',
component: () => import('@/views/toPendManagement/toPendIndex'),
name: 'toPend',
meta: {
title: '待审核',
icon: 'form',
noCache: false // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
}
}
]
},
....
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
]
const createRouter = () => new Router({
mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})

const router = createRouter()

export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}

export default router

src/store/modules/user.js 用户登陆获取数据

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/*
正常流程是在login接口获取到 token,存在vuex中 供登陆
然后在getInfo接口获取到 路由和用户信息 存储并发送出去 供路由的动态配置

但是 我们这个项目把getInfo接口取消了 在login接口就直接获取到路由信息
本来是不需要自己封装info数据的
*/
const actions = {
// user login
login({commit}, userInfo) {
const { username,password } = userInfo
return new Promise((resolve, reject) => {
login({ account: username.trim(), password: password }).then(response => {
const {
data
} = response
// 存储到cookie中 !很重要 不然登陆不了 接口在 src/utils/auth.js
setToken({ username: 'admin', password: 'admin' })
var info = { username: 'admin', password: 'admin', roles: ['admin'], routes: data.permission }
commit('SET_TOKEN', info) // 存储到 vuex 中 数据格式如上 ↑
resolve()
}).catch(error => {
reject(error)
})
})
},
getInfo({ commit,state }) {
return new Promise((resolve) => {
// 可以把这三个写在 info 中 从info里获取
const roles = 'admin'
const name = 'Super Admin'
const avatar = 'http://pic.51yuansu.com/pic3/cover/03/47/85/5badd30d6526d_610.jpg'
commit('SET_ROLES', roles)
commit('SET_NAME', name)
commit('SET_AVATAR', avatar) // 存储在cookie中
resolve(state.token) // 发送 info 值 ,传递给 permission
})
},
// get user info 如果是有info接口的传递方法
/* getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response
if (!data) {
reject('Verification failed, please Login again.')
}
const { roles, name, avatar } = data
// roles must be a non-empty array
if (!roles || roles.length <= 0) {
reject('getInfo: roles must be a non-null array!')
}
commit('SET_ROLES', roles)
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
console.log(data)
resolve(data)
}).catch(error => {
reject(error)
})
})
}, */
......
}
export default {
namespaced: true,
state,
mutations,
actions
}

src/store/modules/permission.js 权限获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
本来是根据角色roles权限过滤路由,并显示到侧边栏
roles.
但是现在是不根据权限 而是后台传递的路由信息 所以不用判断权限 修改为
*/
const actions = {
generateRoutes({ commit }, userinfo) {
return new Promise(resolve => {
const strRoutes = JSON.stringify(userinfo.routes)
const accessedRoutes = filterAsyncRoutes(asyncRoutes, strRoutes) //路由的过滤器 获取相同的路由
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}

src/permission.js 用户信息传递&配置路由

首先确定userinfo需要的格式 就是getInfo函数传递过来的 resolve(state.token)

path对应的是路由中的permission

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
router.beforeEach(async(to, from, next) => {
if (hasToken) {
if (to.path === '/login') {
...
} else {
// determine whether the user has obtained his permission roles through getInfo
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
// 登录后会走这里获取roles
try {
// get user info
// note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
const userinfo = await store.dispatch('user/getInfo') //获取用户信息
// 调用store下module下的permission文件中的generateRoutes方法
const accessRoutes = await store.dispatch('permission/generateRoutes', userinfo) // 获取动态路由
// dynamically add accessible routes 显示获取到的侧边栏
router.addRoutes(accessRoutes) //创建路由

next({ ...to, replace: true })
} catch (error) {
...
}
}
}
}

被遗漏的BUG

描述: 登录完成之后,进入操作页面 刷新 又需要重新登录

原因: 之前这个模板是调用的getInfo接口获取、存储路由信息
页面刷新之后 再次调用接口即可获取到路由等信息

但是我们把getInfo接口废弃了 路由信息是登陆的时候获取
存在vuex中 在getInfo函数内直接使用的vuex存储的数据 并发布出去

原因就在于 vuex刷新之后 数据就清空了!所以又需要重新登录 获取路由等信息。

解决:
在login登陆接口之后获取到路由数据
通过sessionStorage / localStorage 存放在本地
然后在使用 getInfo 函数的时候 不从vuex内获取 从 sessionStorage / localStorage 获取并发送

注意:在退出登陆的时候 即调用logout接口之前要通过.removeItem()进行删除操作 不然不能正常退出登陆

事件循环相关-异步输出顺序问题

题目出处

通过这个题目 大概搞懂了 async/await 的实际意义
也可以复习下事件循环机制

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
51
52
53
54
55
56
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end"); // 相当于async2这个promise对象的一个 .then()
}

async function async2() {
console.log("async2");
}

console.log("script start");

setTimeout(function () {
console.log("settimeout");
},0);

async1();

new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});

console.log("script end");

/**
* 整体同步的执行顺序
* 1. console.log("script start") -> 输出 “ script start ”
*
* * setTimeout() 进入异步队列 *
*
* 2. async1()
* -> 输出 “ async1 start ”
* -> 进入 async2() 输出 “ async2 ”
* -> await等待async2()函数返回一个promise实例 将 console.log('async1 end') 放入异步队列
*
* 3. promise()
* -> 输出 “ promise1 ”
* -> resolve() 进入异步队列
*
* 4. console.log("script end") -> 输出 “ script end ”
*
*
* 异步队列:
* setTimeout()
* console.log('async1 end')
* resolve()
*
* 按宏 / 微任务 顺序输出
* console.log('async1 end') // 微任务 1 -> async1 end
* resolve() // 微任务 2 -> promise2
* setTimeout() // 宏任务 1 -> settimeout
*
*/

async / await

通过看起来是同步的代码来执行 async(异步) 操作。
await阻塞功能,相当于一个 .then()的功能,await后一般是跟的异步操作,加上之后 只有等他执行完之后,后面的代码才能进行
本质还是promise 属于微任务

1
2
3
4
5
6
7
8
9
10
11
12
async function asyncFunc({
    const result = await otherAsyncFunc();
    console.log(result);
}
 
// 等价于:
function asyncFunc({
    return otherAsyncFunc().then(result => {
        console.log(result);
    });
}

事件循环机制 (EventLoop)

JavaScript是单线程的 ( 所有在主线程执行 )
事件轮询机制:同步任务完成后,执行异步队列

单线程特点:

  1. 代码从上至下以此执行
  2. 同步( 会阻塞后面代码的执行 ) & 异步 ( 主线程同步任务之后触发执行 )
  3. 同步代表:alert() console.log() 赋值语句
  4. 异步代表:定时器 事件的回调( xx.onclick=function(){} ) promise

事件循环:

  1. 所有同步任务都在主线程上执行,形成一个执行栈
  2. 主线程之外,还存在一个”消息队列”。只要异步操作执行完成,就到消息队列中排队
  3. 一旦执行栈中的所有同步任务执行完毕,系统就会按次序读取消息队列中的异步任务,于是被读取的异步任务结束等待状态,进入执行栈,开始执行
  4. 主线程不断重复上面的第三步

宏任务和微任务

微任务和宏任务皆为异步任务,但是微任务的优先级高于宏任务。

  • Copyrights © 2019-2026 John Doe
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信