forked from enviPath/enviPy
233 lines
6.3 KiB
JavaScript
233 lines
6.3 KiB
JavaScript
/****************************************************************************
|
|
* Copyright 2017 EPAM Systems
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
***************************************************************************/
|
|
|
|
import { h, Component } from 'preact';
|
|
/** @jsx h */
|
|
|
|
function GenericInput({ value, onChange, type = "text", ...props }) {
|
|
return (
|
|
<input type={type} value={value} onInput={onChange} {...props} />
|
|
);
|
|
}
|
|
|
|
GenericInput.val = function (ev, schema) {
|
|
const input = ev.target;
|
|
const isNumber = (input.type === 'number' || input.type === 'range') ||
|
|
(schema && (schema.type === 'number' || schema.type === 'integer'));
|
|
const value = isNumber ? input.value.replace(/,/g, '.') : input.value;
|
|
|
|
return (isNumber && !isNaN(value - 0)) ? value - 0 : value;
|
|
};
|
|
|
|
function TextArea({ value, onChange, ...props }) {
|
|
return (
|
|
<textarea value={value} onInput={onChange} {...props}/>
|
|
);
|
|
}
|
|
|
|
TextArea.val = (ev) => ev.target.value;
|
|
|
|
function CheckBox({ value, onChange, ...props }) {
|
|
return (
|
|
<input type="checkbox" checked={value} onClick={onChange} {...props} />
|
|
);
|
|
}
|
|
|
|
CheckBox.val = function (ev) {
|
|
ev.stopPropagation();
|
|
return !!ev.target.checked;
|
|
};
|
|
|
|
function Select({ schema, value, selected, onSelect, ...props }) {
|
|
return (
|
|
<select onChange={onSelect} {...props}>
|
|
{
|
|
enumSchema(schema, (title, val) => (
|
|
<option selected={selected(val, value)}
|
|
value={typeof val !== 'object' && val}>
|
|
{title}
|
|
</option>
|
|
))
|
|
}
|
|
</select>
|
|
);
|
|
}
|
|
|
|
Select.val = function (ev, schema) {
|
|
const select = ev.target;
|
|
if (!select.multiple)
|
|
return enumSchema(schema, select.selectedIndex);
|
|
|
|
return [].reduce.call(select.options, function (res, o, i) {
|
|
return !o.selected ? res :
|
|
[enumSchema(schema, i), ...res];
|
|
}, []);
|
|
};
|
|
|
|
function FieldSet({ schema, value, selected, onSelect, type = "radio", ...props }) {
|
|
return (
|
|
<fieldset onClick={onSelect} className="radio">
|
|
{
|
|
enumSchema(schema, (title, val) => (
|
|
<label>
|
|
<input type={type} checked={selected(val, value)}
|
|
value={typeof val !== 'object' && val}
|
|
{...props}/>
|
|
{title}
|
|
</label>
|
|
))
|
|
}
|
|
</fieldset>
|
|
);
|
|
}
|
|
|
|
FieldSet.val = function (ev, schema) {
|
|
const input = ev.target;
|
|
if (ev.target.tagName !== 'INPUT') {
|
|
ev.stopPropagation();
|
|
return undefined;
|
|
}
|
|
// Hm.. looks like premature optimization
|
|
// should we inline this?
|
|
const fieldset = input.parentNode.parentNode;
|
|
const res = [].reduce.call(fieldset.querySelectorAll('input'),
|
|
function (res, inp, i) {
|
|
return !inp.checked ? res :
|
|
[enumSchema(schema, i), ...res];
|
|
}, []);
|
|
return input.type === 'radio' ? res[0] : res;
|
|
};
|
|
|
|
function enumSchema(schema, cbOrIndex) {
|
|
const isTypeValue = Array.isArray(schema);
|
|
if (!isTypeValue && schema.items)
|
|
schema = schema.items;
|
|
|
|
if (typeof cbOrIndex === 'function') {
|
|
return (isTypeValue ? schema : schema.enum).map((item, i) => {
|
|
const title = isTypeValue ? item.title :
|
|
schema.enumNames && schema.enumNames[i];
|
|
return cbOrIndex(title !== undefined ? title : item,
|
|
item.value !== undefined ? item.value : item);
|
|
});
|
|
}
|
|
|
|
if (!isTypeValue)
|
|
return schema.enum[cbOrIndex];
|
|
|
|
const res = schema[cbOrIndex];
|
|
return res.value !== undefined ? res.value : res;
|
|
}
|
|
|
|
function inputCtrl(component, schema, onChange) {
|
|
let props = {};
|
|
if (schema) {
|
|
// TODO: infer maxLength, min, max, step, etc
|
|
if (schema.type === 'number' || schema.type === 'integer')
|
|
props = { type: 'text' };
|
|
}
|
|
|
|
return {
|
|
onChange: function (ev) {
|
|
const val = !component.val ? ev :
|
|
component.val(ev, schema);
|
|
onChange(val);
|
|
},
|
|
...props
|
|
};
|
|
}
|
|
|
|
function singleSelectCtrl(component, schema, onChange) {
|
|
return {
|
|
selected: (testVal, value) => (value === testVal),
|
|
onSelect: function (ev, value) {
|
|
const val = !component.val ? ev :
|
|
component.val(ev, schema);
|
|
if (val !== undefined)
|
|
onChange(val);
|
|
}
|
|
};
|
|
}
|
|
|
|
function multipleSelectCtrl(component, schema, onChange) {
|
|
return {
|
|
multiple: true,
|
|
selected: (testVal, values) =>
|
|
(values && values.indexOf(testVal) >= 0),
|
|
onSelect: function (ev, values) {
|
|
if (component.val) {
|
|
let val = component.val(ev, schema);
|
|
if (val !== undefined)
|
|
onChange(val);
|
|
} else {
|
|
const i = values ? values.indexOf(ev) : -1;
|
|
if (i < 0)
|
|
onChange(values ? [ev, ...values] : [ev]);
|
|
else
|
|
onChange([...values.slice(0, i),
|
|
...values.slice(i + 1)]);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function ctrlMap(component, { schema, multiple, onChange }) {
|
|
if (!schema || !schema.enum && !schema.items && !Array.isArray(schema) || schema.type === 'string')
|
|
return inputCtrl(component, schema, onChange);
|
|
|
|
if (multiple || schema.type === 'array')
|
|
return multipleSelectCtrl(component, schema, onChange);
|
|
|
|
return singleSelectCtrl(component, schema, onChange);
|
|
}
|
|
|
|
function componentMap({ schema, type, multiple }) {
|
|
if (!schema || !schema.enum && !schema.items && !Array.isArray(schema)) {
|
|
if (type === 'checkbox' || schema && schema.type === 'boolean')
|
|
return CheckBox;
|
|
|
|
return (type === 'textarea') ? TextArea : GenericInput;
|
|
}
|
|
if (multiple || schema.type === 'array')
|
|
return (type === 'checkbox') ? FieldSet : Select;
|
|
|
|
return (type === 'radio') ? FieldSet : Select;
|
|
}
|
|
|
|
function shallowCompare(a, b) {
|
|
for (let i in a) if (!(i in b)) return true;
|
|
for (let i in b) if (a[i] !== b[i]) { return true; }
|
|
return false;
|
|
}
|
|
|
|
export default class Input extends Component {
|
|
constructor({ component, ...props }) {
|
|
super(props);
|
|
this.component = component || componentMap(props);
|
|
this.ctrl = ctrlMap(this.component, props);
|
|
}
|
|
|
|
shouldComponentUpdate({ children, onChange, ...nextProps }) {
|
|
var { children, onChange, ...oldProps } = this.props;
|
|
return shallowCompare(oldProps, nextProps);
|
|
}
|
|
|
|
render() {
|
|
var { children, onChange, ...props } = this.props;
|
|
return h(this.component, { ...this.ctrl, ...props });
|
|
}
|
|
}
|