<!DOCTYPE HTML>
<html>
<head>
<link rel="stylesheet" href="codebase/skins/contrast.css" type="text/css">
<link rel="stylesheet" href="css/main.css" type="text/css">
<script src="codebase/webix.js" type="text/javascript"></script>
<script src="codebase/i18n/en.js" type="text/javascript"></script>
<script src="codebase/i18n/ru.js" type="text/javascript"></script>
</head>
<body>
<script src="bundle.js" type="text/javascript"></script>
</body>
</html>
$ node -v
и $ npm -v
проверьте корректность установки Node.js и пакетного менеджера платформы — NPM.var translations = {
// English
"en-US": {
localeName: "en-US",
headerTitle: "Data master",
resetFilters: "Reset filters",
changeLocale: "Change language:",
loadData: "Load data",
addRow: "Add row",
clearSelection: "Clear selection",
deleteRow: "Delete row",
saveData: "Save data",
title: "Title",
noItemSelected: "No item selected",
dataSaved: "Data saved",
reservedButton: "Reserved botton"
},
// Russian
"ru-RU": {
localeName: "ru-RU",
headerTitle: "Мастер данных",
resetFilters: "Сбросить фильтры",
changeLocale: "Сменить язык:",
loadData: "Загрузить данные",
addRow: "Добавить ряд",
clearSelection: "Снять выделение",
deleteRow: "Удалить ряд",
saveData: "Сохранить",
title: "Название",
noItemSelected: "Нет выбранных рядов",
dataSaved: "Данные сохранены",
reservedButton: "Зарезервировано..."
}
};
var defaultLocale = "en-US";
// object from translations.js
var localizator = translations[defaultLocale];
/**
* Get data from backend and fill datatable grid
*/
function getData() {
$$("dataFromBackend").clearAll();
$$("dataFromBackend").load("http://localhost/data_master/data/data.php");
}
/**
* Add new row to datatable
*/
function addRow() {
$$("dataFromBackend").add(
{
title: "-----",
content: "-----",
place: "-----"
//date: "-----",
//priority: "-----"
}
);
}
/**
* Reset selection in datatable grid
*/
function clearSelection() {
$$("dataFromBackend").unselectAll();
}
/**
* Delete selected row
*/
function deleteRow() {
if (!$$("dataFromBackend").getSelectedId()) {
webix.alert(localizator.noItemSelected);
return;
}
//removes the selected item
$$("dataFromBackend").remove($$("dataFromBackend").getSelectedId());
}
/**
* Save data to backend from datatable grid
*/
function saveData() {
var grid = $$("dataFromBackend");
var serializedData = grid.serialize();
webix.ajax().post("http://localhost/data_master/data/save.php", {data: serializedData});
webix.alert(localizator.dataSaved);
}
/**
* Reset filters settings
*/
function resetFilters() {
$$("dataFromBackend").getFilter("title").value = null;
$$("dataFromBackend").getFilter("content").value = null;
$$("dataFromBackend").getFilter("place").value = null;
$$("dataFromBackend").getFilter("date").value = null;
$$("dataFromBackend").getFilter("priority").value = null;
// reload grid
$$("dataFromBackend").clearAll();
$$("dataFromBackend").load("http://localhost/data_master/data/data.php");
}
/**
* Change translation to selected
*/
function changeLocale(locale) {
localizator = translations[locale];
$$("headerContainer").define("template", localizator.headerTitle);
$$("headerContainer").refresh();
$$("resetFiltersContainer").define("value", localizator.resetFilters);
$$("resetFiltersContainer").refresh();
$$("changeLocale").define("label", localizator.changeLocale);
$$("changeLocale").refresh();
$$("loadData").define("value", localizator.loadData);
$$("loadData").refresh();
$$("addRow").define("value", localizator.addRow);
$$("addRow").refresh();
$$("clearSelection").define("value", localizator.clearSelection);
$$("clearSelection").refresh();
$$("deleteRow").define("value", localizator.deleteRow);
$$("deleteRow").refresh();
$$("saveData").define("value", localizator.saveData);
$$("saveData").refresh();
$$("reservedButton").define("value", localizator.reservedButton);
$$("reservedButton").refresh();
webix.i18n.setLocale(locale);
}
/**
* Function for reserved button
*/
function reservedButton() {
// your code...
}
/**
* Create object with type "Button"
*
* @constructor
*/
function Button(id, value, type, width, onClickFunction) {
this.view = "button";
this.id = id;
this.value = value;
this.type = type;
this.width = width;
this.on = {
"onItemClick": function(){
onClickFunction();
}
}
}
/**
* Create main layout
*/
webix.ui({
view: "layout",
id: "page",
rows:[
{
cols: [
{
view:"icon",
id: "headerIconContainer",
icon:"calendar"
},
{
view:"template",
id: "headerContainer",
type:"header",
template:"Data master"
},
new Button("resetFiltersContainer", "Reset filters", "form", 150, resetFilters),
{
id: "divider",
width: 20
},
{
view: "combo",
id: "changeLocale",
label: 'Change locale:',
labelWidth: 130,
width: 230,
align: "right",
value: "en-US",
options: [
"ru-RU",
"en-US"
],
on: {
"onChange": function(newv, oldv) {
changeLocale(newv);
}
}
}
]
},
{
view: "datatable",
id: "dataFromBackend",
columns: [
{
id: "title",
header: [
{
text: "<b>Title</b>"
},
{
content: "textFilter"
}
],
editor: "text",
fillspace: 2
},
{
id: "content",
header: [
{
text: "<b>Content</b>"
},
{
content: "textFilter"
}
],
editor: "popup",
fillspace: 8
},
{
id: "place",
header: [
{
text: "<b>Place</b>"
},
{
content: "textFilter"
}
],
editor: "text",
fillspace: 2
},
{
id: "date",
header: [
"<b>Date</b>",
{
content: "dateFilter"
}
],
editor: "date",
map: "(date)#date#",
format: webix.Date.dateToStr("%d.%m.%Y"),
fillspace: 2
},
{
id: "priority",
header: [
"<b>Priority</b>",
{
content: "selectFilter"
}
],
editor: "select",
options: [1, 2, 3, 4, 5],
fillspace: 1
}
],
editable: true,
select: "row",
multiselect: true,
// initial data load
data: webix.ajax().post("http://localhost/electron_with_backend/data/data.php")
},
{
view: "layout",
id: "buttonContainer",
height: 50,
cols: [
// Webix ui.button structure example:
/*{
view: "button",
id: "loadData",
value: "Load data",
type: "form",
width: 150,
on: {
"onItemClick": function(id, e, trg){
getData();
}
}
},*/
new Button("loadData", "Load data", "form", 150, getData),
new Button("addRow", "Add row", "form", 150, addRow),
new Button("clearSelection", "Clear selection", "form", 150, clearSelection),
new Button("deleteRow", "Delete row", "form", 150, deleteRow),
new Button("saveData", "Save data", "form", 150, saveData),
new Button("reservedButton", "Reserved button", "form", 150, reservedButton),
{}
]
}
]
});
$$("buttonContainer").define("css", "buttonContainerClass");
$$("resetFiltersContainer").define("css", "resetFiltersContainerClass");
$$("headerIconContainer").define("css", "headerIconContainerClass");
$$("headerContainer").define("css", "headerContainerClass");
$$("changeLocale").define("css", "changeLocaleClass");
$$("divider").define("css", "dividerClass");
{
view: "button",
id: "loadData",
value: "Load data",
type: "form",
width: 150,
on: {
"onItemClick": function(id, e, trg){
getData();
}
}
new Button("loadData", "Load data", "form", 150, getData)
Кстати, я добавил зарезервированную кнопку для лучшего UX в скомпилированном приложении. Функциональности для нее я не придумал, поэтому можете использовать ее, как вам вздумается.$$("buttonContainer").define("css", "buttonContainerClass")
Таким способом мы можем определять и изменять свойства элементов (в примере: добавление атрибута класс со значением «buttonContainerClass»). Способ, приведенный здесь, указан для наглядности. Мы можем изначально инициализировать объект каким либо классом, присвоив значение свойству «css».<?php
$dataFromFile = json_decode(file_get_contents("data.json"));
echo json_encode($dataFromFile);
/*$example_json_data = array(
array (title => "My Fair Lady", year => 1964, votes => 533848, rating => 8.9, rank => 5),
array (title => "Film 1", year => 1984, votes => 933848, rating => 6.9, rank => 4),
array (title => "Film 2", year => 1966, votes => 53848, rating => 4.3, rank => 5),
array (title => "Film 3", year => 1975, votes => 567848, rating => 2.9, rank => 2),
array (title => "Film 4", year => 1981, votes => 433788, rating => 6.3, rank => 1)
);*/
//echo json_encode($example_json_data);
<?php
$data = $_POST["data"];
file_put_contents("data.json", $data);
[
{"title":"My Fair Lady", "year":1964, "votes":533848, "rating":8.9, "rank":5},
{"title":"Film 1", "year":1984, "votes":933848, "rating":6.9, "rank":4},
{"title":"Film 2", "year":1966, "votes":53848, "rating":4.3, "rank":5},
{"title":"Film 3", "year":1975, "votes":567848, "rating":2.9, "rank":2},
{"title":"Film 4", "year":1981, "votes":433788, "rating":6.3, "rank":1}
]
{
"name": "data_master",
"description": "Simple ToDo list with desktop building",
"version": "0.0.1",
"homepage": "https://github.com/paratagas/data_master",
"repository": {
"type": "git",
"url": "git+https://github.com/paratagas/data_master.git"
},
"author": {
"name": "Yauheni Svirydzenka",
"email": "partagas@mail.ru",
"url": "https://github.com/paratagas"
},
"tags": [
"node.js",
"webix",
"electron",
"ToDo list"
],
"main": "main.js",
"scripts": {
"start": "electron .",
"package": "electron-packager ./ DataMaster --all --out ~/release/DataMaster --overwrite"
},
"dependencies": {
"electron-prebuilt": "^0.35.6",
"electron-packager": "^8.4.0"
},
"devDependencies": {
"gulp": "^3.9.0",
"gulp-concat": "^2.6.0",
"gulp-uglify": "^1.2.0",
"gulp-sourcemaps": "^1.5.2"
},
"license": "GPL-3.0"
}
$ npm install
для загрузки необходимых компонентов. В файле gulpfile.js в корне проекта зададим настройки нашей сборки.
var gulp = require('gulp'),
uglify = require('gulp-uglify'),
concat = require('gulp-concat');
// to create source mapping
sourcemaps = require('gulp-sourcemaps');
/*
* Collect all js files to one bundle script
* Command: "gulp bundle"
*/
gulp.task('bundle', function() {
// choose any files in directories and it's subfolders
return gulp.src('js/**/*.js')
.pipe(sourcemaps.init())
.pipe(concat('bundle.js'))
.pipe(sourcemaps.write('./'))
//.pipe(uglify())
// output result to current directory
.pipe(gulp.dest('./'));
});
/*
* Watch js files changing and run task
* Command: "gulp watch"
*/
gulp.task('watch', function () {
gulp.watch('./js/**/*.js', ['bundle']);
});
$ gulp bundle
в корне проекта. В процессе разработки команда $ gulp watch
позволяет отслеживать изменения js файлов и при наличии таковых выполнять команду $ gulp bundle
.$ npm install --save-dev electron-prebuilt
. В свою очередь, модуль «electron-packager» позволяет компилировать приложения для целевой платформы или для всех возможных платформ. Отдельно устанавливается командой $ npm install --save-dev electron-packager
. "scripts": {
"start": "electron .",
"package": "electron-packager ./ DataMaster --all --out ~/release/DataMaster --overwrite"
},
$ npm start
, а компиляцию — командой $ npm run-script package
. Кстати, если мы изменим команду package, например, на "package": "electron-packager ./ DataMaster --win32-x64 --out ~/release/DataMaster --overwrite"
то приложение будет скомпилировано для целевой платформы — в нашем случае Windows x64. На данный момент Electron поддерживает платформы: Windows x32/x64, Linux x32/x64/armv7, OS X/x64. Для более полного понимания можно глянуть документацию.
/*
* Commands:
* npm init - initialize npm in current directory
* npm install - install modules
* npm install --save-dev electron-prebuilt - install module for pred-build
* npm install --save-dev electron-packager - install module for build
* npm start - to start app
* npm run-script package - to compile app
*/
const electron = require('electron');
// lifecycle of our app
const app = electron.app;
// create window for our app
const BrowserWindow = electron.BrowserWindow;
// To send crash reports to Electron support
// electron.crashReporter.start();
// set global link
// if not, the window will be closed after garbage collection
var mainWindow = null;
/**
* Check that all windows are closed before quiting app
*/
app.on('window-all-closed', function() {
// OS X apps are active before "Cmd + Q" command. Close app
if (process.platform != 'darwin') {
app.quit();
}
});
/**
* Create main window menu
*/
function createMenu() {
var Menu = electron.Menu;
var menuTemplate = [
{
label: 'File',
submenu: [
{
label: 'New window',
click: function() {
createSubWindow();
}
},
{type: "separator"},
{
label: 'Exit',
click: function() {
app.quit();
}
}
]
},
{
label: 'Edit',
submenu: [
{
label: 'Cut',
role: 'cut'
},
{
label: 'Copy',
role: 'copy'
},
{
label: 'Paste',
role: 'paste'
}
]
},
{
label: 'About',
submenu: [
{
label: 'Name',
click: function() {
console.log(app.getName());
}
},
{
label: 'Version',
click: function() {
console.log(app.getVersion());
}
},
{
label: 'About',
click: function() {
console.log('ToDo list');
}
}
]
},
{
label: 'Help',
submenu: [
{
label: 'Node.js docs',
click: function() {
require('electron').shell.openExternal("https://nodejs.org/api/");
}
},
{
label: 'Webix docs',
click: function() {
require('electron').shell.openExternal("http://docs.webix.com/");
}
},
{
label: 'Electron docs',
click: function() {
require('electron').shell.openExternal("http://electron.atom.io/docs/all");
}
}
]
}
];
var menuItems = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menuItems);
}
/**
* Create main window
*/
function createMainWindow() {
mainWindow = new BrowserWindow({
title: "Data master",
resizable: false,
width: 910,
height: 800,
// set path to icon for compiled app
icon: 'resources/app/img/icon.png',
// set path to icon for launched app
//icon: 'img/icon.png'
center: true
// to open dev console: The first way
//devTools: true
});
createMenu();
// load entry point for desktop app
mainWindow.loadURL('file://' + __dirname + '/index.html');
// to open dev console: The second way
//mainWindow.webContents.openDevTools();
// Close all windows when main window is closed
mainWindow.on('closed', function() {
mainWindow = null;
newWindow = null;
});
}
/**
* Create sub menu window
*/
function createSubWindow() {
newWindow = new BrowserWindow({
title: "Go to GitHub",
resizable: false,
// imitate mobile device
width: 360,
height: 640,
icon: 'resources/app/img/mobile.png',
center: true
});
newWindow.loadURL("https://github.com/");
newWindow.on('closed', function() {
newWindow = null;
});
}
/**
* When Electron finish initialization and is ready to create browser window
*/
app.on('ready', function() {
createMainWindow();
});
mainWindow.loadURL('file://' + __dirname + '/index.html')
. В нашем случае это файл «index.html» в корне проекта. В конце выражением mainWindow = null
удаляем ссылку на окно, так как если приложение поддерживает несколько окон, то нужно ловить момент когда следует удалить соответствующий элемент. Закрытие основного окна приложения в нашем случае закрывает (присваивает null) дочернее окно. В настройках также можно задать иконку готового десктоп-приложения. Для этого указываем icon: 'resources/app/img/icon.png'
, где «resources/app» — место, где хранится исходный код в уже скомпилированном варианте приложения.File > New window
я добавил новое окно. Оно имитирует просмотр контента на мобильном устройстве и открывает страницу GitHub. Можно задать стартовый URL для нового окна и в нашем веб-приложении, создав таким образом еще одну точка входа, если, например, требуется обособить какой-либо функционал.$ npm run-script package
и в "~/release/DataMaster" появляются готовые приложения под различные платформы.
const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
var cors = require('cors');
var path = require("path");
const app = express();
const port = 3000;
// use to parse json data
app.use(bodyParser.json());
// use to create cross-domain requests (CORS)
app.use(cors());
// create path aliases to use them in index.html file
// otherwise the assets in it will not work and icons will not be shown
// scheme:
// app.use('/my_path_alias', express.static(path.join(__dirname, '/path_to_where/my_assets_are')));
app.use('/css', express.static(path.join(__dirname, '/css')));
app.use('/skins', express.static(path.join(__dirname, '/codebase/skins')));
app.use('/bundle', express.static(path.join(__dirname, '/')));
app.use('/codebase', express.static(path.join(__dirname, '/codebase')));
app.use('/i18n', express.static(path.join(__dirname, '/codebase/i18n')));
app.use('/fonts', express.static(path.join(__dirname, '/codebase/fonts')));
const filePath = __dirname + '/data/';
const fileName = "data.json";
/**
* Get index page
*
* @param {string} URL
* @param {function} Callback
*/
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname + '/index.html'));
});
/**
* Send GET request to get data
*
* @param {string} URL
* @param {function} Callback
*/
app.get('/data', (req, res) => {
const options = {
root: filePath
};
res.sendFile(fileName, options, function (err) {
if (err) {
console.log('Error:', err);
} else {
console.log('Received:', fileName);
}
});
});
/**
* Send POST request to save data
*
* @param {string} URL
* @param {function} Callback
*/
app.post('/data', (req, res) => {
// use JSON.stringify() 2nd and 3rd param to create pretty JSON data
// remove them for minified JSON
fs.writeFile(filePath + fileName, JSON.stringify(req.body, null, 4), 'utf-8', (err) => {
if (err) {
console.log('Error:', err);
}
res.status(200).send(req.body);
});
});
/**
* Listen to server with specified port
*
* @param {string} Port
* @param {function} Callback
*/
app.listen(port, () => {
// open browser on http://localhost:3000
console.log('Server is running on http://localhost:' + port);
});
http://localhost:3000
, мне пришлось изменить пути в файлах «js/logic.js» и «js/structure.js». И здесь я столкнулся с первой проблемой. Значение параметра HTTP-заголовка «Content-type» в Webix запросах вида «webix.ajax().post()» по-умолчанию: «application/x-www-form-urlencoded». Это не позволяло правильно обработать наши данные для сохранения в файле «data/data.json». Даже передача заголовков с сервера Express с помощью «app.set()» не работала. Решил с помощью передачи заголовка непосредственно в запрос:
webix.ajax().headers({
"Content-Type": "application/json"
}).post("http://localhost:3000/data", {data: serializedData});
app.use(cors());
.<!DOCTYPE HTML>
<html>
<head>
<link rel="stylesheet" href="skins/contrast.css" type="text/css">
<link rel="stylesheet" href="css/main.css" type="text/css">
<script src="codebase/webix.js" type="text/javascript"></script>
<script src="i18n/en.js" type="text/javascript"></script>
<script src="i18n/ru.js" type="text/javascript"></script>
</head>
<body>
<script src="bundle/bundle.js" type="text/javascript"></script>
</body>
</html>
app.use('/css', express.static(path.join(__dirname, '/css')));
app.use('/skins', express.static(path.join(__dirname, '/codebase/skins')));
app.use('/bundle', express.static(path.join(__dirname, '/')));
app.use('/codebase', express.static(path.join(__dirname, '/codebase')));
app.use('/i18n', express.static(path.join(__dirname, '/codebase/i18n')));
app.use('/fonts', express.static(path.join(__dirname, '/codebase/fonts')));
<link rel="stylesheet" href="skins/contrast.css" type="text/css">
<link rel="stylesheet" href="codebase/skins/contrast.css" type="text/css">
$ npm run nodemon
$ npm run server
К сожалению, не доступен сервер mySQL