Render table as static html under fixed URL (fixes #58)

pull/108/head
Candid Dauth 2017-03-01 20:02:57 +01:00
rodzic 68e2cbdad1
commit 79588d05e1
14 zmienionych plików z 218 dodań i 144 usunięć

Wyświetl plik

@ -39,11 +39,14 @@
<ul uib-dropdown-menu aria-labelledby="toolbox-layers-dropdown" class="dropdown-menu-right">
<!--<li ng-if="!readonly"><a href="javascript:" ng-click="openDialog('copy-pad-dialog')">Copy pad</a></li>-->
<li><a href="javascript:" ng-click="importFile()">Import file</a></li>
<li ng-if="!readonly && padId"><a href="javascript:" ng-click="showHistory()">Show edit history</a></li>
<li ng-if="padId"><a href="{{padData.id}}/gpx?useTracks=1">Export as GPX (tracks)</a></li>
<li ng-if="padId"><a href="{{padData.id}}/gpx?useTracks=0">Export as GPX (routes)</a></li>
<li ng-if="padId"><a href="{{padData.id}}/table" target="_blank">Export as table</a></li>
<li ng-if="padId" role="separator" class="divider"></li>
<li ng-if="padId"><a href="javascript:" ng-click="filter()">Filter</a></li>
<li ng-if="padId"><a href="{{padId}}/gpx?useTracks=1">Export GPX</a></li>
<li ng-if="padId"><a href="javascript:" ng-click="showTable()">View as table</a></li>
<li ng-if="!readonly && padId"><a href="javascript:" ng-click="editPadSettings()">Settings</a></li>
<li ng-if="!readonly && padId"><a href="javascript:" ng-click="showHistory()">Show edit history</a></li>
<li ng-if="padId" role="separator" class="divider"></li>
<li><a href="javascript:" ng-click="showAbout()">About FacilMap</a></li>
</ul>
</li>

Wyświetl plik

@ -1,7 +1,7 @@
import fm from '../../app';
import $ from 'jquery';
fm.app.factory("fmMapToolbox", function($compile, fmTable, fmFilter) {
fm.app.factory("fmMapToolbox", function($compile, fmFilter) {
return function(map) {
var scope = map.socket.$new();
@ -32,10 +32,6 @@ fm.app.factory("fmMapToolbox", function($compile, fmTable, fmFilter) {
scope.editObjectTypes = map.typesUi.editTypes.bind(map.typesUi);
scope.showTable = function() {
fmTable.showTable(map.socket.padId);
};
scope.importFile = function() {
map.importUi.openImportDialog();
};

Wyświetl plik

@ -1,7 +0,0 @@
table th.sort-none,table th.sort-up {
cursor: s-resize;
}
table th.sort-down {
cursor: n-resize;
}

Wyświetl plik

@ -1,44 +0,0 @@
<div class="modal-header">
<button type="button" class="close" ng-click="$dismiss()"><span aria-hidden="true">&times;</span></button>
<h3 class="modal-title">Table</h3>
</div>
<div class="modal-body">
<h2 ng-repeat-start="type in types">{{type.name}}</h2>
<table ng-if="type.type == 'marker'" class="table table-striped table-bordered table-condensed">
<thead>
<tr>
<th class="sort" ng-click="sort(type, '__name')" ng-class="getSortClass(type, '__name')">Name <span ng-class="getSortIcon(type, '__name')"></span></th>
<th>Position</th>
<th class="sort" ng-repeat="field in type.fields" ng-click="sort(type, field.name)" ng-class="getSortClass(type, field.name)">{{field.name}} <span ng-class="getSortIcon(type, field.name)"></span></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="marker in markers | fmType:type.id | orderBy : getSortField(type) : sortOrder[type.id]">
<td>{{marker.name}}</td>
<td>{{marker.lat}},{{marker.lon}}</td>
<td ng-repeat="field in type.fields" fm-type-field-content="field" fm-type-field-model="marker.data[field.name]"></td>
</tr>
</tbody>
</table>
<table ng-repeat-end ng-if="type.type == 'line'" class="table table-striped table-bordered table-condensed">
<thead>
<tr>
<th class="sort" ng-click="sort(type, '__name')" ng-class="getSortClass(type, '__name')">Name <span ng-class="getSortIcon(type, '__name')"></span></th>
<th class="sort" ng-click="sort(type, '__distance')" ng-class="getSortClass(type, '__distance')">Distance <span ng-class="getSortIcon(type, '__distance')"></span></th>
<th class="sort" ng-click="sort(type, '__time')" ng-class="getSortClass(type, '__time')">Time <span ng-class="getSortIcon(type, '__time')"></span></th>
<th class="sort" ng-repeat="field in type.fields" ng-class="getSortClass(type, field.name)" ng-click="sort(type, field.name)">{{field.name}} <span ng-class="getSortIcon(type, field.name)"></span></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="line in lines | fmType:type.id | orderBy : getSortField(type) : sortOrder[type.id]">
<td>{{line.name}}</td>
<td>{{line.distance | fmRound:2}} km</td>
<td><span ng-show="line.time != null">{{line.time | fmFormatTime}} h {{line.mode | fmRoutingMode}}</span></td>
<td ng-repeat="field in type.fields" fm-type-field-content="field" fm-type-field-model="line.data[field.name]"></td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" ng-click="$dismiss()">Close</button>
</div>

Wyświetl plik

@ -1,79 +0,0 @@
import fm from '../app';
import $ from 'jquery';
fm.app.filter("fmType", function() {
return function(input, typeId) {
var res = [ ];
angular.forEach(input, function(it) {
if(it.typeId == typeId)
res.push(it);
});
return res;
};
});
fm.app.factory("fmTable", function(fmSocket, $rootScope, $uibModal) {
return {
showTable : function(padId) {
var socket = fmSocket(padId);
socket.updateBbox({ top: 90, left: -180, right: 180, bottom: -90, zoom: 0 });
$uibModal.open({
template: require("./table.html"),
scope: socket,
controller: "fmTableCtrl",
size: "fs"
});
}
};
});
fm.app.controller("fmTableCtrl", function($scope, fmTypeFields) {
function _getField(type, fieldName) {
for(var i=0; i<type.fields.length; i++) {
if(type.fields[i].name == fieldName)
return type.fields[i];
}
}
function _normaliseNumbers(text) { // Pads any number in the string with zeros, so that numbers have a unified length and can be string-sorted
return (text ? ""+text : "").trim().toLowerCase().replace(/\d+/g, function(m) { return ("000000000"+m).slice(-10) });
}
$scope.sort = function(type, field) {
$scope.sortOrder[type.id] = (($scope.sortField[type.id] == null ? "__name" : $scope.sortField[type.id]) == field ? !$scope.sortOrder[type.id] : false);
$scope.sortField[type.id] = field;
};
$scope.getSortField = function(type) {
var f = $scope.sortField[type.id];
if(f == null || f == "__name" || f == "__distance" || f == "__time")
return function(it) { return _normaliseNumbers(it[f ? f.replace(/^__/, "") : "name"]) };
else
return function(it) { return _normaliseNumbers($("<div/>").append(fmTypeFields.formatField(_getField(type, f), it.data[f])).text()); };
};
$scope.getSortClass = function(type, fieldName) {
if(($scope.sortField[type.id] == null ? "__name" : $scope.sortField[type.id]) == fieldName) {
return $scope.sortOrder[type.id] ? "sort-up" : "sort-down";
} else {
return "sort-none";
}
};
$scope.getSortIcon = function(type, fieldName) {
if(($scope.sortField[type.id] == null ? "__name" : $scope.sortField[type.id]) == fieldName) {
return {
'glyphicon': true,
'glyphicon-triangle-bottom': !$scope.sortOrder[type.id],
'glyphicon-triangle-top': $scope.sortOrder[type.id]
};
} else {
return { };
}
};
$scope.sortField = { };
$scope.sortOrder = { };
});

Wyświetl plik

@ -74,7 +74,7 @@ gulp.task("webpack", [ "icons" ], function() {
return Promise.denodeify(fs.unlink)(staticFrontendFile);
}).then(() => {
// Create symlink with fixed file name so that people can include https://facilmap.org/frontend.js
return Promise.denodeify(fs.symlink)(`frontend-${stats.hash}.js`, `${__dirname}/build/frontend.js`);
return Promise.denodeify(fs.symlink)(`frontend-index-${stats.hash}.js`, `${__dirname}/build/frontend.js`);
});
});
});

Wyświetl plik

@ -46,7 +46,8 @@
"leaflet.markercluster": "^1.0.3",
"linkifyjs": "^2.1.3",
"marked": "^0.3.6",
"osmtogeojson": "^2.2.12"
"osmtogeojson": "^2.2.12",
"tablesorter": "^2.28.5"
},
"devDependencies": {
"babel-core": "^6.21.0",

Wyświetl plik

@ -0,0 +1,7 @@
h2[aria-expanded=true] .glyphicon-chevron-right {
transform: rotate(90deg);
}
table.collapse.in {
display: table;
}

Wyświetl plik

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><%=padData.name%> – FacilMap</title>
<base href="../" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<%
if(!padData || padData.searchEngines) {
%>
<meta name="robots" content="index,nofollow" />
<meta name="description" content="<%= padData && padData.description || "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration." %>" />
<%
} else {
%>
<meta name="robots" content="noindex,nofollow" />
<%
}
%>
<link rel="shortcut icon" href="../static/favicon.png">
</head>
<body>
<div class="container-fluid">
<h1><%=padData.name%> – FacilMap</h1>
<%
for(let type of Object.values(types)) {
%>
<h2 role="button" data-toggle="collapse" data-target="#type-<%=type.id%>" aria-expanded="true" aria-controls="type-<%=type.id%>"><small><span class="glyphicon glyphicon-chevron-right hidden-print"></span></small> <%=type.name%></h2>
<table id="type-<%=type.id%>" class="collapse in table table-striped table-bordered table-condensed tablesorter" data-sortlist="[[0,0]]">
<thead>
<tr>
<th>Name</th>
<%
if(type.type == "marker") {
%>
<th data-sorter="false">Position</th>
<%
} else {
%>
<th>Distance</th>
<th>Time</th>
<%
}
for(let field of type.fields) {
%>
<th><%=field.name%></th>
<%
}
%>
</tr>
</thead>
<tbody>
<%
for(let object of type.type == "marker" ? type.markers : type.lines) {
%>
<tr>
<td><%=object.name%></td>
<%
if(type.type == "marker") {
%>
<td><%=object.lat%>,<%=object.lon%></td>
<%
} else {
%>
<td><%=format.round(object.distance, 2)%> km</td>
<td><% if(object.time != null) { %><%=format.formatTime(object.time)%> h <%=format.routingMode(object.mode)%><% } %></td>
<%
}
for(let field of type.fields) {
%>
<td><%-format.formatField(field, object.data[field.name])%></td>
<%
}
%>
</tr>
<%
}
%>
</tbody>
</table>
<%
}
%>
</div>
</body>
</html>

Wyświetl plik

@ -0,0 +1,27 @@
import 'bootstrap';
import $ from 'jquery';
import 'tablesorter/dist/js/jquery.tablesorter';
import 'tablesorter/dist/js/widgets/widget-uitheme.min.js';
import 'tablesorter/dist/css/theme.bootstrap_3.min.css';
import './table.css';
// Dereferrer
$(document).on("click", "a", function(e) {
var el = $(this);
var href = el.attr("href");
if(href && href.match(/^\s*(https?:)?\/\//i)) {
el.attr("href", "deref.html?"+encodeURIComponent(href));
setTimeout(function() {
el.attr("href", href);
}, 0);
}
});
$(document).ready(() => {
$("table.tablesorter").tablesorter({
theme: "bootstrap",
headerTemplate: "{content} {icon}",
widgets: ["uitheme"]
});
});

Wyświetl plik

@ -34,9 +34,12 @@ for(let i in addDeps) {
}
module.exports = {
entry: __dirname + "/index/index.js",
entry: {
index: __dirname + "/index/index.js",
table: __dirname + "/table/table.js"
},
output: {
filename: "frontend-[hash].js",
filename: "frontend-[name]-[hash].js",
path: __dirname + "/build/"
},
resolve: {
@ -72,7 +75,13 @@ module.exports = {
}),
new htmlPlugin({
template: `${__dirname}/index/index.ejs`,
filename: "index.ejs"
filename: "index.ejs",
chunks: ["index"]
}),
new htmlPlugin({
template: `${__dirname}/table/table.ejs`,
filename: "table.ejs",
chunks: ["table"]
}),
new webpack.ProvidePlugin({
$: "jquery",

Wyświetl plik

@ -2538,7 +2538,7 @@ jquery-ui@^1.12.1:
version "1.12.1"
resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.12.1.tgz#bcb4045c8dd0539c134bc1488cdd3e768a7a9e51"
jquery@<3.0.0, jquery@>=1.9.0:
jquery@<3.0.0, jquery@>=1.2.6, jquery@>=1.9.0:
version "2.2.4"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.4.tgz#2c89d6889b5eac522a7eea32c14521559c6cbf02"
@ -4478,6 +4478,12 @@ svgo@^0.7.0, svgo@^0.7.1:
sax "~1.2.1"
whet.extend "~0.9.9"
tablesorter@^2.28.5:
version "2.28.5"
resolved "https://registry.yarnpkg.com/tablesorter/-/tablesorter-2.28.5.tgz#94b6f8326e9ff8b001ae7cf57804476caf8b3437"
dependencies:
jquery ">=1.2.6"
tapable@^0.2.5, tapable@~0.2.5:
version "0.2.6"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d"

44
server/table.js 100644
Wyświetl plik

@ -0,0 +1,44 @@
const ejs = require("ejs");
const fs = require("fs");
const Promise = require("promise");
const commonUtils = require("facilmap-frontend/common/utils");
const commonFormat = require("facilmap-frontend/common/format");
const utils = require("./utils");
const table = module.exports = {
createTable(database, padId, template) {
return utils.promiseAuto({
padData: database.getPadData(padId),
types: () => {
var types = { };
return utils.streamEachPromise(database.getTypes(padId), function(type) {
types[type.id] = type;
type.markers = [];
type.lines = [];
}).then(() => types);
},
markers: (types) => {
return utils.streamEachPromise(database.getPadMarkers(padId), function(marker) {
types[marker.typeId].markers.push(marker);
});
},
lines: (types) => {
return utils.streamEachPromise(database.getPadLines(padId), function(line) {
types[line.typeId].lines.push(line);
});
}
}).then((results) => {
return ejs.render(template, {
padData: results.padData,
types: results.types,
utils: commonUtils,
format: commonFormat
})
})
}
};

Wyświetl plik

@ -8,6 +8,7 @@ const Promise = require("promise");
const database = require("./database/database");
const gpx = require("./gpx");
const table = require("./table");
const utils = require("./utils");
const frontendPath = path.dirname(require.resolve("facilmap-frontend/package.json")); // Do not resolve main property
@ -80,6 +81,7 @@ const webserver = module.exports = {
app.get("/", padMiddleware);
app.get("/index.ejs", padMiddleware);
app.get("/table.ejs", padMiddleware);
app.use(staticMiddleware);
@ -104,6 +106,27 @@ const webserver = module.exports = {
}).catch(next);
});
app.get("/:padId/table", function(req, res, next) {
Promise.resolve().then(() => {
if (process.env.FM_DEV) {
let intercept = utils.interceptWriteStream(res);
req.url = req.originalUrl = "/table.ejs";
staticMiddleware(req, res, next);
return intercept;
} else {
// We don't want express.static's ETag handling, as it sometimes returns an empty template,
// so we have to read it directly from the file system
return Promise.denodeify(fs.readFile)(`${frontendPath}/build/table.ejs`, "utf8");
}
}).then((template) => {
return table.createTable(database, req.params.padId, template);
}).then((renderedTable) => {
res.type("html");
res.send(renderedTable);
}).catch(next);
});
let server = http.createServer(app);
return Promise.denodeify(server.listen.bind(server))(port, host).then(() => server);
}