Skip to content
55 changes: 55 additions & 0 deletions src/components/svg/svg_editor.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export const greekLetters = {
alpha: 'α',
beta: 'β',
gamma: 'γ',
delta: 'δ',
epsilon: 'ε',
zeta: 'ζ',
eta: 'η',
theta: 'θ',
} as const;
const greekLetterNames = Object.keys(greekLetters);
const greeksFirstLine = greekLetterNames.slice(0, 6);
const greeksLastLine = greekLetterNames.slice(6);

export const primes = {
prime1: '′',
prime2: '″',
prime3: '‴',
};
const primeNames = Object.keys(primes);

export const atomLabelEditCss = `
form.react-ocl-atom-label-edit {
position: absolute;
z-index: 1;
display: grid;
grid-template-columns: repeat(4, 1.5em);
grid-template-areas:
"input input input input submit cancel"
"${greeksFirstLine.join(' ')}"
"${greeksLastLine.join(' ')} . ${primeNames.join(' ')}";
align-items: stretch;
gap: 0.25em;
border: 1px solid lightgray;
background-color: white;
padding: 0.25em;
}

form.react-ocl-atom-label-edit button.react-ocl {
padding: 0.25em;
background-color: #efefef;
border: none;
border-radius: 5px;
}

form.react-ocl-atom-label-edit input.react-ocl {
padding: 0.25em;
border: solid 1px lightgrey;
border-radius: 3px;
}

form.react-ocl-atom-label-edit input.react-ocl:focus {
outline: auto;
}
`;
54 changes: 14 additions & 40 deletions src/components/svg/svg_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
import { useEffect, useMemo, useReducer, useRef, useState } from 'react';

import { useRefUpToDate } from '../../hooks/use_ref_up_to_date.js';
import { useCSS } from '../../styling/use_css.ts';
import type { BaseEditorProps } from '../types.js';

import {
Expand All @@ -22,6 +23,7 @@ import {
import type { State } from './editor/reducer.js';
import { stateReducer } from './editor/reducer.js';
import { useHighlight } from './editor/use_highlight.js';
import { atomLabelEditCss, greekLetters, primes } from './svg_editor.css.ts';
import type { SvgRendererProps } from './svg_renderer.js';
import { SvgRenderer } from './svg_renderer.js';

Expand Down Expand Up @@ -181,26 +183,6 @@ interface AtomLabelEditFormProps {
onCancel: () => void;
}

const greekLetters = {
alpha: 'α',
beta: 'β',
gamma: 'γ',
delta: 'δ',
epsilon: 'ε',
zeta: 'ζ',
eta: 'η',
theta: 'θ',
} as const;
const greekLetterNames = Object.keys(greekLetters);
const greeksFirstLine = greekLetterNames.slice(0, 6);
const greeksLastLine = greekLetterNames.slice(6);
const primes = {
prime1: '′',
prime2: '″',
prime3: '‴',
};
const primeNames = Object.keys(primes);

function AtomLabelEditForm(props: AtomLabelEditFormProps) {
const { defaultValue, formCoords, onSubmit, onCancel } = props;

Expand Down Expand Up @@ -275,30 +257,17 @@ function AtomLabelEditForm(props: AtomLabelEditFormProps) {
input.focus();
}

useCSS(atomLabelEditCss);
return (
<form
ref={formRef}
onSubmit={onFormSubmit}
onKeyDown={onKeyDown}
style={{
position: 'absolute',
top: formCoords.y,
left: formCoords.x,
display: 'grid',
gridTemplateAreas: `
"input input input input submit cancel"
"${greeksFirstLine.join(' ')}"
"${greeksLastLine.join(' ')} . ${primeNames.join(' ')}"
`,
gridTemplateColumns: 'repeat(4, 1.5em)',
alignItems: 'stretch',
gap: '0.25em',
border: '1px solid lightgray',
backgroundColor: 'white',
padding: '0.25em',
}}
className="react-ocl react-ocl-atom-label-edit"
style={{ top: formCoords.y, left: formCoords.x }}
>
<input
className="react-ocl"
style={{ gridArea: 'input' }}
type="text"
name="label"
Expand All @@ -307,13 +276,16 @@ function AtomLabelEditForm(props: AtomLabelEditFormProps) {
autoFocus
ref={autoSelectText}
/>
<input
<button
className="react-ocl"
style={{ gridArea: 'submit' }}
type="submit"
value="✔️"
aria-label="Submit"
/>
>
✔️
</button>
<button
className="react-ocl"
style={{ gridArea: 'cancel' }}
type="button"
aria-label="Cancel"
Expand All @@ -324,6 +296,7 @@ function AtomLabelEditForm(props: AtomLabelEditFormProps) {

{Object.entries(greekLetters).map(([charName, greekChar]) => (
<button
className="react-ocl"
key={charName}
type="submit"
style={{ gridArea: charName }}
Expand All @@ -335,6 +308,7 @@ function AtomLabelEditForm(props: AtomLabelEditFormProps) {

{Object.entries(primes).map(([primeName, primeChar]) => (
<button
className="react-ocl"
key={primeName}
type="submit"
style={{ gridArea: primeName }}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './components/svg/molfile/molfile_svg_editor.js';
export * from './components/svg/smiles_svg_renderer.js';
export * from './components/canvas/canvas_editor.js';
export * from './components/canvas/canvas_editor_hook.js';
export { OclReset, type OclResetProps } from './styling/reset/ocl_reset.js';

export type {
BaseSvgRendererProps,
Expand Down
32 changes: 32 additions & 0 deletions src/styling/reset/ocl_reset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { ReactNode } from 'react';
import { useLayoutEffect } from 'react';

import { useCSS } from '../use_css.ts';

import { OclResetContext } from './ocl_reset_context.tsx';
import { oclResetCss } from './ocl_reset_css.ts';

export interface OclResetProps {
children: ReactNode;
}

// eslint-disable-next-line jsdoc/require-returns
/**
* Enable react-ocl-reset styles.
* @param props - The children to render
*/
export function OclReset(props: OclResetProps) {
useLayoutEffect(() => {
document.body.classList.add('react-ocl-reset');

return () => {
document.body.classList.remove('react-ocl-reset');
};
}, []);

useCSS(oclResetCss);

return (
<OclResetContext.Provider value>{props.children}</OclResetContext.Provider>
);
}
3 changes: 3 additions & 0 deletions src/styling/reset/ocl_reset_context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createContext } from 'react';

export const OclResetContext = createContext(false);
10 changes: 10 additions & 0 deletions src/styling/reset/ocl_reset_css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const oclResetCss = `
.react-ocl-reset input.react-ocl, .react-ocl-reset button.react-ocl {
border: none;
outline: none;
background: none;
box-sizing: border-box;
padding: 0;
margin: 0;
}
`;
21 changes: 21 additions & 0 deletions src/styling/use_css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useInsertionEffect } from 'react';

/**
* Little util to inject CSS into the head.
* @param css - The CSS to inject.
*/
export function useCSS(css: string) {
useInsertionEffect(() => {
const sheet = new CSSStyleSheet();
sheet.replaceSync(css);

document.adoptedStyleSheets.push(sheet);

return () => {
const index = document.adoptedStyleSheets.indexOf(sheet);
if (index === -1) return;

document.adoptedStyleSheets.splice(index, 1);
};
}, [css]);
}
10 changes: 10 additions & 0 deletions stories/svg_editors/svg_editor.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
import { useState } from 'react';

import { SvgEditor } from '../../src/index.js';
import { OclReset } from '../../src/styling/reset/ocl_reset.tsx';
import { molecule as defaultBaseMolecule } from '../data.js';

const defaultMolecule = defaultBaseMolecule.getCompactCopy();
Expand All @@ -21,3 +22,12 @@ export default {
type Story = StoryObj<typeof SvgEditor>;

export const Control: Story = {};
export const WithCssReset: Story = {
decorators: (Story) => {
return (
<OclReset>
<Story />
</OclReset>
);
},
};
Loading