forked from enviPath/enviPy
Current Dev State
This commit is contained in:
44
static/js/ketcher2/script/ui/action/atoms.js
Normal file
44
static/js/ketcher2/script/ui/action/atoms.js
Normal 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;
|
||||
}, {});
|
||||
38
static/js/ketcher2/script/ui/action/debug.js
Normal file
38
static/js/ketcher2/script/ui/action/debug.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
184
static/js/ketcher2/script/ui/action/index.js
Normal file
184
static/js/ketcher2/script/ui/action/index.js
Normal 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 + '.');
|
||||
}
|
||||
58
static/js/ketcher2/script/ui/action/server.js
Normal file
58
static/js/ketcher2/script/ui/action/server.js
Normal 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
|
||||
}
|
||||
};
|
||||
39
static/js/ketcher2/script/ui/action/templates.js
Normal file
39
static/js/ketcher2/script/ui/action/templates.js
Normal 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);
|
||||
|
||||
142
static/js/ketcher2/script/ui/action/tools.js
Normal file
142
static/js/ketcher2/script/ui/action/tools.js
Normal 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);
|
||||
56
static/js/ketcher2/script/ui/action/zoom.js
Normal file
56
static/js/ketcher2/script/ui/action/zoom.js
Normal 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]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
138
static/js/ketcher2/script/ui/app.jsx
Normal file
138
static/js/ketcher2/script/ui/app.jsx
Normal 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;
|
||||
50
static/js/ketcher2/script/ui/component/accordion.jsx
Normal file
50
static/js/ketcher2/script/ui/component/accordion.jsx
Normal 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;
|
||||
99
static/js/ketcher2/script/ui/component/actionmenu.jsx
Normal file
99
static/js/ketcher2/script/ui/component/actionmenu.jsx
Normal 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;
|
||||
41
static/js/ketcher2/script/ui/component/atom.jsx
Normal file
41
static/js/ketcher2/script/ui/component/atom.jsx
Normal 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;
|
||||
136
static/js/ketcher2/script/ui/component/cliparea.jsx
Normal file
136
static/js/ketcher2/script/ui/component/cliparea.jsx
Normal 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;
|
||||
75
static/js/ketcher2/script/ui/component/combobox.jsx
Normal file
75
static/js/ketcher2/script/ui/component/combobox.jsx
Normal 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;
|
||||
82
static/js/ketcher2/script/ui/component/dialog.jsx
Normal file
82
static/js/ketcher2/script/ui/component/dialog.jsx
Normal 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;
|
||||
196
static/js/ketcher2/script/ui/component/form.jsx
Normal file
196
static/js/ketcher2/script/ui/component/form.jsx
Normal 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 };
|
||||
232
static/js/ketcher2/script/ui/component/input.jsx
Normal file
232
static/js/ketcher2/script/ui/component/input.jsx
Normal 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 });
|
||||
}
|
||||
}
|
||||
71
static/js/ketcher2/script/ui/component/measure-input.jsx
Normal file
71
static/js/ketcher2/script/ui/component/measure-input.jsx
Normal 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;
|
||||
|
||||
109
static/js/ketcher2/script/ui/component/openbutton.jsx
Normal file
109
static/js/ketcher2/script/ui/component/openbutton.jsx
Normal 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;
|
||||
77
static/js/ketcher2/script/ui/component/savebutton.jsx
Normal file
77
static/js/ketcher2/script/ui/component/savebutton.jsx
Normal 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;
|
||||
40
static/js/ketcher2/script/ui/component/select.jsx
Normal file
40
static/js/ketcher2/script/ui/component/select.jsx
Normal 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;
|
||||
26
static/js/ketcher2/script/ui/component/spin.jsx
Normal file
26
static/js/ketcher2/script/ui/component/spin.jsx
Normal 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;
|
||||
78
static/js/ketcher2/script/ui/component/structeditor.jsx
Normal file
78
static/js/ketcher2/script/ui/component/structeditor.jsx
Normal 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;
|
||||
71
static/js/ketcher2/script/ui/component/structrender.jsx
Normal file
71
static/js/ketcher2/script/ui/component/structrender.jsx
Normal 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;
|
||||
88
static/js/ketcher2/script/ui/component/systemfonts.jsx
Normal file
88
static/js/ketcher2/script/ui/component/systemfonts.jsx
Normal 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;
|
||||
53
static/js/ketcher2/script/ui/component/tabs.jsx
Normal file
53
static/js/ketcher2/script/ui/component/tabs.jsx
Normal 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;
|
||||
101
static/js/ketcher2/script/ui/component/visibleview.jsx
Normal file
101
static/js/ketcher2/script/ui/component/visibleview.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
223
static/js/ketcher2/script/ui/data/options-schema.js
Normal file
223
static/js/ketcher2/script/ui/data/options-schema.js
Normal 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;
|
||||
}, {});
|
||||
}
|
||||
402
static/js/ketcher2/script/ui/data/sdata-schema.js
Normal file
402
static/js/ketcher2/script/ui/data/sdata-schema.js
Normal 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 :
|
||||
'';
|
||||
}
|
||||
154
static/js/ketcher2/script/ui/data/templates.js
Normal file
154
static/js/ketcher2/script/ui/data/templates.js
Normal 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));
|
||||
70
static/js/ketcher2/script/ui/dialog/about.jsx
Normal file
70
static/js/ketcher2/script/ui/dialog/about.jsx
Normal 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);
|
||||
136
static/js/ketcher2/script/ui/dialog/analyse.jsx
Normal file
136
static/js/ketcher2/script/ui/dialog/analyse.jsx
Normal 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);
|
||||
78
static/js/ketcher2/script/ui/dialog/atom.jsx
Normal file
78
static/js/ketcher2/script/ui/dialog/atom.jsx
Normal 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);
|
||||
40
static/js/ketcher2/script/ui/dialog/attach.jsx
Normal file
40
static/js/ketcher2/script/ui/dialog/attach.jsx
Normal 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);
|
||||
59
static/js/ketcher2/script/ui/dialog/automap.jsx
Normal file
59
static/js/ketcher2/script/ui/dialog/automap.jsx
Normal 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);
|
||||
41
static/js/ketcher2/script/ui/dialog/bond.jsx
Normal file
41
static/js/ketcher2/script/ui/dialog/bond.jsx
Normal 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);
|
||||
89
static/js/ketcher2/script/ui/dialog/check.jsx
Normal file
89
static/js/ketcher2/script/ui/dialog/check.jsx
Normal 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);
|
||||
134
static/js/ketcher2/script/ui/dialog/generic-groups.jsx
Normal file
134
static/js/ketcher2/script/ui/dialog/generic-groups.jsx
Normal 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;
|
||||
32
static/js/ketcher2/script/ui/dialog/help.jsx
Normal file
32
static/js/ketcher2/script/ui/dialog/help.jsx
Normal 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;
|
||||
64
static/js/ketcher2/script/ui/dialog/index.js
Normal file
64
static/js/ketcher2/script/ui/dialog/index.js
Normal 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
|
||||
};
|
||||
96
static/js/ketcher2/script/ui/dialog/labeledit.jsx
Normal file
96
static/js/ketcher2/script/ui/dialog/labeledit.jsx
Normal 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);
|
||||
223
static/js/ketcher2/script/ui/dialog/miew.jsx
Normal file
223
static/js/ketcher2/script/ui/dialog/miew.jsx
Normal 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;
|
||||
95
static/js/ketcher2/script/ui/dialog/open.jsx
Normal file
95
static/js/ketcher2/script/ui/dialog/open.jsx
Normal 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);
|
||||
129
static/js/ketcher2/script/ui/dialog/options.jsx
Normal file
129
static/js/ketcher2/script/ui/dialog/options.jsx
Normal 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);
|
||||
272
static/js/ketcher2/script/ui/dialog/period-table.jsx
Normal file
272
static/js/ketcher2/script/ui/dialog/period-table.jsx
Normal 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);
|
||||
104
static/js/ketcher2/script/ui/dialog/recognize.jsx
Normal file
104
static/js/ketcher2/script/ui/dialog/recognize.jsx
Normal 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);
|
||||
72
static/js/ketcher2/script/ui/dialog/rgroup-logic.jsx
Normal file
72
static/js/ketcher2/script/ui/dialog/rgroup-logic.jsx
Normal 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);
|
||||
101
static/js/ketcher2/script/ui/dialog/rgroup.jsx
Normal file
101
static/js/ketcher2/script/ui/dialog/rgroup.jsx
Normal 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}/>);
|
||||
90
static/js/ketcher2/script/ui/dialog/save.jsx
Normal file
90
static/js/ketcher2/script/ui/dialog/save.jsx
Normal 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);
|
||||
89
static/js/ketcher2/script/ui/dialog/sdata.jsx
Normal file
89
static/js/ketcher2/script/ui/dialog/sdata.jsx
Normal 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);
|
||||
61
static/js/ketcher2/script/ui/dialog/sgroup.jsx
Normal file
61
static/js/ketcher2/script/ui/dialog/sgroup.jsx
Normal 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);
|
||||
114
static/js/ketcher2/script/ui/dialog/template-attach.jsx
Normal file
114
static/js/ketcher2/script/ui/dialog/template-attach.jsx
Normal 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;
|
||||
}
|
||||
174
static/js/ketcher2/script/ui/dialog/template-lib.jsx
Normal file
174
static/js/ketcher2/script/ui/dialog/template-lib.jsx
Normal 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);
|
||||
24
static/js/ketcher2/script/ui/index.js
Normal file
24
static/js/ketcher2/script/ui/index.js
Normal 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;
|
||||
95
static/js/ketcher2/script/ui/keynorm.js
Normal file
95
static/js/ketcher2/script/ui/keynorm.js
Normal 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;
|
||||
72
static/js/ketcher2/script/ui/state/action.js
Normal file
72
static/js/ketcher2/script/ui/state/action.js
Normal 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;
|
||||
}
|
||||
}
|
||||
106
static/js/ketcher2/script/ui/state/editor.js
Normal file
106
static/js/ketcher2/script/ui/state/editor.js
Normal 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 => {}
|
||||
};
|
||||
}
|
||||
129
static/js/ketcher2/script/ui/state/form.js
Normal file
129
static/js/ketcher2/script/ui/state/form.js
Normal 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);
|
||||
}
|
||||
156
static/js/ketcher2/script/ui/state/hotkeys.js
Normal file
156
static/js/ketcher2/script/ui/state/hotkeys.js
Normal 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;
|
||||
}
|
||||
167
static/js/ketcher2/script/ui/state/index.js
Normal file
167
static/js/ketcher2/script/ui/state/index.js
Normal 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));
|
||||
};
|
||||
33
static/js/ketcher2/script/ui/state/miew.js
Normal file
33
static/js/ketcher2/script/ui/state/miew.js
Normal 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));
|
||||
});
|
||||
});
|
||||
}
|
||||
114
static/js/ketcher2/script/ui/state/options.js
Normal file
114
static/js/ketcher2/script/ui/state/options.js
Normal 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;
|
||||
}
|
||||
114
static/js/ketcher2/script/ui/state/sdata.js
Normal file
114
static/js/ketcher2/script/ui/state/sdata.js
Normal 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,
|
||||
}
|
||||
};
|
||||
};
|
||||
155
static/js/ketcher2/script/ui/state/server.js
Normal file
155
static/js/ketcher2/script/ui/state/server.js
Normal 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));
|
||||
}, []);
|
||||
}
|
||||
250
static/js/ketcher2/script/ui/state/templates.js
Normal file
250
static/js/ketcher2/script/ui/state/templates.js
Normal 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]
|
||||
));
|
||||
});
|
||||
}
|
||||
110
static/js/ketcher2/script/ui/state/toolbar.js
Normal file
110
static/js/ketcher2/script/ui/state/toolbar.js
Normal 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;
|
||||
}
|
||||
277
static/js/ketcher2/script/ui/structconv.js
Normal file
277
static/js/ketcher2/script/ui/structconv.js
Normal 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
|
||||
};
|
||||
}
|
||||
144
static/js/ketcher2/script/ui/structformat.js
Normal file
144
static/js/ketcher2/script/ui/structformat.js
Normal 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');
|
||||
});
|
||||
}
|
||||
}
|
||||
272
static/js/ketcher2/script/ui/structschema.js
Normal file
272
static/js/ketcher2/script/ui/structschema.js
Normal 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
|
||||
}
|
||||
}
|
||||
};
|
||||
266
static/js/ketcher2/script/ui/toolbar.jsx
Normal file
266
static/js/ketcher2/script/ui/toolbar.jsx
Normal 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}/>
|
||||
));
|
||||
82
static/js/ketcher2/script/ui/utils.js
Normal file
82
static/js/ketcher2/script/ui/utils.js
Normal 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
|
||||
};
|
||||
Reference in New Issue
Block a user