本身動手製做更好用的markdown編輯器-01

這裏文章都是從我的的github博客直接複製過來的,排版可能有點亂. 原始地址   http://benq.im
 
文章目錄
  1. 1. 簡介
  2. 2. 項目結構
  3. 3. 程序主界面
  4. 4. 拖動窗口
  5. 5. app.js
  6. 6. 導航欄按鈕
  7. 7. 實現簡單的markdown編輯器
    1. 7.1. 定義路由
    2. 7.2. 實現controller
    3. 7.3. 添加視圖模版
    4. 7.4. 實現editor
    5. 7.5. 實現directive
    6. 7.6. 最終效果
  8. 8. 總結
  9. 9. 附件

前段時間用hexo從新搭了我的博客,順便寫了個簡單的博客搭建教程.javascript

markdown寫起博客流暢不少,可是用了幾個markdown編輯器,都沒有一個適合本身使用的。因而就想本身動手作一個,固然不是徹底從0開始作,語法高亮和markdown解析都用的是開源的項目.

從這篇開始,我會把整個開發過程記錄成系列隨筆,所以開發進度較爲緩慢.css

博客寫得少,像這樣寫長一點的隨筆就有點混亂,看不懂的請用力噴,我會努力改進.html

簡介

先介紹下開發過程當中用到的一些比較重要的開源項目:java

  1. nw.js,原名node-webkit,用webkit和node來作基於web技術的跨平臺客戶端軟件.
  2. CodeMirror,基於web技術實現的文本編輯器,實現了大部分的IDE功能以及幾乎所有你會用到的語言的支持.目前我平常開發都是用這個IDE,甚至在作hexomd這個項目時用的IDE也是CodeMirror作的.
  3. angularjs,google的mvvm開發框架,這個相信不用我多作介紹.我用的不熟,以爲好用就拿來即用,沒有深刻的瞭解過.

關於這些開源項目的使用,我在這系列文章裏不會詳細解釋,若是有疑問,能夠去看官網的入門教程和wiki,固然也歡迎討論.node

項目結構

圖片裏的是我目前的項目結構,大概講解一下一些目錄和文件的用途。jquery

  1. icudtl.dat,nw.exe,nw.pak
    這3個是nw.js在windows運行所必須的文件.git

  2. package.json
    nw.js的配置文件angularjs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    {
    "name": "HexoMD",
    "description": "Markdown for hexo",
    "main": "app/index.html",//程序入口頁面
    "author": "hmjlr123@gmail.com",
    "license": "MIT",
    "directories": {
    "test": "no"
    },
    "devDependencies": {},
    //窗口配置
    "window": {
    "title": "HexoMD",
    "icon": "app/img/logo.png", //logo
    "toolbar": true, //是否顯示地址欄工具條(調試的時候啓用)
    "frame": false, //是否顯示程序邊框
    "width": 1000, //默認寬度
    "height": 700, //默認高度
    "position": "center", //啓動時在屏幕中的位置
    "min_width": 600, //最小寬度
    "min_height": 400 //最小高度
    }
    }
  3. app目錄
    程序的全部源代碼的根目錄.github

  4. app/lib
    存放angular,jquery,codemirror等開源庫/框架的源代碼web

  5. app/helpers
    存放一些node的工具函數

  6. app/modules
    程序代碼在這個目錄,按功能模塊分紅不一樣的子目錄.
    modules/app.js是整個程序的入口點

  7. app/package.json
    node模塊配置,注意與上層的package.json意義不一樣

  8. app/index.html
    程序的主界面窗口

程序主界面

index.html

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hexo Markdown</title>
<link href="css/bootstrap.css" rel="stylesheet">
<link href="lib/codemirror/lib/codemirror.css" rel="stylesheet" />
<link href="css/index.css" rel="stylesheet">
</head>
<body>
<!--導航欄-->
<nav class="navbar navbar-inverse navbar-fixed-top">
省略...
</nav>
<!--模塊視圖區域-->
<article class="container app" ui-view ng-animate="'view'"></article>
<!--工具欄-->
<footer class="tool"></footer>
<!--end codemirror-->
<script src="lib/jquery-2.1.3.js"></script>
<script src="lib/angular.js"></script>
<script src="lib/angular-ui-router.js"></script>
<!--程序入口函數-->
<script src="modules/app.js"></script>
<script>
//初始化angular,hmd爲自定義的根模塊名
angular.bootstrap($('body'), ['hmd']);
</script>

</body>
</html>

 

只貼出部分代碼.之後的全部代碼也相似,都只會把重要的貼出來,並給出完整的連接.


界面採用比較簡潔的三欄佈局,分別爲導航欄內容區狀態欄/工具條.
最頂部的地址欄只有在開發的時候爲了方便調試纔開啓,發佈時會關閉掉.

拖動窗口

爲了美觀,咱們在配置裏去掉了系統自帶的邊框.所以要實現自定義的拖動窗口功能還須要增長一些設置.
所謂的設置,其實只要加上對應的樣式便可,功能都由nw.js實現了.

1
2
3
.navbar{
-webkit-app-region: drag;
}

帶有此樣式的元素能夠做爲窗口的拖拽區域,而且雙擊時最大化/還原窗口.

1
2
3
.navbar .navbar-collapse a {
-webkit-app-region: no-drag;
}

被標誌爲可drag的容器裏的連接將不可點擊,所以要特別爲連接加上no-drag

另外爲了讓程序看起來更像客戶端一點,我默認禁用掉了文本選擇,防止一些被做爲按鈕的a標籤的文本被選中

1
2
3
4
html {
height: 100%;
-webkit-user-select: none;
}

 

app.js

app.js做爲程序的入口點,定義了整個項目代碼的結構,須要特別拿出來講明一下.

1
angular.module('hmd', ['ui.router','hmd.studio'])

定義angular模塊,modules全部的業務模塊都會放到單獨的子目錄裏,如這裏註冊的hmd.studio

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//模塊根目錄
var baseModuleDir = './app/modules/';
//引入模塊,模塊內js文件會被自動加載到頁面中
hmd.regModule = function (name, reqModule) {
hmd[name] = angular.module('hmd.' + name, reqModule || []);
hmd[name].moduleName = name;
//模塊存儲數據的目錄
hmd[name].dataPath = hmd.storeDir + '\\' + hmd[name].moduleName;
fs.readdirSync(baseModuleDir + name)
.forEach(function (file) {
if (~file.indexOf('.js')) {
document.write('<script src="modules/' + name + '/' + file + '"></script>');
}
});
};

regModule方法實現最簡單的模塊載入,自動加載模塊內的全部腳本到頁面中,併爲每一個模塊賦予一個單獨的數據存儲目錄dataPath

1
hmd.storeDir =  require('nw.gui').App.dataPath;

程序的數據存儲目錄

導航欄按鈕

導航欄右邊有4個按鈕,分別爲:檢查更新最小化最大化關閉

1
2
3
4
5
6
7
8
9
10
...
<!--導航欄功能按鈕-->
<div class="btn-group window-tool">
<a class="btn rectbtn" href="javascript://" title="點擊檢查更新">
<i class="glyphicon mdfi_action_system_update_tv"></i></a>
<a class="btn rectbtn" href="javascript://"><i class="glyphicon glyphicon-minus"></i></a>
<a class="btn rectbtn" href="javascript://"><i class="glyphicon glyphicon-fullscreen"></i></a>
<a class="btn rectbtn" href="javascript://"><i class="glyphicon glyphicon-remove"></i></a>
</div>
...

 

檢查更新等之後再實現.如今先實現後面3個功能
由於這3個功能是全局的,所以在modules根目錄新建directives.js用於實現全局的Directive.

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
(function () {
var gui = require('nw.gui'), win = gui.Window.get(),winMaximize = false;
angular.module('hmd.directives', [])
//最小化窗口
.directive('hmdMinisize', [function () {
return function (scope, elem) {
$(elem[0]).on('click', function () {
win.minimize();
});
};
}])
//最大化與還原窗口
.directive('hmdMaxToggle', [function () {
return function (scope, elem) {
//窗口最大化和還原時會觸發對應的事件,在事件裏去控制按鈕樣式.
//TODO:這裏的實現應該能夠優化得更優雅一點,之後再說
win.on('maximize', function () {
winMaximize = true;
$(elem[0]).find('i').removeClass('glyphicon-fullscreen').addClass('glyphicon-resize-small');
});
win.on('unmaximize', function () {
winMaximize = false;
$(elem[0]).find('i').removeClass('glyphicon-resize-small').addClass('glyphicon-fullscreen');
});
//切換窗口狀態
$(elem[0]).on('click', function () {
if (winMaximize) {
win.unmaximize();
}
else {
win.maximize();
}
});
};
}])
//關閉應用程序
.directive('hmdClose', [function () {
return function (scope, elem) {
$(elem[0]).on('click', function () {
require('nw.gui').Window.get().close();
});
};
}]);
})();

 

定義了全局directive模塊angular.module('hmd.directives', []),實現了3個Directive.

接下來將directive應用到按鈕上

1
2
3
4
5
...
<a class="btn rectbtn" href="javascript://" hmd-minisize><i class="glyphicon glyphicon-minus"></i></a>
<a class="btn rectbtn" href="javascript://" hmd-max-toggle><i class="glyphicon glyphicon-fullscreen"></i></a>
<a class="btn rectbtn" href="javascript://" hmd-close><i class="glyphicon glyphicon-remove"></i></a>
...

 

將腳本引用<script src="modules/directives.js"></script>添加到index.html的app.js以後

app.js裏的angular模塊註冊裏增長hmd.directives模塊angular.module('hmd', ['ui.router','hmd.directives','hmd.studio'])

刷新程序,三個按鈕已經生效.

實現簡單的markdown編輯器

先在頁面添加相應的codemirror腳本引用

1
2
3
4
5
6
7
8
9
10
11
...
<footer class="tool"></footer>
<!--codemirror-->
<script src="lib/codemirror/lib/codemirror.js"></script>
<script src="lib/codemirror/addon/mode/overlay.js"></script>
<script src="lib/codemirror/addon/edit/continuelist.js"></script>
<script src="lib/codemirror/mode/markdown/markdown.js"></script>
<script src="lib/codemirror/mode/gfm/gfm.js"></script>
<!--end codemirror-->
<script src="lib/jquery-2.1.3.js"></script>
...

 

而後在modules目錄下新增studio子目錄,全部編輯器功能都在這個模塊裏實現.
app.js裏增長加載studio模塊的代碼

1
hmd.regModule('studio');

 

每一個子模塊通常都會包含route.js,controllers.js,directive.js這三個基本的angular功能.以及views子目錄,用於存放模塊用到的html視圖

studio模塊多了一個editor.js,咱們將編輯器的一些基本功能封裝在這個腳本里

定義路由

route.js

1
2
3
4
5
6
7
8
hmd.studio.config(function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state('studio', {
url: "/studio",
templateUrl: "modules/studio/views/studio.html",
controller: 'studio'
});
});

 

修改app.js,將默認路由指定到/studio模塊

1
2
3
hmd.config(function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("/studio");
});

 

實現controller

controllers.js

1
2
3
4
5
var studio = hmd.studio;
studio
.controller('studio', function ($scope, $state, $stateParams) {
console.log('stuido controller');
});

 

添加視圖模版

views/studio.html

1
2
3
<div class="content studio-wrap">
<textarea name="" cols="30" rows="10"></textarea>
</div>

 

從新打開應用,能夠看到模塊跳到了studio路由,而且執行了對應的控制器

實現editor

editor.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
71
72
73
74
75
var util = require('./helpers/util');
var defaultConfig = {
theme: 'ambiance',
mode: 'gfm',
lineNumbers: false,
extraKeys: {"Enter": "newlineAndIndentContinueMarkdownList"},
dragDrop: false,
autofocus: true,
lineWrapping: true,
foldGutter: true,
styleActiveLine: true
};

hmd.editor = {
init: function (options,filepath) {
var el = options.el,txt,me = this;
options = $.extend({}, defaultConfig, options);
//編輯器樣式文件動態加載,用於之後增長樣式選擇功能
if(options.theme != 'default'){
$('head').append('<link href="lib/codemirror/theme/'+options.theme+'.css" rel="stylesheet" />');
}
this.cm = this.cm || CodeMirror.fromTextArea(el, options);
//指定要打開的文件,若是未指定,則保存時會彈出文件選擇對話框
filepath && this.setFile(filepath);
//編輯器內容修改時觸發change事件
this.cm.on('change', function (em, changeObj) {
me.hasChange = true;
me.fire('change', {
em: em,
changeObj: changeObj
});
});
//綁定按鍵
this.cm.addKeyMap({
"Ctrl-S": function () {
me.save();
}
});
},
//設置當前文件
setFile:function(filepath){
var txt = util.readFileSync(filepath);
this.filepath = filepath;
this.cm.setValue(txt);
},
//彈出保存文件對話框
saveAs:function(){
hmd.msg('保存新文件');
},
//保存文件
save: function () {
var txt = this.cm.getValue();
if(this.filepath){
util.writeFileSync(this.filepath, txt);
this.hasChange = false;
var fileNameArr = this.filepath.split('\\');
hmd.msg('文件:' + fileNameArr[fileNameArr.length - 1] + '保存成功!');
this.fire('save');
}
else{
this.saveAs();
}
},
events: {},
fire: function (eventName, obj) {
var me = this;
this.events[eventName] && this.events[eventName].forEach(function (fn) {
fn.call(me,obj);
});
},
on: function (eventName, fn) {
this.events[eventName] = this.events[eventName] || [];
this.events[eventName].push(fn);
}
};

咱們將編輯器的實現封裝在hmd.editor這個對象上.

編輯器的模式設置爲GFM.

實現directive

directives.js

1
2
3
4
5
6
7
8
var studio = hmd.studio;

studio.directive('hmdEditor', function () {
return function ($scope, elem) {
//第二個參數爲測試用的本地md文件,由於選擇文件的功能還沒實現.你能夠改爲你電腦上的文件.
hmd.editor.init({el:elem[0]},'E:\\Temp\\test\\test.md');
};
});

 

定義了'hmd-editor,用於綁定hmd.editor的調用.
在視圖模版裏調用hmd-deitor

1
2
3
<div class="content studio-wrap">
<textarea name="" cols="30" rows="10" hmd-editor></textarea>
</div>

 

刷新應用,能夠看到textarea已經變成markdown編輯器,按ctrl+s保存會有簡單的提示.

最終效果

總結

到目前爲止,只是搭建了開發環境,實現了基礎的編輯器功能,還徹底不能真正的使用.
接下來幾篇暫定計劃是:

  • 打開文件,保存新文件,系統設置等基本功能.
  • 自動更新功能.
  • 實時預覽窗口.
  • 自動上傳圖片.
  • 表情功能.
  • 集成hexo命令.

附件

本篇結果打包
github項目地址

相關文章
相關標籤/搜索