보조지표 전체보기 후 복귀버튼 추가
This commit is contained in:
@@ -2526,6 +2526,26 @@ html.theme-blue {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 보조지표 단독 전체보기 — pane 우측 상단 복원 */
|
||||||
|
.indicator-pane-controls {
|
||||||
|
display: flex;
|
||||||
|
pointer-events: auto;
|
||||||
|
opacity: 0.88;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
.indicator-pane-controls:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.chart-right-toolbar-cluster--focus-restore {
|
||||||
|
right: 0;
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
|
.chart-right-toolbar-btn--restore.active {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: rgba(122, 162, 247, 0.65);
|
||||||
|
background: rgba(122, 162, 247, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── PaneLegend 드래그 핸들 ─────────────────────────────────────────── */
|
/* ── PaneLegend 드래그 핸들 ─────────────────────────────────────────── */
|
||||||
.pane-legend-item {
|
.pane-legend-item {
|
||||||
/* fixed position 은 인라인 style 로 지정되므로 여기선 공통 속성만 */
|
/* fixed position 은 인라인 style 로 지정되므로 여기선 공통 속성만 */
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|||||||
import type { IndicatorConfig } from '../types';
|
import type { IndicatorConfig } from '../types';
|
||||||
import { ChartManager } from '../utils/ChartManager';
|
import { ChartManager } from '../utils/ChartManager';
|
||||||
import { getPaneHostId } from '../utils/indicatorPaneMerge';
|
import { getPaneHostId } from '../utils/indicatorPaneMerge';
|
||||||
|
import { isIndicatorPaneFocused, resolveFocusedPaneLayoutId } from '../utils/indicatorPaneFocus';
|
||||||
import { buildPaneItems, resolvePaneItemLayout } from './paneLegendLayout';
|
import { buildPaneItems, resolvePaneItemLayout } from './paneLegendLayout';
|
||||||
import { PAPER_OVERLAY_MENU_TITLE } from '../utils/paperChartOverlayVisibility';
|
import { PAPER_OVERLAY_MENU_TITLE } from '../utils/paperChartOverlayVisibility';
|
||||||
|
|
||||||
@@ -154,8 +155,8 @@ function buildMenuItems(
|
|||||||
const itemHostId = getPaneHostId(item.id, indicators);
|
const itemHostId = getPaneHostId(item.id, indicators);
|
||||||
const isMergedPane = samePaneItems.length > 1;
|
const isMergedPane = samePaneItems.length > 1;
|
||||||
const showSplit = isMergedPane && item.id === itemHostId && !!opts.onSplit;
|
const showSplit = isMergedPane && item.id === itemHostId && !!opts.onSplit;
|
||||||
const isFocused = opts.focusedId === item.id;
|
const isFocused = isIndicatorPaneFocused(item.id, opts.focusedId, indicators);
|
||||||
const showExpand = !!(opts.onExpand || (isFocused && opts.onRestore));
|
const showExpand = !!(opts.onExpand || (opts.focusedId && opts.onRestore));
|
||||||
const done = opts.onDone ?? (() => {});
|
const done = opts.onDone ?? (() => {});
|
||||||
const cfg = indicators.find(ind => ind.id === item.id);
|
const cfg = indicators.find(ind => ind.id === item.id);
|
||||||
const isHidden = cfg?.hidden === true;
|
const isHidden = cfg?.hidden === true;
|
||||||
@@ -302,6 +303,17 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
|||||||
const showOverlayMenu = !!(candleOverlayToggles?.length && candlePane && onToggleCandleOverlay);
|
const showOverlayMenu = !!(candleOverlayToggles?.length && candlePane && onToggleCandleOverlay);
|
||||||
const overlayMenuOpen = openMenuId === CANDLE_OVERLAY_MENU_ID;
|
const overlayMenuOpen = openMenuId === CANDLE_OVERLAY_MENU_ID;
|
||||||
|
|
||||||
|
const focusedLayoutId = focusedId
|
||||||
|
? resolveFocusedPaneLayoutId(focusedId, indicators)
|
||||||
|
: null;
|
||||||
|
const focusedPaneLayout = focusedLayoutId
|
||||||
|
? (() => {
|
||||||
|
const item = paneItems.find(p => isIndicatorPaneFocused(p.id, focusedId, indicators));
|
||||||
|
if (item) return resolvePaneItemLayout(manager, item);
|
||||||
|
return manager.getIndicatorPaneScreenLayout(focusedLayoutId);
|
||||||
|
})()
|
||||||
|
: null;
|
||||||
|
|
||||||
const overlayMenuItems: MenuItemDef[] = showOverlayMenu
|
const overlayMenuItems: MenuItemDef[] = showOverlayMenu
|
||||||
? candleOverlayToggles!.map(item => ({
|
? candleOverlayToggles!.map(item => ({
|
||||||
key: item.key,
|
key: item.key,
|
||||||
@@ -320,6 +332,22 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
|||||||
className="chart-right-toolbar-inner"
|
className="chart-right-toolbar-inner"
|
||||||
style={{ height: Math.max(innerHeight, chartHeight) }}
|
style={{ height: Math.max(innerHeight, chartHeight) }}
|
||||||
>
|
>
|
||||||
|
{focusedId && onRestore && focusedPaneLayout && (
|
||||||
|
<div
|
||||||
|
className="chart-right-toolbar-cluster chart-right-toolbar-cluster--focus-restore"
|
||||||
|
style={{ top: focusedPaneLayout.topY + 4 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="tv-side-btn chart-right-toolbar-btn chart-right-toolbar-btn--restore active"
|
||||||
|
title="기본 보기로 복원 (모든 보조지표 표시)"
|
||||||
|
onClick={e => { e.stopPropagation(); onRestore(); }}
|
||||||
|
>
|
||||||
|
<IconRestore />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showOverlayMenu && (
|
{showOverlayMenu && (
|
||||||
<div
|
<div
|
||||||
className="chart-right-toolbar-cluster chart-right-toolbar-cluster--overlay"
|
className="chart-right-toolbar-cluster chart-right-toolbar-cluster--overlay"
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* 보조지표 단독 전체보기 모드 — 해당 pane 우측 상단 «기본 보기로 복원» 버튼
|
||||||
|
*/
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import type { IndicatorConfig } from '../types';
|
||||||
|
import { ChartManager } from '../utils/ChartManager';
|
||||||
|
import { resolveFocusedPaneLayoutId } from '../utils/indicatorPaneFocus';
|
||||||
|
|
||||||
|
const IconRestore = () => (
|
||||||
|
<svg width="13" height="13" viewBox="0 0 14 14" fill="none"
|
||||||
|
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="5,13 1,13 1,9"/>
|
||||||
|
<line x1="1" y1="13" x2="5.5" y2="8.5"/>
|
||||||
|
<polyline points="9,1 13,1 13,5"/>
|
||||||
|
<line x1="13" y1="1" x2="8.5" y2="5.5"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface IndicatorPaneControlsProps {
|
||||||
|
manager: ChartManager;
|
||||||
|
containerEl: HTMLElement;
|
||||||
|
indicators: IndicatorConfig[];
|
||||||
|
focusedId: string | null;
|
||||||
|
onRestore: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IndicatorPaneControls: React.FC<IndicatorPaneControlsProps> = ({
|
||||||
|
manager,
|
||||||
|
containerEl,
|
||||||
|
indicators,
|
||||||
|
focusedId,
|
||||||
|
onRestore,
|
||||||
|
}) => {
|
||||||
|
const [pos, setPos] = useState<{ left: number; top: number } | null>(null);
|
||||||
|
|
||||||
|
const updatePos = useCallback(() => {
|
||||||
|
if (!focusedId) {
|
||||||
|
setPos(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const layoutId = resolveFocusedPaneLayoutId(focusedId, indicators);
|
||||||
|
const lay = manager.getIndicatorPaneScreenLayout(layoutId);
|
||||||
|
if (!lay) {
|
||||||
|
setPos(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rect = containerEl.getBoundingClientRect();
|
||||||
|
setPos({
|
||||||
|
left: rect.left + rect.width - 40,
|
||||||
|
top: rect.top + lay.topY + 4,
|
||||||
|
});
|
||||||
|
}, [manager, containerEl, focusedId, indicators]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updatePos();
|
||||||
|
const ro = new ResizeObserver(() => updatePos());
|
||||||
|
ro.observe(containerEl);
|
||||||
|
const unsub = manager.subscribePaneLayout(() => updatePos());
|
||||||
|
const id = setInterval(updatePos, 400);
|
||||||
|
return () => {
|
||||||
|
ro.disconnect();
|
||||||
|
unsub();
|
||||||
|
clearInterval(id);
|
||||||
|
};
|
||||||
|
}, [containerEl, manager, updatePos, focusedId]);
|
||||||
|
|
||||||
|
if (!focusedId || !pos) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="indicator-pane-controls"
|
||||||
|
style={{ position: 'fixed', left: pos.left, top: pos.top, zIndex: 505 }}
|
||||||
|
onMouseDown={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="pane-btn pane-btn-expand active"
|
||||||
|
onClick={e => { e.stopPropagation(); onRestore(); }}
|
||||||
|
title="기본 보기로 복원 (모든 보조지표 표시)"
|
||||||
|
>
|
||||||
|
<IconRestore />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IndicatorPaneControls;
|
||||||
@@ -56,6 +56,7 @@ import ChartHoverToolbar from './ChartHoverToolbar';
|
|||||||
import ChartMagnifier from './ChartMagnifier';
|
import ChartMagnifier from './ChartMagnifier';
|
||||||
import ChartContextMenu from './ChartContextMenu';
|
import ChartContextMenu from './ChartContextMenu';
|
||||||
import CandlePaneControls from './CandlePaneControls';
|
import CandlePaneControls from './CandlePaneControls';
|
||||||
|
import IndicatorPaneControls from './IndicatorPaneControls';
|
||||||
import { getKoreanName } from '../utils/marketNameCache';
|
import { getKoreanName } from '../utils/marketNameCache';
|
||||||
import { DEFAULT_DISPLAY_TIMEZONE } from '../utils/timezone';
|
import { DEFAULT_DISPLAY_TIMEZONE } from '../utils/timezone';
|
||||||
import { chartHasStaleIndicators, classifyIndicatorChartChange, singleIndParamKey } from '../utils/indicatorChartSync';
|
import { chartHasStaleIndicators, classifyIndicatorChartChange, singleIndParamKey } from '../utils/indicatorChartSync';
|
||||||
@@ -1667,6 +1668,15 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
onRestore={() => setCandleOnlyMode(false)}
|
onRestore={() => setCandleOnlyMode(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{chartMgr && focusedIndicatorId && onRestoreIndicators && chartPaintReady && (
|
||||||
|
<IndicatorPaneControlsPortal
|
||||||
|
manager={chartMgr}
|
||||||
|
indicators={indicators}
|
||||||
|
getContainer={() => containerRef.current}
|
||||||
|
focusedId={focusedIndicatorId}
|
||||||
|
onRestore={onRestoreIndicators}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{chartMgr && !candleOnlyMode && showPaneLegend && chartPaintReady && (
|
{chartMgr && !candleOnlyMode && showPaneLegend && chartPaintReady && (
|
||||||
<PaneLegendPortal
|
<PaneLegendPortal
|
||||||
key={indKey(indicators)}
|
key={indKey(indicators)}
|
||||||
@@ -1723,6 +1733,33 @@ const CandlePaneTimeAxisPortal: React.FC<{
|
|||||||
return <CandlePaneTimeAxis manager={manager} containerEl={el} />;
|
return <CandlePaneTimeAxis manager={manager} containerEl={el} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const IndicatorPaneControlsPortal: React.FC<{
|
||||||
|
manager: ChartManager;
|
||||||
|
indicators: IndicatorConfig[];
|
||||||
|
getContainer: () => HTMLElement | null;
|
||||||
|
focusedId: string;
|
||||||
|
onRestore: () => void;
|
||||||
|
}> = ({ manager, indicators, getContainer, focusedId, onRestore }) => {
|
||||||
|
const [el, setEl] = useState<HTMLElement | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const c = getContainer();
|
||||||
|
if (c) { setEl(c); return; }
|
||||||
|
const tid = setTimeout(() => setEl(getContainer()), 100);
|
||||||
|
return () => clearTimeout(tid);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
if (!el) return null;
|
||||||
|
return (
|
||||||
|
<IndicatorPaneControls
|
||||||
|
manager={manager}
|
||||||
|
containerEl={el}
|
||||||
|
indicators={indicators}
|
||||||
|
focusedId={focusedId}
|
||||||
|
onRestore={onRestore}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const CandlePaneControlsPortal: React.FC<{
|
const CandlePaneControlsPortal: React.FC<{
|
||||||
manager: ChartManager;
|
manager: ChartManager;
|
||||||
getContainer: () => HTMLElement | null;
|
getContainer: () => HTMLElement | null;
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { IndicatorConfig } from '../types';
|
||||||
|
import { getPaneHostId } from './indicatorPaneMerge';
|
||||||
|
|
||||||
|
/** 단독 전체보기(focusedId) 대상 pane 항목인지 — 호스트·병합 멤버 id 모두 매칭 */
|
||||||
|
export function isIndicatorPaneFocused(
|
||||||
|
itemId: string,
|
||||||
|
focusedId: string | null | undefined,
|
||||||
|
indicators: IndicatorConfig[],
|
||||||
|
): boolean {
|
||||||
|
if (!focusedId) return false;
|
||||||
|
if (itemId === focusedId) return true;
|
||||||
|
|
||||||
|
const itemCfg = indicators.find(i => i.id === itemId);
|
||||||
|
const focusCfg = indicators.find(i => i.id === focusedId);
|
||||||
|
if (itemCfg?.mergedWith === focusedId) return true;
|
||||||
|
if (focusCfg?.mergedWith === itemId) return true;
|
||||||
|
if (focusCfg?.mergedWith && itemCfg?.mergedWith === focusCfg.mergedWith) return true;
|
||||||
|
|
||||||
|
const itemHost = getPaneHostId(itemId, indicators);
|
||||||
|
const focusHost = getPaneHostId(focusedId, indicators);
|
||||||
|
return itemHost === focusedId || itemHost === focusHost || itemId === focusHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** focusedId 에 해당하는 pane 레이아웃 조회용 id (병합 시 호스트 우선) */
|
||||||
|
export function resolveFocusedPaneLayoutId(
|
||||||
|
focusedId: string,
|
||||||
|
indicators: IndicatorConfig[],
|
||||||
|
): string {
|
||||||
|
const cfg = indicators.find(i => i.id === focusedId);
|
||||||
|
if (cfg?.mergedWith) return cfg.mergedWith;
|
||||||
|
return getPaneHostId(focusedId, indicators);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user