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,44 @@
/****************************************************************************
* 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.
***************************************************************************/
export const basic = ['H', 'C', 'N', 'O', 'S', 'P',
'F', 'Cl', 'Br', 'I'];
export const atomCuts = {
"H": "h",
"C": "c",
"N": "n",
"O": "o",
"S": "s",
"P": "p",
"F": "f",
"Cl": "Shift+c",
"Br": "Shift+b",
"I": "i",
"A": "a"
};
export default Object.keys(atomCuts).reduce((res, label) => {
res[`atom-${label.toLowerCase()}`] = {
title: `Atom ${label}`,
shortcut: atomCuts[label],
action: {
tool: 'atom',
opts: { label }
}
};
return res;
}, {});

View File

@ -0,0 +1,38 @@
/****************************************************************************
* 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 molfile from '../../chem/molfile';
export default {
// original: for dev purposes
"force-update": {
shortcut: "Ctrl+Shift+r",
action: editor => {
editor.update(true);
}
},
"qs-serialize": {
shortcut: "Alt+Shift+r",
action: editor => {
const molStr = molfile.stringify(editor.struct());
const molQs = 'mol=' + encodeURIComponent(molStr).replace(/%20/g, '+');
const qs = document.location.search;
document.location.search = !qs ? '?' + molQs :
qs.search('mol=') === -1 ? qs + '&' + molQs :
qs.replace(/mol=[^&$]*/, molQs);
}
}
}

View File

@ -0,0 +1,184 @@
/****************************************************************************
* 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 tools from './tools';
import atoms from './atoms';
import zoom from './zoom';
import server from './server';
import debug from './debug';
import templates from './templates';
import { exec } from '../component/cliparea';
import { miewAction } from '../state/miew';
export default {
"new": {
shortcut: "Mod+Delete",
title: "Clear Canvas",
action: {
thunk: (dispatch, getState) => {
let editor = getState().editor;
if (!editor.struct().isBlank())
editor.struct(null);
dispatch({ type: 'ACTION', action: tools['select-lasso'].action });
}
}
},
"open": {
shortcut: "Mod+o",
title: "Open…",
action: { dialog: 'open' }
},
"save": {
shortcut: "Mod+s",
title: "Save As…",
action: { dialog: 'save' }
},
"undo": {
shortcut: "Mod+z",
title: "Undo",
action: editor => {
editor.undo();
},
disabled: editor => (
editor.historySize().undo === 0
)
},
"redo": {
shortcut: ["Mod+Shift+z", "Mod+y"],
title: "Redo",
action: editor => {
editor.redo();
},
disabled: editor => (
editor.historySize().redo === 0
)
},
"cut": {
shortcut: "Mod+x",
title: "Cut",
action: () => {
exec('cut') || dontClipMessage('Cut');
},
disabled: editor => !hasSelection(editor)
},
"copy": {
shortcut: "Mod+c",
title: "Copy",
action: () => {
exec('copy') || dontClipMessage('Copy');
},
disabled: editor => !hasSelection(editor)
},
"paste": {
shortcut: "Mod+v",
title: "Paste",
action: () => {
exec('paste') || dontClipMessage('Paste')
},
selected: ({ actions }) => (
actions && // TMP
actions.active && actions.active.tool === 'paste'
)
},
"check": {
title: "Check Structure",
action: { dialog: 'check' },
disabled: (editor, server, options) => !options.app.server
},
"analyse": {
title: "Calculated Values",
action: { dialog: 'analyse' },
disabled: (editor, server, options) => !options.app.server
},
"recognize": {
title: "Recognize Molecule",
action: { dialog: 'recognize' },
disabled: (editor, server, options) => !options.app.server
},
"miew": {
title: "3D Viewer",
action: { thunk: miewAction },
disabled: (editor, server, options) => !options.app.server || !options.app.miewPath
},
"settings": {
title: "Settings",
action: { dialog: 'settings' }
},
"help": {
shortcut: ["?", "Shift+/"],
title: "Help",
action: { dialog: 'help' }
},
"about": {
title: "About",
action: { dialog: 'about' }
},
"reaction-automap": {
title: "Reaction Auto-Mapping Tool",
action: { dialog: 'automap' },
disabled: (editor, server, options) => !options.app.server || !editor.struct().hasRxnArrow()
},
"period-table": {
title: "Periodic Table",
action: { dialog: 'period-table' }
},
"select-all": {
title: "Select All",
shortcut: "Mod+a",
action: {
thunk: (dispatch, getState) => {
getState().editor.selection('all');
dispatch({ type: 'ACTION', action: tools['select-lasso'].action });
}
}
},
"deselect-all": {
title: "Deselect All",
shortcut: "Mod+Shift+a",
action: editor => {
editor.selection(null);
}
},
"select-descriptors": {
title: "Select descriptors",
shortcut: "Mod+d",
action: {
thunk: (dispatch, getState) => {
const editor = getState().editor;
editor.alignDescriptors();
editor.selection('descriptors');
dispatch({ type: 'ACTION', action: tools['select-lasso'].action });
}
}
},
...server,
...debug,
...tools,
...atoms,
...zoom,
...templates
};
function hasSelection(editor) {
let selection = editor.selection();
return selection && // if not only sgroupData selected
(Object.keys(selection).length > 1 || !selection.sgroupData);
}
function dontClipMessage(title) {
alert('These action is unavailble via menu.\n' +
'Instead, use shortcut to ' + title + '.');
}

View File

@ -0,0 +1,58 @@
/****************************************************************************
* 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 { serverTransform } from '../state/server';
export default {
"layout": {
shortcut: "Mod+l",
title: "Layout",
action: {
thunk: serverTransform('layout')
},
disabled: (editor, server, options) => !options.app.server
},
"clean": {
shortcut: "Mod+Shift+l",
title: "Clean Up",
action: {
thunk: serverTransform('clean')
},
disabled: (editor, server, options) => !options.app.server
},
"arom": {
title: "Aromatize",
action: {
thunk: serverTransform('aromatize')
},
disabled: (editor, server, options) => !options.app.server
},
"dearom": {
title: "Dearomatize",
action: {
thunk: serverTransform('dearomatize')
},
disabled: (editor, server, options) => !options.app.server
},
"cip": {
shortcut: "Mod+p",
title: "Calculate CIP",
action: {
thunk: serverTransform('calculateCip')
},
disabled: (editor, server, options) => !options.app.server
}
};

View File

@ -0,0 +1,39 @@
/****************************************************************************
* 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 templates from '../data/templates';
const templateLib = {
"template-lib": {
shortcut: "Shift+t",
title: "Custom Templates",
action: { dialog: 'templates' },
disabled: (editor, server, options) => !options.app.templates
}
};
export default templates.reduce((res, struct, i) => {
res[`template-${i}`] = {
title: `${struct.name}`,
shortcut: 't',
action: {
tool: 'template',
opts: { struct }
}
};
return res;
}, templateLib);

View File

@ -0,0 +1,142 @@
/****************************************************************************
* 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 { bond as bondSchema } from '../structschema';
import { toBondType } from '../structconv';
const toolActions = {
"select-lasso": {
title: "Lasso Selection",
shortcut: "Escape",
action: { tool: 'select', opts: 'lasso' }
},
"select-rectangle": {
title: "Rectangle Selection",
shortcut: "Escape",
action: { tool: 'select', opts: 'rectangle' }
},
"select-fragment": {
title: "Fragment Selection",
shortcut: "Escape",
action: { tool: 'select', opts: 'fragment' }
},
"erase": {
title: "Erase",
shortcut: ["Delete", "Backspace"],
action: { tool: 'eraser', opts: 1 } // TODO last selector mode is better
},
"chain": {
title: "Chain",
action: { tool: 'chain' }
},
"chiral-flag": {
title: "Chiral Flag",
action: { tool: 'chiralFlag' },
selected: editor => editor.struct().isChiral
},
"charge-plus": {
shortcut: "5",
title: "Charge Plus",
action: { tool: 'charge', opts: 1 }
},
"charge-minus": {
shortcut: "5",
title: "Charge Minus",
action: { tool: 'charge', opts: -1 }
},
"transform-rotate": {
shortcut: "Alt+r",
title: "Rotate Tool",
action: { tool: 'rotate' }
},
"transform-flip-h": {
shortcut: "Alt+h",
title: "Horizontal Flip",
action: { tool: 'rotate', opts: 'horizontal' }
},
"transform-flip-v": {
shortcut: "Alt+v",
title: "Vertical Flip",
action: { tool: 'rotate', opts: 'vertical' }
},
"sgroup": {
shortcut: "Mod+g",
title: "S-Group",
action: { tool: 'sgroup' }
},
"sgroup-data": {
shortcut: "Mod+g",
title: "Data S-Group",
action: { tool: 'sgroup', opts: 'DAT' }
},
"reaction-arrow": {
title: "Reaction Arrow Tool",
action: { tool: 'reactionarrow' }
},
"reaction-plus": {
title: "Reaction Plus Tool",
action: { tool: 'reactionplus' }
},
"reaction-map": {
title: "Reaction Mapping Tool",
action: { tool: 'reactionmap' }
},
"reaction-unmap": {
title: "Reaction Unmapping Tool",
action: { tool: 'reactionunmap' }
},
"rgroup-label": {
shortcut: "Mod+r",
title: "R-Group Label Tool",
action: { tool: 'rgroupatom' }
},
"rgroup-fragment": {
shortcut: ["Mod+Shift+r", "Mod+r"],
title: "R-Group Fragment Tool",
action: { tool: 'rgroupfragment' }
},
"rgroup-attpoints": {
shortcut: "Mod+r",
title: "Attachment Point Tool",
action: { tool: 'apoint' }
},
};
const bondCuts = {
"single": "1",
"double": "2",
"triple": "3",
"up": "1",
"down": "1",
"updown": "1",
"crossed": "2",
"any": "0",
"aromatic": "4",
};
const typeSchema = bondSchema.properties.type;
export default typeSchema.enum.reduce((res, type, i) => {
res[`bond-${type}`] = {
title: `${typeSchema.enumNames[i]} Bond`,
shortcut: bondCuts[type],
action: {
tool: 'bond',
opts: toBondType(type)
}
};
return res;
}, toolActions);

View File

@ -0,0 +1,56 @@
/****************************************************************************
* 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 { findIndex, findLastIndex } from 'lodash/fp';
export const zoomList = [
0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1,
1.1, 1.2, 1.3, 1.4, 1.5, 1.7, 2, 2.5, 3, 3.5, 4
];
export default {
"zoom": {
selected: editor => editor.zoom()
},
"zoom-out": {
shortcut: ["-", "_", "Shift+-"],
title: "Zoom Out",
disabled: editor => (
editor.zoom() <= zoomList[0] // unsave
),
action: editor => {
let zoom = editor.zoom();
let i = findIndex(z => z >= zoom, zoomList);
editor.zoom(
zoomList[(zoomList[i] === zoom && i > 0) ? i - 1 : i]
);
}
},
"zoom-in": {
shortcut: ["+", "=", "Shift+="],
title: "Zoom In",
disabled: editor => (
zoomList[zoomList.length - 1] <= editor.zoom()
),
action: editor => {
let zoom = editor.zoom();
let i = findLastIndex(z => z <= zoom, zoomList);
editor.zoom(
zoomList[(zoomList[i] === zoom && i < zoomList.length - 1) ? i + 1 : i]
);
}
}
}

View File

@ -0,0 +1,138 @@
/****************************************************************************
* 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 { Provider, connect } from 'preact-redux';
import { omit } from 'lodash/fp';
import state, { onAction, load } from './state';
import { initTmplLib } from './state/templates';
import { initEditor } from './state/editor';
import { checkServer } from './state/server';
import { initKeydownListener, initClipboard } from './state/hotkeys';
import { initResize } from './state/toolbar';
import { h, Component, render } from 'preact';
/** @jsx h */
import Toolbar from './toolbar';
import StructEditor from './component/structeditor';
import ClipArea from './component/cliparea';
import modals from './dialog';
const AppEditor = connect(
state => ({
options: state.options.settings
}),
dispatch => dispatch(initEditor)
)(StructEditor);
const AppModal = connect(
state => ({
modal: state.modal
}),
dispatch => ({
onOk: function (res) {
console.info('Output:', res);
dispatch({ type: 'MODAL_CLOSE' });
},
onCancel: function () {
dispatch({ type: 'MODAL_CLOSE' });
}
}),
(stateProps, dispatchProps) => {
let prop = stateProps.modal && stateProps.modal.prop;
let initProps = prop ? omit(['onResult', 'onCancel'], prop) : {};
return {
modal: stateProps.modal,
...initProps,
onOk: function (res) {
if (prop && prop.onResult) prop.onResult(res);
dispatchProps.onOk(res);
},
onCancel: function () {
if (prop && prop.onCancel) prop.onCancel();
dispatchProps.onCancel();
}
};
}
)(({modal, ...props}) => {
if (!modal)
return null;
let Modal = modals[modal.name];
if (!Modal)
throw new Error(`There is no modal window named ${modal.name}`);
return (
<div className="overlay">
<Modal {...props}/>
</div>
);
});
const AppTemplates = connect(
null,
dispatch => ({
onInitTmpls: (cacheEl) => initTmplLib(dispatch, '', cacheEl)
})
)(class extends Component {
componentDidMount() {
this.props.onInitTmpls(this.cacheEl);
}
render = () => (<div className="cellar" ref={c => this.cacheEl = c} />)
});
const AppCliparea = connect(
null,
dispatch => (dispatch(initClipboard))
)(ClipArea);
const App = connect(
null,
{ onAction, checkServer }
)(class extends Component {
componentDidMount() {
this.props.checkServer();
}
render = props => (
<main role="application">
<AppEditor id="canvas" />
<Toolbar {...props}/>
<AppCliparea/>
<AppModal/>
<AppTemplates/>
</main>
)
});
function init(el, options, server) {
const store = state(options, server);
store.dispatch(initKeydownListener(el));
store.dispatch(initResize());
render((
<Provider store={store}>
<App/>
</Provider>
), el);
return {
load: (structStr, options) => store.dispatch(load(structStr, options))
}
}
export default init;

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>
);
}
}

View File

@ -0,0 +1,223 @@
/****************************************************************************
* 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';
const editor = {
resetToSelect: {
title: "Reset to Select Tool",
enum: [true, 'paste', false],
enumNames: ['on', 'After Paste', 'off'],
default: 'paste'
},
rotationStep: {
title: "Rotation Step, º",
type: "integer",
minimum: 1,
maximum: 90,
default: 15
},
};
const miew = {
miewMode: {
title: "Display mode",
enum: ['lines', 'balls and sticks', 'licorice'],
enumNames: ['Lines', 'Balls and Sticks', 'Licorice'],
default: 'lines'
},
miewTheme: {
title: "Background color",
enum: ['light', 'dark'],
enumNames: ['Light', 'Dark'],
default: 'light'
},
miewAtomLabel: {
title: "Label coloring",
enum: ['no', 'bright', 'inverse', 'black and white', 'black'],
enumNames: ['No', 'Bright', 'Inverse', 'Black and White', 'Black'],
default: 'bright'
},
};
const render = {
showValenceWarnings: {
title: "Show valence warnings",
type: "boolean",
default: true
},
atomColoring: {
title: "Atom coloring",
type: "boolean",
default: true
},
hideChiralFlag: {
title: "Do not show the Chiral flag",
type: "boolean",
default: false
},
font: {
title: "Font",
type: "string",
default: '30px Arial'
},
fontsz: {
title: "Font size",
type: "integer",
default: 13,
minimum: 1,
maximum: 96
},
fontszsub: {
title: "Sub font size",
type: "integer",
default: 13,
minimum: 1,
maximum: 96
},
// Atom
carbonExplicitly: {
title: "Display carbon explicitly",
type: "boolean",
default: false
},
showCharge: {
title: "Display charge",
type: "boolean",
default: true
},
showValence: {
title: "Display valence",
type: "boolean",
default: true
},
showHydrogenLabels: {
title: "Show hydrogen labels",
enum: ['off', 'Hetero', 'Terminal', 'Terminal and Hetero', 'on'],
default: 'on',
},
// Bonds
aromaticCircle: {
title: "Aromatic Bonds as circle",
type: "boolean",
default: true
},
doubleBondWidth: {
title: "Double bond width",
type: "integer",
default: 6,
minimum: 1,
maximum: 96
},
bondThickness: {
title: "Bond thickness",
type: "integer",
default: 2,
minimum: 1,
maximum: 96
},
stereoBondWidth: {
title: "Stereo (Wedge) bond width",
type: "integer",
default: 6,
minimum: 1,
maximum: 96
}
};
const server = {
'smart-layout': {
title: "Smart-layout",
type: "boolean",
default: true
},
'ignore-stereochemistry-errors': {
title: "Ignore stereochemistry errors",
type: "boolean",
default: true
},
'mass-skip-error-on-pseudoatoms': {
title: "Ignore pseudoatoms at mass",
type: "boolean",
default: false
},
'gross-formula-add-rsites': {
title: "Add Rsites at mass calculation",
type: "boolean",
default: true
}
};
export const SERVER_OPTIONS = Object.keys(server);
const debug = {
showAtomIds: {
title: "Show atom Ids",
type: "boolean",
default: false
},
showBondIds: {
title: "Show bonds Ids",
type: "boolean",
default: false
},
showHalfBondIds: {
title: "Show half bonds Ids",
type: "boolean",
default: false
},
showLoopIds: {
title: "Show loop Ids",
type: "boolean",
default: false
}
};
const optionsSchema = {
title: "Settings",
type: "object",
required: [],
properties: {
...editor,
...render,
...miew,
...server,
...debug
}
};
export default optionsSchema;
export function getDefaultOptions() {
return Object.keys(optionsSchema.properties).reduce((res, prop) => {
res[prop] = optionsSchema.properties[prop].default;
return res;
}, {});
}
export function validation(settings) {
if (typeof settings !== 'object' || settings === null) return null;
const v = new jsonschema.Validator();
const { errors } = v.validate(settings, optionsSchema);
const errProps = errors.map(err => err.property.split('.')[1]);
return Object.keys(settings).reduce((res, prop) => {
if (optionsSchema.properties[prop] && errProps.indexOf(prop) === -1)
res[prop] = settings[prop];
return res;
}, {});
}

View File

@ -0,0 +1,402 @@
/****************************************************************************
* 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 { mapOf } from '../utils';
const radioButtonsSchema = {
enum: [
"Absolute",
"Relative",
"Attached"
],
default: "Absolute"
};
const contextSchema = {
title: 'Context',
enum: [
'Fragment',
'Multifragment',
'Bond',
'Atom',
'Group'
],
default: 'Fragment'
};
const sData = {
Fragment: {
title: 'Fragment',
type: 'Object',
oneOf: [
{
key: 'FRG_STR',
title: 'MDLBG_FRAGMENT_STEREO',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: 'Field name',
enum: ["MDLBG_FRAGMENT_STEREO"],
default: "MDLBG_FRAGMENT_STEREO"
},
fieldValue: {
title: "Field value",
type: 'array',
items: {
enum: [
"abs",
"(+)-enantiomer",
"(-)-enantiomer",
"racemate",
"steric",
"rel",
"R(a)",
"S(a)",
"R(p)",
"S(p)"
]
},
default: ["abs"]
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
},
{
key: 'FRG_COEFF',
title: 'MDLBG_FRAGMENT_COEFFICIENT',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: "Field name",
enum: ["MDLBG_FRAGMENT_COEFFICIENT"],
default: "MDLBG_FRAGMENT_COEFFICIENT"
},
fieldValue: {
title: "Field value",
type: "string",
default: "",
minLength: 1,
invalidMessage: "Please, specify field name"
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
},
{
key: 'FRG_CHRG',
title: 'MDLBG_FRAGMENT_CHARGE',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: 'Field name',
enum: ["MDLBG_FRAGMENT_CHARGE"],
default: "MDLBG_FRAGMENT_CHARGE"
},
fieldValue: {
title: "Field value",
type: "string",
default: "",
minLength: 1,
invalidMessage: "Please, specify field name"
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
},
{
key: 'FRG_RAD',
title: 'MDLBG_FRAGMENT_RADICALS',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: "Field name",
enum: ["MDLBG_FRAGMENT_RADICALS"],
default: "MDLBG_FRAGMENT_RADICALS"
},
fieldValue: {
title: "Field value",
type: "string",
default: "",
minLength: 1,
invalidMessage: "Please, specify field name"
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
},
]
},
Multifragment: {
title: 'Multifragment',
type: 'Object',
oneOf: [
{
key: 'MLT_FRG',
title: 'KETCHER_MULTIPLE_FRAGMENT',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: 'Field name',
enum: ["KETCHER_MULTIPLE_FRAGMENT"],
default: "KETCHER_MULTIPLE_FRAGMENT"
},
fieldValue: {
title: "Field value",
type: 'array',
items: {
enum: [
"aerosol",
"alloy",
"catenane",
"complex",
"composite",
"co-polymer",
"emulsion",
"host-guest complex",
"mixture",
"rotaxane",
"suspension"
]
},
default: ["aerosol"]
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
}
]
},
Bond: {
title: 'Bond',
type: 'Object',
oneOf: [
{
key: 'SB_STR',
title: 'MDLBG_STEREO_KEY',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: "Field name",
enum: ["MDLBG_STEREO_KEY"],
default: "MDLBG_STEREO_KEY"
},
fieldValue: {
title: "Field value",
type: 'array',
items: {
enum: [
"erythro",
"threo",
"alpha",
"beta",
"endo",
"exo",
"anti",
"syn",
"ECL",
"STG"
]
},
default: ["erythro"]
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
},
{
key: 'SB_BND',
title: 'MDLBG_BOND_KEY',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: "Field name",
enum: ["MDLBG_BOND_KEY"],
default: "MDLBG_BOND_KEY"
},
fieldValue: {
title: "Field value",
type: 'array',
items: {
enum: [
"Value=4"
]
},
default: ["Value=4"]
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
}
]
},
Atom: {
title: 'Atom',
type: 'Object',
oneOf: [
{
key: 'AT_STR',
title: 'MDLBG_STEREO_KEY',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: "Field name",
enum: ["MDLBG_STEREO_KEY"],
default: "MDLBG_STEREO_KEY"
},
fieldValue: {
title: "Field value",
type: 'array',
items: {
enum: [
"RS",
"SR",
"P-3",
"P-3-PI",
"SP-4",
"SP-4-PI",
"T-4",
"T-4-PI",
"SP-5",
"SP-5-PI",
"TB-5",
"TB-5-PI",
"OC-6",
"TP-6",
"PB-7",
"CU-8",
"SA-8",
"DD-8",
"HB-9",
"TPS-9"
],
},
default: ["RS"]
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
}
]
},
Group: {
title: 'Group',
type: 'Object',
oneOf: [
{
key: 'GRP_STR',
title: 'MDLBG_STEREO_KEY',
properties: {
type: { enum: ["DAT"] },
fieldName: {
title: "Field name",
enum: ["MDLBG_STEREO_KEY"],
default: "MDLBG_STEREO_KEY"
},
fieldValue: {
title: "Field value",
type: 'array',
items: {
enum: [
"cis",
"trans"
]
},
default: ["cis"]
},
radiobuttons: radioButtonsSchema
},
required: ["fieldName", "fieldValue", "radiobuttons"]
}
]
}
};
export const sdataCustomSchema = {
key: 'Custom',
properties: {
type: { enum: ["DAT"] },
context: {
title: 'Context',
enum: [
'Fragment',
'Multifragment',
'Bond',
'Atom',
'Group'
],
default: 'Fragment'
},
fieldName: {
title: 'Field name',
type: "string",
default: "",
minLength: 1,
invalidMessage: "Please, specify field name"
},
fieldValue: {
title: 'Field value',
type: "string",
default: "",
minLength: 1,
invalidMessage: "Please, specify field value"
},
radiobuttons: {
enum: [
"Absolute",
"Relative",
"Attached"
],
default: "Absolute"
}
},
required: ["context", "fieldName", "fieldValue", "radiobuttons"]
};
export const sdataSchema = Object.keys(sData).reduce((acc, title) => {
acc[title] = mapOf(sData[title], 'fieldName');
Object.keys(acc[title]).forEach(fieldName => acc[title][fieldName].properties.context = contextSchema);
return acc;
}, {});
/**
* Returns first key of passed object
* @param obj { object }
*/
function firstKeyOf(obj) {
return Object.keys(obj)[0];
}
/**
* Returns schema default values. Depends on passed arguments:
* pass schema only -> returns default context
* pass schema & context -> returns default fieldName
* pass schema & context & fieldName -> returns default fieldValue
* @param context? { string }
* @param fieldName? { string }
* @returns { string }
*/
export function getSdataDefault(context, fieldName) {
if (!context && !fieldName)
return firstKeyOf(sdataSchema);
if (!fieldName)
return firstKeyOf(sdataSchema[context]);
return sdataSchema[context][fieldName] ?
sdataSchema[context][fieldName].properties.fieldValue.default :
'';
}

View File

@ -0,0 +1,154 @@
/****************************************************************************
* 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 molfile from '../../chem/molfile';
export default [
'Benzene\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 6 6 0 0 0 999 V2000\n' +
' 0.8660 2.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7320 1.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7320 0.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.8660 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.0000 0.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.0000 1.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1 2 1 0 0 0\n' +
' 2 3 2 0 0 0\n' +
' 3 4 1 0 0 0\n' +
' 4 5 2 0 0 0\n' +
' 5 6 1 0 0 0\n' +
' 6 1 2 0 0 0\n' +
'M END\n',
'Cyclopentadiene\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 5 5 0 0 0 999 V2000\n' +
' 0.0000 1.4257 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.8090 0.8379 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.5000 -0.1132 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' -0.5000 -0.1132 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' -0.8090 0.8379 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1 2 1 0 0 0\n' +
' 2 3 2 0 0 0\n' +
' 3 4 1 0 0 0\n' +
' 4 5 2 0 0 0\n' +
' 5 1 1 0 0 0\n' +
'M END\n',
'Cyclohexane\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 6 6 0 0 0 999 V2000\n' +
' 0.8660 2.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7320 1.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7320 0.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.8660 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.0000 0.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.0000 1.5000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1 2 1 0 0 0\n' +
' 2 3 1 0 0 0\n' +
' 3 4 1 0 0 0\n' +
' 4 5 1 0 0 0\n' +
' 5 6 1 0 0 0\n' +
' 6 1 1 0 0 0\n' +
'M END\n',
'Cyclopentane\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 5 5 0 0 0 999 V2000\n' +
' 0.8090 1.5389 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.6180 0.9511 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.3090 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.3090 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.0000 0.9511 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1 2 1 0 0 0\n' +
' 2 3 1 0 0 0\n' +
' 3 4 1 0 0 0\n' +
' 4 5 1 0 0 0\n' +
' 5 1 1 0 0 0\n' +
'M END\n',
'Cyclopropane\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 3 3 0 0 0 999 V2000\n' +
' -3.2250 -0.2750 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' -2.2250 -0.2750 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' -2.7250 0.5910 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1 2 1 0 0 0\n' +
' 2 3 1 0 0 0\n' +
' 1 3 1 0 0 0\n' +
'M END\n',
'Cyclobutane\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 4 4 0 0 0 999 V2000\n' +
' -3.8250 1.5500 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' -3.8250 0.5500 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' -2.8250 1.5500 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' -2.8250 0.5500 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1 2 1 0 0 0\n' +
' 1 3 1 0 0 0\n' +
' 3 4 1 0 0 0\n' +
' 4 2 1 0 0 0\n' +
'M END\n',
'Cycloheptane\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 7 7 0 0 0 999 V2000\n' +
' 0.0000 1.6293 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.7835 2.2465 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7559 2.0242 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 2.1897 1.1289 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.0000 0.6228 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7566 0.2224 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.7835 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 6 7 1 0 0 0\n' +
' 5 7 1 0 0 0\n' +
' 1 5 1 0 0 0\n' +
' 4 6 1 0 0 0\n' +
' 3 4 1 0 0 0\n' +
' 2 3 1 0 0 0\n' +
' 1 2 1 0 0 0\n' +
'M END\n',
'Cyclooctane\n' +
' Ketcher 11161218352D 1 1.00000 0.00000 0\n' +
'\n' +
' 8 8 0 0 0 999 V2000\n' +
' 0.0000 0.7053 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.0000 1.7078 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.7053 2.4131 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 0.7056 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7079 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 2.4133 0.7053 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 2.4133 1.7078 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 1.7079 2.4131 0.0000 C 0 0 0 0 0 0 0 0 0\n' +
' 8 3 1 0 0 0\n' +
' 7 8 1 0 0 0\n' +
' 6 7 1 0 0 0\n' +
' 5 6 1 0 0 0\n' +
' 4 5 1 0 0 0\n' +
' 1 4 1 0 0 0\n' +
' 2 3 1 0 0 0\n' +
' 1 2 1 0 0 0\n' +
'M END\n'
].map(structStr => molfile.parse(structStr));

View File

@ -0,0 +1,70 @@
/****************************************************************************
* 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';
import { connect } from 'preact-redux';
/** @jsx h */
import Dialog from '../component/dialog';
function About(props) {
return (
<Dialog title="About"
className="about" params={props}
buttons={["Close"]}>
<a href="http://lifescience.opensource.epam.com/ketcher/" target="_blank">
<img src="images/ketcher-logo.svg"/>
</a>
<dl>
<dt>
<a href="http://lifescience.opensource.epam.com/ketcher/help.html" target="_blank">Ketcher</a>
</dt>
<dd>
version <var>{props.version}</var>
</dd>
{
props.buildNumber ? (
<dd>
build #<var>{props.buildNumber}</var>
{" at "}
<time>{props.buildDate}</time>
</dd> ) : null
}
{
props.indigoVersion ? (
<div>
<dt>
<a href="http://lifescience.opensource.epam.com/indigo/" target="_blank">Indigo
Toolkit</a>
</dt>
<dd>version <var>{props.indigoVersion}</var></dd>
</div>
) : ( <dd>standalone</dd> )
}
<dt>
<a href="http://lifescience.opensource.epam.com/" target="_blank">EPAM Life Sciences</a>
</dt>
<dd>
<a href="http://lifescience.opensource.epam.com/ketcher/#feedback" target="_blank">Feedback</a>
</dd>
</dl>
</Dialog>
);
}
export default connect(
store => ({ ...store.options.app })
)(About);

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 { range } from 'lodash/fp';
import { h, Component } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import keyName from 'w3c-keyname';
import Dialog from '../component/dialog';
import Input from '../component/input';
import { changeRound } from '../state/options';
import { analyse } from '../state/server';
function FrozenInput({value}) {
return (
<input type="text" spellCheck={false} value={value}
onKeyDown={ev => allowMovement(ev)}/>
);
}
const formulaRegexp = /\b([A-Z][a-z]{0,3})(\d*)\s*\b/g;
const errorRegexp = /error:.*/g;
function formulaInputMarkdown(value) {
return (
<div className="chem-input" spellCheck={false} contentEditable={true}
onKeyDown={ev => allowMovement(ev)}>{value}</div>
);
}
function FormulaInput({value}) {
if (errorRegexp.test(value)) {
return formulaInputMarkdown(value);
}
const content = [];
var cnd;
var pos = 0;
while (cnd = formulaRegexp.exec(value)) {
content.push(value.substring(pos, cnd.index) + cnd[1]);
if (cnd[2].length > 0) content.push(<sub>{cnd[2]}</sub>);
pos = cnd.index + cnd[0].length;
}
if (pos === 0) content.push(value);
else content.push(value.substring(pos, value.length));
return formulaInputMarkdown(content);
}
class Analyse extends Component {
constructor(props) {
super(props);
props.onAnalyse();
}
render() {
const { values, round, onAnalyse, onChangeRound, ...props } = this.props;
return (
<Dialog title="Calculated Values" className="analyse"
buttons={["Close"]} params={props}>
<ul>{[
{ name: 'Chemical Formula', key: 'gross' },
{ name: 'Molecular Weight', key: 'molecular-weight', round: 'roundWeight' },
{ name: 'Exact Mass', key: 'monoisotopic-mass', round: 'roundMass' },
{ name: 'Elemental Analysis', key: 'mass-composition' }
].map(item => (
<li>
<label>{item.name}:</label>
{item.key === 'gross'
? <FormulaInput value={values ? values[item.key] : 0}/>
: <FrozenInput value={values ? roundOff(values[item.key], round[item.round]) : 0}/>
}
{item.round
? <Input schema={{
enum: range(0, 8),
enumNames: range(0, 8).map(i => `${i} decimal places`)
}} value={round[item.round]} onChange={val => onChangeRound(item.round, val)}/>
: null
}
</li>
))
}</ul>
</Dialog>
);
}
}
function allowMovement(event) {
const movementKeys = ['Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End'];
const key = keyName(event);
if (movementKeys.indexOf(key) === -1)
event.preventDefault();
}
function roundOff(value, round) {
if (typeof value === 'number')
return value.toFixed(round);
return value.replace(/[0-9]*\.[0-9]+/g, (str) => (
(+str).toFixed(round)
));
}
export default connect(
store => ({
values: store.options.analyse.values,
round: {
roundWeight: store.options.analyse.roundWeight,
roundMass: store.options.analyse.roundMass
}
}),
dispatch => ({
onAnalyse: () => dispatch(analyse()),
onChangeRound: (roundName, val) => dispatch(changeRound(roundName, val))
})
)(Analyse);

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 { capitalize } from 'lodash/fp';
import { h } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import { atom as atomSchema } from '../structschema';
import { Form, Field } from '../component/form';
import Dialog from '../component/dialog';
import element from '../../chem/element';
function ElementNumber(props, {stateStore}) {
let { result } = stateStore.props;
return (
<label>Number:
<input className="number" type="text" readOnly={true}
value={element.map[capitalize(result.label)] || ''}/>
</label>
);
}
function Atom(props) {
let { formState, ...prop } = props;
return (
<Dialog title="Atom Properties" className="atom-props"
result={() => formState.result} valid={() => formState.valid} params={prop}>
<Form schema={atomSchema} customValid={{ label: l => atomValid(l) }}
init={prop} {...formState}>
<fieldset className="main">
<Field name="label"/>
<Field name="alias"/>
<ElementNumber/>
<Field name="charge" maxlength="5"/>
<Field name="explicitValence"/>
<Field name="isotope"/>
<Field name="radical"/>
</fieldset>
<fieldset className="query">
<legend>Query specific</legend>
<Field name="ringBondCount"/>
<Field name="hCount"/>
<Field name="substitutionCount"/>
<Field name="unsaturatedAtom"/>
</fieldset>
<fieldset className="reaction">
<legend>Reaction flags</legend>
<Field name="invRet"/>
<Field name="exactChangeFlag"/>
</fieldset>
</Form>
</Dialog>
);
}
function atomValid(label) {
return label && !!element.map[capitalize(label)];
}
export default connect(
(store) => ({ formState: store.modal.form })
)(Atom);

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';
import { connect } from 'preact-redux';
/** @jsx h */
import { attachmentPoints as attachmentPointsSchema } from '../structschema';
import { Form, Field } from '../component/form';
import Dialog from '../component/dialog';
function AttachmentPoints (props) {
let { formState, ...prop} = props;
return (
<Dialog title="Attachment Points" className="attach-points"
result={() => formState.result} valid={() => formState.valid} params={prop}>
<Form schema={attachmentPointsSchema} init={prop} {...formState}>
<Field name="primary"/>
<Field name="secondary"/>
</Form>
</Dialog>
);
}
export default connect(
(store) => ({ formState: store.modal.form })
)(AttachmentPoints);

View File

@ -0,0 +1,59 @@
/****************************************************************************
* 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';
import { connect } from 'preact-redux';
/** @jsx h */
import { Form, Field } from '../component/form';
import Dialog from '../component/dialog';
import { automap } from '../state/server';
export const automapSchema = {
title: "Reaction Auto-Mapping",
type: "object",
required: ["mode"],
properties: {
mode: {
title: "Mode",
enum: ["discard", "keep", "alter", "clear"],
enumNames: ["Discard", "Keep", "Alter", "Clear"],
default: "discard"
}
}
};
function Automap (props) {
let { formState, ...prop} = props;
return (
<Dialog title="Reaction Auto-Mapping" className="automap"
result={() => formState.result} valid={() => formState.valid} params={prop}>
<Form schema={automapSchema} {...formState}>
<Field name="mode"/>
</Form>
</Dialog>
);
}
export default connect(
(store) => ({ formState: store.modal.form }),
(dispatch, props) => ({
onOk: (res) => {
dispatch(automap(res));
props.onOk(res);
}
})
)(Automap);

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';
import { connect } from 'preact-redux';
/** @jsx h */
import { bond as bondSchema } from '../structschema';
import { Form, Field } from '../component/form';
import Dialog from '../component/dialog';
function Bond(props) {
let { formState, ...prop} = props;
return (
<Dialog title="Bond Properties" className="bond"
result={() => formState.result} valid={() => formState.valid} params={prop} >
<Form schema={bondSchema} init={prop} {...formState}>
<Field name="type"/>
<Field name="topology"/>
<Field name="center"/>
</Form>
</Dialog>
);
}
export default connect(
(store) => ({ formState: store.modal.form })
)(Bond);

View File

@ -0,0 +1,89 @@
/****************************************************************************
* 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';
import { connect } from 'preact-redux';
/** @jsx h */
import Dialog from '../component/dialog';
import Tabs from '../component/tabs';
import { Form, Field } from '../component/form';
import { check } from '../state/server';
const checkSchema = {
title: 'Check',
type: 'object',
properties: {
checkOptions: {
type: 'array',
items: {
type: "string",
enum: ['valence', 'radicals', 'pseudoatoms', 'stereo', 'query', 'overlapping_atoms',
'overlapping_bonds', 'rgroups', 'chiral', '3d'],
enumNames: ['Valence', 'Radical', 'Pseudoatom', 'Stereochemistry', 'Query', 'Overlapping Atoms',
'Overlapping Bonds', 'R-Groups', 'Chirality', '3D Structure']
}
}
}
};
function getOptionName(opt) {
const d = checkSchema.properties.checkOptions.items;
return d.enumNames[d.enum.indexOf(opt)];
}
function Check(props) {
const tabs = ['Check', 'Settings'];
const { formState, onCheck, ...prop } = props;
const { result, moleculeErrors } = formState;
return (
<Dialog title="Structure Check" className="check"
result={() => result} params={prop}>
<Form schema={checkSchema} {...formState}>
<Tabs className="tabs" captions={tabs}
changeTab={(i) => i === 0 ? onCheck(result.checkOptions) : null}>
<ErrorsCheck moleculeErrors={moleculeErrors}/>
<Field name="checkOptions" multiple={true} type="checkbox"/>
</Tabs>
</Form>
</Dialog>
);
}
function ErrorsCheck(props) {
const { moleculeErrors } = props;
const moleculeErrorsTypes = Object.keys(moleculeErrors);
return (
<fieldset {...props}>
{moleculeErrorsTypes.length === 0 ?
<dt>No errors found</dt> :
moleculeErrorsTypes.map(type => (
<div>
<dt>{getOptionName(type)} error :</dt>
<dd>{moleculeErrors[type]}</dd>
</div>
))}
</fieldset>
);
}
export default connect(
store => ({ formState: store.modal.form }),
dispatch => ({
onCheck: (opts) => dispatch(check(opts))
})
)(Check);

View File

@ -0,0 +1,134 @@
/****************************************************************************
* 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 generics from '../../chem/generics';
const viewSchema = {
'atom': {
caption: 'Atom Generics',
order: ['any', 'no-carbon', 'metal', 'halogen']
},
'group': {
caption: 'Group Generics',
order: ['acyclic', 'cyclic']
},
'special': {
caption: 'Special Nodes',
order: []
},
'group/acyclic': {
caption: 'Acyclic',
order: ['carbo', 'hetero']
},
'group/cyclic': {
caption: 'Cyclic',
order: ['no-carbon', 'carbo', 'hetero']
},
'group/acyclic/carbo': {
caption: 'Carbo',
order: ['alkynyl', 'alkyl', 'alkenyl']
},
'group/acyclic/hetero': {
caption: 'Hetero',
order: ['alkoxy']
},
'group/cyclic/carbo': {
caption: 'Carbo',
order: ['aryl', 'cycloalkyl', 'cycloalkenyl']
},
'group/cyclic/hetero': {
caption: 'Hetero',
order: ['aryl']
},
'atom/any': 'any atom',
'atom/no-carbon': 'except C or H',
'atom/metal': 'any metal',
'atom/halogen': 'any halogen',
'group/cyclic/no-carbon': 'no carbon',
'group/cyclic/hetero/aryl': 'hetero aryl'
};
function GenSet({labels, caption='', selected, onSelect, ...props}) {
return (
<fieldset {...props}>
{
labels.map(label => (
<button onClick={e => onSelect(label)}
className={selected(label) ? 'selected' : ''}>
{label}</button>
))
}
{
caption ? (
<legend>{caption}</legend>
) : null
}
</fieldset>
);
}
function GenGroup({gen, name, path, selected, onSelect}) {
const group = gen[name];
const pk = path ? `${path}/${name}` : name;
const schema = viewSchema[pk];
return (schema && schema.caption) ? (
<fieldset className={name}>
<legend>{schema.caption}</legend>
{
group.labels ? (
<GenSet labels={group.labels}
selected={selected} onSelect={onSelect} />
) : null
}
{
schema.order.map(child => ( // TODO:order = Object.keys ifndef
<GenGroup gen={group} name={child} path={pk}
selected={selected} onSelect={onSelect}/>
))
}
</fieldset>
) : (
<GenSet labels={group.labels}
caption={schema || name} className={name}
selected={selected} onSelect={onSelect} />
);
}
function GenericGroups({ selected, onSelect, ...props }) {
return (
<div summary="Generic Groups" {...props}>
<div className="col">
<GenGroup gen={generics} name='atom'
selected={l => selected(l)}
onSelect={l => onSelect(l)}/>
<GenGroup gen={generics} name='special'
selected={l => selected(l)}
onSelect={l => onSelect(l)}/>
</div>
<div className="col">
<GenGroup gen={generics} name='group'
selected={l => selected(l)}
onSelect={l => onSelect(l)}/>
</div>
</div>
);
}
export default GenericGroups;

View File

@ -0,0 +1,32 @@
/****************************************************************************
* 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 Dialog from '../component/dialog';
function Help(props) {
return (
<Dialog title="Help"
className="help" params={props}
buttons={["Close"]}>
<iframe className="help" src="doc/help.html"></iframe>
</Dialog>
);
}
export default Help;

View File

@ -0,0 +1,64 @@
/****************************************************************************
* 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 Open from './open';
import Save from './save';
import Analyse from './analyse';
import Recognize from './recognize';
import PeriodTable from './period-table';
import Rgroup from './rgroup';
import TemplateAttach from './template-attach';
import TemplatesLib from './template-lib';
import About from './about';
import Help from './help';
import Miew from './miew';
// schemify dialogs
import Atom from './atom';
import AttachPoints from './attach';
import Automap from './automap';
import Bond from './bond';
import Check from './check';
import LabelEdit from './labeledit';
import RgroupLogic from './rgroup-logic';
import Settings from './options';
import Sgroup from './sgroup';
import Sdata from './sdata';
export default {
open: Open,
save: Save,
analyse: Analyse,
recognize: Recognize,
'period-table': PeriodTable,
rgroup: Rgroup,
attach: TemplateAttach,
templates: TemplatesLib,
about: About,
help: Help,
miew: Miew,
atomProps: Atom,
attachmentPoints: AttachPoints,
automap: Automap,
bondProps: Bond,
check: Check,
labelEdit: LabelEdit,
rgroupLogic: RgroupLogic,
settings: Settings,
sgroup: Sgroup,
sdata: Sdata
};

View File

@ -0,0 +1,96 @@
/****************************************************************************
* 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 { capitalize } from 'lodash/fp';
import { h } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import element from '../../chem/element';
import Dialog from '../component/dialog';
import { Form, Field } from '../component/form';
export const labelEditSchema = {
title: "Label Edit",
type: "object",
required: ["label"],
properties: {
label: {
title: "Atom",
default: ''
}
}
};
function serialize(lc) {
const charge = Math.abs(lc.charge);
const radical = ['', ':', '.', '^^'][lc.radical] || '';
let sign = '';
if (charge)
sign = lc.charge < 0 ? '-' : '+';
return (lc.isotope || '') + lc.label + radical +
(charge > 1 ? charge: '') + sign;
}
function deserialize(value) {
const match = value.match(/^(\d+)?([a-z*]{1,3})(\.|:|\^\^)?(\d+[-+]|[-+])?$/i); // TODO: radical on last place
if (match) {
const label = match[2] === '*' ? 'A' : capitalize(match[2]);
let charge = 0;
let isotope = 0;
let radical = 0;
if (match[1])
isotope = parseInt(match[1]);
if (match[3])
radical = { ':': 1, '.': 2, '^^': 3 }[match[3]];
if (match[4]) {
charge = parseInt(match[4]);
if (isNaN(charge)) // NaN => [-+]
charge = 1;
if (match[4].endsWith('-'))
charge = -charge;
}
// Not consistant
if (label === 'A' || label === 'Q' || label === 'X' || label === 'M' || element.map[label])
return { label, charge, isotope, radical };
}
return null;
}
function LabelEdit(props) {
const init = { label: props.letter || serialize(props) };
const { formState, ...prop} = props;
const { result, valid } = formState;
return (
<Dialog title="Label Edit" className="labeledit" valid={() => valid}
result={() => deserialize(result.label)} params={prop}>
<Form schema={labelEditSchema} customValid={{label: l => deserialize(l)}}
init={init} {...formState}>
<Field name="label" maxlength="20" size="10"/>
</Form>
</Dialog>
);
}
export default connect(
(store) => ({ formState: store.modal.form })
)(LabelEdit);

View File

@ -0,0 +1,223 @@
/****************************************************************************
* 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 { camelCase } from 'lodash/fp';
import { h, Component } from 'preact';
/** @jsx h */
import Dialog from '../component/dialog';
import { storage } from '../utils';
const MIEW_PATH = '__MIEW_PATH__';
const MIEW_OPTIONS = {
preset: 'small',
settings: {
theme: 'light',
atomLabel: 'bright',
autoPreset: false,
inversePanning: true
},
reps: [{
mode: 'LN',
colorer: 'AT',
selector: 'all'
}]
};
const MIEW_WINDOW = {
location: 'no',
menubar: 'no',
toolbar: 'no',
directories: 'no',
modal: 'yes',
alwaysRaised: 'yes'
};
const MIEW_MODES = {
'lines': 'LN',
'ballsAndSticks': 'BS',
'licorice': 'LC'
};
function getLocalMiewOpts() {
let userOpts = storage.getItem("ketcher-opts");
if (!userOpts)
return MIEW_OPTIONS;
const opts = MIEW_OPTIONS;
if (userOpts.miewTheme)
opts.settings.theme = camelCase(userOpts.miewTheme);
if (userOpts.miewAtomLabel)
opts.settings.atomLabel = camelCase(userOpts.miewAtomLabel);
if (userOpts.miewMode)
opts.reps[0].mode = MIEW_MODES[camelCase(userOpts.miewMode)];
return opts;
}
function origin (url) {
let loc = url;
if (!loc.href) {
loc = document.createElement('a');
loc.href = url;
}
if (loc.origin)
return loc.origin;
if (!loc.hostname) // relative url, IE
loc = document.location;
return loc.protocol + '//' + loc.hostname +
(!loc.port ? '' : ':' + loc.port);
}
function queryOptions(options, sep='&') {
if (Array.isArray(options)) {
return options.reduce((res, item) => {
let value = queryOptions(item);
if (value !== null)
res.push(value);
return res;
}, []).join(sep);
} else if (typeof options === 'object') {
return Object.keys(options).reduce((res, item) => {
let value = options[item];
res.push(typeof value === 'object' ?
queryOptions(value) :
encodeURIComponent(item) + '=' +
encodeURIComponent(value));
return res;
}, []).join(sep);
} else {
return null;
}
}
function miewLoad(wnd, url, options={}) { // TODO: timeout
return new Promise(function (resolve, reject) {
addEventListener('message', function onload(event) {
if (event.origin === origin(url) && event.data === 'miewLoadComplete') {
window.removeEventListener('message', onload);
let miew = wnd.MIEWS[0];
miew._opts.load = false; // setOptions({ load: '' })
miew._menuDisabled = true; // no way to disable menu after constructor return
if (miew.init()) {
miew.setOptions(options);
miew.benchmarkGfx().then(() => {
miew.run();
setTimeout(() => resolve(miew), 10);
// see setOptions message handler
});
}
}
});
});
}
function miewSave(miew, url) {
miew.saveData();
return new Promise(function (resolve, reject) {
addEventListener('message', function onsave(event) {
if (event.origin === origin(url) && event.data.startsWith('CML:')) {
window.removeEventListener('message', onsave);
resolve(atob(event.data.slice(4)));
}
});
});
}
class Miew extends Component {
constructor(props) {
console.info('init');
super(props);
this.opts = getLocalMiewOpts();
}
load(ev) {
let miew = miewLoad(ev.target.contentWindow,
MIEW_PATH, this.opts);
this.setState({ miew });
this.state.miew.then(miew => {
miew.parse(this.props.structStr, {
fileType: 'cml',
loaded: true
});
this.setState({ miew });
});
}
save(ev) {
if (this.props.onOk) {
let structStr = miewSave(this.state.miew, MIEW_PATH);
this.setState({ structStr });
this.state.structStr.then(structStr => {
this.props.onOk({ structStr });
});
}
}
window() {
let opts = {
...this.opts,
load: `CML:${btoa(this.props.structStr)}`,
sourceType: 'message'
};
let br = this.base.getBoundingClientRect(); // Preact specifiec
// see: epa.ms/1NAYWp
let wndProps = {
...MIEW_WINDOW,
top: Math.round(br.top),
left: Math.round(br.left),
width: Math.round(br.width),
height: Math.round(br.height)
};
let wnd = window.open(`${MIEW_PATH}?${queryOptions(opts)}`,
'miew', queryOptions(wndProps, ','));
if (wnd) {
this.props.onCancel && this.props.onCancel();
wnd.onload = function () {
console.info('windowed');
};
}
}
render(props) {
let {miew, structStr} = this.state;
return (
<Dialog title="3D View"
className="miew" params={props}
buttons={[
"Close",
<button disabled={miew instanceof Promise || structStr instanceof Promise}
onClick={ ev => this.save(ev) }>
Apply
</button>,
<button className="window"
disabled={/MSIE|rv:11/i.test(navigator.userAgent)}
onClick={ ev => this.window() }>
Detach to new window
</button>
]}>
<iframe id="miew-iframe"
src={MIEW_PATH}
onLoad={ev => this.load(ev) }></iframe>
</Dialog>
);
}
}
export default Miew;

View File

@ -0,0 +1,95 @@
/****************************************************************************
* 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 { connect } from 'preact-redux';
/** @jsx h */
import { map as formatMap } from '../structformat';
import Dialog from '../component/dialog';
import OpenButton from '../component/openbutton';
import ClipArea, { exec } from '../component/cliparea';
import { load } from '../state';
class Open extends Component {
constructor(props) {
super(props);
this.state = {
structStr: '',
fragment: false
};
}
result() {
let { structStr, fragment } = this.state;
return structStr ? { structStr, fragment } : null;
}
changeStructStr(structStr) {
this.setState({ structStr });
}
changeFragment(target) {
this.setState({
fragment: target.checked
});
}
render () {
let { structStr, fragment } = this.state;
return (
<Dialog title="Open Structure"
className="open" result={() => this.result()}
params={this.props}
buttons={[(
<OpenButton className="open" server={this.props.server}
type={structAcceptMimes()}
onLoad={s => this.changeStructStr(s)}>
Open From File
</OpenButton>
), "Cancel", "OK"]}>
<textarea value={structStr}
onInput={ev => this.changeStructStr(ev.target.value)}/>
<label>
<input type="checkbox" checked={fragment}
onClick={ev => this.changeFragment(ev.target)}/>
Load as a fragment and copy to the Clipboard
</label>
<ClipArea focused={() => true}
onCopy={() => ({ 'text/plain': structStr })}/>
</Dialog>
);
}
}
function structAcceptMimes() {
return Object.keys(formatMap).reduce((res, key) => (
res.concat(formatMap[key].mime, ...formatMap[key].ext)
), []).join(',');
}
export default connect(
store => ({ server: store.server }),
(dispatch, props) => ({
onOk: (res) => {
if (res.fragment) exec('copy');
dispatch(
load(res.structStr, {
badHeaderRecover: true,
fragment: res.fragment
})
);
props.onOk(res);
}
})
)(Open);

View File

@ -0,0 +1,129 @@
/****************************************************************************
* 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';
import { connect } from 'preact-redux';
/** @jsx h */
import { updateFormState, setDefaultSettings } from '../state/form';
import { saveSettings } from '../state/options';
import settingsSchema from '../data/options-schema';
import { Form, Field } from '../component/form';
import { storage } from '../utils';
import Dialog from '../component/dialog';
import Accordion from '../component/accordion';
import SystemFonts from '../component/systemfonts';
import SaveButton from '../component/savebutton';
import OpenButton from '../component/openbutton';
import MeasureInput from '../component/measure-input';
function Settings(props) {
const { initState, formState, server, onOpenFile, onReset, appOpts, ...prop } = props;
const tabs = ['Rendering customization options', 'Atoms', 'Bonds', 'Server', '3D Viewer', 'Options for debugging'];
const activeTabs = { 0: true, 1: false, 2: false, 3: false, 4: false, 5: false };
return (
<Dialog title="Settings" className="settings"
result={() => formState.result} valid={() => formState.valid} params={prop}
buttons={[
<OpenButton className="open" server={ server } onLoad={ onOpenFile }>
Open From File
</OpenButton>,
<SaveButton className="save" data={JSON.stringify(formState.result)} filename={'ketcher-settings'}>
Save To File
</SaveButton>,
<button onClick={ onReset }>Reset</button>,
"OK", "Cancel"]} >
<Form schema={settingsSchema} init={initState} {...formState}>
<Accordion className="accordion" captions={tabs} active={activeTabs}>
<fieldset className="render">
<Field name="resetToSelect"/>
<Field name="rotationStep"/>
<SelectCheckbox name="showValenceWarnings"/>
<SelectCheckbox name="atomColoring"/>
<SelectCheckbox name="hideChiralFlag"/>
<Field name="font" component={SystemFonts}/>
<FieldMeasure name="fontsz"/>
<FieldMeasure name="fontszsub"/>
</fieldset>
<fieldset className="atoms">
<SelectCheckbox name="carbonExplicitly"/>
<SelectCheckbox name="showCharge"/>
<SelectCheckbox name="showValence"/>
<Field name="showHydrogenLabels"/>
</fieldset>
<fieldset className="bonds">
<SelectCheckbox name="aromaticCircle"/>
<FieldMeasure name="doubleBondWidth"/>
<FieldMeasure name="bondThickness"/>
<FieldMeasure name="stereoBondWidth"/>
</fieldset>
<fieldset className="server" disabled={!appOpts.server}>
<SelectCheckbox name="smart-layout"/>
<SelectCheckbox name="ignore-stereochemistry-errors"/>
<SelectCheckbox name="mass-skip-error-on-pseudoatoms"/>
<SelectCheckbox name="gross-formula-add-rsites"/>
</fieldset>
<fieldset className="3dView" disabled={!appOpts.miewPath}>
<Field name="miewMode"/>
<Field name="miewTheme"/>
<Field name="miewAtomLabel"/>
</fieldset>
<fieldset className="debug">
<SelectCheckbox name="showAtomIds"/>
<SelectCheckbox name="showBondIds"/>
<SelectCheckbox name="showHalfBondIds"/>
<SelectCheckbox name="showLoopIds"/>
</fieldset>
</Accordion>
{ !storage.isAvailable() ? <div className="warning">{storage.warningMessage}</div> : null }
</Form>
</Dialog>
);
}
function SelectCheckbox(props, {schema}) {
const desc = {
title: schema.properties[props.name].title,
enum: [true, false],
enumNames: ['on', 'off'],
};
return <Field schema={desc} {...props}/>;
}
function FieldMeasure(props, {schema}) {
return <Field schema={schema.properties[props.name]} component={MeasureInput} {...props}/>
}
export default connect(store => ({
appOpts: store.options.app,
initState: store.options.settings,
formState: store.modal.form
}), (dispatch, props) => ({
onOpenFile: newOpts => {
try {
dispatch(updateFormState({ result: JSON.parse(newOpts) }));
} catch (ex) {
console.info('Bad file');
}
},
onReset: () => dispatch(setDefaultSettings()),
onOk: (res) => {
dispatch(saveSettings(res));
props.onOk(res);
}
}))(Settings);

View File

@ -0,0 +1,272 @@
/****************************************************************************
* 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 { range } from 'lodash/fp';
import { h, Component } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import element from '../../chem/element';
import Dialog from '../component/dialog';
import Atom from '../component/atom';
import Tabs from '../component/tabs';
import GenericGroups from './generic-groups';
import { fromElement, toElement } from '../structconv';
import { onAction } from '../state';
import { addAtoms } from '../state/toolbar';
const typeSchema = [
{ title: 'Single', value: 'atom' },
{ title: 'List', value: 'list'},
{ title: 'Not List', value: 'not-list'}
];
const beforeSpan = {
'He': 16,
'B': 10,
'Al': 10,
'Hf': 1,
'Rf': 1
};
const main = rowPartition(element.filter(el => el && el.type !== 'actinide' &&
el.type !== 'lanthanide'));
const lanthanides = element.filter(el => el && el.type === 'lanthanide');
const actinides = element.filter(el => el && el.type === 'actinide');
function Header() {
return (
<tr>
{
range(0, 19).map(i => (
<th>{i || ''}</th>
))
}
</tr>
);
}
function TypeChoise({value, onChange, ...props}) {
return (
<fieldset>
{
typeSchema.map(sc => (
<label>
<input type="radio" value={sc.value}
checked={sc.value === value}
onClick={ev => onChange(sc.value) } {...props}/>
{sc.title}
</label>
))
}
</fieldset>
);
}
function MainRow({row, caption, refer, selected, onSelect, curEvents}) {
return (
<tr>
<th>{caption}</th>
{
row.map(el => (typeof el !== 'number') ? (
<td>
<Atom el={el}
className={selected(el.label) ? 'selected' : ''}
onClick={ev => onSelect(el.label)} {...curEvents(el)}/>
</td>
) : (
refer(el) ? ( <td className="ref">{refer(el)}</td> ) :
( <td colspan={el}/> )
))
}
</tr>
);
}
function OutinerRow({row, caption, selected, onSelect, curEvents}) {
return (
<tr>
<th colspan="3" className="ref">{caption}</th>
{
row.map(el => (
<td>
<Atom el={el}
className={selected(el.label) ? 'selected' : ''}
onClick={ev => onSelect(el.label)} {...curEvents(el)}/>
</td>
))
}
<td></td>
</tr>
);
}
function AtomInfo({el, isInfo}) {
const numberStyle = { color: el.color || 'black', 'font-size': '1.2em' };
const elemStyle = { color: el.color || 'black', 'font-weight': 'bold', 'font-size': '2em' };
return (
<div className={`atom-info ${isInfo ? '' : 'none'}`}>
<div style={numberStyle}>{element.map[el.label]}</div>
<span style={elemStyle}>{el.label}</span><br/>
{el.title}<br/>
{el.atomic_mass}
</div>
);
}
class PeriodTable extends Component {
constructor(props) {
super(props);
let genType = !!this.props.pseudo ? 'gen' : null;
this.state = {
type: props.type || genType || 'atom',
value: props.values || props.label || null,
cur: element[2],
isInfo: false
};
this.firstType = true;
}
changeType(type) {
if (this.firstType)
return this.firstType = false;
let pl = this.state.type === 'list' || this.state.type === 'not-list';
let l = type === 'list' || type === 'not-list';
if (l && pl)
this.setState({type});
else
this.setState({
type,
value: type === 'atom' || type === 'gen' ? null : []
});
}
selected(label) {
let {type, value} = this.state;
return (type === 'atom' || type === 'gen') ? value === label :
value.includes(label);
}
onSelect(label) {
let {type, value} = this.state;
if (type === 'atom' || type === 'gen')
this.setState({ value: label });
else {
let i = value.indexOf(label);
if (i < 0)
value.push(label);
else
value.splice(i, 1);
this.setState({ value });
}
}
result() {
let {type, value} = this.state;
if (type === 'atom')
return value ? { label: value, pseudo: null } : null;
else if (type === 'gen')
return value ? { type, label: value, pseudo: value} : null;
else
return value.length ? { type, values: value } : null;
}
curEvents = (el) => {
return {
onMouseEnter: () => this.setState({ cur: el, isInfo: true }),
onMouseLeave: () => this.setState({ isInfo: false })
};
};
render () {
const tabs = ['Table', 'Extended'];
let { type } = this.state;
return (
<Dialog title="Periodic table" className="elements-table"
params={this.props} result={() => this.result()}>
<Tabs className="tabs" captions={tabs} tabIndex={type !== 'gen' ? 0 : 1}
changeTab={(i) => this.changeType(i === 0 ? 'atom' : 'gen')}>
<div className="period-table">
<table summary="Periodic table of the chemical elements">
<Header/>
<AtomInfo el={this.state.cur} isInfo={this.state.isInfo}/>
{
main.map((row, i) => (
<MainRow row={row} caption={i + 1}
refer={o => o === 1 && (i === 5 ? '*' : '**')}
curEvents={this.curEvents}
selected={l => this.selected(l)}
onSelect={l => this.onSelect(l)}/>
))
}
<OutinerRow row={lanthanides} caption="*"
curEvents={this.curEvents}
selected={l => this.selected(l)}
onSelect={l => this.onSelect(l)}/>
<OutinerRow row={actinides} caption="**"
curEvents={this.curEvents}
selected={l => this.selected(l)}
onSelect={l => this.onSelect(l)}/>
</table>
<TypeChoise value={type}
onChange={t => this.changeType(t) }/>
</div>
<GenericGroups className="generic-groups"
selected={this.selected.bind(this)}
onSelect={this.onSelect.bind(this)}/>
</Tabs>
</Dialog>
);
}
}
function rowPartition(elements) {
return elements.reduce(function (res, el) {
let row = res[el.period - 1];
if (!row)
res.push([el]);
else {
if (beforeSpan[el.label])
row.push(beforeSpan[el.label]);
row.push(el);
}
return res;
}, []);
}
function mapSelectionToProps(editor) {
const selection = editor.selection();
if (selection && Object.keys(selection).length === 1 &&
selection.atoms && Object.keys(selection.atoms).length === 1) {
let struct = editor.struct();
let atom = struct.atoms.get(selection.atoms[0]);
return { ...fromElement(atom) }
}
return {};
}
export default connect(
(store, props) => {
if (props.values || props.label) return {};
return mapSelectionToProps(store.editor);
},
(dispatch, props) => ({
onOk: (res) => {
if (!res.type || res.type === 'atom') dispatch(addAtoms(res.label));
dispatch(onAction({ tool: 'atom', opts: toElement(res) }));
props.onOk(res);
}
})
)(PeriodTable);

View File

@ -0,0 +1,104 @@
/****************************************************************************
* 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';
import { connect } from 'preact-redux';
/** @jsx h */
import { changeImage, shouldFragment } from '../state/options';
import { load } from '../state';
import { recognize } from '../state/server';
import Dialog from '../component/dialog';
import Input from '../component/input';
import StructRender from '../component/structrender';
import OpenButton from '../component/openbutton';
import Spin from '../component/spin';
function Recognize(prop) {
const {file, structStr, fragment, onRecognize, isFragment, onImage, ...props} = prop;
const result = () =>
structStr && !(structStr instanceof Promise) ? {structStr, fragment} : null;
return (
<Dialog title="Import From Image" className="recognize"
params={props} result={() => result(structStr, fragment) }
buttons={[
<OpenButton className="open" onLoad={onImage} type="image/*">
Choose file
</OpenButton>,
<span className="open-filename">{file ? file.name : null}</span>,
file && !structStr ? (
<button onClick={() => onRecognize(file) }>Recognize</button>
) : null,
"Cancel",
"OK"
]}>
<div className="picture">
{
file ? (
<img id="pic" src={url(file) || ""}
onError={() => {
onImage(null);
alert("Error, it isn't a picture");
}}/>
) : null
}
</div>
<div className="output">
{
structStr ? (
structStr instanceof Promise || typeof structStr !== 'string' ? // in Edge 38:
( <Spin/> ) : // instanceof Promise always `false`
( <StructRender className="struct" struct={structStr}/> )
) : null
}
</div>
<label>
<Input type="checkbox" value={fragment} onChange={v => isFragment(v)}/>
Load as a fragment
</label>
</Dialog>
);
}
function url(file) {
if (!file) return null;
const URL = window.URL || window.webkitURL;
return URL ? URL.createObjectURL(file) : "No preview";
}
export default connect(
store => ({
file: store.options.recognize.file,
structStr: store.options.recognize.structStr,
fragment: store.options.recognize.fragment
}),
(dispatch, props) => ({
isFragment: (v) => dispatch(shouldFragment(v)),
onImage: (file) => dispatch(changeImage(file)),
onRecognize: (file) => dispatch(recognize(file)),
onOk: (res) => {
dispatch(
load(res.structStr, {
rescale: true,
fragment: res.fragment
})
);
props.onOk(res);
}
})
)(Recognize);

View File

@ -0,0 +1,72 @@
/****************************************************************************
* 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';
import { connect } from 'preact-redux';
/** @jsx h */
import { rgroup as rgroupSchema } from '../structschema';
import { Form, Field } from '../component/form';
import Dialog from '../component/dialog';
function IfThenSelect(props, { schema }) {
const { name, rgids } = props;
const desc = {
title: schema.properties[name].title,
enum: [0],
enumNames: ['Always']
};
rgids.forEach(label => {
if (props.label !== label) {
desc.enum.push(label);
desc.enumNames.push(`IF R${props.label} THEN R${label}`);
}
});
return <Field name={name} schema={desc} {...props}/>;
}
function RgroupLogic (props) {
const { formState, label, rgroupLabels, ...prop } = props;
return (
<Dialog title="R-Group Logic" className="rgroup-logic"
result={() => formState.result} valid={() => formState.valid} params={prop}>
<Form schema={rgroupSchema}
customValid={{range: r => rangeConv(r)}} init={prop} {...formState}>
<Field name="range"/>
<Field name="resth"/>
<IfThenSelect name="ifthen" className="cond" label={label} rgids={rgroupLabels}/>
</Form>
</Dialog>
);
}
function rangeConv(range) { // structConv
const res = range.replace(/\s*/g, '').replace(/,+/g, ',')
.replace(/^,/, '').replace(/,$/, '');
return res.split(',').every(function (s) {
return s.match(/^[>,<=]?[0-9]+$/g) ||
s.match(/^[0-9]+-[0-9]+$/g);
});
}
export default connect(
store => ({ formState: store.modal.form })
)(RgroupLogic);

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 { range } from 'lodash/fp';
import { h, Component } from 'preact';
/** @jsx h */
import Dialog from '../component/dialog';
function RGroup({ selected, onSelect, result, ...props }) {
return (
<Dialog title="R-Group"
className="rgroup" params={props}
result={() => result()}>
<ul>
{ range(1, 33).map(i => (
<li>
<button
className={ selected(i) ? 'selected' : ''}
onClick={ev => onSelect(i)}>
{`R${i}`}
</button>
</li>
)) }
</ul>
</Dialog>
);
}
class RGroupFragment extends Component {
constructor({label}) {
super();
this.state.label = label || null;
}
onSelect(label) {
this.setState({
label: label !== this.state.label ? label : null
});
}
selected(label) {
return label === this.state.label;
}
result() {
return { label: this.state.label };
}
render() {
return (
<RGroup selected={i => this.selected(i)}
onSelect={i => this.onSelect(i)}
result={() => this.result()} {...this.props}/>
);
}
}
class RGroupAtom extends Component {
constructor({values}) {
super();
this.state.values = values || [];
}
onSelect(index) {
const {values} = this.state;
const i = values.indexOf(index);
if (i < 0)
values.push(index);
else
values.splice(i, 1);
this.setState({ values });
}
selected(index) {
return this.state.values.includes(index);
}
result() {
return {
type: 'rlabel',
values: this.state.values
};
}
render() {
return (
<RGroup selected={i => this.selected(i)}
onSelect={i => this.onSelect(i)}
result={() => this.result() } {...this.props}/>
);
}
}
export default params => params.type === 'rlabel' ? (<RGroupAtom {...params}/>) : (<RGroupFragment {...params}/>);

View File

@ -0,0 +1,90 @@
/****************************************************************************
* 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 { connect } from 'preact-redux';
/** @jsx h */
import * as structFormat from '../structformat';
import { saveUserTmpl } from '../state/templates';
import Dialog from '../component/dialog';
import SaveButton from '../component/savebutton';
class Save extends Component {
constructor(props) {
super(props);
this.state = { type: props.struct.hasRxnArrow() ? 'rxn' : 'mol' };
this.changeType().catch(props.onCancel);
}
changeType(ev) {
let { type } = this.state;
if (ev) {
type = ev.target.value;
ev.preventDefault();
}
let converted = structFormat.toString(this.props.struct, type, this.props.server, this.props.options);
return converted.then(structStr => this.setState({ type, structStr }),
e => { alert(e); });
}
render () {
// $('[value=inchi]').disabled = ui.standalone;
let { type, structStr } = this.state;
let format = structFormat.map[type];
console.assert(format, "Unknown chemical file type");
return (
<Dialog title="Save Structure"
className="save" params={this.props}
buttons={[(
<SaveButton className="save"
data={structStr}
filename={'ketcher' + format.ext[0]}
type={format.mime}
server={this.props.server}
onSave={ () => this.props.onOk() }>
Save To File
</SaveButton>
), (
<button className="save-tmpl"
onClick={ () => this.props.onTmplSave(structStr) }>
Save to Templates</button>
), "Close"]}>
<label>Format:
<select value={type} onChange={ev => this.changeType(ev)}>{
[this.props.struct.hasRxnArrow() ? 'rxn' : 'mol', 'smiles', 'smarts', 'cml', 'inchi'].map(type => (
<option value={type}>{structFormat.map[type].name}</option>
))
}</select>
</label>
<textarea className={type} value={structStr} readonly
ref={ el => el && setTimeout(() => el.select(), 10) }/>
</Dialog>
);
}
}
export default connect(
store => ({
server: store.server,
struct: store.editor.struct(),
options: store.options.getServerSettings()
}),
dispatch => ({
onTmplSave: struct => dispatch(saveUserTmpl(struct))
})
)(Save);

View File

@ -0,0 +1,89 @@
/****************************************************************************
* 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';
import { connect } from 'preact-redux';
import { Form, Field, SelectOneOf } from '../component/form';
import Dialog from '../component/dialog';
import ComboBox from '../component/combobox';
import { sdataSchema, sdataCustomSchema, getSdataDefault } from '../data/sdata-schema'
/** @jsx h */
function SelectInput({ title, name, schema, ...prop }) {
const inputSelect = Object.keys(schema).reduce((acc, item) => {
acc.enum.push(item);
acc.enumNames.push(schema[item].title || item);
return acc;
},
{
title: title,
type: 'string',
default: '',
minLength: 1,
enum: [],
enumNames: []
}
);
return <Field name={name} schema={inputSelect} component={ComboBox} {...prop} />
}
function SData({ context, fieldName, fieldValue, type, radiobuttons, formState, ...prop }) {
const { result, valid } = formState;
const init = {
context,
fieldName: fieldName || getSdataDefault(context),
type,
radiobuttons
};
init.fieldValue = fieldValue || getSdataDefault(context, init.fieldName);
const formSchema = sdataSchema[result.context][result.fieldName] || sdataCustomSchema;
const serialize = {
context: result.context.trim(),
fieldName: result.fieldName.trim(),
fieldValue: typeof (result.fieldValue) === 'string' ? result.fieldValue.trim() : result.fieldValue
};
return (
<Dialog title={"S-Group Properties"} className="sgroup"
result={() => result} valid={() => valid} params={prop}>
<Form serialize={serialize} schema={formSchema} init={init} {...formState}>
<SelectOneOf title="Context" name="context" schema={sdataSchema}/>
<fieldset className={"data"}>
<SelectInput title="Field name" name="fieldName" schema={sdataSchema[result.context]}/>
{
content(formSchema, result.context, result.fieldName)
}
</fieldset>
</Form>
</Dialog>
);
}
const content = (schema, context, fieldName) => Object.keys(schema.properties)
.filter(prop => prop !== "type" && prop !== "context" && prop !== "fieldName")
.map(prop => prop === "radiobuttons" ?
<Field name={prop} type="radio" key={`${context}-${fieldName}-${prop}-radio`}/> :
<Field name={prop} type="textarea" multiple={true} size="10" key={`${context}-${fieldName}-${prop}-select`}/>
);
export default connect(
store => ({ formState: store.modal.form })
)(SData);

View File

@ -0,0 +1,61 @@
/****************************************************************************
* 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';
import { connect } from 'preact-redux';
/** @jsx h */
import { sgroup as sgroupSchema } from '../structschema';
import { Form, Field, SelectOneOf } from '../component/form';
import { mapOf } from '../utils';
import Dialog from '../component/dialog';
const schemes = mapOf(sgroupSchema, 'type');
function Sgroup({ formState, ...prop }) {
const { result, valid } = formState;
const type = result.type;
return (
<Dialog title="S-Group Properties" className="sgroup"
result={() => result} valid={() => valid} params={prop}>
<Form schema={schemes[type]} init={prop} {...formState}>
<SelectOneOf title="Type" name="type" schema={schemes}/>
<fieldset className={type === 'DAT' ? 'data' : 'base'}>
{ content(type) }
</fieldset>
</Form>
</Dialog>
);
}
const content = type => Object.keys(schemes[type].properties)
.filter(prop => prop !== 'type')
.map(prop => {
let props = {};
if (prop === 'name') props.maxlength = 15;
if (prop === 'fieldName') props.maxlength = 30;
if (prop === 'fieldValue') props.type = 'textarea';
if (prop === 'radiobuttons') props.type = 'radio';
return <Field name={prop} key={`${type}-${prop}`} {...props}/>;
}
);
export default connect(
(store) => ({ formState: store.modal.form })
)(Sgroup);

View File

@ -0,0 +1,114 @@
/****************************************************************************
* 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 { connect } from 'preact-redux';
/** @jsx h */
import Dialog from '../component/dialog';
import Input from '../component/input';
import StructEditor from '../component/structeditor';
import { storage } from '../utils';
import { initAttach, setAttachPoints, setTmplName } from '../state/templates';
const EDITOR_STYLES = {
selectionStyle: { fill: '#47b3ec', stroke: 'none' },
highlightStyle: { stroke: '#1a7090', 'stroke-width': 1.2 }
};
class Attach extends Component {
constructor({ onInit, ...props }) {
super();
this.tmpl = initTmpl(props.tmpl);
onInit(this.tmpl.struct.name, this.tmpl.props);
this.onResult = this.onResult.bind(this);
}
onResult() {
const { name, atomid, bondid } = this.props;
return name && (
name !== this.tmpl.struct.name ||
atomid !== this.tmpl.props.atomid ||
bondid !== this.tmpl.props.bondid
) ? { name, attach: { atomid, bondid } } : null;
}
render() {
const {
name, atomid, bondid,
onNameEdit, onAttachEdit, ...prop
} = this.props;
const struct = this.tmpl.struct;
const options = Object.assign(EDITOR_STYLES, { scale: getScale(struct) });
return (
<Dialog title="Template Edit" className="attach"
result={this.onResult} params={prop}>
<label>Template name:
<Input value={name} onChange={onNameEdit}/>
</label>
<label>Choose attachment atom and bond:</label>
<StructEditor className="editor"
struct={struct}
onAttachEdit={onAttachEdit}
tool="attach" toolOpts={{ atomid, bondid }}
options={options}/>
{!storage.isAvailable() ? <div className="warning">{storage.warningMessage}</div> : null}
</Dialog>
);
}
}
export default connect(
store => ({ ...store.templates.attach }),
dispatch => ({
onInit: (name, ap) => dispatch(initAttach(name, ap)),
onAttachEdit: ap => dispatch(setAttachPoints(ap)),
onNameEdit: name => dispatch(setTmplName(name))
})
)(Attach);
function initTmpl(tmpl) {
const normTmpl = {
struct: structNormalization(tmpl.struct),
props: {
atomid: +tmpl.props.atomid || 0,
bondid: +tmpl.props.bondid || 0
}
};
normTmpl.struct.name = tmpl.struct.name;
return normTmpl;
}
function structNormalization(struct) {
const normStruct = struct.clone();
const cbb = normStruct.getCoordBoundingBox();
normStruct.atoms.each(function (aid, atom) { // only atoms ?? mb arrow etc ...
atom.pp = atom.pp.sub(cbb.min);
});
return normStruct;
}
function getScale(struct) {
const cbb = struct.getCoordBoundingBox();
const VIEW_SIZE = 200;
let scale = VIEW_SIZE / Math.max(cbb.max.y - cbb.min.y, cbb.max.x - cbb.min.x);
if (scale < 35) scale = 35;
if (scale > 75) scale = 75;
return scale;
}

View File

@ -0,0 +1,174 @@
/****************************************************************************
* 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 { escapeRegExp, chunk, flow, filter as _filter, reduce, omit } from 'lodash/fp';
import { createSelector } from 'reselect';
import { h, Component } from 'preact';
import { connect } from 'preact-redux';
/** @jsx h */
import sdf from '../../chem/sdf';
import VisibleView from '../component/visibleview';
import StructRender from '../component/structrender';
import Dialog from '../component/dialog';
import SaveButton from '../component/savebutton';
import Input from '../component/input';
import SelectList from '../component/select';
import { changeFilter, changeGroup, selectTmpl, editTmpl } from '../state/templates';
import { onAction } from "../state/";
const GREEK_SIMBOLS = {
'Alpha': 'A', 'alpha': 'α',
'Beta': 'B', 'beta': 'β',
'Gamma': 'Г', 'gamma': 'γ'
};
function tmplName(tmpl, i) {
console.assert(tmpl.props && tmpl.props.group, "No group");
return tmpl.struct.name || `${tmpl.props.group} template ${i + 1}`;
}
function partition(n, array) {
console.warn('partition', n);
return chunk(n)(array);
}
const greekRe = new RegExp('\\b' + Object.keys(GREEK_SIMBOLS).join('\\b|\\b') + '\\b', 'g');
function greekify(str) {
return str.replace(greekRe, sym => GREEK_SIMBOLS[sym]);
}
const filterLibSelector = createSelector(
(props) => props.lib,
(props) => props.filter,
filterLib
);
function filterLib(lib, filter) {
console.warn('Filter', filter);
let re = new RegExp(escapeRegExp(greekify(filter)), 'i');
return flow(
_filter(item => !filter || re.test(greekify(item.struct.name)) || re.test(greekify(item.props.group))),
reduce((res, item) => {
!res[item.props.group] ? res[item.props.group] = [item] : res[item.props.group].push(item);
return res;
}, {})
)(lib)
}
const libRowsSelector = createSelector(
(props) => props.lib,
(props) => props.group,
(props) => props.COLS,
libRows
);
function libRows(lib, group, COLS) {
console.warn("Group", group);
return partition(COLS, lib[group])
}
function RenderTmpl({ tmpl, ...props }) {
return tmpl.props && tmpl.props.prerender ?
( <svg {...props}><use xlinkHref={tmpl.props.prerender}/></svg> ) :
( <StructRender struct={tmpl.struct} options={{ autoScaleMargin: 15 }} {...props}/> );
}
class TemplateLib extends Component {
select(tmpl) {
if (tmpl === this.props.selected)
this.props.onOk(this.result());
else
this.props.onSelect(tmpl);
}
result() {
const tmpl = this.props.selected;
console.assert(!tmpl || tmpl.props, 'Incorrect SDF parse');
return tmpl ? {
struct: tmpl.struct,
aid: parseInt(tmpl.props.atomid) || null,
bid: parseInt(tmpl.props.bondid) || null
} : null;
}
renderRow(row, index, COLS) {
return (
<div className="tr" key={index}>{ row.map((tmpl, i) => (
<div className={tmpl === this.props.selected ? 'td selected' : 'td'}
title={greekify(tmplName(tmpl, index * COLS + i))}>
<RenderTmpl tmpl={tmpl} className="struct" onClick={() => this.select(tmpl)}/>
<button className="attach-button" onClick={() => this.props.onAttach(tmpl)}>
Edit
</button>
</div>
))}</div>
);
}
render() {
const COLS = 3;
let { group, filter, onFilter, onChangeGroup, ...props } = this.props;
const lib = filterLibSelector(this.props);
group = lib[group] ? group : Object.keys(lib)[0];
return (
<Dialog title="Template Library"
className="template-lib" params={props}
result={() => this.result()}
buttons={[
<SaveButton className="save"
data={ sdf.stringify(this.props.lib) }
filename={'ketcher-tmpls.sdf'}>
Save To SDF
</SaveButton>,
"OK", "Cancel"]}>
<label>
<Input type="search" placeholder="Filter"
value={ filter } onChange={value => onFilter(value)}/>
</label>
<Input className="groups" component={SelectList}
splitIndexes={[Object.keys(lib).indexOf('User Templates')]}
value={ group } onChange={g => onChangeGroup(g)}
schema={{
enum: Object.keys(lib),
enumNames: Object.keys(lib).map(g => greekify(g))
}}/>
<VisibleView data={libRowsSelector({ lib, group, COLS })}
rowHeight={120} className="table">
{ (row, i) => this.renderRow(row, i, COLS) }
</VisibleView>
</Dialog>
);
}
}
export default connect(
store => ({ ...omit(['attach'], store.templates) }),
(dispatch, props) => ({
onFilter: filter => dispatch(changeFilter(filter)),
onSelect: tmpl => dispatch(selectTmpl(tmpl)),
onChangeGroup: group => dispatch(changeGroup(group)),
onAttach: tmpl => dispatch(editTmpl(tmpl)),
onOk: res => {
dispatch(onAction({ tool: 'template', opts: res }));
props.onOk(res);
}
})
)(TemplateLib);

View File

@ -0,0 +1,24 @@
/****************************************************************************
* 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 app from './app';
function init(opts, apiServer) {
const ketcherWindow = document.querySelector('[role=application]') || document.body;
return app(ketcherWindow, opts, apiServer);
}
export default init;

View File

@ -0,0 +1,95 @@
/****************************************************************************
* 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 keyName from "w3c-keyname";
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
function normalizeKeyName(name) {
let parts = name.split(/\+(?!$)/), result = parts[parts.length - 1];
if (result === "Space") result = " ";
let alt, ctrl, shift, meta;
for (let i = 0; i < parts.length - 1; i++) {
let mod = parts[i];
if (/^(cmd|meta|m)$/i.test(mod)) meta = true;
else if (/^a(lt)?$/i.test(mod)) alt = true;
else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true;
else if (/^s(hift)?$/i.test(mod)) shift = true;
else if (/^mod$/i.test(mod)) { if (mac) meta = true; else ctrl = true; }
else throw new Error("Unrecognized modifier name: " + mod);
}
if (alt) result = "Alt+" + result;
if (ctrl) result = "Ctrl+" + result;
if (meta) result = "Meta+" + result;
if (shift) result = "Shift+" + result;
return result;
}
function normalizeKeyMap(map) {
const copy = Object.create(null);
for (let prop in map) {
if (map.hasOwnProperty(prop))
copy[normalizeKeyName(prop)] = map[prop];
}
return copy;
}
function modifiers(name, event, shift) {
if (event.altKey) name = "Alt+" + name;
if (event.ctrlKey) name = "Ctrl+" + name;
if (event.metaKey) name = "Meta+" + name;
if (shift !== false && event.shiftKey) name = "Shift+" + name;
return name;
}
function normalizeKeyEvent(event, base=false) {
const name = keyName(event);
const isChar = name.length === 1 && name !== " ";
return isChar && !base ? modifiers(name, event, !isChar) :
modifiers(keyName.base[event.keyCode], event, true);
}
function keyNorm(obj) {
if (obj instanceof KeyboardEvent)
return normalizeKeyEvent(...arguments);
return typeof obj === 'object' ? normalizeKeyMap(obj) :
normalizeKeyName(obj);
}
function lookup(map, event) {
const name = keyName(event);
const isChar = name.length === 1 && name !== " ";
let res = map[modifiers(name, event, !isChar)];
let baseName;
if (event.shiftKey && isChar && (baseName = keyName.base[event.keyCode])) {
res = map[modifiers(baseName, event, true)] || res;
}
return res;
}
keyNorm.lookup = lookup;
export default keyNorm;

View File

@ -0,0 +1,72 @@
/****************************************************************************
* 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 acts from '../action';
import { isEqual, isEmpty, pickBy } from 'lodash/fp';
function execute(activeTool, { action, editor, server, options }) {
if (action.tool) {
if (editor.tool(action.tool, action.opts))
return action;
}
else if (typeof action === 'function')
action(editor, server, options);
else
console.info('no action');
return activeTool;
}
function selected(actObj, activeTool, { editor, server }) {
if (typeof actObj.selected === 'function')
return actObj.selected(editor, server);
else if (actObj.action && actObj.action.tool)
return isEqual(activeTool, actObj.action);
return false;
}
function disabled(actObj, { editor, server, options }) {
if (typeof actObj.disabled === 'function')
return actObj.disabled(editor, server, options);
return false;
}
function status(key, activeTool, params) {
let actObj = acts[key];
return pickBy(x => x, {
selected: selected(actObj, activeTool, params),
disabled: disabled(actObj, params)
});
}
export default function (state=null, { type, action, ...params }) {
switch(type) {
case 'INIT':
action = acts['select-lasso'].action;
case 'ACTION':
const activeTool = execute(state && state.activeTool, {
...params, action
});
case 'UPDATE':
return Object.keys(acts).reduce((res, key) => {
const value = status(key, res.activeTool, params);
if (!isEmpty(value))
res[key] = value;
return res;
}, { activeTool: activeTool || state.activeTool });
default:
return state;
}
}

View File

@ -0,0 +1,106 @@
/****************************************************************************
* 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 { debounce } from 'lodash/fp';
import element from '../../chem/element';
import acts from '../action';
import { openDialog } from './';
import { fromBond, toBond, fromSgroup, toSgroup, fromElement, toElement } from '../structconv';
export function initEditor(dispatch, getState) {
const updateAction = debounce(100, () => dispatch({ type: 'UPDATE' }));
const sleep = (time) => new Promise(resolve => setTimeout(resolve, time));
function resetToSelect(dispatch, getState) {
const resetToSelect = getState().options.settings.resetToSelect;
const activeTool = getState().actionState.activeTool.tool;
if (resetToSelect === true || resetToSelect === activeTool) // example: 'paste'
dispatch({ type: 'ACTION', action: acts['select-lasso'].action });
else
updateAction();
}
return {
onInit: editor => {
dispatch({ type: 'INIT', editor });
},
onChange: () => {
dispatch(resetToSelect);
},
onSelectionChange: () => {
updateAction();
},
onElementEdit: selem => {
const elem = fromElement(selem);
let dlg = null;
if (element.map[elem.label]) {
dlg = openDialog(dispatch, 'atomProps', elem);
} else if (Object.keys(elem).length === 1 && 'ap' in elem) {
dlg = openDialog(dispatch, 'attachmentPoints', elem.ap)
.then((res) => ({ ap: res }));
} else if (elem.type === 'list' || elem.type === 'not-list') {
dlg = openDialog(dispatch, 'period-table', elem);
} else if (elem.type === 'rlabel') {
dlg = openDialog(dispatch, 'rgroup', elem);
} else {
dlg = openDialog(dispatch, 'period-table', elem);
}
return dlg.then(toElement);
},
onQuickEdit: atom => {
return openDialog(dispatch, 'labelEdit', atom)
},
onBondEdit: bond => {
return openDialog(dispatch, 'bondProps', fromBond(bond))
.then(toBond);
},
onRgroupEdit: rgroup => {
if (Object.keys(rgroup).length > 1) {
const rgids = [];
getState().editor.struct().rgroups.each(rgid => rgids.push(rgid));
if (!rgroup.range) rgroup.range = '>0';
return openDialog(dispatch, 'rgroupLogic',
Object.assign({ rgroupLabels: rgids }, rgroup));
}
return openDialog(dispatch, 'rgroup', rgroup);
},
onSgroupEdit: sgroup => {
return sleep(0) // huck to open dialog after dispatch sgroup tool action
.then(() => openDialog(dispatch, 'sgroup', fromSgroup(sgroup)))
.then(toSgroup);
},
onSdataEdit: sgroup => {
return sleep(0)
.then(() => openDialog(dispatch, sgroup.type === 'DAT' ? 'sdata' : 'sgroup', fromSgroup(sgroup)))
.then(toSgroup);
},
onMessage: msg => {
if (msg.error)
alert(msg.error);
else {
let act = Object.keys(msg)[0];
console[act](msg[act]);
}
},
onMouseDown: event => {}
};
}

View File

@ -0,0 +1,129 @@
/****************************************************************************
* 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 { getDefaultOptions } from '../data/options-schema';
import { initSdata, sdataReducer } from './sdata';
export const formsState = {
atomProps: {
errors: {},
valid: true,
result: {
label: '',
charge: 0,
explicitValence: -1,
hCount: 0,
invRet: 0,
isotope: 0,
radical: 0,
ringBondCount: 0,
substitutionCount: 0
}
} ,
attachmentPoints: {
errors: {},
valid: true,
result: {
primary: false,
secondary: false
}
},
automap: {
errors: {},
valid: true,
result: {
mode: "discard"
}
},
bondProps: {
errors: {},
valid: true,
result: {
type: 'single',
topology: 0,
center: 0
}
},
check: {
errors: {},
moleculeErrors: {},
result: {
checkOptions: ['valence', 'radicals', 'pseudoatoms', 'stereo', 'query', 'overlapping_atoms',
'overlapping_bonds', 'rgroups', 'chiral', '3d']
}
},
labelEdit: {
errors: {},
valid: true,
result: {
label: '',
}
},
rgroupLogic: {
errors: {},
valid: true,
result: {
ifthen: 0,
range: '>0',
resth: false
}
},
settings: {
errors: {},
valid: true,
result: getDefaultOptions()
},
sgroup: {
errors: {},
valid: true,
result: {
type: 'GEN'
}
},
sdata: initSdata()
};
export function updateFormState(data) {
return {
type: 'UPDATE_FORM',
data: data
};
}
export function checkErrors(errors) {
return {
type: 'UPDATE_FORM',
data: { moleculeErrors: errors }
};
}
export function setDefaultSettings() {
return {
type: 'UPDATE_FORM',
data: {
result: getDefaultOptions(),
valid: true,
errors: {}
}
};
}
export function formReducer(state, action, formName) {
if (formName === 'sdata')
return sdataReducer(state, action);
return Object.assign({}, state, action.data);
}

View File

@ -0,0 +1,156 @@
/****************************************************************************
* 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 { isEqual, debounce } from 'lodash/fp';
import molfile from '../../chem/molfile';
import keyNorm from '../keynorm';
import actions from '../action';
import * as clipArea from '../component/cliparea';
import * as structFormat from '../structformat';
import { onAction, openDialog, load } from './';
export function initKeydownListener(element) {
return function (dispatch, getState) {
const hotKeys = initHotKeys();
element.addEventListener('keydown', (event) => keyHandle(dispatch, getState, hotKeys, event));
}
}
/* HotKeys */
function keyHandle(dispatch, getState, hotKeys, event) {
const state = getState();
if (state.modal) return;
const editor = state.editor;
const actionState = state.actionState;
const actionTool = actionState.activeTool;
const key = keyNorm(event);
const atomsSelected = editor.selection() && editor.selection().atoms;
let group = null;
if (key && key.length === 1 && atomsSelected && key.match(/\w/)) {
console.assert(atomsSelected.length > 0);
openDialog(dispatch, 'labelEdit', { letter: key }).then(res => {
dispatch(onAction({ tool: 'atom', opts: res }));
});
event.preventDefault();
} else if (group = keyNorm.lookup(hotKeys, event)) {
let index = checkGroupOnTool(group, actionTool); // index currentTool in group || -1
index = (index + 1) % group.length;
let actName = group[index];
if (actionState[actName] && actionState[actName].disabled === true)
return event.preventDefault();
if (clipArea.actions.indexOf(actName) === -1) {
let newAction = actions[actName].action;
dispatch(onAction(newAction));
event.preventDefault();
} else if (window.clipboardData) // IE support
clipArea.exec(event);
}
}
function setHotKey(key, actName, hotKeys) {
if (Array.isArray(hotKeys[key]))
hotKeys[key].push(actName);
else
hotKeys[key] = [actName];
}
function initHotKeys() {
const hotKeys = {};
let act;
for (let actName in actions) {
act = actions[actName];
if (!act.shortcut) continue;
if (Array.isArray(act.shortcut))
act.shortcut.forEach(key => setHotKey(key, actName, hotKeys));
else
setHotKey(act.shortcut, actName, hotKeys);
}
return keyNorm(hotKeys);
}
function checkGroupOnTool(group, actionTool) {
let index = group.indexOf(actionTool.tool);
group.forEach((actName, i) => {
if (isEqual(actions[actName].action, actionTool))
index = i;
});
return index;
}
/* ClipArea */
export function initClipboard(dispatch, getState) {
const formats = Object.keys(structFormat.map).map(function (fmt) {
return structFormat.map[fmt].mime;
});
const debAction = debounce(0, (action) => dispatch( onAction(action) ));
const loadStruct = debounce(0, (structStr, opts) => dispatch( load(structStr, opts) ));
return {
formats: formats,
focused: function () {
return !getState().modal;
},
onCut: function () {
let data = clipData(getState().editor);
debAction({ tool: 'eraser', opts: 1 });
return data;
},
onCopy: function () {
let editor = getState().editor;
let data = clipData(editor);
editor.selection(null);
return data;
},
onPaste: function (data) {
const structStr = data['chemical/x-mdl-molfile'] ||
data['chemical/x-mdl-rxnfile'] ||
data['text/plain'];
if (structStr)
loadStruct(structStr, { fragment: true });
}
};
}
function clipData(editor) {
const res = {};
const struct = editor.structSelected();
if (struct.isBlank())
return null;
const type = struct.isReaction ?
'chemical/x-mdl-molfile' : 'chemical/x-mdl-rxnfile';
res['text/plain'] = res[type] = molfile.stringify(struct);
// res['chemical/x-daylight-smiles'] =
// smiles.stringify(struct);
return res;
}

View File

@ -0,0 +1,167 @@
/****************************************************************************
* 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 { pick } from 'lodash/fp';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import { logger } from 'redux-logger';
import * as structFormat from '../structformat';
import { formsState, formReducer } from './form';
import { optionsState, optionsReducer } from './options';
import { initTmplState, templatesReducer } from './templates';
import action from './action';
import toolbar from './toolbar';
function modal(state = null, action) {
const { type, data } = action;
if (type === 'UPDATE_FORM') {
const formState = formReducer(state.form, action, state.name);
return { ...state, form: formState }
}
switch (type) {
case 'MODAL_CLOSE':
return null;
case 'MODAL_OPEN':
return {
name: data.name,
form: formsState[data.name] || null,
prop: data.prop || null
};
default:
return state;
}
}
const shared = combineReducers({
actionState: action,
toolbar,
modal,
server: (store=null) => store,
editor: (store=null) => store,
options: optionsReducer,
templates: templatesReducer
});
export function onAction(action) {
if (action && action.dialog)
return {
type: 'MODAL_OPEN',
data: { name: action.dialog }
};
if (action && action.thunk)
return action.thunk;
return {
type: 'ACTION',
action
};
}
export function openDialog(dispatch, dialogName, props) {
return new Promise((resolve, reject) => {
dispatch({
type: 'MODAL_OPEN',
data: {
name: dialogName,
prop: {
...props,
onResult: resolve,
onCancel: reject
}
}
})
});
}
export function load(structStr, options) {
return (dispatch, getState) => {
const state = getState();
const editor = state.editor;
const server = state.server;
options = options || {};
// TODO: check if structStr is parsed already
//utils.loading('show');
const parsed = structFormat.fromString(structStr,
options, server);
parsed.catch(function (err) {
//utils.loading('hide');
alert("Can't parse molecule!");
});
return parsed.then(function (struct) {
//utils.loading('hide');
console.assert(struct, 'No molecule to update');
if (options.rescale)
struct.rescale(); // TODO: move out parsing?
if (options.fragment && !struct.isBlank())
dispatch(onAction({ tool: 'paste', opts: struct }));
else
editor.struct(struct);
return struct;
}, function (err) {
alert(err);
});
}
}
function root(state, action) {
switch (action.type) {
case 'INIT':
global._ui_editor = action.editor;
case 'UPDATE':
let {type, ...data} = action;
if (data)
state = { ...state, ...data };
}
const sh = shared(state, {
...action,
...pick(['editor', 'server', 'options'], state)
});
return (sh === state.shared) ? state : {
...state, ...sh
};
}
export default function(options, server) {
// TODO: redux localStorage here
const initState = {
actionState: null,
options: Object.assign(optionsState, { app: options }),
server: server || Promise.reject("Standalone mode!"),
editor: null,
modal: null,
templates: initTmplState
};
const middleware = [ thunk ];
if (process.env.NODE_ENV !== 'production')
middleware.push(logger);
return createStore(root, initState, applyMiddleware(...middleware));
};

View File

@ -0,0 +1,33 @@
/****************************************************************************
* 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 { openDialog, load } from './';
import * as structFormat from '../structformat';
export function miewAction(dispatch, getState) {
const editor = getState().editor;
const server = getState().server;
let convert = structFormat.toString(editor.struct(),
'cml', server);
convert.then(function (cml) {
openDialog(dispatch, 'miew', {
structStr: cml
}).then(function (res) {
if (res.structStr)
dispatch(load(res.structStr));
});
});
}

View File

@ -0,0 +1,114 @@
/****************************************************************************
* 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 { pick } from 'lodash/fp';
import { SERVER_OPTIONS, getDefaultOptions, validation } from '../data/options-schema';
import { storage } from '../utils';
export const optionsState = {
app: {
server: false,
templates: false
},
analyse: {
values: null,
roundWeight: 3,
roundMass: 3
},
recognize: {
file: null,
structStr: null,
fragment: false
},
settings: Object.assign(getDefaultOptions(), validation(storage.getItem("ketcher-opts"))),
getServerSettings: function() {
return pick(SERVER_OPTIONS, this.settings);
}
};
export function appUpdate(data) {
return dispatch => {
dispatch({ type: 'APP_OPTIONS', data });
dispatch({ type: 'UPDATE' })
}
}
/* SETTINGS */
export function saveSettings(newSettings) {
storage.setItem("ketcher-opts", newSettings);
return {
type: 'SAVE_SETTINGS',
data: newSettings
};
}
/* ANALYZE */
export function changeRound(roundName, value) {
return {
type: 'CHANGE_ANALYSE',
data: { [roundName]: value }
};
}
/* RECOGNIZE */
const recognizeActions = [
'SET_RECOGNIZE_STRUCT',
'CHANGE_RECOGNIZE_FILE',
'IS_FRAGMENT_RECOGNIZE'
];
export function setStruct(str) {
return {
type: 'SET_RECOGNIZE_STRUCT',
data: { structStr: str }
};
}
export function changeImage(file) {
return {
type: 'CHANGE_RECOGNIZE_FILE',
data: {
file: file,
structStr: null
}
};
}
export function shouldFragment(isFrag) {
return {
type: 'IS_FRAGMENT_RECOGNIZE',
data: { fragment: isFrag }
};
}
export function optionsReducer(state = {}, action) {
let { type, data } = action;
if (type === 'APP_OPTIONS')
return {...state, app: { ...state.app, ...data }};
if (type === 'SAVE_SETTINGS')
return {...state, settings: data};
if (type === 'CHANGE_ANALYSE')
return {...state, analyse: { ...state.analyse, ...data }};
if (recognizeActions.includes(type)) {
return {...state, recognize: { ...state.recognize, ...data }}
}
return state;
}

View File

@ -0,0 +1,114 @@
/****************************************************************************
* 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 { sdataSchema, getSdataDefault } from '../data/sdata-schema'
export const initSdata = () => {
const context = getSdataDefault();
const fieldName = getSdataDefault(context);
const fieldValue = getSdataDefault(context, fieldName);
const radiobuttons = 'Absolute';
return {
errors: {},
valid: true,
result: {
context,
fieldName,
fieldValue,
radiobuttons,
type: 'DAT'
}
}
};
export function sdataReducer(state, action) {
if (action.data.result.init)
return correctErrors({
...state,
result: Object.assign({}, state.result, action.data.result)
}, action.data);
const actionContext = action.data.result.context;
const actionFieldName = action.data.result.fieldName;
let newstate = null;
if (actionContext !== state.result.context)
newstate = onContextChange(state, action.data.result);
else if (actionFieldName !== state.result.fieldName)
newstate = onFieldNameChange(state, action.data.result);
newstate = newstate || {
...state,
result: Object.assign({}, state.result, action.data.result)
};
return correctErrors(newstate, action.data);
}
const correctErrors = (state, payload) => {
const { valid, errors } = payload;
const { fieldName, fieldValue } = state.result;
return {
result: state.result,
valid: valid && !!fieldName && !!fieldValue,
errors: errors,
};
};
const onContextChange = (state, payload) => {
const { context, fieldValue } = payload;
const fieldName = getSdataDefault(context);
let fValue = fieldValue;
if (fValue === state.result.fieldValue)
fValue = getSdataDefault(context, fieldName);
return {
result: {
...payload,
context,
fieldName,
fieldValue: fValue
}
};
};
const onFieldNameChange = (state, payload) => {
let { fieldName } = payload;
const context = state.result.context;
let fieldValue = payload.fieldValue;
if (sdataSchema[context][fieldName]) {
fieldValue = getSdataDefault(context, fieldName);
}
if (fieldValue === state.result.fieldValue && sdataSchema[context][state.result.fieldName])
fieldValue = '';
return {
result: {
...payload,
fieldName,
fieldValue,
}
};
};

View File

@ -0,0 +1,155 @@
/****************************************************************************
* 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 { pick, omit } from 'lodash/fp';
import molfile from '../../chem/molfile';
import { setStruct, appUpdate } from './options';
import { checkErrors } from './form';
import { load } from './';
export function checkServer() {
return (dispatch, getState) => {
const server = getState().server;
server.then(
(res) => dispatch(appUpdate({
indigoVersion: res.indigoVersion,
server: true
})),
(err) => console.info(err)
);
}
}
export function recognize(file) {
return (dispatch, getState) => {
const recognize = getState().server.recognize;
let process = recognize(file).then(res => {
dispatch(setStruct(res.struct));
}, err => {
dispatch(setStruct(null));
setTimeout(() => alert("Error! The picture isn't recognized."), 200); // TODO: remove me...
});
dispatch(setStruct(process));
};
}
export function check(optsTypes) {
return (dispatch, getState) => {
const { editor, server } = getState();
const options = getState().options.getServerSettings();
options.data = { 'types': optsTypes };
serverCall(editor, server, 'check', options)
.then(res => dispatch(checkErrors(res)))
.catch(console.error);
}
}
export function automap(res) {
return serverTransform('automap', res);
}
export function analyse() {
return (dispatch, getState) => {
const { editor, server } = getState();
const options = getState().options.getServerSettings();
options.data = {
properties: ['molecular-weight', 'most-abundant-mass',
'monoisotopic-mass', 'gross', 'mass-composition']
};
serverCall(editor, server, 'calculate', options).then(function (values) {
dispatch({
type: 'CHANGE_ANALYSE',
data: { values }
});
});
}
}
export function serverTransform(method, data, struct) {
return (dispatch, getState) => {
const state = getState();
let opts = state.options.getServerSettings();
opts.data = data;
serverCall(state.editor, state.server, method, opts, struct).then(function (res) {
dispatch( load(res.struct, { rescale: method === 'layout' }) );
});
};
}
function serverCall(editor, server, method, options, struct) {
const selection = editor.selection();
let selectedAtoms = [];
if (selection)
selectedAtoms = selection.atoms ? selection.atoms : editor.explicitSelected().atoms;
if (!struct) {
const aidMap = {};
struct = editor.struct().clone(null, null, false, aidMap);
const reindexMap = getReindexMap(struct.getComponents());
selectedAtoms = selectedAtoms.map(function (aid) {
return reindexMap[aidMap[aid]];
});
}
let request = server.then(function () {
return server[method](Object.assign({
struct: molfile.stringify(struct, { ignoreErrors: true })
}, selectedAtoms && selectedAtoms.length > 0 ? {
selected: selectedAtoms
} : null, options.data), omit('data', options));
});
//utils.loading('show');
request.catch(function (err) {
alert(err);
}).then(function (er) {
//utils.loading('hide');
});
return request;
}
function getReindexMap(components) {
return flatten(components.reactants)
.concat(flatten(components.products))
.reduce(function (acc, item, index) {
acc[item] = index;
return acc;
}, {});
}
/**
* Flats passed object
* Ex: [ [1, 2], [3, [4, 5] ] ] -> [1, 2, 3, 4, 5]
* { a: 1, b: { c: 2, d: 3 } } -> [1, 2, 3]
* @param source { object }
*/
function flatten(source) {
if (typeof source !== 'object')
return source;
return Object.keys(source).reduce(function (acc, key) {
const item = source[key];
return acc.concat(flatten(item));
}, []);
}

View File

@ -0,0 +1,250 @@
/****************************************************************************
* 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 { omit } from 'lodash/fp';
import sdf from '../../chem/sdf';
import molfile from '../../chem/molfile';
import { appUpdate } from './options';
import { openDialog } from './';
import { storage } from '../utils';
/* TEMPLATES */
export function selectTmpl(tmpl) {
return {
type: 'TMPL_SELECT',
data: { selected: tmpl }
}
}
export function changeGroup(group) {
return {
type: 'TMPL_CHANGE_GROUP',
data: { group: group, selected: null }
}
}
export function changeFilter(filter) {
return {
type: 'TMPL_CHANGE_FILTER',
data: { filter: filter.trim(), selected: null } // TODO: change this
}
}
/* TEMPLATE-ATTACH-EDIT */
export function initAttach(name, attach) {
return {
type: 'INIT_ATTACH',
data: {
name,
atomid: attach.atomid,
bondid: attach.bondid
}
};
}
export function setAttachPoints(attach) {
return {
type: 'SET_ATTACH_POINTS',
data: {
atomid: attach.atomid,
bondid: attach.bondid
}
};
}
export function setTmplName(name) {
return {
type: 'SET_TMPL_NAME',
data: { name }
};
}
export function editTmpl(tmpl) {
return (dispatch, getState) => {
openDialog(dispatch, 'attach', { tmpl }).then(
({ name, attach }) => {
tmpl.struct.name = name;
tmpl.props = Object.assign({}, tmpl.props, attach);
if (tmpl.props.group === 'User Templates')
updateLocalStore(getState().templates.lib);
openDialog(dispatch, 'templates');
}, () => {
openDialog(dispatch, 'templates');
}
);
}
}
/* SAVE */
export function saveUserTmpl(structStr) {
const tmpl = { struct: molfile.parse(structStr), props: {} };
return (dispatch, getState) => {
openDialog(dispatch, 'attach', { tmpl }).then(
({ name, attach }) => {
tmpl.struct.name = name;
tmpl.props = { ...attach, group: 'User Templates' };
let lib = getState().templates.lib.concat(tmpl);
dispatch(initLib(lib));
updateLocalStore(lib);
}
);
}
}
function updateLocalStore(lib) {
const userLib = lib
.filter(item => item.props.group === 'User Templates')
.map(item => {
return {
struct: molfile.stringify(item.struct),
props: Object.assign({}, omit(['group'], item.props))
};
});
storage.setItem("ketcher-tmpls", userLib);
}
/* REDUCER */
export const initTmplState = {
lib: [],
selected: null,
filter: '',
group: null,
attach: {}
};
const tmplActions = [
'TMPL_INIT',
'TMPL_SELECT',
'TMPL_CHANGE_GROUP',
'TMPL_CHANGE_FILTER'
];
const attachActions = [
'INIT_ATTACH',
'SET_ATTACH_POINTS',
'SET_TMPL_NAME'
];
export function templatesReducer(state = initTmplState, action) {
if (tmplActions.includes(action.type)) {
return Object.assign({}, state, action.data);
}
if (attachActions.includes(action.type)) {
const attach = Object.assign({}, state.attach, action.data);
return { ...state, attach };
}
return state;
}
/* INIT TEMPLATES LIBRARY */
function initLib(lib) {
return {
type: 'TMPL_INIT',
data: { lib: lib }
}
}
export function initTmplLib(dispatch, baseUrl, cacheEl) {
prefetchStatic(baseUrl + 'library.sdf').then(text => {
const tmpls = sdf.parse(text);
const prefetch = prefetchRender(tmpls, baseUrl, cacheEl);
return prefetch.then(cachedFiles => (
tmpls.map(tmpl => {
const pr = prefetchSplit(tmpl);
if (pr.file)
tmpl.props.prerender = cachedFiles.indexOf(pr.file) !== -1 ? `#${pr.id}` : '';
return tmpl;
})
));
}).then(res => {
const lib = res.concat(userTmpls());
dispatch(initLib(lib));
dispatch(appUpdate({ templates: true }));
});
}
function userTmpls() {
const userLib = storage.getItem("ketcher-tmpls");
if (!Array.isArray(userLib) || userLib.length === 0) return [];
return userLib
.map(tmpl => {
try {
if (tmpl.props === '') tmpl.props = {};
tmpl.props.group = 'User Templates';
return {
struct: molfile.parse(tmpl.struct),
props: tmpl.props
};
} catch (ex) {
return null;
}
})
.filter(tmpl => tmpl !== null);
}
function prefetchStatic(url) {
return fetch(url, { credentials: 'same-origin' }).then(function (resp) {
if (resp.ok)
return resp.text();
throw "Could not fetch " + url;
});
}
function prefetchSplit(tmpl) {
const pr = tmpl.props.prerender;
const res = pr && pr.split('#', 2);
return {
file: pr && res[0],
id: pr && res[1]
};
}
function prefetchRender(tmpls, baseUrl, cacheEl) {
const files = tmpls.reduce((res, tmpl) => {
const file = prefetchSplit(tmpl).file;
if (file && res.indexOf(file) === -1)
res.push(file);
return res;
}, []);
const fetch = Promise.all(files.map(fn => (
prefetchStatic(baseUrl + fn).catch(() => null)
)));
return fetch.then(svgs => {
svgs.forEach(svgContent => {
if (svgContent)
cacheEl.innerHTML += svgContent;
});
return files.filter((file, i) => (
!!svgs[i]
));
});
}

View File

@ -0,0 +1,110 @@
/****************************************************************************
* 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 { capitalize, debounce, isEqual } from 'lodash/fp';
import { basic as basicAtoms } from '../action/atoms';
import tools from '../action/tools';
const initial = {
freqAtoms: [],
currentAtom: 0,
opened: null,
visibleTools: {}
};
const MAX_ATOMS = 7;
export function initResize() {
return function (dispatch, getState) {
const onResize = debounce(100, () => {
getState().editor.render.update();
dispatch({ type: 'CLEAR_VISIBLE' })
});
addEventListener('resize', onResize);
}
}
export default function (state=initial, action) {
let { type, data } = action;
switch (type) {
case 'ACTION':
let visibleTool = toolInMenu(action.action);
return visibleTool
? { ...state, opened: null, visibleTools: { ...state.visibleTools, ...visibleTool } }
: state;
case 'ADD_ATOMS':
const newState = addFreqAtom(data, state.freqAtoms, state.currentAtom);
return { ...state, ...newState };
case 'CLEAR_VISIBLE':
return { ...state, opened: null, visibleTools: {} };
case 'OPENED':
return { ...state, opened: data };
case 'UPDATE':
return { ...state, opened: null };
default:
return state;
}
}
function addFreqAtom(label, freqAtoms, index) {
label = capitalize(label);
if (basicAtoms.indexOf(label) > -1 || freqAtoms.indexOf(label) !== -1) return { freqAtoms };
freqAtoms[index] = label;
index = (index + 1) % MAX_ATOMS;
return { freqAtoms, currentAtom: index };
}
export function addAtoms(atomLabel) {
return {
type: 'ADD_ATOMS',
data: atomLabel
};
}
function getToolFromAction(action) {
let tool = null;
for (let toolName in tools) {
if (tools.hasOwnProperty(toolName) && isEqual(action, tools[toolName].action))
tool = toolName;
}
return tool;
}
function toolInMenu(action) {
let tool = getToolFromAction(action);
let sel = document.getElementById(tool);
let dropdown = sel && hiddenAncestor(sel);
return dropdown ? { [dropdown.id]: sel.id } : null;
}
export function hiddenAncestor(el, base) {
base = base || document.body;
let findEl = el;
while (window.getComputedStyle(findEl).overflow !== 'hidden' && !findEl.classList.contains('opened')) {
if (findEl === base) return null;
findEl = findEl.parentNode;
}
return findEl;
}

View File

@ -0,0 +1,277 @@
/****************************************************************************
* 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 { capitalize } from 'lodash/fp';
import Struct from '../chem/struct';
import element from '../chem/element';
export function fromElement(selem) {
if (selem.label === 'R#')
return {
type: 'rlabel',
values: fromRlabel(selem.rglabel)
};
if (selem.label === 'L#')
return fromAtomList(selem);
if (element.map[selem.label])
return fromAtom(selem);
if (!selem.label && 'attpnt' in selem)
return { ap: fromApoint(selem.attpnt) };
return selem; // probably generic
}
export function toElement(elem) {
if (elem.type === 'rlabel')
return {
label: elem.values.length ? 'R#' : 'C',
rglabel: toRlabel(elem.values)
};
if (elem.type === 'list' || elem.type === 'not-list')
return toAtomList(elem);
if (!elem.label && 'ap' in elem)
return { attpnt: toApoint(elem.ap) };
if (element.map[capitalize(elem.label)])
return toAtom(elem);
if (elem.label === 'A' || elem.label === '*' || elem.label === 'Q' ||
elem.label === 'X' || elem.label === 'R') {
elem.pseudo = elem.label;
return toAtom(elem);
}
return elem;
}
function fromAtom(satom) {
const alias = satom.alias || '';
return {
alias: alias,
label: satom.label,
charge: satom.charge,
isotope: satom.isotope,
explicitValence: satom.explicitValence,
radical: satom.radical,
invRet: satom.invRet,
exactChangeFlag: !!satom.exactChangeFlag,
ringBondCount: satom.ringBondCount,
substitutionCount: satom.substitutionCount,
unsaturatedAtom: !!satom.unsaturatedAtom,
hCount: satom.hCount
};
}
function toAtom(atom) {
// TODO merge this to Struct.Atom.attrlist?
// see ratomtool
return Object.assign({}, atom, {
label: capitalize(atom.label)
});
}
function fromAtomList(satom) {
return {
type: satom.atomList.notList ? 'not-list' : 'list',
values: satom.atomList.ids.map(i => element[i].label)
};
}
function toAtomList(atom) {
return {
pseudo: null,
label: 'L#',
atomList: new Struct.AtomList({
notList: atom.type === 'not-list',
ids: atom.values.map(el => element.map[el])
})
};
}
function fromApoint(sap) {
return {
primary: ((sap || 0) & 1) > 0,
secondary: ((sap || 0) & 2) > 0
};
}
function toApoint(ap) {
return (ap.primary && 1) + (ap.secondary && 2);
}
function fromRlabel(rg) {
var res = [];
for (var rgi = 0; rgi < 32; rgi++) {
if (rg & (1 << rgi)) {
var val = rgi + 1;
res.push(val); // push the string
}
}
return res;
}
function toRlabel(values) {
var res = 0;
values.forEach(function (val) {
var rgi = val - 1;
res |= 1 << rgi;
});
return res;
}
export function fromBond(sbond) {
const type = sbond.type;
const stereo = sbond.stereo;
return {
type: fromBondType(type, stereo),
topology: sbond.topology || 0,
center: sbond.reactingCenterStatus || 0
};
}
export function toBond(bond) {
return {
topology: bond.topology,
reactingCenterStatus: bond.center,
...toBondType(bond.type)
};
}
export function toBondType(caption) {
return Object.assign({}, bondCaptionMap[caption]);
}
function fromBondType(type, stereo) {
for (let caption in bondCaptionMap) {
if (bondCaptionMap[caption].type === type &&
bondCaptionMap[caption].stereo === stereo)
return caption;
}
throw 'No such bond caption';
}
const bondCaptionMap = {
single: {
type: Struct.Bond.PATTERN.TYPE.SINGLE,
stereo: Struct.Bond.PATTERN.STEREO.NONE
},
up: {
type: Struct.Bond.PATTERN.TYPE.SINGLE,
stereo: Struct.Bond.PATTERN.STEREO.UP
},
down: {
type: Struct.Bond.PATTERN.TYPE.SINGLE,
stereo: Struct.Bond.PATTERN.STEREO.DOWN
},
updown: {
type: Struct.Bond.PATTERN.TYPE.SINGLE,
stereo: Struct.Bond.PATTERN.STEREO.EITHER
},
double: {
type: Struct.Bond.PATTERN.TYPE.DOUBLE,
stereo: Struct.Bond.PATTERN.STEREO.NONE
},
crossed: {
type: Struct.Bond.PATTERN.TYPE.DOUBLE,
stereo: Struct.Bond.PATTERN.STEREO.CIS_TRANS
},
triple: {
type: Struct.Bond.PATTERN.TYPE.TRIPLE,
stereo: Struct.Bond.PATTERN.STEREO.NONE
},
aromatic: {
type: Struct.Bond.PATTERN.TYPE.AROMATIC,
stereo: Struct.Bond.PATTERN.STEREO.NONE
},
singledouble: {
type: Struct.Bond.PATTERN.TYPE.SINGLE_OR_DOUBLE,
stereo: Struct.Bond.PATTERN.STEREO.NONE
},
singlearomatic: {
type: Struct.Bond.PATTERN.TYPE.SINGLE_OR_AROMATIC,
stereo: Struct.Bond.PATTERN.STEREO.NONE
},
doublearomatic: {
type: Struct.Bond.PATTERN.TYPE.DOUBLE_OR_AROMATIC,
stereo: Struct.Bond.PATTERN.STEREO.NONE
},
any: {
type: Struct.Bond.PATTERN.TYPE.ANY,
stereo: Struct.Bond.PATTERN.STEREO.NONE
}
};
import { sdataSchema } from './data/sdata-schema'
export function fromSgroup(ssgroup) {
const type = ssgroup.type || 'GEN';
const { context, fieldName, fieldValue, absolute, attached } = ssgroup.attrs;
if (absolute === false && attached === false)
ssgroup.attrs.radiobuttons = 'Relative';
else ssgroup.attrs.radiobuttons = attached ? 'Attached' : 'Absolute';
if (sdataSchema[context][fieldName] && sdataSchema[context][fieldName].properties.fieldValue.items) {
ssgroup.attrs.fieldValue = fieldValue.split('\n');
}
return Object.assign({ type: type }, ssgroup.attrs);
}
export function toSgroup(sgroup) {
const { type, radiobuttons, ...props } = sgroup;
const attrs = { ...props };
const absolute = 'absolute';
const attached = 'attached';
switch (radiobuttons) {
case 'Absolute':
attrs[absolute] = true;
attrs[attached] = false;
break;
case 'Attached':
attrs[absolute] = false;
attrs[attached] = true;
break;
case 'Relative':
attrs[absolute] = false;
attrs[attached] = false;
break;
default:
break;
}
if (attrs.fieldName)
attrs.fieldName = attrs.fieldName.trim();
if (attrs.fieldValue) {
attrs.fieldValue = typeof (attrs.fieldValue) === 'string' ?
attrs.fieldValue.trim() :
attrs.fieldValue;
}
return {
type,
attrs
};
}

View File

@ -0,0 +1,144 @@
/****************************************************************************
* 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 molfile from '../chem/molfile';
export const map = {
'mol': {
name: 'MDL Molfile',
mime: 'chemical/x-mdl-molfile',
ext: ['.mol'],
supportsCoords: true
},
'rxn': {
name: 'MDL Rxnfile',
mime:'chemical/x-mdl-rxnfile',
ext: ['.rxn'],
supportsCoords: true
},
'cml': {
name: 'CML',
mime: 'chemical/x-cml',
ext: ['.cml', '.mrv'],
supportsCoords: true
},
'smiles': {
name: 'Daylight SMILES',
mime: 'chemical/x-daylight-smiles',
ext: ['.smi', '.smiles']
},
'smarts': {
name: 'Daylight SMARTS',
mime: 'chemical/x-daylight-smarts',
ext: ['.smarts']
},
'inchi': {
name: 'InChI String',
mime: 'chemical/x-inchi',
ext: ['.inchi']
}
};
export function guess (structStr, strict) {
// Mimic Indigo/molecule_auto_loader.cpp as much as possible
const molStr = structStr.trim();
if (molStr.indexOf('$RXN') !== -1)
return 'rxn';
const molMatch = molStr.match(/^(M END|\$END MOL)$/m);
if (molMatch) {
const end = molMatch.index + molMatch[0].length;
if (end === molStr.length ||
molStr.slice(end, end + 20).search(/^\$(MOL|END CTAB)$/m) !== -1)
return 'mol';
}
if (molStr[0] === '<' && molStr.indexOf('<molecule') !== -1)
return 'cml';
if (molStr.slice(0, 5) === 'InChI')
return 'inchi';
if (molStr.indexOf('\n') === -1) // TODO: smiles regexp
return 'smiles';
// Molfile by default as Indigo does
return strict ? null : 'mol';
}
export function toString (struct, format, server, serverOpts) {
console.assert(map[format], 'No such format');
return new Promise((resolve, reject) => {
var moldata = molfile.stringify(struct);
if (format === 'mol' || format === 'rxn')
resolve(moldata);
else
resolve(server.then(() => (
server.convert({
struct: moldata,
output_format: map[format].mime
}, serverOpts)
), () => {
throw Error(map[format].name + ' is not supported in the standalone mode');
}).then(res => res.struct));
});
}
export function fromString (structStr, opts, server, serverOpts) {
return new Promise(function (resolve, reject) {
const format = guess(structStr);
console.assert(map[format], 'No such format');
if (format === 'mol' || format === 'rxn') {
const struct = molfile.parse(structStr, opts);
resolve(struct);
} else {
let withCoords = map[format].supportsCoords;
resolve(server.then(() => (
withCoords ? server.convert({
struct: structStr,
output_format: map['mol'].mime
}, serverOpts) : server.layout({
struct: structStr.trim(),
output_format: map['mol'].mime
}, serverOpts)
), () => {
throw Error(map[format].name + ' is not supported in the standalone mode');
}).then(res => {
let struct = molfile.parse(res.struct);
if (!withCoords)
struct.rescale();
return struct;
}));
}
});
}
// Pretty stupid Inchi check (extract from save)
export function couldBeSaved(struct, format) {
if (format === 'inchi') {
if (struct.rgroups.count() !== 0)
throw 'R-group fragments are not supported and will be discarded';
struct = struct.clone(); // need this: .getScaffold()
struct.sgroups.each((sgid, sg) => {
// ? Not sure we should check it client side
if (sg.type !== 'MUL' && !/^INDIGO_.+_DESC$/i.test(sg.data.fieldName))
throw Error('InChi data format doesn\'t support s-groups');
});
}
}

View File

@ -0,0 +1,272 @@
/****************************************************************************
* 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.
***************************************************************************/
export const atom = {
title: "Atom",
type: "object",
required: "label",
properties: {
label: {
title: "Label",
type: "string", // TODO:should really be enum of elements
maxLength: 3,
invalidMessage: "Wrong label"
},
alias: {
title: "Alias",
type: "string",
invalidMessage: "Leading and trailing spaces are not allowed"
},
charge: {
title: "Charge",
type: "integer",
minimum: -1000,
maximum: 1000,
default: 0
},
explicitValence: {
title: "Valence",
enum: [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8],
enumNames: [
'', "0", "I", "II", "III",
"IV", "V", "VI", "VII", "VIII"
],
default: -1
},
isotope: {
title: "Isotope",
type: "integer",
minimum: 0,
default: 0
},
radical: {
title: "Radical",
enum: [0, 2, 1, 3],
enumNames: [
'',
"Monoradical",
"Diradical (singlet)",
"Diradical (triplet)"
],
default: 0
},
ringBondCount: {
title: "Ring bond count",
enum: [0, -2, -1, 2, 3, 4],
enumNames: [
'', "As drawn",
"0", "2", "3", "4"
],
default: 0
},
hCount: {
title: "H count",
enum: [0, 1, 2, 3, 4, 5],
enumNames: [
'', "0", "1", "2", "3", "4"
],
default: 0
},
substitutionCount: {
title: "Substitution count",
enum: [0, -2, -1, 1, 2, 3, 4, 5, 6],
enumNames: [
'', "As drawn",
"0", "1", "2", "3", "4", "5", "6"
],
default: 0
},
unsaturatedAtom: {
title: "Unsaturated",
type: "boolean",
default: false
},
invRet: {
title: "Inversion",
enum: [0, 1, 2],
enumNames: [
'',
"Inverts",
"Retains"
],
default: 0
},
exactChangeFlag: {
title: "Exact change",
type: "boolean",
default: false
}
}
};
export const attachmentPoints = {
title: "Attachment Points",
type: "object",
properties: {
primary: {
title: "Primary attachment point",
type: "boolean"
},
secondary: {
title: "Secondary attachment point",
type: "boolean"
}
}
};
export const bond = {
title: "Bond",
type: "object",
required: ["type"],
properties: {
type: {
title: "Type",
enum: ["single", "up", "down",
"updown", "double", "crossed",
"triple", "aromatic", "any",
"singledouble", "singlearomatic",
"doublearomatic"],
enumNames: [
"Single",
"Single Up",
"Single Down",
"Single Up/Down",
"Double",
"Double Cis/Trans",
"Triple",
"Aromatic",
"Any",
"Single/Double",
"Single/Aromatic",
"Double/Aromatic"
],
default: "single"
},
topology: {
title: "Topology",
enum: [0, 1, 2],
enumNames: ["Either", "Ring", "Chain"],
default: 0
},
center: {
title: "Reacting Center",
enum: [0, -1, 1, 2, 4, 8, 12], // 5, 9, 13
enumNames: [
"Unmarked",
"Not center",
"Center",
"No change",
"Made/broken",
"Order changes",
"Made/broken and changes"
], // "Order changes" x 3
default: 0
}
}
};
export const sgroup = {
title: "SGroup",
type: "object",
required: ["type"],
oneOf: [
{
key: 'GEN',
title: "Generic",
properties: {
type: { enum: ['GEN'] }
}
},
{
key: 'MUL',
title: "Multiple group",
type: "object",
properties: {
type: { enum: ["MUL"] },
mul: {
title: "Repeat count",
type: "integer",
default: 1,
minimum: 1,
maximum: 1000,
invalidMessage: 'Value out of range: must be between 1 and 1000'
}
},
required: ["mul"]
},
{
key: 'SRU',
title: "SRU polymer",
properties: {
type: { enum: ["SRU"] },
subscript: {
title: "Polymer label",
type: "string",
default: "n",
pattern: "^[a-zA-Z]$",
invalidMessage: "SRU subscript should consist of a single letter"
},
connectivity: {
title: 'Repeat Pattern',
enum: ["ht", "hh", "eu"],
enumNames: [
'Head-to-tail',
'Head-to-head',
'Either unknown'
],
default: "ht"
}
},
required: ["subscript", "connectivity"]
},
{
key: 'SUP',
title: 'Superatom',
properties: {
type: { enum: ["SUP"] },
name: {
title: "Name",
type: "string",
default: "",
minLength: 1,
invalidMessage: "Please, provide a name for the superatom"
}
},
required: ["name"]
}
]
};
export const rgroup = {
title: "R-Group",
type: "object",
properties: {
range: {
title: "Occurrence",
type: "string",
maxLength: 50
},
resth: {
title: "RestH",
type: "boolean"
},
ifthen: {
title: "Condition",
type: "integer",
minium: 0
}
}
};

View File

@ -0,0 +1,266 @@
/****************************************************************************
* 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 { connect } from 'preact-redux';
import { h } from 'preact';
/** @jsx h */
import classNames from 'classnames';
import element from '../chem/element';
import Atom from './component/atom';
import ActionMenu, { shortcutStr } from './component/actionmenu';
import action from './action';
import { atomCuts, basic as basicAtoms } from './action/atoms';
import { zoomList } from './action/zoom';
import templates from './data/templates';
const mainmenu = [
{
id: 'document',
menu: [
'new',
'open',
'save'
]
},
{
id: 'edit',
menu: [
'undo',
'redo',
'cut',
'copy',
'paste'
]
},
{
id: 'zoom',
menu: [
'zoom-in',
'zoom-out',
{
id: 'zoom-list',
component: ZoomList
}
]
},
{
id: 'process',
menu: [
'layout',
'clean',
'arom',
'dearom',
'cip',
'check',
'analyse',
'recognize',
'miew'
]
},
{
id: 'meta',
menu: [
'settings',
'help',
'about'
]
}
];
const toolbox = [
{
id: 'select',
menu: [
'select-lasso',
'select-rectangle',
'select-fragment'
]
},
'erase',
{
id: 'bond',
menu: [
{
id: 'bond-common',
menu: [
'bond-single',
'bond-double',
'bond-triple'
]
},
{
id: 'bond-stereo',
menu: [
'bond-up',
'bond-down',
'bond-updown',
'bond-crossed'
]
},
{
id: 'bond-query',
menu: [
'bond-any',
'bond-aromatic',
'bond-singledouble',
'bond-singlearomatic',
'bond-doublearomatic'
]
}
]
},
'chain',
{
id: 'charge',
menu: [
'charge-plus',
'charge-minus'
]
},
{
id: 'transform',
menu: [
'transform-rotate',
'transform-flip-h',
'transform-flip-v'
]
},
'sgroup',
'sgroup-data',
{
id: 'reaction',
menu: [
'reaction-arrow',
'reaction-plus',
'reaction-automap',
'reaction-map',
'reaction-unmap'
]
},
{
id: 'rgroup',
menu: [
'rgroup-label',
'rgroup-fragment',
'rgroup-attpoints'
]
}
];
const template = [
{
id: 'template-common',
component: TemplatesList
},
'template-lib',
'chiral-flag'
];
const elements = [
{
id: 'atom',
component: props => AtomsList(basicAtoms, props)
},
{
id: 'freq-atoms',
component: props => AtomsList(props['freqAtoms'], props)
},
'period-table'
];
const toolbar = [
{ id: 'mainmenu', menu: mainmenu },
{ id: 'toolbox', menu: toolbox },
{ id: 'template', menu: template },
{ id: 'elements', menu: elements }
];
function ZoomList({status, onAction}) {
let zoom = status.zoom && status.zoom.selected; // TMP
return (
<select value={zoom}
onChange={ev => onAction(editor => editor.zoom(parseFloat(ev.target.value)))}>
{
zoomList.map(val => (
<option value={val}>{`${(val * 100).toFixed()}%`}</option>
))
}
</select>
);
}
function AtomsList(atoms, {active, onAction, ...props}) {
let isAtom = active && active.tool === 'atom';
return (
<menu>
{
atoms.map(label => {
let index = element.map[label];
let shortcut = basicAtoms.indexOf(label) > -1 ? shortcutStr(atomCuts[label]) : null;
return (
<li className={classNames({
selected: isAtom && active.opts.label === label
})}>
<Atom el={element[index]}
shortcut={shortcut}
onClick={() => onAction({ tool: 'atom', opts: { label } })}/>
</li>
);
})
}
</menu>
);
}
function TemplatesList({active, onAction, ...props}) {
let shortcut = shortcutStr(action[`template-0`].shortcut);
let isTmpl = active && active.tool === 'template';
return (
<menu>
{
templates.map((struct, i) => (
<li id={`template-${i}`}
className={classNames({
selected: isTmpl && active.opts.struct === struct
})}>
<button title={`${struct.name} (${shortcut})`}
onClick={() => onAction({ tool: 'template', opts: { struct } })}>
{struct.name}
</button>
</li>
))
}
</menu>
);
}
export default connect(
(state, props) => ({
active: state.actionState && state.actionState.activeTool,
status: state.actionState || {},
freqAtoms: state.toolbar.freqAtoms,
opened: state.toolbar.opened,
visibleTools: state.toolbar.visibleTools
}), {
onOpen: menuName => ({ type: 'OPENED', data: menuName })
}
)(props => (
<ActionMenu menu={toolbar} role="toolbar" {...props}/>
));

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.
***************************************************************************/
/* local storage */
const storage = {
warningMessage: "Your changes will be lost after the tab closing. See Help (Note 2).",
isAvailable: function () {
try {
return localStorage;
} catch (ex) {
return false;
}
},
getItem: function (key) {
let item = null;
try {
item = JSON.parse(localStorage.getItem(key));
} catch (ex) {
console.info('LocalStorage:', ex.name);
}
return item;
},
setItem: function (key, data) {
let isSet = null;
try {
localStorage.setItem(key, JSON.stringify(data));
isSet = true;
} catch (ex) {
console.info('LocalStorage:', ex.name);
isSet = false;
}
return isSet;
}
};
/* schema utils */
function constant(schema, prop) {
let desc = schema.properties[prop];
return desc.constant || desc.enum[0]; // see https://git.io/v6hyP
}
function mapOf(schema, prop) {
console.assert(schema.oneOf);
return schema.oneOf.reduce((res, desc) => {
res[constant(desc, prop)] = desc;
return res;
}, {});
}
function selectListOf(schema, prop) {
let desc = schema.properties && schema.properties[prop];
if (desc)
return desc.enum.map((value, i) => {
let title = desc.enumNames && desc.enumNames[i];
return title ? { title, value } : value;
});
return schema.oneOf.map(desc => (
!desc.title ? constant(desc, prop) : {
title: desc.title,
value: constant(desc, prop)
}
));
}
module.exports = {
mapOf: mapOf,
selectListOf: selectListOf,
storage: storage
};