diff --git a/package-lock.json b/package-lock.json index d571a30892..544b3db718 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,8 @@ "@hookform/resolvers": "^5.2.2", "@tanstack/react-form": "^1.27.7", "@zakodium/nmr-types": "^0.5.1", - "@zakodium/nmrium-core": "^0.6.1", - "@zakodium/nmrium-core-plugins": "^0.6.33", + "@zakodium/nmrium-core": "^0.6.3", + "@zakodium/nmrium-core-plugins": "^0.6.35", "@zakodium/pdnd-esm": "^1.0.2", "@zip.js/zip.js": "^2.8.15", "cheminfo-font": "^1.13.1", @@ -43,7 +43,7 @@ "ml-spectra-processing": "^14.19.0", "ml-tree-similarity": "^2.2.0", "nmr-correlation": "2.3.3", - "nmr-processing": "^22.1.0", + "nmr-processing": "^22.4.0", "numeral": "^2.0.6", "openchemlib": "^9.19.0", "openchemlib-utils": "^8.12.1", @@ -3981,9 +3981,9 @@ } }, "node_modules/@zakodium/nmrium-core": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core/-/nmrium-core-0.6.2.tgz", - "integrity": "sha512-YU+OV96lxVS/kGUiOp7I9vGJNxTV4A7BpoHi7abPJ0/pVe9AaUU8va/6YAQWLLM/IVDcERWMvu7mLIEvv9Rxvg==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core/-/nmrium-core-0.6.3.tgz", + "integrity": "sha512-dImNbF4SAGkm8joNPzePI4buF5duoMbEtIcHk0XkN3+ZN2qKBUz8GItkxLYNBOS+XqdLFJ/3FCRPRkTjARoHLQ==", "license": "CC-BY-NC-SA-4.0", "dependencies": { "cheminfo-types": "^1.8.1", @@ -3996,13 +3996,13 @@ } }, "node_modules/@zakodium/nmrium-core-plugins": { - "version": "0.6.34", - "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core-plugins/-/nmrium-core-plugins-0.6.34.tgz", - "integrity": "sha512-yp2Y+oqtQBbHroWT6NNFXohpLuVEeYNwhKXu3Mx3bOveyERQ+ql12VqqaeBEtHvGNqRSk4JZPpzjkrC1mqTUpw==", + "version": "0.6.35", + "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core-plugins/-/nmrium-core-plugins-0.6.35.tgz", + "integrity": "sha512-agPVpp/p0hCjVOBFGYqnSiEr0zLN/kZHB+LrX15odPUaHFbwPZDxAB24wLrMY/cBYpTyKoJ24BBApLe6bu2Cxw==", "license": "CC-BY-NC-SA-4.0", "dependencies": { "@date-fns/utc": "^2.1.1", - "@zakodium/nmrium-core": "^0.6.2", + "@zakodium/nmrium-core": "^0.6.3", "cheminfo-types": "^1.8.1", "convert-to-jcamp": "^6.0.0", "date-fns": "^4.1.0", diff --git a/package.json b/package.json index 67064ee858..45e6ce26c0 100644 --- a/package.json +++ b/package.json @@ -70,8 +70,8 @@ "@hookform/resolvers": "^5.2.2", "@tanstack/react-form": "^1.27.7", "@zakodium/nmr-types": "^0.5.1", - "@zakodium/nmrium-core": "^0.6.1", - "@zakodium/nmrium-core-plugins": "^0.6.33", + "@zakodium/nmrium-core": "^0.6.3", + "@zakodium/nmrium-core-plugins": "^0.6.35", "@zakodium/pdnd-esm": "^1.0.2", "@zip.js/zip.js": "^2.8.15", "cheminfo-font": "^1.13.1", @@ -97,7 +97,7 @@ "ml-spectra-processing": "^14.19.0", "ml-tree-similarity": "^2.2.0", "nmr-correlation": "2.3.3", - "nmr-processing": "^22.1.0", + "nmr-processing": "^22.4.0", "numeral": "^2.0.6", "openchemlib": "^9.19.0", "openchemlib-utils": "^8.12.1", diff --git a/src/component/1d/FloatPublicationString.tsx b/src/component/1d/FloatPublicationString.tsx index 7ec7ea1e03..a056ee5f3e 100644 --- a/src/component/1d/FloatPublicationString.tsx +++ b/src/component/1d/FloatPublicationString.tsx @@ -1,8 +1,8 @@ import styled from '@emotion/styled'; -import type { BoundingBox, TextStyle } from '@zakodium/nmrium-core'; -import { useEffect, useMemo, useState } from 'react'; +import type { BoundingBox, Spectrum, TextStyle } from '@zakodium/nmrium-core'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { BsArrowsMove } from 'react-icons/bs'; -import { FaTimes } from 'react-icons/fa'; +import { FaEdit, FaTimes } from 'react-icons/fa'; import { Rnd } from 'react-rnd'; import { SVGStyledText } from 'react-science/ui'; @@ -17,6 +17,7 @@ import { useTextMetrics } from '../hooks/useTextMetrics.js'; import { useCheckExportStatus } from '../hooks/useViewportSize.js'; import { useACSSettings } from '../hooks/use_acs_settings.js'; import { usePublicationStrings } from '../hooks/use_publication_strings.js'; +import { PublicationStringModal } from '../modal/PublicationStringModal.js'; const ReactRnd = styled(Rnd)` border: 1px solid transparent; @@ -33,25 +34,29 @@ const ReactRnd = styled(Rnd)` interface UseWrapSVGTextParams { text: string; + style: TextStyle; width: number; - fontSize: number; - fontStyle: string | undefined; - fontWeight: string | undefined; } function useWrapSVGText(params: UseWrapSVGTextParams) { - const { text, width, fontSize, fontStyle, fontWeight } = params; - const { getTextWidth } = useTextMetrics({ - labelSize: fontSize, - labelStyle: fontStyle, - labelWeight: fontWeight, + const { text, width, style } = params; + + const debugCanvas = false; + const labelSize = style.fontSize ?? 12; + // ctx used only for debug canvas purpose. + // eslint-disable-next-line @typescript-eslint/no-deprecated + const { getTextWidth, ctx } = useTextMetrics({ + labelSize, + labelStyle: style.fontStyle, + labelWeight: style.fontWeight, + debugCanvasWidth: debugCanvas ? width : undefined, }); const formattedText = text .replaceAll(/(?.*?)<\/sup>/g, '++$1++ ') .replaceAll(/(?.*?)<\/i>/g, '**$1**'); - const lineHeight = fontSize * 1.6; + const lineHeight = labelSize * 1.6; const lines: string[][] = []; let line: string[] = []; @@ -73,31 +78,59 @@ function useWrapSVGText(params: UseWrapSVGTextParams) { } if (line.length > 0) lines.push(line); + useEffect(() => { + if (!debugCanvas) return; + if (!ctx) return; + + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + let y = lineHeight; + for (const line of lines) { + let x = 0; + for (const word of line) { + const isSuper = word.startsWith('++') && word.endsWith('++'); + const isItalic = word.startsWith('**') && word.endsWith('**'); + const baseLine = ctx.textBaseline; + + let finalWord = `${word} `; + if (isSuper) { + finalWord = word.replaceAll('++', ''); + + ctx.textBaseline = 'bottom'; + } else if (isItalic) { + finalWord = word.replaceAll('**', ''); + } + + ctx?.fillText(finalWord, x, y); + x += getTextWidth(finalWord); + + ctx.textBaseline = baseLine; + } + + y += lineHeight; + } + }); + return { lines, lineHeight }; } interface PublicationTextProps { text: string; textStyle: TextStyle; - fontSize?: number; width: number; - padding?: number; } function PublicationText(props: PublicationTextProps) { const { text, width } = props; + const padding = 10; + const boxWidth = width - padding * 2; + const textStyle = { ...props.textStyle, - fontSize: props.textStyle.fontSize ?? props.fontSize ?? 12, + fontSize: props.textStyle.fontSize ?? 12, }; - const { fontSize = textStyle.fontSize, padding = 10 } = props; - const boxWidth = width - padding * 2; - const { lineHeight, lines } = useWrapSVGText({ width: boxWidth, - fontSize, - fontStyle: textStyle.fontStyle, - fontWeight: textStyle.fontWeight, + style: textStyle, text, }); @@ -115,8 +148,12 @@ function PublicationText(props: PublicationTextProps) { {line.map((word, wordIndex) => { if (word.startsWith('++') && word.endsWith('++')) { return ( - // eslint-disable-next-line react/no-array-index-key - + {word.replaceAll('++', '')} ); @@ -140,52 +177,115 @@ function PublicationText(props: PublicationTextProps) { interface DraggablePublicationStringProps { value: string; - bonding: BoundingBox; - spectrumKey: string; - nucleus: string | undefined; + bounding: BoundingBox; + nucleus: string; + spectrum: Spectrum; +} + +function useBoundingBox(externalBoundingPercent: BoundingBox) { + const { percentToPixel, pixelToPercent } = useSVGUnitConverter(); + + const convertToPixel = useCallback( + (bounding: Partial) => { + const { x, y, height, width } = bounding; + const output: Partial = {}; + + if (typeof x === 'number') { + output.x = percentToPixel(x, 'x'); + } + if (typeof y === 'number') { + output.y = percentToPixel(y, 'y'); + } + if (typeof width === 'number') { + output.width = width; + } + if (typeof height === 'number') { + output.height = height; + } + + return output; + }, + [percentToPixel], + ); + + const convertToPercent = useCallback( + (bounding: Partial) => { + const { x, y, height, width } = bounding; + const output: Partial = {}; + + if (typeof x === 'number') { + output.x = pixelToPercent(x, 'x'); + } + if (typeof y === 'number') { + output.y = pixelToPercent(y, 'y'); + } + if (typeof width === 'number') { + output.width = width; + } + if (typeof height === 'number') { + output.height = height; + } + + return output; + }, + [pixelToPercent], + ); + + const [bounding, setBounding] = useState(() => { + return convertToPixel(externalBoundingPercent) as BoundingBox; + }); + + useEffect(() => { + setBounding(convertToPixel(externalBoundingPercent) as BoundingBox); + }, [convertToPixel, externalBoundingPercent]); + + return { + bounding, + setBounding, + convertToPixel, + convertToPercent, + }; } function DraggablePublicationString(props: DraggablePublicationStringProps) { - const { value, bonding: externalBounding, spectrumKey, nucleus } = props; + const { + value, + bounding: externalBoundingInPercent, + nucleus, + spectrum, + } = props; + const spectrumKey = spectrum.id; + const dispatch = useDispatch(); const { viewerRef } = useGlobal(); - const [bounding, setBounding] = useState(externalBounding); const [isMoveActive, setIsMoveActive] = useState(false); - const { percentToPixel, pixelToPercent } = useSVGUnitConverter(); const isExportProcessStart = useCheckExportStatus(); const acsOptions = useACSSettings(nucleus); + const [isDialogOpen, setIsDialogOpen] = useState(false); - useEffect(() => { - setBounding({ ...externalBounding }); - }, [externalBounding]); - - function handleResize( - internalBounding: Pick, - ) { - const { width = 0, height = 0 } = convertToPixel(externalBounding); - internalBounding.width += width; - internalBounding.height += height; - setBounding((prevBounding) => ({ - ...prevBounding, - ...convertToPercent(internalBounding), - })); + const { bounding, setBounding, convertToPercent } = useBoundingBox( + externalBoundingInPercent, + ); + + function handleResize(bounding: Pick) { + setBounding((prevBounding) => { + return { + ...prevBounding, + width: bounding.width, + height: bounding.height, + }; + }); } - function handleDrag(internalBounding: Pick) { + function handleDrag(newPosition: Pick) { setBounding((prevBounding) => ({ ...prevBounding, - ...convertToPercent(internalBounding), + ...newPosition, })); } + function handleChangeInsetBounding(bounding: Partial) { - if ( - typeof bounding?.width === 'number' && - typeof bounding?.height === 'number' - ) { - const { width, height } = externalBounding; - bounding.width += width; - bounding.height += height; - } + setBounding((prev) => ({ ...prev, ...bounding })); dispatch({ type: 'CHANGE_RANGES_VIEW_FLOATING_BOX_BOUNDING', @@ -197,45 +297,6 @@ function DraggablePublicationString(props: DraggablePublicationStringProps) { }); } - function convertToPixel(bounding: Partial) { - const { x, y, height, width } = bounding; - const output: Partial = {}; - - if (x) { - output.x = percentToPixel(x, 'x'); - } - if (y) { - output.y = percentToPixel(y, 'y'); - } - if (width) { - output.width = width; - } - if (height) { - output.height = height; - } - - return output; - } - function convertToPercent(bounding: Partial) { - const { x, y, height, width } = bounding; - const output: Partial = {}; - - if (x) { - output.x = pixelToPercent(x, 'x'); - } - if (y) { - output.y = pixelToPercent(y, 'y'); - } - if (width) { - output.width = width; - } - if (height) { - output.height = height; - } - - return output; - } - function handleRemove() { dispatch({ type: 'TOGGLE_RANGES_VIEW_PROPERTY', @@ -246,12 +307,19 @@ function DraggablePublicationString(props: DraggablePublicationStringProps) { const actionButtons: ActionsButtonsPopoverProps['buttons'] = [ { icon: , - intent: 'none', title: 'Move publication string', style: { cursor: 'move' }, className: 'handle', }, + { + icon: , + intent: 'primary', + title: 'Configure publication string', + onClick: () => { + setIsDialogOpen(true); + }, + }, { icon: , intent: 'danger', @@ -261,10 +329,7 @@ function DraggablePublicationString(props: DraggablePublicationStringProps) { ]; if (!viewerRef || !value) return null; - const { width, height, x: xInPercent, y: yInPercent } = bounding; - - const x = percentToPixel(xInPercent, 'x'); - const y = percentToPixel(yInPercent, 'y'); + const { width, height, x = 0, y = 0 } = bounding; if (isExportProcessStart) { return ( @@ -279,51 +344,71 @@ function DraggablePublicationString(props: DraggablePublicationStringProps) { } return ( - setIsMoveActive(true)} - onResize={(e, dir, eRef, size, position) => - handleResize({ ...size, ...position }) - } - onResizeStop={(e, dir, eRef, size, position) => - handleChangeInsetBounding({ ...size, ...position }) - } - onDrag={(e, { x, y }) => { - handleDrag({ x, y }); - }} - onDragStop={(e, { x, y }) => { - handleChangeInsetBounding({ x, y }); - setIsMoveActive(false); - }} - resizeHandleWrapperStyle={{ backgroundColor: 'white' }} - > - + { + handleResize({ + ...position, + height: eRef.clientHeight, + width: eRef.clientWidth, + }); + }} + onResizeStop={(e, dir, eRef, size, position) => { + handleChangeInsetBounding({ + ...position, + height: eRef.clientHeight, + width: eRef.clientWidth, + }); + }} + onDragStart={() => setIsMoveActive(true)} + onDrag={(e, data) => { + handleDrag({ x: data.x, y: data.y }); + }} + onDragStop={(e, data) => { + handleChangeInsetBounding({ x: data.x, y: data.y }); + setIsMoveActive(false); + }} + resizeHandleWrapperStyle={{ backgroundColor: 'white' }} > - - - - - + + + + + + + setIsDialogOpen(false)} + acsExportOptions={acsOptions} + spectrum={spectrum} + allowTextStyle + /> + ); } @@ -333,30 +418,41 @@ export function FloatPublicationString() { data: spectra, view: { ranges }, } = useChartData(); - const options = useMemo(() => Object.entries(ranges), [ranges]); - const spectraToNucleusMap = useMemo(() => { - const map = new Map(); + const spectraMap = useMemo(() => { + const map = new Map(); for (const spectrum of spectra) { - if (!isSpectrum1D(spectrum)) continue; - const { nucleus } = spectrum.info; - map.set(spectrum.id, nucleus); + map.set(spectrum.id, spectrum); } return map; }, [spectra]); + const options = useMemo(() => { + return Object.entries(ranges).map(([spectrumKey, viewOptions]) => ({ + spectrum: spectraMap.get(spectrumKey), + viewOptions, + })); + }, [ranges, spectraMap]); + + return options.map((options) => { + const { viewOptions, spectrum } = options; - return options.map(([spectrumKey, viewOptions]) => { const { showPublicationString, publicationStringBounding } = viewOptions; if (!showPublicationString) return null; + if (!isSpectrum1D(spectrum)) return null; + + const { + id, + info: { nucleus }, + } = spectrum; return ( ); }); diff --git a/src/component/hooks/useSVGUnitConverter.ts b/src/component/hooks/useSVGUnitConverter.ts index b544683c18..faf9effa18 100644 --- a/src/component/hooks/useSVGUnitConverter.ts +++ b/src/component/hooks/useSVGUnitConverter.ts @@ -1,3 +1,5 @@ +import { useMemo } from 'react'; + import { useChartData } from '../context/ChartContext.js'; function truncate(value: any, numberOfDigits = 0) { @@ -15,14 +17,16 @@ export function convertPercentToPixel(value: number, baseValue: number) { export function useSVGUnitConverter() { const { width, height } = useChartData(); - function pixelToPercent(value: number, axis: 'x' | 'y') { - const size = axis === 'x' ? width : height; - return convertPixelToPercent(value, size); - } - function percentToPixel(value: number, axis: 'x' | 'y') { - const size = axis === 'x' ? width : height; - return convertPercentToPixel(value, size); - } + return useMemo(() => { + function pixelToPercent(value: number, axis: 'x' | 'y') { + const size = axis === 'x' ? width : height; + return convertPixelToPercent(value, size); + } + function percentToPixel(value: number, axis: 'x' | 'y') { + const size = axis === 'x' ? width : height; + return convertPercentToPixel(value, size); + } - return { pixelToPercent, percentToPixel }; + return { pixelToPercent, percentToPixel }; + }, [width, height]); } diff --git a/src/component/hooks/useTextMetrics.ts b/src/component/hooks/useTextMetrics.ts index 0fb43fed22..c5c77b513c 100644 --- a/src/component/hooks/useTextMetrics.ts +++ b/src/component/hooks/useTextMetrics.ts @@ -1,17 +1,15 @@ -import { useRef } from 'react'; - -function measureTextWidth( - context: CanvasRenderingContext2D | null, - text: string, -) { - if (!context) return 0; - return Math.round(context.measureText(text).width); -} +import { useEffect, useMemo } from 'react'; interface UseTextMetricsOptions { labelSize?: number; labelStyle?: string; labelWeight?: string; + + /** + * Dev purpose only. + * Set a width to see the canvas used to measure text. + */ + debugCanvasWidth?: number; } export function useTextMetrics(options: UseTextMetricsOptions = {}) { @@ -19,20 +17,59 @@ export function useTextMetrics(options: UseTextMetricsOptions = {}) { labelSize = 12, labelStyle = 'normal', labelWeight = 'normal', + debugCanvasWidth, } = options; - const contextRef = useRef(null); + const canvas = useCanvas(debugCanvasWidth); - if (!contextRef.current) { - const canvas = document.createElement('canvas'); - contextRef.current = canvas.getContext('2d'); - if (contextRef.current) { - contextRef.current.font = `${labelStyle} ${labelWeight} ${labelSize}px Arial`; - } - } + const ctx = canvas.getContext('2d'); + if (ctx) ctx.font = `${labelStyle} ${labelWeight} ${labelSize}px Arial`; function getTextWidth(text: string): number { - return measureTextWidth(contextRef.current, text); + return measureTextWidth(ctx, text); } - return { getTextWidth }; + return { + getTextWidth, + /** + * @deprecated Use only for debug mode + */ + ctx, + }; +} + +function useCanvas(width: number | undefined) { + const canvas = useMemo(() => { + const canvas = document.createElement('canvas'); + if (width) { + canvas.width = width; + canvas.height = 450; + } + canvas.style.position = 'absolute'; + canvas.style.bottom = '0'; + canvas.style.left = '0'; + canvas.style.zIndex = '100'; + canvas.style.opacity = '50%'; + canvas.style.background = 'white'; + return canvas; + }, [width]); + + useEffect(() => { + if (!width) return; + + document.body.append(canvas); + + return () => { + canvas.remove(); + }; + }, [canvas, width]); + + return canvas; +} + +function measureTextWidth( + context: CanvasRenderingContext2D | null, + text: string, +) { + if (!context) return 0; + return Math.round(context.measureText(text).width); } diff --git a/src/component/modal/PublicationStringModal.tsx b/src/component/modal/PublicationStringModal.tsx index 3b8950a000..153cdf7b9b 100644 --- a/src/component/modal/PublicationStringModal.tsx +++ b/src/component/modal/PublicationStringModal.tsx @@ -1,8 +1,11 @@ -import { Button, DialogFooter } from '@blueprintjs/core'; +import { Button, DialogFooter, Tooltip } from '@blueprintjs/core'; import styled from '@emotion/styled'; -import type { ACSExportOptions, Spectrum1D } from '@zakodium/nmrium-core'; +import type { + ACSExportOptions, + Spectrum1D, + Spectrum, +} from '@zakodium/nmrium-core'; import type { FormEvent } from 'react'; -import { useMemo } from 'react'; import { FieldGroupSVGTextStyleFields, Form, @@ -13,12 +16,13 @@ import { import { z } from 'zod'; import { isSpectrum1D } from '../../data/data1d/Spectrum1D/index.js'; +import { ClipboardFallbackModal } from '../../utils/clipboard/clipboardComponents.tsx'; +import { useClipboard } from '../../utils/clipboard/clipboardHooks.ts'; import { usePreferences } from '../context/PreferencesContext.js'; +import { useToaster } from '../context/ToasterContext.tsx'; import { EmptyText } from '../elements/EmptyText.js'; import { StandardDialog } from '../elements/StandardDialog.tsx'; import { StyledDialogBody } from '../elements/StyledDialogBody.js'; -import useSpectrum from '../hooks/useSpectrum.js'; -import { useActiveACSSettings } from '../hooks/use_acs_settings.js'; import { buildPublicationString } from '../hooks/use_publication_strings.js'; const Body = styled.div` @@ -52,11 +56,11 @@ const validationSchema = z.object({ const exportOptions: Array> = [ { - label: 'Export all', + label: 'All', value: 'all', }, { - label: 'Export only signals', + label: 'Only signals', value: 'signal', }, ]; @@ -77,10 +81,17 @@ const exportFormats: Array> = [ interface InnerPublicationStringModalProps { onClose: () => void; - onCopyClick: (text: string) => void; - isPublicationStringShown: boolean; - togglePublicationStringVisibility: () => void; + acsExportOptions: ACSExportOptions; + spectrum: Spectrum; + publicationStringVisibility?: { + isShown: boolean; + toggle: () => void; + }; + + allowTextStyle?: boolean; + saveLabel?: string; + copyOnSave?: boolean; } interface PublicationStringModalProps extends InnerPublicationStringModalProps { @@ -98,44 +109,70 @@ export function PublicationStringModal(props: PublicationStringModalProps) { function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { const { onClose, - onCopyClick, - isPublicationStringShown, - togglePublicationStringVisibility, + acsExportOptions, + spectrum, + publicationStringVisibility, + allowTextStyle = false, + saveLabel = 'Apply', + copyOnSave = false, } = props; - const spectrum = useSpectrum(); const { dispatch } = usePreferences(); - const currentACSOptions = useActiveACSSettings(); - const defaultValues = useMemo(() => { - const values = validationSchema.encode({ - acs: currentACSOptions, - isPublicationStringShown, - }); + const toaster = useToaster(); + const { rawWriteWithType, shouldFallback, text, cleanShouldFallback } = + useClipboard(); + function sendToClipboard(value: string) { + void rawWriteWithType(value, 'text/html').then(() => + toaster.show({ message: 'Data copied to clipboard', intent: 'success' }), + ); + } - if (values.acs.textStyle.fontSize === undefined) { - values.acs.textStyle.fontSize = '12'; - } + const defaultValues = validationSchema.encode({ + acs: acsExportOptions, + isPublicationStringShown: publicationStringVisibility?.isShown ?? false, + }); + if (defaultValues.acs.textStyle.fontSize === undefined) { + defaultValues.acs.textStyle.fontSize = '12'; + } - return values; - }, [currentACSOptions, isPublicationStringShown]); const form = useForm({ defaultValues, validators: { onChange: validationSchema }, - onSubmit: ({ value }) => { + onSubmit: ({ value, formApi }) => { assert(spectrum && isSpectrum1D(spectrum)); const nucleus = spectrum.info.nucleus; const parsedValues = validationSchema.parse(value); - if (parsedValues.acs.textStyle.fontSize === 12) { + if ( + !formApi.state.fieldMeta['acs.textStyle.fontSize']?.isTouched && + parsedValues.acs.textStyle.fontSize === 12 + ) { parsedValues.acs.textStyle.fontSize = undefined; } + + // Apply dispatch({ type: 'CHANGE_EXPORT_ACS_SETTINGS', payload: { options: parsedValues.acs, nucleus }, }); - if (parsedValues.isPublicationStringShown !== isPublicationStringShown) { - togglePublicationStringVisibility(); + if ( + publicationStringVisibility && + parsedValues.isPublicationStringShown !== + publicationStringVisibility.isShown + ) { + publicationStringVisibility.toggle(); + } + + // Copy + if (copyOnSave) { + const publicationString = buildPublicationString({ + spectrum, + acs: parsedValues.acs, + }); + sendToClipboard(publicationString); } + + // Close onClose(); }, }); @@ -152,20 +189,20 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { {spectrum.info.nucleus} + } onClose={onClose} style={{ minWidth: 600 }} >
+ This configuration is at the nucleus level. - {(field) => ( - - )} + {(field) => } - {(field) => ( - - )} + {(field) => } {(field) => } @@ -177,12 +214,14 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { {(field) => } - + {allowTextStyle && ( + + )} s.values}> @@ -190,7 +229,7 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { )} @@ -199,31 +238,43 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { - Apply and close + {saveLabel} } > - - {(field) => ( - - )} - + {publicationStringVisibility && ( + + {(field) => ( + + )} + + )}
+ + ); } const CopyPreviewButton = styled(Button)` - float: right; margin-left: 5px; margin-bottom: 5px; `; +const HelpForm = styled.p` + margin: 8px; +`; + interface PublicationStringPreviewProps { spectrum: Spectrum1D; values: z.input; @@ -240,7 +291,13 @@ function PublicationStringPreview(props: PublicationStringPreviewProps) { return ( <> - onCopy(value)} icon="duplicate" /> + + onCopy(value)} icon="duplicate" /> + {/* eslint-disable-next-line react/no-danger */}
diff --git a/src/component/panels/RangesPanel/RangesHeader.tsx b/src/component/panels/RangesPanel/RangesHeader.tsx index 5d75bdbfc0..51a63d3dd7 100644 --- a/src/component/panels/RangesPanel/RangesHeader.tsx +++ b/src/component/panels/RangesPanel/RangesHeader.tsx @@ -10,30 +10,24 @@ import { FaChartBar, FaCopy, FaDownload, - FaEye, - FaEyeSlash, FaFileExport, FaSitemap, FaUnlink, } from 'react-icons/fa'; import { ImLink } from 'react-icons/im'; import { LuMessageSquareText } from 'react-icons/lu'; -import { assert, assertUnreachable } from 'react-science/ui'; -import { ClipboardFallbackModal } from '../../../utils/clipboard/clipboardComponents.js'; -import { useClipboard } from '../../../utils/clipboard/clipboardHooks.js'; import { useDispatch } from '../../context/DispatchContext.js'; -import { useToaster } from '../../context/ToasterContext.js'; import { useAlert } from '../../elements/Alert.js'; import type { ToolbarPopoverMenuDivider, ToolbarPopoverMenuItem, } from '../../elements/ToolbarPopoverItem.js'; import { ToolbarPopoverItem } from '../../elements/ToolbarPopoverItem.js'; -import { useActiveSpectrum } from '../../hooks/useActiveSpectrum.ts'; import { useActiveSpectrumRangesViewState } from '../../hooks/useActiveSpectrumRangesViewState.js'; import { useDialogToggle } from '../../hooks/useDialogToggle.js'; -import { usePublicationStrings } from '../../hooks/use_publication_strings.ts'; +import useSpectrum from '../../hooks/useSpectrum.ts'; +import { useActiveACSSettings } from '../../hooks/use_acs_settings.ts'; import { PublicationStringModal } from '../../modal/PublicationStringModal.js'; import ChangeSumModal from '../../modal/changeSum/ChangeSumModal.js'; import { booleanToString } from '../../utility/booleanToString.js'; @@ -93,7 +87,8 @@ function RangesHeader(props: RangesHeaderProps) { } = props; const dispatch = useDispatch(); const alert = useAlert(); - const toaster = useToaster(); + const currentACSOptions = useActiveACSSettings(); + const spectrum = useSpectrum(); const { openDialog, dialog, closeDialog } = useDialogToggle({ publicationStringModal: false, }); @@ -195,15 +190,6 @@ function RangesHeader(props: RangesHeaderProps) { }); } - const { rawWriteWithType, shouldFallback, cleanShouldFallback, text } = - useClipboard(); - - function saveToClipboardHandler(value: string) { - void rawWriteWithType(value, 'text/html').then(() => - toaster.show({ message: 'Data copied to clipboard', intent: 'success' }), - ); - } - function changeSumConstantFlagHandler() { dispatch({ type: 'CHANGE_RANGES_SUM_FLAG', @@ -224,15 +210,10 @@ function RangesHeader(props: RangesHeaderProps) { saveAs({ blob, name, extension: '.tsv' }); } - const publicationStrings = usePublicationStrings(); - const currentSpectra = useActiveSpectrum(); - function exportHandler(data?: ExportData) { switch (data?.id) { case 'publicationString': { - assert(currentSpectra?.id); - const publicationString = publicationStrings[currentSpectra?.id]; - saveToClipboardHandler(publicationString); + openDialog('publicationStringModal'); break; } case 'rangesToTSV': @@ -333,52 +314,14 @@ function RangesHeader(props: RangesHeaderProps) { active: showAssignmentsLabels, }, { - component: ( - - disabled={!hasRanges} - icon={} - tooltip="Publication string" - options={[ - { - menuItemType: 'divider', - key: 'divider-publication-string', - title: 'Publication string', - }, - { - icon: 'cog', - text: 'Configure', - data: { - id: 'publicationString', - }, - }, - { - icon: !showPublicationString ? : , - text: booleanToString(!showPublicationString, { - trueLabel: 'Show', - falseLabel: 'Hide', - }), - data: { - id: 'toggle', - }, - active: showPublicationString, - }, - ]} - onClick={(data) => { - assert(data?.id); - - switch (data?.id) { - case 'toggle': - handleShowPublicationString(); - break; - case 'publicationString': - openDialog('publicationStringModal'); - break; - default: - assertUnreachable(data?.id); - } - }} - /> - ), + disabled: !hasRanges, + icon: , + tooltip: booleanToString(!showPublicationString, { + trueLabel: 'Display publication string', + falseLabel: 'Hide publication string', + }), + onClick: handleShowPublicationString, + active: showPublicationString, }, { disabled: !hasRanges, @@ -393,10 +336,15 @@ function RangesHeader(props: RangesHeaderProps) {
- -
); }