From bb67223abd3d4a5c3654393b4a962daae0588741 Mon Sep 17 00:00:00 2001 From: bozokopic Date: Sun, 15 Apr 2018 19:14:15 +0200 Subject: WIP web frontend --- src_js/opcut/common.js | 69 ++++++++++++++++- src_js/opcut/grid.js | 186 ++++++++++++++++++++++++++++----------------- src_js/opcut/validators.js | 56 ++++++++++++++ src_js/opcut/vt.js | 123 ++++++++++++++++++++---------- 4 files changed, 320 insertions(+), 114 deletions(-) create mode 100644 src_js/opcut/validators.js (limited to 'src_js') 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) + ] + ]; +} -- cgit v1.2.3-70-g09d2