给文章的高亮代码加上点击运行

为了方便在文章插入一些简单的前端代码可以直接运行,加上一个点击运行的按钮;另一方面也让自己好好写写代码 。

效果如下

  • 鼠标放上去会显示出运行的按钮
  • 点击运行单个:运行当前的代码块
  • 点击运行全部:会同时载入文章中所有的代码块
  • 点击运行同组:会运行连续的兄弟代码块

ps: 只在详情页显示运行按钮!! 写这几行 ES5 的代码真不容易 !!

测试分组第一组:

1
2
3
4
5
<div class="test">
<h1 class="ymm">嘿嘿嘿嘿</h1>
<h2>HTML 功能测试</h2>
<button id="button">我是 Button1</button>
</div>
1
2
3
4
5
6
7
8
.ymm {
color: red;
}
.test {
position: relative;
padding: 20px;
background-color: #ccc;
}
1
2
3
4
5
6
7
8
9
// button1
var a = '1111';
setTimeout(() => {
console.log(`独立作用域:a = ${a}`);
}, 200);
document.getElementById('button').addEventListener('click', function() {
alert('Button1 (*^▽^*)');
console.log('Button1 (*^▽^*)');
});

测试分组第二组:

1
2
3
4
<div class="test">
<h2>多个HTML 节点测试 </h2>
<button id="button2">我是 Button2</button>
</div>
1
2
3
4
5
6
7
8
9
// button2
var a = '22222';
setTimeout(() => {
console.log(`独立作用域:a = ${a}`);
}, 200);
document.getElementById('button2').addEventListener('click', function() {
alert('Button2 (*^▽^*)');
console.log('Button2 (*^▽^*)');
});

使用

实现:

  1. 通过遍历对应的代码块,提取代码字符串,
  2. open一个新窗口把需要执行的代码加进去
  3. 新窗口内注入一个自定义js用来解析需要执行的代码,也可以自己eval执行

页面执行:

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
var onlineRun = {
init: function () {
var domList = this.getAllHighlightJqDom();
var that = this;

// 创建btn按钮
if (domList.length > 0) {
this.insertBtnCss();
domList.each(function(index, dom) {
that.createBtn(dom);
});

this.bindEvents();
}
},

insertStyleCode: function(code) {
var style = window.document.createElement('style');
style.type = 'text/css';
style.innerHTML = code;
window.document.getElementsByTagName('head').item(0).appendChild(style);
},

insertBtnCss: function () {
var cssCode = '.runBtn{position: absolute;bottom: 5px; right: 5px; margin-left: 20px;width: 60px; text-align: center; background: #000;font-size: 12px; cursor: pointer; opacity: 0;color:#FFF;transition: 0.3s ease;visibility: hidden;}';
cssCode += '.runBtn:hover{opacity: 1!important;}';
cssCode += '.highlight:hover .runBtn{opacity: 0.4;visibility: visible;}';
this.insertStyleCode(cssCode);
},

getCode: function (jqDom) {
var codeArr = [];
// 兼容 数组
var addItem = function (item) {
var type = item.className.replace(/highlight\s+/, '');
codeArr.push({
type: type,
code: $(item).find('.code')[0].innerText
});
};
if (jqDom instanceof Array) {
jqDom.forEach(function (item) {
addItem(item);
});
} else {
jqDom.each(function (index, item) {
addItem(item);
});
}

return codeArr;
},
runCodeFn: function (codeObj) {
var openWindow = window.open("", "", "");
openWindow.opener = null; // 父级窗口

// 插入vConsole / jQuery、layer
var scripts = ['https://cdn.bootcss.com/vConsole/3.3.4/vconsole.min.js', window.libUri.jquery, window.libUri.layer];
var promiseArr = scripts.map(function (item) {
return window.loadScript(item, {
async: false,
container: openWindow
});
});

// 保证公共js 先执行完
window.Promise.all(promiseArr).then(function () {
codeObj = [
{
type: 'charset',
code: 'utf-8',
},{
type: 'link',
code: 'https://cdn.bootcss.com/normalize/8.0.1/normalize.min.css',
},{
type: 'link',
code: window.libUri.customCssUri,
}
].concat(codeObj);

// 插入待执行代码字符串
var jsCode = openWindow.document.createElement('script');
jsCode.text = 'var runCodeObj = ' + JSON.stringify(codeObj);
openWindow.document.body.appendChild(jsCode);

// 加载自定义js
window.loadScript(window.libUri.customJsUri, {
async: false,
container: openWindow
});
openWindow.document.title = '在线运行';
openWindow.document.close();
});
},
bindEvents: function () {
var that = this;
$(window.document).on('click', '.js_runItem', function () {
var codeObj = that.getCode($(this).parent());
that.runCodeFn(codeObj);
});
$(window.document).on('click', '.js_runAllItem', function () {
var codeObj = that.getCode(that.getAllHighlightJqDom());
that.runCodeFn(codeObj);
});
$(window.document).on('click', '.js_runNearItem', function (event) {
var codeObj = that.getCode(that.getNearHighlightJqDom($(event.currentTarget).parent()));
that.runCodeFn(codeObj);
});
},
getNearHighlightJqDom: function($currentCodeBlock) {
// 获取附近的可运行高亮代码
var checkDom = function ($dom) {
return $dom.hasClass('js') || $dom.hasClass('css') || $dom.hasClass('html');
};

var nodeArr = [$currentCodeBlock[0]];
// 向下查找
var $next = $currentCodeBlock.next();
while ($next.length > 0) {
if (checkDom($next)) {
nodeArr.push($next[0]);
// $currentCodeBlock.add($next); // todo: 无法add ?
$next = $next.next();
} else {
break;
}
}

// 向上查找
var $perv = $currentCodeBlock.prev();
while ($perv.length > 0) {
if (checkDom($perv)) {
nodeArr.unshift($perv[0]); // 保持顺序
// $currentCodeBlock.add($perv[0]);
$perv = $perv.prev();
} else {
break;
}
}

return nodeArr;
},
getAllHighlightJqDom: function() {
// css html js
if (!this.highlightList) {
this.highlightList = $('.highlight.css, .highlight.js, .highlight.html');
}
return this.highlightList;
},
createBtn: function (parentDom) {
var btns = [
{
className: 'runBtn runNearItem js_runNearItem',
text: '运行同组',
title: '运行当前代码块以及连续的兄弟代码块',
},
{
className: 'runBtn runAllBtn js_runAllItem',
text: '运行全部',
title: '运行所有代码块',
},
{
className: 'runBtn js_runItem',
text: '运行单个',
title: '运行当前代码块',
}
];

var tempFragment = window.document.createDocumentFragment();
btns.forEach(function (item, index) {
var btnItemDom = window.document.createElement('span');
btnItemDom.className = item.className;
btnItemDom.title = item.title;
btnItemDom.innerText = item.text;
btnItemDom.style.bottom = 25 * index + 'px';
tempFragment.appendChild(btnItemDom);
});

parentDom.style.position = 'relative';
parentDom.appendChild(tempFragment);
}
};

// 需要的时候执行这个
// onlineRun.init();

新窗口执行:

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
/**
* Created by Liu.Jun on 2019/11/9 12:38 上午.
*/

// from: https://github.com/jserz/js_piece/blob/master/DOM/ParentNode/prepend()/prepend().md
(function (arr) {
arr.forEach(function (item) {
item.prepend = item.prepend || function () {
var argArr = Array.prototype.slice.call(arguments),
docFrag = document.createDocumentFragment();

argArr.forEach(function (argItem) {
var isNode = argItem instanceof Node;
docFrag.appendChild(isNode ? argItem : document.createTextNode(String(argItem)));
});

this.insertBefore(docFrag, this.firstChild);
};
});
})([Element.prototype, Document.prototype, DocumentFragment.prototype]);

var MyFragment = function (parentDom) {
this.fragment = window.document.createDocumentFragment();
this.parentDom = String(parentDom) === parentDom ? window.document.getElementsByTagName(parentDom).item(0) : parentDom;
};
MyFragment.prototype.append = function (dom) {
this.fragment.appendChild(dom);
};
MyFragment.prototype.prepend = function (dom) {
this.fragment.prepend(dom);
};

MyFragment.prototype.insertPage = function () {
this.parentDom.appendChild(this.fragment);
};

var genCodeStrategy = {
charset: function() {
var meta = window.document.createElement('meta');
meta.content = 'text/html;charset=utf-8';
return meta;
},
css: function (code) {
var style = window.document.createElement('style');
style.type = 'text/css';
style.innerHTML = code;
return style;
},
js: function (code) {
var js = window.document.createElement('script');
// 各代码块独立作用域
js.text = code;
return js;
},
script: function (src) {
var script = window.document.createElement('script');
script.src = src;
return script;
},
link: function(href) {
var link = window.document.createElement('link');
link.href = href;
link.rel = 'stylesheet';
link.type = 'text/css';
return link;
},
html: function (code) {
var dom = window.document.createElement('div');
dom.classList.add('section');
dom.innerHTML = code;
return dom;
}
};

var customObj = {
init: function(runCodeObj) {
this.layerId = null;

this.initDoctype();

this.fragmentObj = this.initRunCode(runCodeObj);

// 头部代码同步先插入
this.fragmentObj.headFragment.insertPage();

// 初始化 vConsole
this.vConsole = this.initConsole();
this.vConsole.hideSwitch();

this.openConsole();
this.vConsole.show();

// JS 等部分需要后执行
this.fragmentObj.bodyFragment.insertPage();

// 底部按钮
this.insertSwitchBtn();
},
initDoctype: function() {
// prepend polyfill IE不兼容
window.document.insertBefore(window.document.implementation.createDocumentType('html', '', ''), window.document.documentElement);
},
insertSwitchBtn: function() {
var that = this;
var btn = window.document.createElement('span');
btn.className = 'vc-switch showConsole';
btn.innerText = 'vConsole';
window.document.getElementById('__vconsole').appendChild(btn);

$(btn).on('click', function () {
if (that.layerId) {
window.layer.close(that.layerId);
} else {
that.openConsole();
}
});
},
initRunCode: function(runCodeObj) {
var headFragment = new MyFragment('head');
var bodyFragment = new MyFragment('body');

runCodeObj.forEach(function (item) {
var type = item.type;
var code = item.code;
var domItem = genCodeStrategy[type](code);
if (type === 'js') {
bodyFragment.append(domItem);
} else if (type === 'html') {
bodyFragment.prepend(domItem);
} else {
headFragment.append(domItem);
}
});

return {
headFragment: headFragment,
bodyFragment: bodyFragment,
};
},
initConsole: function () {
return new window.VConsole({
defaultPlugins: ['network', 'storage']
});
},
openConsole: function() {
var that = this;
that.layerId = window.layer.open({
title: 'vConsole',
type: 1,
area: ['400px', '560px'],
shadeClose: true,
resize: true,
offset: 'rt',
anim: -1,
isOutAnim: false,
shade: 0,
content: $(that.vConsole.$dom).find('.vc-panel'),
end: function () {
that.layerId = null;
}
});
}
};

// 需要的地方执行
// customObj.init(window.runCodeObj);