package.json
{
"name": "view-koa",
"version": "1.0.0",
"description": "mini-excel example with vue",
"main": "app.js",
"scripts": {
"start": "node --use_strict app.js"
},
"keywords": [
"vue",
"mvvm"
],
"author": "Michael Liao",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/michaelliao/learn-javascript.git"
},
"dependencies": {
"koa": "2.0.0",
"koa-bodyparser": "3.2.0",
"koa-router": "7.0.0",
"mime": "1.3.4",
"mz": "2.4.0"
}
}
static
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="learn javascript by www.liaoxuefeng.com">
<title>Mini Excel</title>
<link rel="stylesheet" href="/static/css/bootstrap.css">
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
<script src="/static/js/vue.js"></script>
<script src="https://cdn.jsdelivr.net/vue.resource/1.0.3/vue-resource.min.js"></script>
<script src="/static/js/excel.js"></script>
<style>
#sheet {
table-layout: fixed;
min-width: auto;
margin-bottom: 0px;
}
#sheet tr>th {
width: 150px;
background-color: #e6e6e6;
}
#sheet tr>th:first-child {
width: 50px;
background-color: #e6e6e6;
}
#sheet tr>td {
width: 150px !important;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: none;
word-wrap: normal;
white-space: nowrap;
}
#sheet tr>td:first-child {
width: 50px;
}
</style>
<script>
var ID = 'S-001';
var COLUMNS = 10;
function createHeader() {
var hdr = [{
row: 0,
col: 0,
text: ''
}];
for (var i=1; i<=COLUMNS; i++) {
hdr.push({
row: 0,
col: i,
text: String.fromCharCode(64 + i)
});
}
return hdr;
}
function createRow(rIndex) {
var row = [{
row: rIndex,
col: 0,
contentEditable: false,
text: '' + rIndex,
align: 'left'
}];
for (var i=1; i<=COLUMNS; i++) {
row.push({
row: rIndex,
col: i,
contentEditable: true,
text: '',
align: 'left'
});
}
return row;
}
function createRows() {
var rows = [];
for (var i=1; i<=100; i++) {
rows.push(createRow(i));
}
return rows;
}
$(function () {
var vm = new Vue({
el: '#sheet',
data: {
title: 'New Sheet',
header: createHeader(),
rows: createRows(),
selectedRowIndex: 0,
selectedColIndex: 0
},
methods: {
open: function () {
var that = this;
that.$resource('/api/sheets/' + ID).get().then(function (resp) {
resp.json().then(function (result) {
that.title = result.title;
that.rows = result.rows;
});
}, function (resp) {
alert('Failed to load.');
});
},
save: function () {
this.$resource('/api/sheets/' + ID).update({
title: this.title,
rows: this.rows
}).then(function (resp) {
console.log('saved ok.');
}, function (resp) {
alert('failed to save.');
});
},
focus: function (cell) {
this.selectedRowIndex = cell.row;
this.selectedColIndex = cell.col;
},
change: function (e) {
var
rowIndex = this.selectedRowIndex,
colIndex = this.selectedColIndex,
text;
if (rowIndex > 0 && colIndex > 0) {
text = e.target.innerText;
this.rows[rowIndex - 1][colIndex].text = text;
}
}
}
});
window.vm = vm;
var setAlign = function (align) {
var
rowIndex = vm.selectedRowIndex,
colIndex = vm.selectedColIndex,
row, cell;
if (rowIndex > 0 && colIndex > 0) {
row = vm.rows[rowIndex - 1];
cell = row[colIndex];
cell.align = align;
}
};
$('#cmd-open').click(function () {
vm.open();
});
$('#cmd-save').click(function () {
vm.save();
});
$('#cmd-left').click(function () {
setAlign('left');
});
$('#cmd-center').click(function () {
setAlign('center');
});
$('#cmd-right').click(function () {
setAlign('right');
});
$('#cmd-download').click(function () {
var
fileName = vm.title + '.xls',
a = document.createElement('a');
a.setAttribute('href', 'data:text/xml,' + encodeURIComponent(makeXls(vm.rows)));
a.setAttribute('download', fileName);
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
$('#toolbar button').focus(function () {
$(this).blur();
});
});
</script>
</head>
<body style="overflow:hidden">
<header class="navbar navbar-static-top">
<div class="container-fluid">
<div class="navbar-header">
<a href="#0" class="navbar-brand">Sheet</a>
</div>
<nav id="toolbar" class="collapse navbar-collapse">
<div class="btn-group">
<button id="cmd-open" type="button" class="btn btn-default navbar-btn"><i class="glyphicon glyphicon-folder-open"></i> Open</button>
<button id="cmd-save" type="button" class="btn btn-default navbar-btn"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
<button id="cmd-download" type="button" class="btn btn-default navbar-btn"><i class="glyphicon glyphicon-save"></i> Download</button>
</div>
<div class="btn-group">
<button id="cmd-left" type="button" class="btn btn-default navbar-btn"><i class="glyphicon glyphicon-align-left"></i></button>
<button id="cmd-center" type="button" class="btn btn-default navbar-btn"><i class="glyphicon glyphicon-align-center"></i></button>
<button id="cmd-right" type="button" class="btn btn-default navbar-btn"><i class="glyphicon glyphicon-align-right"></i></button>
</div>
</nav>
</div>
</header>
<div id="important" style="position:absolute; margin:50px 0 35px 0; left: 0; right: 0; top: 0; bottom: 0; overflow:scroll;">
<table id="sheet" class="table table-bordered">
<thead>
<tr>
<th v-for="cell in header" v-on:click="focus(cell)" v-text="cell.text"></th>
</tr>
</thead>
<tbody>
<tr v-for="tr in rows">
<td v-for="cell in tr" v-on:click="focus(cell)" v-on:blur="change" v-bind:contenteditable="cell.contentEditable" v-bind:style="{ textAlign: cell.align }" v-text="cell.text"></td>
</tr>
</tbody>
</table>
</div>
<footer class="navbar navbar-fixed-bottom" style="background-color:#e7e7e7; height:35px; min-height:35px; overflow:hidden;">
<div class="container-fluid">
<nav class="collapse navbar-collapse">
<p class="text-right" style="padding-top:5px">
<a target="_blank" href="http://www.liaoxuefeng.com">Website</a> -
<a target="_blank" href="https://github.com/michaelliao/learn-javascript">GitHub</a> -
<a target="_blank" href="http://weibo.com/liaoxuefeng">Weibo</a>
This JavaScript course is created by <a target="_blank" href="http://weibo.com/liaoxuefeng">@廖雪峰</a>.
Code licensed <a target="_blank" href="https://github.com/michaelliao/learn-javascript/blob/master/LICENSE">Apache</a>.
</p>
</nav>
</div>
</footer>
</body>
</html>
static-files.js
const path = require('path');
const mime = require('mime');
const fs = require('mz/fs');
function staticFiles(url, dir) {
return async (ctx, next) => {
let rpath = ctx.request.path;
if (rpath.startsWith(url)) {
let fp = path.join(dir, rpath.substring(url.length));
if (await fs.exists(fp)) {
ctx.response.type = mime.lookup(rpath);
ctx.response.body = await fs.readFile(fp);
} else {
ctx.response.status = 404;
}
} else {
await next();
}
};
}
module.exports = staticFiles;
rest.js
module.exports = {
APIError: function (code, message) {
this.code = code || 'internal:unknown_error';
this.message = message || '';
},
restify: (pathPrefix) => {
pathPrefix = pathPrefix || '/api/';
return async (ctx, next) => {
if (ctx.request.path.startsWith(pathPrefix)) {
console.log(`Process API ${ctx.request.method} ${ctx.request.url}...`);
ctx.rest = (data) => {
ctx.response.type = 'application/json';
ctx.response.body = data;
}
try {
await next();
} catch (e) {
console.log('Process API error...');
ctx.response.status = 400;
ctx.response.type = 'application/json';
ctx.response.body = {
code: e.code || 'internal:unknown_error',
message: e.message || ''
};
}
} else {
await next();
}
};
}
};
contriller.js
const fs = require('fs');
// add url-route in /controllers:
function addMapping(router, mapping) {
for (var url in mapping) {
if (url.startsWith('GET ')) {
var path = url.substring(4);
router.get(path, mapping[url]);
console.log(`register URL mapping: GET ${path}`);
} else if (url.startsWith('POST ')) {
var path = url.substring(5);
router.post(path, mapping[url]);
console.log(`register URL mapping: POST ${path}`);
} else if (url.startsWith('PUT ')) {
var path = url.substring(4);
router.put(path, mapping[url]);
console.log(`register URL mapping: PUT ${path}`);
} else if (url.startsWith('DELETE ')) {
var path = url.substring(7);
router.del(path, mapping[url]);
console.log(`register URL mapping: DELETE ${path}`);
} else {
console.log(`invalid URL: ${url}`);
}
}
}
function addControllers(router, dir) {
fs.readdirSync(__dirname + '/' + dir).filter((f) => {
return f.endsWith('.js');
}).forEach((f) => {
console.log(`process controller: ${f}...`);
let mapping = require(__dirname + '/' + dir + '/' + f);
addMapping(router, mapping);
});
}
module.exports = function (dir) {
let
controllers_dir = dir || 'controllers',
router = require('koa-router')();
addControllers(router, controllers_dir);
return router.routes();
};
controllers
api.js
const APIError = require('../rest').APIError;
const path = require('path');
const fs = require('mz/fs');
const saved_dir = path.normalize(__dirname + path.sep + '..' + path.sep + 'saved-docs');
console.log(`documents will be saved in ${saved_dir}.`);
module.exports = {
'GET /api/sheets/:id': async (ctx, next) => {
var s, fp = path.join(saved_dir, '.' + ctx.params.id);
console.log(`load from file ${fp}...`);
s = await fs.readFile(fp, 'utf8');
ctx.rest(JSON.parse(s));
},
'PUT /api/sheets/:id': async (ctx, next) => {
var
fp = path.join(saved_dir, '.' + ctx.params.id),
title = ctx.request.body.title,
rows = ctx.request.body.rows,
data;
if (!title) {
throw new APIError('invalid_data', 'invalid title');
}
if (!Array.isArray(rows)){
throw new APIError('invalid_data', 'invalid rows');
}
data = {
title: title,
rows: rows
};
await fs.writeFile(fp, JSON.stringify({
title: title,
rows: rows
}), 'utf8');
console.log(`wrote to file ${fp}.`);
ctx.rest({
id: ctx.params.id
});
}
}
app.js
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const controller = require('./controller');
const rest = require('./rest');
const app = new Koa();
const isProduction = process.env.NODE_ENV === 'production';
// log request URL:
app.use(async (ctx, next) => {
console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
var
start = new Date().getTime(),
execTime;
await next();
execTime = new Date().getTime() - start;
ctx.response.set('X-Response-Time', `${execTime}ms`);
});
// static file support:
let staticFiles = require('./static-files');
app.use(staticFiles('/static/', __dirname + '/static'));
app.use(async (ctx, next) => {
if (ctx.request.path === '/') {
ctx.response.redirect('/static/index.html');
} else {
await next();
}
});
// parse request body:
app.use(bodyParser());
// bind .rest() for ctx:
app.use(rest.restify());
// add controllers:
app.use(controller());
app.listen(3000);
console.log('app started at port 3000...');