aboutsummaryrefslogtreecommitdiff
path: root/src_js
diff options
context:
space:
mode:
Diffstat (limited to 'src_js')
-rw-r--r--src_js/opcut/common.js69
-rw-r--r--src_js/opcut/grid.js186
-rw-r--r--src_js/opcut/validators.js56
-rw-r--r--src_js/opcut/vt.js123
4 files changed, 320 insertions, 114 deletions
diff --git a/src_js/opcut/common.js b/src_js/opcut/common.js
index 3f39115..89088ac 100644
--- a/src_js/opcut/common.js
+++ b/src_js/opcut/common.js
@@ -1,16 +1,77 @@
+import * as URI from 'uri-js';
+import iziToast from 'izitoast';
+import r from 'opcut/renderer';
+import * as u from 'opcut/util';
-export function submit() {
+const calculateUrl = URI.resolve(window.location.href, './calculate');
+const generateOutputUrl = URI.resolve(window.location.href, './generate_output');
-}
+export function calculate() {
+ const msg = {
+ method: r.get('form', 'method'),
+ params: {
+ cut_width: u.strictParseFloat(r.get('form', 'cut_width')),
+ panels: u.pipe(
+ u.map(panel => [panel.name, {width: u.strictParseFloat(panel.width),
+ height: u.strictParseFloat(panel.height)}]),
+ u.fromPairs
+ )(r.get('form', 'panels', 'items')),
+ items: u.pipe(
+ u.map(item => [item.name, {width: u.strictParseFloat(item.width),
+ height: u.strictParseFloat(item.height),
+ can_rotate: item.can_rotate}]),
+ u.fromPairs
+ )(r.get('form', 'items', 'items')),
+ }
+ };
+ try {
+ validateCalculateRequest(msg);
+ } catch (e) {
+ showNotification(e, 'error');
+ return;
+ }
+ const req = new XMLHttpRequest();
+ req.onload = () => parseCalculateResponse(JSON.parse(req.responseText));
+ req.open('POST', calculateUrl);
+ req.setRequestHeader('Content-Type', 'application/json');
+ req.send(JSON.stringify(msg));
+}
-export function addPanel() {
+function validateCalculateRequest(msg) {
+ if (!Number.isFinite(msg.params.cut_width) || msg.params.cut_width < 0)
+ throw 'Invalid cut width';
+ if (u.equals(msg.params.panels, {}))
+ throw 'No panels defined';
+ for (let [name, panel] of u.toPairs(msg.params.panels)) {
+ if (!name)
+ throw 'Invalid panel name';
+ if (!Number.isFinite(panel.width) || panel.width <= 0)
+ throw 'Invalid width for panel ' + name;
+ if (!Number.isFinite(panel.height) || panel.height <= 0)
+ throw 'Invalid height for panel ' + name;
+ }
+ if (u.equals(msg.params.items, {}))
+ throw 'No items defined';
+ for (let [name, item] of u.toPairs(msg.params.items)) {
+ if (!name)
+ throw 'Invalid item name';
+ if (!Number.isFinite(item.width) || item.width <= 0)
+ throw 'Invalid width for item ' + name;
+ if (!Number.isFinite(item.height) || item.height <= 0)
+ throw 'Invalid height for item ' + name;
+ }
}
-export function addItem() {
+function parseCalculateResponse(msg) {
+
+}
+
+function showNotification(message, type) {
+ iziToast[type]({message: message});
}
diff --git a/src_js/opcut/grid.js b/src_js/opcut/grid.js
index 972341d..4403195 100644
--- a/src_js/opcut/grid.js
+++ b/src_js/opcut/grid.js
@@ -30,39 +30,50 @@ export function tbody(gridPath, columns, validators) {
content = checkboxColumn(gridPath, column)(row, rowIndex);
} else if (selected) {
content = ['input.grid-input', {
- type: 'text',
- 'ev-change': (evt) => r.set([gridPath, 'items', rowIndex, column],
- evt.target.value),
- 'ev-blur': _ => {
- if (u.equals(gridState.selectedItem, [rowIndex, column]))
- r.set([gridPath, 'selectedItem'], null);
+ props: {
+ type: 'text',
+ value: content
},
- 'ev-keyup': (evt) => {
- switch (evt.key) {
- case 'Enter':
- evt.target.blur();
- break;
- case 'Escape':
- evt.target.value = u.get(column, row);
- evt.target.blur();
- break;
+ on: {
+ change: evt => r.set([gridPath, 'items', rowIndex, column],
+ evt.target.value),
+ blur: _ => {
+ if (u.equals(gridState.selectedItem, [rowIndex, column]))
+ r.set([gridPath, 'selectedItem'], null);
+ },
+ keyup: evt => {
+ switch (evt.key) {
+ case 'Enter':
+ evt.target.blur();
+ break;
+ case 'Escape':
+ evt.target.value = u.get(column, row);
+ evt.target.blur();
+ break;
+ }
}
- },
- value: content
- }];
+ }}
+ ];
}
}
const title = (validator ? validator(u.get(column, row), row) : null);
- return ['td' + (title ? '.invalid' : ''), {
- title: (title ? title : ''),
- 'ev-click': evt => {
- if (u.equals(gridState.selectedItem, [rowIndex, column]))
- return;
- ev.one(r, 'render', () => {
- if (evt.target.firstChild && evt.target.firstChild.focus)
- evt.target.firstChild.focus();
- });
- r.set([gridPath, 'selectedItem'], [rowIndex, column]);
+ return ['td', {
+ class: {
+ invalid: title
+ },
+ props: {
+ title: (title ? title : '')
+ },
+ on: {
+ click: evt => {
+ if (u.equals(gridState.selectedItem, [rowIndex, column]))
+ return;
+ ev.one(r, 'render', () => {
+ if (evt.target.firstChild && evt.target.firstChild.focus)
+ evt.target.firstChild.focus();
+ });
+ r.set([gridPath, 'selectedItem'], [rowIndex, column]);
+ }
}},
content];
})]
@@ -75,36 +86,39 @@ export function tfoot(gridPath, colspan, newItem, csvColumns) {
return ['tfoot',
['tr',
['td', {
- colSpan: colspan},
+ props: {
+ colSpan: colspan
+ }},
['div',
['button', {
- 'ev-click': () => r.change(itemsPath, u.append(newItem))},
+ on: {
+ click: () => r.change(itemsPath, u.append(newItem))
+ }},
['span.fa.fa-plus'],
' Add'
],
['span.spacer'],
- ['button', {
- 'ev-click': () => r.set(itemsPath, [])},
- ['span.fa.fa-trash-o'],
- ' Remove all'
- ],
(!csvColumns ?
[] :
[
['button', {
- 'ev-click': () => {
- const items = importCsv(csvColumns, newItem);
- if (!items)
- return;
- r.change(itemsPath, state => state.concat(items));
+ on: {
+ click: () => {
+ const items = importCsv(csvColumns, newItem);
+ if (!items)
+ return;
+ r.change(itemsPath, state => state.concat(items));
+ }
}},
['span.fa.fa-download'],
- ' Import from CSV'
+ ' CSV Import'
],
['button', {
- 'ev-click': () => exportCsv(r.get(itemsPath), csvColumns)},
+ on: {
+ click: () => exportCsv(r.get(itemsPath), csvColumns)
+ }},
['span.fa.fa-upload'],
- ' Export to CSV'
+ ' CSV Export'
]
]
)
@@ -126,41 +140,58 @@ export function createStringCsvColumns(...columns) {
}
+export function createBooleanCsvColumns(...columns) {
+ return u.pipe(
+ u.map(i => [i, {
+ toString: u.pipe(u.get(i), x => x ? 'true' : 'false'),
+ toItem: (x, acc) => u.set(i, x == 'true', acc)
+ }]),
+ u.fromPairs
+ )(columns);
+}
+
+
export function deleteColumn(gridPath, showUpDown, onDeleteCb) {
const itemsPath = [gridPath, 'items'];
return (i, index) => [
(!showUpDown ? [] : [
['button', {
- 'ev-click': () => {
- const gridState = r.get(gridPath);
- if (index < 1)
- return;
- r.change(itemsPath, u.pipe(
- u.set(index, gridState.items[index-1]),
- u.set(index-1, i)
- ));
+ on: {
+ click: () => {
+ const gridState = r.get(gridPath);
+ if (index < 1)
+ return;
+ r.change(itemsPath, u.pipe(
+ u.set(index, gridState.items[index-1]),
+ u.set(index-1, i)
+ ));
+ }
}},
['span.fa.fa-arrow-up']
],
['button', {
- 'ev-click': () => {
- const gridState = r.get(gridPath);
- if (index > gridState.items.length - 2)
- return;
- r.change(itemsPath, u.pipe(
- u.set(index, gridState.items[index+1]),
- u.set(index+1, i)
- ));
+ on: {
+ click: () => {
+ const gridState = r.get(gridPath);
+ if (index > gridState.items.length - 2)
+ return;
+ r.change(itemsPath, u.pipe(
+ u.set(index, gridState.items[index+1]),
+ u.set(index+1, i)
+ ));
+ }
}},
['span.fa.fa-arrow-down']
]
]),
['button', {
- 'ev-click': () => {
- const item = r.get(itemsPath, index);
- r.change(itemsPath, u.omit(index));
- if (onDeleteCb)
- onDeleteCb(item);
+ on: {
+ click: () => {
+ const item = r.get(itemsPath, index);
+ r.change(itemsPath, u.omit(index));
+ if (onDeleteCb)
+ onDeleteCb(item);
+ }
}},
['span.fa.fa-minus']
]
@@ -172,11 +203,17 @@ export function checkboxColumn(gridPath, column) {
return (i, index) => {
const columnPath = [gridPath, 'items', index, column];
return ['div', {
- style: 'text-align: center'},
+ props: {
+ style: 'text-align: center'
+ }},
['input', {
- type: 'checkbox',
- 'ev-change': (evt) => r.set(columnPath, evt.target.checked),
- checked: r.get(columnPath)}
+ props: {
+ type: 'checkbox',
+ checked: r.get(columnPath)
+ },
+ on: {
+ change: evt => r.set(columnPath, evt.target.checked)
+ }}
]
];
};
@@ -191,9 +228,16 @@ export function selectColumn(gridPath, column, values) {
i => u.equals(selectedValue, (u.isArray(i) ? i[0] : i))) === undefined;
const allValues = (invalid ? u.append(selectedValue, values) : values);
return ['select' + (invalid ? '.invalid' : ''), {
- title: (invalid ? 'invalid value' : ''),
- style: 'width: 100%',
- 'ev-change': (evt) => r.set(columnPath, evt.target.value)},
+ class: {
+ invalid: invalid
+ },
+ props: {
+ title: (invalid ? 'invalid value' : ''),
+ style: 'width: 100%'
+ },
+ on: {
+ change: evt => r.set(columnPath, evt.target.value)
+ }},
allValues.map(i => {
const value = (u.isArray(i) ? i[0] : i);
const label = (u.isArray(i) ? i[1] : i);
diff --git a/src_js/opcut/validators.js b/src_js/opcut/validators.js
new file mode 100644
index 0000000..eb49fac
--- /dev/null
+++ b/src_js/opcut/validators.js
@@ -0,0 +1,56 @@
+import * as u from 'opcut/util';
+
+
+export function notEmptyValidator(value) {
+ if (!value)
+ return 'invalid value';
+}
+
+
+export function floatValidator(value) {
+ const floatValue = u.strictParseFloat(value);
+ if (!Number.isFinite(floatValue))
+ return 'not valid number';
+}
+
+
+export function integerValidator(value) {
+ const intValue = u.strictParseInt(value);
+ if (!Number.isFinite(intValue))
+ return 'not valid number';
+}
+
+
+export function tcpPortValidator(value) {
+ const intValue = u.strictParseInt(value);
+ if (!Number.isFinite(intValue) || intValue < 0 || intValue > 0xFFFF)
+ return 'not valid TCP port';
+}
+
+
+export function createChainValidator(...validators) {
+ return value => {
+ for (let validator of validators) {
+ let result = validator(value);
+ if (result)
+ return result;
+ }
+ };
+}
+
+
+export function createUniqueValidator() {
+ const values = new Set();
+ return value => {
+ if (values.has(value))
+ return 'duplicate value';
+ values.add(value);
+ };
+}
+
+
+export function dimensionValidator(value) {
+ const floatValue = u.strictParseFloat(value);
+ if (!Number.isFinite(floatValue) || floatValue <= 0)
+ return 'not valid dimension';
+}
diff --git a/src_js/opcut/vt.js b/src_js/opcut/vt.js
index c13d35a..f942e4b 100644
--- a/src_js/opcut/vt.js
+++ b/src_js/opcut/vt.js
@@ -1,6 +1,9 @@
import r from 'opcut/renderer';
-
+import * as u from 'opcut/util';
+import * as grid from 'opcut/grid';
import * as common from 'opcut/common';
+import * as states from 'opcut/states';
+import * as validators from 'opcut/validators';
export function main() {
@@ -13,6 +16,11 @@ export function main() {
function leftPanel() {
+ const methodPath = ['form', 'method'];
+ const cutWidthPath = ['form', 'cut_width'];
+ const cutWidthTitle = (cutWidth =>
+ (Number.isFinite(cutWidth) && cutWidth >= 0) ? '' : 'not valid cut width'
+ )(u.strictParseFloat(r.get(cutWidthPath)));
return ['div.left-panel',
['div.header',
['div.title', 'OPCUT'],
@@ -30,7 +38,10 @@ function leftPanel() {
['option', {
props: {
value: method,
- selected: r.get('form', 'method') == method
+ selected: r.get(methodPath) == method
+ },
+ on: {
+ change: evt => r.set(methodPath, evt.target.value)
}},
method
])
@@ -39,53 +50,87 @@ function leftPanel() {
['div.group',
['label', 'Cut width'],
['input', {
+ class: {
+ invalid: cutWidthTitle
+ },
props: {
- value: r.get('form', 'cut_width')
+ value: r.get(cutWidthPath),
+ title: cutWidthTitle
},
on: {
- change: evt => r.set(['form', 'cut_width'], evt.target.value)
+ change: evt => r.set(cutWidthPath, evt.target.value)
}}
]
],
- ['div.list',
- ['label', 'Panels'],
- ['div.content',
- 'sdfsdfssfd', ['br'],
- 'sdfsdfssfd', ['br'],
- 'sdfsdfssfd', ['br'],
- 'sdfsdfssfd', ['br'],
- 'sdfsdfssfd', ['br']
- ],
- ['button.add', {
- on: {
- click: common.addPanel
- }},
- ['span.fa.fa-plus'],
- ' Add panel'
- ]
- ],
- ['div.list',
- ['label', 'Items'],
- ['div.content',
- 'sdfsdfssfd', ['br'],
- 'sdfsdfssfd', ['br'],
- 'sdfsdfssfd', ['br'],
- 'sdfsdfssfd', ['br'],
- 'sdfsdfssfd', ['br']
- ],
- ['button.add', {
- on: {
- click: common.addItem
- }},
- ['span.fa.fa-plus'],
- ' Add item'
- ]
+ ['div.content',
+ leftPanelPanels(),
+ leftPanelItems()
],
- ['button.submit', {
+ ['button.calculate', {
on: {
- click: common.submit
+ click: common.calculate
}},
'Calculate'
]
];
}
+
+
+function leftPanelPanels() {
+ const panelsPath = ['form', 'panels'];
+ const deleteColumn = grid.deleteColumn(panelsPath);
+ const nameValidator = validators.createChainValidator(
+ validators.notEmptyValidator,
+ validators.createUniqueValidator());
+ const widthValidator = validators.dimensionValidator;
+ const heightValidator = validators.dimensionValidator;
+ const csvColumns = grid.createStringCsvColumns('name', 'width', 'height');
+ return ['div',
+ ['table.grid',
+ ['thead',
+ ['tr',
+ ['th', 'Panel name'],
+ ['th.fixed', 'Width'],
+ ['th.fixed', 'Height'],
+ ['th.fixed']
+ ]
+ ],
+ grid.tbody(panelsPath,
+ ['name', 'width', 'height', deleteColumn],
+ [nameValidator, widthValidator, heightValidator]),
+ grid.tfoot(panelsPath, 4, states.panelsItem, csvColumns)
+ ]
+ ];
+}
+
+
+function leftPanelItems() {
+ const itemsPath = ['form', 'items'];
+ const rotateColumn = grid.checkboxColumn(itemsPath, 'can_rotate');
+ const deleteColumn = grid.deleteColumn(itemsPath);
+ const nameValidator = validators.createChainValidator(
+ validators.notEmptyValidator,
+ validators.createUniqueValidator());
+ const widthValidator = validators.dimensionValidator;
+ const heightValidator = validators.dimensionValidator;
+ const csvColumns = u.merge(
+ grid.createStringCsvColumns('name', 'width', 'height'),
+ grid.createBooleanCsvColumns('can_rotate'));
+ return ['div',
+ ['table.grid',
+ ['thead',
+ ['tr',
+ ['th', 'Item name'],
+ ['th.fixed', 'Width'],
+ ['th.fixed', 'Height'],
+ ['th.fixed', 'Rotate'],
+ ['th.fixed']
+ ]
+ ],
+ grid.tbody(itemsPath,
+ ['name', 'width', 'height', rotateColumn, deleteColumn],
+ [nameValidator, widthValidator, heightValidator]),
+ grid.tfoot(itemsPath, 5, states.itemsItem, csvColumns)
+ ]
+ ];
+}