Current Dev State

This commit is contained in:
Tim Lorsbach
2025-06-23 20:13:54 +02:00
parent b4f9bb277d
commit ded50edaa2
22617 changed files with 4345095 additions and 174 deletions

View File

@ -0,0 +1,50 @@
/****************************************************************************
* 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 */
class Accordion extends Component {
constructor(props) {
super(props);
this.state.active = props.active ? props.active : {};
}
onActive(index) {
let newActive = {};
newActive[index] = !this.state.active[index];
this.setState({ active: Object.assign(this.state.active, newActive)});
if (this.props.onActive) this.props.onActive();
}
render() {
let {children, captions, ...props} = this.props;
return (
<ul {...props}>
{ captions.map((caption, index) => (
<li className="tab">
<a className={this.state.active[index] ? 'active' : ''}
onClick={() => this.onActive(index)}>
{caption}
</a>
{this.state.active[index] ? children[index] : null }
</li>
)) }
</ul>
);
}
}
export default Accordion;

View File

@ -0,0 +1,99 @@
/****************************************************************************
* 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 } from 'preact';
/** @jsx h */
import classNames from 'classnames';
import action from '../action';
import { hiddenAncestor } from '../state/toolbar';
const isMac = /Mac/.test(navigator.platform);
const shortcutAliasMap = {
'Escape': 'Esc',
'Delete': 'Del',
'Mod': isMac ? '⌘' : 'Ctrl'
};
export function shortcutStr(shortcut) {
const key = Array.isArray(shortcut) ? shortcut[0] : shortcut;
return key.replace(/(\b[a-z]\b$|Mod|Escape|Delete)/g, function (key) {
return shortcutAliasMap[key] || key.toUpperCase();
});
}
function ActionButton({action, status={}, onAction, ...props}) {
let shortcut = action.shortcut && shortcutStr(action.shortcut);
return (
<button disabled={status.disabled}
onClick={(ev) => {
if (!status.selected || action.action.tool === 'chiralFlag') {
onAction(action.action);
ev.stopPropagation();
}
} }
title={shortcut ? `${action.title} (${shortcut})` : action.title}>
{action.title}
</button>
)
}
function ActionMenu({name, menu, className, role, ...props}) {
return (
<menu className={className} role={role}
style={toolMargin(name, menu, props.visibleTools)}>
{
menu.map(item => (
<li id={item.id || item}
className={classNames(props.status[item]) + ` ${item.id === props.opened ? 'opened' : ''}`}
onClick={(ev) => openHandle(ev, props.onOpen) }>
{ typeof item !== 'object' ?
( <ActionButton {...props} action={action[item]}
status={props.status[item]} /> ) :
item.menu ?
( <ActionMenu {...props} name={item.id} menu={item.menu} /> ) :
item.component(props)
}
</li>
))
}
</menu>
);
}
function toolMargin(menuName, menu, visibleTools) {
if (!visibleTools[menuName]) return {};
let iconHeight = (window.innerHeight < 600 || window.innerWidth < 1040) ? 32 : 40;
// now not found better way
let index = menu.indexOf(visibleTools[menuName]); // first level
if (index === -1) {
let tools = [];
menu.forEach(item => tools = tools.concat(item.menu));
index = tools.indexOf(visibleTools[menuName]); // second level. example: `bond: bond-any`
}
return (index !== -1) ? { marginTop: -(iconHeight * index) + 'px' } : {};
}
function openHandle(event, onOpen) {
let hiddenEl = hiddenAncestor(event.currentTarget);
if (hiddenEl) onOpen(hiddenEl.id);
event.stopPropagation();
}
export default ActionMenu;

View File

@ -0,0 +1,41 @@
/****************************************************************************
* 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 } from 'preact';
/** @jsx h */
import element from '../../chem/element';
const metPrefix = ['alkali', 'alkaline-earth', 'transition',
'post-transition']; // 'lanthanide', 'actinide'
function atomClass(el) {
let own = `atom-${el.label.toLowerCase()}`;
let type = metPrefix.indexOf(el.type) >= 0 ? `${el.type} metal` :
(el.type || 'unknown-props');
return [own, type, el.state || 'unknown-state', el.origin];
}
function Atom({el, shortcut, className, ...props}) {
return (
<button title={shortcut ? `${el.title} (${shortcut})` : el.title}
className={[...atomClass(el), className].join(' ')}
value={element.map[el.label]} {...props}>
{el.label}
</button>
);
}
export default Atom;

View File

@ -0,0 +1,136 @@
/****************************************************************************
* 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 */
const ieCb = window.clipboardData;
class ClipArea extends Component {
shouldComponentUpdate() {
return false;
}
componentDidMount() {
const el = this.refs ? this.refs.base : this.base;
this.target = this.props.target || el.parentNode;
this.listeners = {
'mouseup': event => {
if (this.props.focused() && !isFormElement(event.target))
autofocus(el);
},
'copy': event => {
if (this.props.focused() && this.props.onCopy) {
const data = this.props.onCopy();
if (data)
copy(event.clipboardData, data);
event.preventDefault();
}
},
'cut': event => {
if (this.props.focused() && this.props.onCut) {
const data = this.props.onCut();
if (data)
copy(event.clipboardData, data);
event.preventDefault();
}
},
'paste': event => {
if (this.props.focused() && this.props.onPaste) {
const data = paste(event.clipboardData, this.props.formats);
if (data)
this.props.onPaste(data);
event.preventDefault();
}
}
};
Object.keys(this.listeners).forEach(en => {
this.target.addEventListener(en, this.listeners[en]);
});
}
componentWillUnmount() {
Object.keys(this.listeners).forEach(en => {
this.target.removeEventListener(en, this.listeners[en]);
});
}
render() {
return (
<textarea className="cliparea" contentEditable={true}
autoFocus={true}/>
);
}
}
function isFormElement(el) {
if (el.tagName === 'INPUT' && el.type === 'button') return false;
return ['INPUT', 'SELECT', 'TEXTAREA'].indexOf(el.tagName) > -1;
}
function autofocus(cliparea) {
cliparea.value = ' ';
cliparea.focus();
cliparea.select();
}
function copy(cb, data) {
if (!cb && ieCb) {
ieCb.setData('text', data['text/plain']);
} else {
cb.setData('text/plain', data['text/plain']);
try {
Object.keys(data).forEach(function (fmt) {
cb.setData(fmt, data[fmt]);
});
} catch (ex) {
console.info('Could not write exact type', ex);
}
}
}
function paste(cb, formats) {
let data = {};
if (!cb && ieCb) {
data['text/plain'] = ieCb.getData('text');
} else {
data['text/plain'] = cb.getData('text/plain');
data = formats.reduce(function (data, fmt) {
const d = cb.getData(fmt);
if (d)
data[fmt] = d;
return data;
}, data);
}
return data;
}
export const actions = ['cut', 'copy', 'paste'];
export function exec(action) {
let enabled = document.queryCommandSupported(action);
if (enabled) try {
enabled = document.execCommand(action) || ieCb;
} catch (ex) {
// FF < 41
enabled = false;
}
return enabled;
}
export default ClipArea;

View File

@ -0,0 +1,75 @@
/****************************************************************************
* 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 */
class ComboBox extends Component {
constructor(props) {
super(props);
this.state = {
suggestsHidden: true
};
this.click = this.click.bind(this);
this.blur = this.blur.bind(this);
this.updateInput = this.updateInput.bind(this);
}
updateInput(event) {
const value = (event.target.value || event.target.textContent);
this.setState({ suggestsHidden: true });
this.props.onChange(value);
}
click() {
this.setState({ suggestsHidden: false });
}
blur() {
this.setState({ suggestsHidden: true });
}
render(props) {
const { value, type = 'text', schema } = props;
const suggestList = schema.enumNames
.filter(item => item !== value)
.map(item => <li onMouseDown={this.updateInput}>{item}</li>);
return (
<div>
<input type={type} value={value} onClick={this.click}
onBlur={this.blur} onInput={this.updateInput} autocomplete="off"
/>
{
suggestList.length !== 0 ?
(
<ui className='suggestList'
style={`display: ${this.state.suggestsHidden ? 'none' : 'block'}`}
>
{
suggestList
}
</ui>
) : ''
}
</div>
);
}
}
export default ComboBox;

View File

@ -0,0 +1,82 @@
/****************************************************************************
* 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 */
import keyName from 'w3c-keyname';
class Dialog extends Component {
exit(mode) {
let { params, result=() => null,
valid=() => !!result() } = this.props;
let key = (mode === 'OK') ? 'onOk' : 'onCancel';
if (params && key in params && (key !== 'onOk' || valid()) )
params[key](result());
}
keyDown(ev) {
let key = keyName(ev);
let active = document.activeElement;
let activeTextarea = active && active.tagName === 'TEXTAREA';
if (key === 'Escape' || key === 'Enter' && !activeTextarea) {
this.exit(key === 'Enter' ? 'OK': 'Cancel');
ev.preventDefault();
}
ev.stopPropagation();
}
componentDidMount() {
const fe = this.base.querySelector(['input:not([type=checkbox]):not([type=button])', 'textarea',
'[contenteditable]','select'].join(',')) ||
this.base.querySelector(['button.close'].join(','));
console.assert(fe, 'No active buttons');
if (fe.focus) fe.focus();
}
componentWillUnmount() {
(document.querySelector('.cliparea') || document.body).focus();
}
render() {
let {
children, title, params = {},
result = () => null, valid = () => !!result(), // Hmm, dublicate.. No simple default props
buttons = ["Cancel", "OK"], ...props
} = this.props; // see: https://git.io/v1KR6
return (
<form role="dialog" onSubmit={ev => ev.preventDefault()}
onKeyDown={ev => this.keyDown(ev)} tabIndex="-1" {...props}>
<header>{title}
{params.onCancel && title && (
<button className="close"
onClick={() => this.exit('Cancel')}>×
</button> )
}
</header>
{children}
<footer>{
buttons.map(b => (
typeof b !== 'string' ? b :
<input type="button" value={b}
disabled={b === 'OK' && !valid()}
onClick={() => this.exit(b)}/>
))
}</footer>
</form>
);
}
}
export default Dialog;

View File

@ -0,0 +1,196 @@
/****************************************************************************
* 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 jsonschema from 'jsonschema';
import { h, Component } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import Input from './input';
import { updateFormState } from '../state/form';
class Form extends Component {
constructor({ onUpdate, schema, init, ...props }) {
super();
this.schema = propSchema(schema, props);
if (init) {
let { valid, errors } = this.schema.serialize(init);
const errs = getErrorsObj(errors);
init = Object.assign({}, init, { init: true });
onUpdate(init, valid, errs);
}
}
updateState(newstate) {
const { instance, valid, errors } = this.schema.serialize(newstate);
const errs = getErrorsObj(errors);
this.props.onUpdate(instance, valid, errs);
}
getChildContext() {
const { schema } = this.props;
return { schema, stateStore: this };
}
field(name, onChange) {
const { result, errors } = this.props;
const value = result[name];
const self = this;
return {
dataError: errors && errors[name] || false,
value: value,
onChange(value) {
const newstate = Object.assign({}, self.props.result, { [name]: value });
self.updateState(newstate);
if (onChange) onChange(value);
}
};
}
render(props) {
const { result, children, schema, ...prop } = props;
if (schema.key && schema.key !== this.schema.key) {
this.schema = propSchema(schema, prop);
this.schema.serialize(result); // hack: valid first state
this.updateState(result);
}
return (
<form {...prop}>
{children}
</form>
);
}
}
Form = connect(
null,
dispatch => ({
onUpdate: function (result, valid, errors) {
dispatch(updateFormState({ result, valid, errors }));
}
})
)(Form);
function Label({ labelPos, title, children, ...props }) {
return (
<label {...props}>{ title && labelPos !== 'after' ? `${title}:` : '' }
{children}
{ title && labelPos === 'after' ? title : '' }
</label>
);
}
class Field extends Component {
render(props) {
const { name, onChange, className, component, ...prop } = props;
const { schema, stateStore } = this.context;
const desc = prop.schema || schema.properties[name];
const { dataError, ...fieldOpts } = stateStore.field(name, onChange);
return (
<Label className={className} data-error={dataError} title={prop.title || desc.title} >
{
component ?
h(component, { ...fieldOpts, ...prop }) :
<Input name={name} schema={desc}
{...fieldOpts} {...prop}/>
}
</Label>
);
}
}
const SelectOneOf = (props) => {
const { title, name, schema, ...prop } = props;
const selectDesc = {
title: title,
enum: [],
enumNames: []
};
Object.keys(schema).forEach(item => {
selectDesc.enum.push(item);
selectDesc.enumNames.push(schema[item].title || item);
});
return <Field name={name} schema={selectDesc} {...prop}/>;
};
////
function propSchema(schema, { customValid, serialize = {}, deserialize = {} }) {
const v = new jsonschema.Validator();
if (customValid) {
schema = Object.assign({}, schema); // copy
schema.properties = Object.keys(customValid).reduce((res, prop) => {
v.customFormats[prop] = customValid[prop];
res[prop] = { format: prop, ...res[prop] };
return res;
}, schema.properties);
}
return {
key: schema.key || '',
serialize: inst => v.validate(inst, schema, {
rewrite: serializeRewrite.bind(null, serialize)
}),
deserialize: inst => v.validate(inst, schema, {
rewrite: deserializeRewrite.bind(null, deserialize)
})
};
}
function serializeRewrite(serializeMap, instance, schema) {
const res = {};
if (typeof instance !== 'object' || !schema.properties) {
return instance !== undefined ? instance :
schema.default;
}
for (let p in schema.properties) {
if (schema.properties.hasOwnProperty(p) && (p in instance)) {
res[p] = instance[serializeMap[p]] || instance[p];
}
}
return res;
}
function deserializeRewrite(deserializeMap, instance, schema) {
return instance;
}
function getErrorsObj(errors) {
let errs = {};
let field;
errors.forEach(item => {
field = item.property.split('.')[1];
if (!errs[field])
errs[field] = item.schema.invalidMessage || item.message;
});
return errs;
}
export { Form, Field, SelectOneOf };

View File

@ -0,0 +1,232 @@
/****************************************************************************
* 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 });
}
}

View File

@ -0,0 +1,71 @@
/****************************************************************************
* 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 */
import Input from './input';
class MeasureInput extends Component {
constructor(props) {
super(props);
this.state = { meas: 'px' };
}
handleChange(value, onChange) {
const convValue = convertValue(value, this.state.meas, 'px');
this.state.cust = value;
onChange(convValue);
}
render() {
const { meas, cust } = this.state;
const { schema, value, onChange, ...props } = this.props;
if (convertValue(cust, meas, 'px') !== value)
this.setState({ meas: 'px', cust: value }); // Hack: New store (RESET)
return (
<div style="display: inline-flex;" {...props}>
<Input schema={schema} step={meas === 'px' || meas === 'pt' ? '1' : '0.001'} style="width: 75%;"
value={cust} onChange={(v) => this.handleChange(v, onChange)} />
<Input schema={{ enum: ['cm', 'px', 'pt', 'inch'] }} style="width: 25%;"
value={meas}
onChange={(m) => this.setState({
meas: m,
cust: convertValue(this.state.cust, this.state.meas, m)
})} />
</div>
);
}
}
const measureMap = {
'px': 1,
'cm': 37.795278,
'pt': 1.333333,
'inch': 96,
};
function convertValue(value, measureFrom, measureTo) {
if (!value && value !== 0 || isNaN(value)) return null;
return (measureTo === 'px' || measureTo === 'pt')
? (value * measureMap[measureFrom] / measureMap[measureTo]).toFixed( ) - 0
: (value * measureMap[measureFrom] / measureMap[measureTo]).toFixed(3) - 0;
}
export default MeasureInput;

View File

@ -0,0 +1,109 @@
/****************************************************************************
* 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 */
class OpenButton extends Component {
constructor(props) {
super(props);
if (props.server) {
fileOpener(props.server).then(opener => {
this.setState({opener});
});
}
}
open(ev) {
const files = ev.target.files;
const noop = () => null;
const { onLoad = noop, onError = noop } = this.props;
if (this.state.opener && files.length) {
this.state.opener(files[0]).then(onLoad, onError);
} else if (files.length)
onLoad(files[0]);
ev.target.value = null;
ev.preventDefault();
}
render() {
const { children, type, ...props } = this.props;
return (
<div { ...props }>
<input id="input-file" onChange={ ev => this.open(ev) }
accept={ type } type="file"/>
<label for="input-file">
{ children }
</label>
</div>
);
}
}
function fileOpener (server) {
return new Promise((resolve, reject) => {
// TODO: refactor return
if (global.FileReader)
resolve(throughFileReader);
else if (global.ActiveXObject) {
try {
const fso = new ActiveXObject('Scripting.FileSystemObject');
resolve(file => Promise.resolve(throughFileSystemObject(fso, file)));
} catch (e) {
reject(e);
}
} else if (server) {
resolve(server.then(() => {
throw "Server doesn't still support echo method";
//return resolve(throughForm2IframePosting);
}));
} else
reject(new Error("Your browser does not support " +
"opening files locally"));
});
}
function throughFileReader(file) {
return new Promise((resolve, reject) => {
const rd = new FileReader();
rd.onload = () => {
const content = rd.result;
if (file.msClose)
file.msClose();
resolve(content);
};
rd.onerror = event => {
reject(event);
};
rd.readAsText(file, 'UTF-8');
});
}
function throughFileSystemObject(fso, file) {
// IE9 and below
const fd = fso.OpenTextFile(file.name, 1),
content = fd.ReadAll();
fd.Close();
return content;
}
export default OpenButton;

View File

@ -0,0 +1,77 @@
/****************************************************************************
* 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 */
import fs from 'filesaver.js';
class SaveButton extends Component {
constructor({filename="unnamed", type="text/plain", className='', ...props}) {
super({filename, type, className, ...props});
fileSaver(props.server).then(saver => {
this.setState({saver});
});
}
save(ev) {
const noop = () => null;
const { filename, data, type, onSave = noop, onError = noop } = this.props;
if (this.state.saver && data)
try {
this.state.saver(data, filename, type);
onSave();
}
catch(e) {
onError(e);
}
ev.preventDefault();
}
render() {
let { children, filename, data, className, ...props } = this.props;
if (!this.state.saver || !data)
className = `disabled ${className}`;
return (
<a download={filename} onClick={ev => this.save(ev)}
className={className} {...props}>
{ children }
</a>
);
}
}
function fileSaver(server) {
return new Promise((resolve, reject) => {
if (global.Blob && fs.saveAs) {
resolve((data, fn, type) => {
const blob = new Blob([data], { type });
fs.saveAs(blob, fn);
});
} else if (server) {
resolve(server.then(() => {
throw "Server doesn't still support echo method";
}));
} else
reject(new Error("Your browser does not support " +
"opening files locally"));
});
}
export default SaveButton;

View File

@ -0,0 +1,40 @@
/****************************************************************************
* 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 } from 'preact';
/** @jsx h */
function SelectList({ schema, value, onSelect, splitIndexes, ...props }) {
return (
<ul {...props}>{
schema.enum.map((opt, index) => (
<li onClick={() => onSelect(opt, index) }
className={
(opt === value ? 'selected ' : '') +
(isSplitIndex(index, splitIndexes) ? 'split' : '')
}>
{schema.enumNames ? schema.enumNames[index] : opt}
</li>
))
}</ul>
);
}
function isSplitIndex(index, splitIndexes) {
return index > 0 && splitIndexes && splitIndexes.includes(index);
}
export default SelectList;

View File

@ -0,0 +1,26 @@
/****************************************************************************
* 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 } from 'preact';
/** @jsx h */
function Spin({...props}) {
return (
<div className="spinner" {...props}></div>
);
}
export default Spin;

View File

@ -0,0 +1,78 @@
/****************************************************************************
* 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 { upperFirst } from 'lodash/fp';
import { h, Component } from 'preact';
/** @jsx h */
import Editor from '../../editor'
function setupEditor(editor, props, oldProps = {}) {
const { struct, tool, toolOpts, options } = props;
if (struct !== oldProps.struct)
editor.struct(struct);
if (tool !== oldProps.tool || toolOpts !== oldProps.toolOpts)
editor.tool(tool, toolOpts);
if (oldProps.options && options !== oldProps.options)
editor.options(options);
// update handlers
for (let name in editor.event) {
if (!editor.event.hasOwnProperty(name))
continue;
let eventName = `on${upperFirst(name)}`;
if (props[eventName] !== oldProps[eventName]) {
console.info('update editor handler', eventName);
if (oldProps[eventName])
editor.event[name].remove(oldProps[eventName]);
if (props[eventName])
editor.event[name].add(props[eventName]);
}
}
}
class StructEditor extends Component {
shouldComponentUpdate() {
return false;
}
componentWillReceiveProps(props) {
setupEditor(this.instance, props, this.props);
}
componentDidMount() {
console.assert(this.base, "No backing element");
this.instance = new Editor(this.base, { ...this.props.options });
setupEditor(this.instance, this.props);
if (this.props.onInit)
this.props.onInit(this.instance);
}
render () {
let { Tag="div", struct, tool, toolOpts, options, ...props } = this.props;
return (
<Tag onMouseDown={ev => ev.preventDefault()} {...props} />
);
}
}
export default StructEditor;

View File

@ -0,0 +1,71 @@
/****************************************************************************
* 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 */
import Struct from '../../chem/struct';
import molfile from '../../chem/molfile';
import Render from '../../render';
function renderStruct(el, struct, options={}) {
if (el) {
if (struct.prerender) // Should it sit here?
el.innerHTML = struct.prerender;
else {
console.info('render!', el.clientWidth, el.clientWidth);
const rnd = new Render(el, {
autoScale: true,
...options
});
rnd.setMolecule(struct);
rnd.update();
// console.info('render!');//, el.innerHTML);
// struct.prerender = el.innerHTML;
}
}
}
class StructRender extends Component {
constructor(props) {
super(props);
if (!(props.struct instanceof Struct)) try {
this.props.struct = molfile.parse(props.struct);
} catch (e) {
alert("Could not parse structure\n" + e);
this.props.struct = null;
}
}
shouldComponentUpdate() {
return false;
}
componentDidMount() {
const el = this.refs ? this.refs.base : this.base;
const { struct, options } = this.props;
renderStruct(el, struct, options);
}
render () {
let { struct, Tag="div", ...props } = this.props;
return (
<Tag /*ref="el"*/ {...props}>{ struct ? null : 'No molecule' }</Tag>
);
}
}
export default StructRender;

View File

@ -0,0 +1,88 @@
/****************************************************************************
* 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';
import FontFaceObserver from "font-face-observer";
import Input from './input';
/** @jsx h */
const commonFonts = [
"Arial",
"Arial Black",
"Comic Sans MS",
"Courier New",
"Georgia",
"Impact", "Charcoal",
"Lucida Console", "Monaco",
"Palatino Linotype", "Book Antiqua", "Palatino",
"Tahoma", "Geneva",
"Times New Roman", "Times",
"Verdana",
"Symbol",
"MS Serif", "MS Sans Serif", "New York",
"Droid Sans", "Droid Serif", "Droid Sans Mono", "Roboto"
];
function checkInSystem() {
const availableFontsPromises = commonFonts.map((fontName) => {
const observer = new FontFaceObserver(fontName);
return observer.check().then(() => fontName, () => null);
});
return Promise.all(availableFontsPromises);
}
let cache = null;
class SystemFonts extends Component {
constructor(props) {
super(props);
this.state = { availableFonts: [subfontname(props.value)] };
this.setAvailableFonts();
}
setAvailableFonts() {
cache ? this.setState({ availableFonts: cache }) :
checkInSystem().then((results) => {
cache = results.filter((i) => i !== null);
this.setState({ availableFonts: cache });
});
}
render() {
const {...props} = this.props;
const desc = {
enum: [],
enumNames: []
};
this.state.availableFonts.forEach((font) => {
desc.enum.push(`30px ${font}`);
desc.enumNames.push(font);
});
return desc.enum.length !== 1
? <Input schema={desc} {...props} />
: <select><option>{desc.enumNames[0]}</option></select>;
}
}
function subfontname(name) {
return name.substring(name.indexOf('px ') + 3);
}
export default SystemFonts;

View File

@ -0,0 +1,53 @@
/****************************************************************************
* 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 */
class Tabs extends Component {
constructor(props) {
super(props);
this.state.tabIndex = props.tabIndex || 0;
this.props.changeTab(this.state.tabIndex);
}
changeTab(ev, index) {
this.setState({ tabIndex: index });
if (this.props.changeTab)
this.props.changeTab(index);
}
render() {
const {children, captions, ...props} = this.props;
return (
<ul {...props}>
<li className="tabs">
{ captions.map((caption, index) => (
<a className={this.state.tabIndex === index ? 'active' : ''}
onClick={ ev => this.changeTab(ev, index)}>
{caption}
</a>
)) }
</li>
<li className="tabs-content">
{ children[this.state.tabIndex] }
</li>
</ul>
);
}
}
export default Tabs;

View File

@ -0,0 +1,101 @@
/****************************************************************************
* 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 */
const STYLE_INNER = 'position:relative; overflow:hidden; width:100%; min-height:100%;';
const STYLE_CONTENT = 'position:absolute; top:0; left:0; height:100%; width:100%; overflow:visible;';
export default class VirtualList extends Component {
constructor(props) {
super(props);
this.state = {
offset: 0,
height: 0
};
}
resize = (ev, reset) => {
const height = this.base.offsetHeight;
if (this.state.height !== height) {
this.setState({ height });
}
if (reset) {
this.setState({offset: 0});
this.base.scrollTop = 0;
}
};
handleScroll = () => {
this.setState({ offset: this.base.scrollTop });
if (this.props.sync) this.forceUpdate();
};
componentDidUpdate({data}) {
const equal = (data.length === this.props.data.length &&
this.props.data.every((v, i)=> v === data[i]));
this.resize(null, !equal);
}
componentDidMount() {
this.resize();
addEventListener('resize', this.resize);
}
componentWillUnmount() {
removeEventListener('resize', this.resize);
}
render() {
const { data, rowHeight, children, Tag="div", overscanCount=1, sync, ...props } = this.props;
const { offset, height } = this.state;
// first visible row index
let start = (offset / rowHeight) || 0;
const renderRow = children[0];
// actual number of visible rows (without overscan)
let visibleRowCount = (height / rowHeight) || 0;
// Overscan: render blocks of rows modulo an overscan row count
// This dramatically reduces DOM writes during scrolling
if (overscanCount) {
start = Math.max(0, start - (start % overscanCount));
visibleRowCount += overscanCount;
}
// last visible + overscan row index
const end = start + 1 + visibleRowCount;
// data slice currently in viewport plus overscan items
let selection = data.slice(start, end);
return (
<div onScroll={this.handleScroll} {...props}>
<div style={`${STYLE_INNER} height:${data.length*rowHeight}px;`}>
<Tag style={`${STYLE_CONTENT} top:${start*rowHeight}px;`}>
{ selection.map((d, i) => renderRow(d, start + i)) }
</Tag>
</div>
</div>
);
}
}