/**************************************************************************** * 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 ( ); } 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.val = (ev) => ev.target.value; function CheckBox({ value, onChange, ...props }) { return ( ); } CheckBox.val = function (ev) { ev.stopPropagation(); return !!ev.target.checked; }; function Select({ schema, value, selected, onSelect, ...props }) { return ( ); } 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.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 }); } }