Commit 406b445e authored by Vasily Belolapotkov's avatar Vasily Belolapotkov

refactor: extract preview window into separate component

parent 94cc2964
import { applyElementConfig, makeHtmlElement, toPx } from '../utils/ui';
import { ChartComponentFactory } from './types';
import { makeHtmlElement, toPx } from '../utils/ui';
import { ChartComponentFactory, ISize } from './types';
import { renderPlots } from './plot';
import { makeTransparentImage } from './transparent-image';
import { makePreviewWindow } from './preview-window';
interface IChartPreviewProps {
width: number;
height: number;
}
interface IResizeWindowHandlers {
moveLeftEdge: (offsetX: number) => void;
moveRightEdge: (offsetX: number) => void;
moveResizeWindow: (offsetX: number) => void;
}
export const makeChartPreview: ChartComponentFactory<
IChartPreviewProps
> = function(chartStateController) {
export const makeChartPreview: ChartComponentFactory<ISize> = function(
chartStateController,
) {
const stateController = chartStateController;
let previewWidth = 0;
let previewHeight = 0;
......@@ -25,21 +14,21 @@ export const makeChartPreview: ChartComponentFactory<
render,
});
function render(container: HTMLElement, props: IChartPreviewProps): void {
previewWidth = props && props.width;
previewHeight = props && props.height;
function render(container: HTMLElement, props: ISize): void {
previewWidth = props.width;
previewHeight = props.height;
const previewContainer = makePreviewAreaContainer();
container.appendChild(previewContainer);
const plotSize = {
width: previewWidth,
height: previewHeight,
};
renderPlots(previewContainer, stateController, {
plotSize,
zoom: { x: false, y: false },
});
const zoom = { x: false, y: false };
renderPlots(previewContainer, stateController, { plotSize, zoom });
renderPreviewWindow(previewContainer);
container.appendChild(previewContainer);
}
function makePreviewAreaContainer(): HTMLElement {
......@@ -50,272 +39,10 @@ export const makeChartPreview: ChartComponentFactory<
}
function renderPreviewWindow(previewContainer: HTMLElement): void {
const previewWindow = makePreviewWindow();
previewContainer.appendChild(previewWindow);
}
function makePreviewWindow(): HTMLElement {
const previewWindow = makeHtmlElement('div', {
classList: ['preview-window'],
});
const rightOffset = 0;
const leftOffset = Math.round(previewWidth * 0.7);
const previewWindowWidth = previewWidth - leftOffset - rightOffset;
const xScale = previewWindowWidth / previewWidth;
stateController.changeXViewRange(xScale, getXOffset(leftOffset));
const leftShadow = makeHtmlElement('div', {
classList: ['shadow', 'left'],
style: {
width: toPx(leftOffset),
},
});
const rightShadow = makeHtmlElement('div', {
classList: ['shadow', 'right'],
style: { width: toPx(rightOffset) },
});
const resizeWindow = makeHtmlElement('div', {
classList: ['window'],
style: {
left: toPx(leftOffset),
right: toPx(rightOffset),
},
});
const resizeHandlers = makeResizeHandlers({
resizeWindow,
leftShadow,
rightShadow,
});
renderResizeEdges(resizeWindow, resizeHandlers);
previewWindow.appendChild(leftShadow);
previewWindow.appendChild(rightShadow);
previewWindow.appendChild(resizeWindow);
return previewWindow;
}
function makeResizeHandlers(elements: {
resizeWindow: HTMLElement;
leftShadow: HTMLElement;
rightShadow: HTMLElement;
}): IResizeWindowHandlers {
return {
moveLeftEdge(offsetX: number) {
const {
offsetLeft: resizeWindowLeft,
offsetWidth: resizeWindowWidth,
} = elements.resizeWindow;
const minWidth = 10;
if (offsetX + minWidth >= resizeWindowWidth) {
return;
}
const leftOffset = Math.max(resizeWindowLeft + offsetX, 0);
applyElementConfig(elements.leftShadow, {
style: { width: toPx(leftOffset) },
});
applyElementConfig(elements.resizeWindow, {
style: { left: toPx(leftOffset) },
});
const previewWindowWidth = elements.resizeWindow.offsetWidth;
const xScale = previewWindowWidth / previewWidth;
stateController.changeXViewRange(xScale, getXOffset(leftOffset));
},
moveRightEdge(offsetX: number) {
const totalWidth = elements.resizeWindow.parentElement.offsetWidth;
const {
offsetLeft: resizeWindowLeft,
offsetWidth: resizeWindowWidth,
} = elements.resizeWindow;
const offsetRight = totalWidth - (resizeWindowLeft + resizeWindowWidth);
const newRight = offsetRight - offsetX;
if (resizeWindowLeft + newRight >= totalWidth) {
return;
}
const rightOffset = Math.max(newRight, 0);
applyElementConfig(elements.rightShadow, {
style: { width: toPx(rightOffset) },
});
applyElementConfig(elements.resizeWindow, {
style: { right: toPx(rightOffset) },
});
const xScale = resizeWindowWidth / previewWidth;
stateController.changeXViewRange(xScale);
},
moveResizeWindow(offsetX: number) {
const {
offsetLeft: resizeWindowLeft,
offsetWidth: resizeWindowWidth,
} = elements.resizeWindow;
const totalWidth = elements.resizeWindow.parentElement.offsetWidth;
let newLeft = Math.max(resizeWindowLeft + offsetX, 0);
let newRight = totalWidth - newLeft - resizeWindowWidth;
if (newRight < 0) {
newRight = 0;
newLeft = totalWidth - resizeWindowWidth;
}
applyElementConfig(elements.leftShadow, {
style: { width: toPx(newLeft) },
});
applyElementConfig(elements.resizeWindow, {
style: { left: toPx(newLeft) },
});
applyElementConfig(elements.rightShadow, {
style: { width: toPx(newRight) },
});
applyElementConfig(elements.resizeWindow, {
style: { right: toPx(newRight) },
});
stateController.changeXViewRange(undefined, getXOffset(newLeft));
},
};
}
function renderResizeEdges(
resizeWindowElement: HTMLElement,
resizeHandlers: IResizeWindowHandlers,
): void {
const dragAttributes = {
draggable: 'true',
};
const edgesContainer = makeHtmlElement('div', {
classList: ['edges-container'],
attributes: dragAttributes,
});
edgesContainer.addEventListener('touchmove', (evt: TouchEvent) => {
if (evt.touches.length !== 1) {
return;
}
const { clientX } = evt.touches[0];
const { left, width } = (<HTMLElement>(
evt.currentTarget
)).getBoundingClientRect();
const windowOffsetX = clientX - left - width / 2;
resizeHandlers.moveResizeWindow(windowOffsetX);
});
edgesContainer.addEventListener('dragstart', evt => {
setGhostImage(evt);
edgesContainer.classList.add('dragging');
});
edgesContainer.addEventListener('dragend', () => {
edgesContainer.classList.remove('dragging');
});
edgesContainer.addEventListener('drag', evt => {
if (evt.target !== edgesContainer) {
return;
}
const { offsetX, pageX, clientX } = evt;
if (pageX === 0 || clientX === 0) {
return;
}
const { offsetWidth: resizeWindowWidth } = resizeWindowElement;
const windowOffsetX = offsetX - resizeWindowWidth / 2;
resizeHandlers.moveResizeWindow(windowOffsetX);
});
const leftEdge = makeHtmlElement('div', {
classList: ['edge', 'left'],
attributes: dragAttributes,
});
const rightEdge = makeHtmlElement('div', {
classList: ['edge', 'right'],
attributes: dragAttributes,
});
leftEdge.addEventListener('dragstart', setGhostImage);
leftEdge.addEventListener('drag', evt => {
evt.preventDefault();
const { offsetX, pageX, clientX } = evt;
if (pageX === 0 || clientX === 0) {
return;
}
resizeHandlers.moveLeftEdge(offsetX);
});
leftEdge.addEventListener('touchmove', (evt: TouchEvent) => {
evt.preventDefault();
evt.stopPropagation();
if (evt.touches.length !== 1) {
return;
}
const { clientX } = evt.touches[0];
const { left } = edgesContainer.getBoundingClientRect();
resizeHandlers.moveLeftEdge(clientX - left);
});
rightEdge.addEventListener('dragstart', setGhostImage);
rightEdge.addEventListener('drag', evt => {
evt.preventDefault();
const { offsetX, pageX, clientX } = evt;
if (pageX === 0 || clientX === 0) {
return;
}
resizeHandlers.moveRightEdge(offsetX);
});
rightEdge.addEventListener('touchmove', (evt: TouchEvent) => {
console.log('rightEdgeMove');
evt.preventDefault();
evt.stopPropagation();
if (evt.touches.length !== 1) {
return;
}
const { clientX } = evt.touches[0];
const { left, width } = edgesContainer.getBoundingClientRect();
resizeHandlers.moveRightEdge(clientX - left - width);
const previewWindow = makePreviewWindow(stateController);
previewWindow.render(previewContainer, {
width: previewWidth,
height: previewHeight,
});
edgesContainer.appendChild(leftEdge);
edgesContainer.appendChild(rightEdge);
resizeWindowElement.appendChild(edgesContainer);
}
function getXOffset(leftOffset: number): number {
return leftOffset / previewWidth;
}
function setGhostImage(evt: DragEvent) {
evt.dataTransfer.setDragImage(makeTransparentImage(), 0, 0);
}
};
import { ChartComponentFactory, ISize } from './types';
import { applyElementConfig, makeHtmlElement, toPx } from '../utils/ui';
import { makeTransparentImage } from './transparent-image';
type EdgeTouchEventHandler = (args: {
clientX: number;
left: number;
width: number;
}) => void;
type EdgeDragEventHandler = (offsetX: number) => void;
export const makePreviewWindow: ChartComponentFactory<ISize> = function(
chartStateController,
) {
const stateController = chartStateController;
let width: number;
let height: number;
let previewWindow: HTMLElement;
let leftShadow: HTMLElement;
let rightShadow: HTMLElement;
let resizeWindow: HTMLElement;
let edgesContainer: HTMLElement;
let leftEdge: HTMLElement;
let rightEdge: HTMLElement;
return Object.freeze({
render,
});
function render(container: HTMLElement, size: ISize) {
initProps(size);
renderContainer(container);
renderResizeWindow();
attachEventHandlers();
}
function initProps(size: ISize) {
width = size.width;
height = size.height;
}
function renderContainer(container: HTMLElement) {
previewWindow = makeHtmlElement('div', {
classList: ['preview-window'],
});
container.appendChild(previewWindow);
}
function renderResizeWindow() {
const { left, right } = getResizeWindowEdgesCoords();
resizeWindow = makeHtmlElement('div', {
classList: ['window'],
style: {
left: toPx(left),
right: toPx(right),
},
});
previewWindow.appendChild(resizeWindow);
renderShadows(left, right);
renderResizeEdges();
}
function getResizeWindowEdgesCoords() {
const { xScale, xOffset } = stateController.getState();
const left = Math.round(width * xOffset);
const previewWindowWidth = Math.round(width * xScale);
const right = Math.max(width - left - previewWindowWidth, 0);
return { left, right };
}
function renderShadows(left: number, right: number) {
leftShadow = makeHtmlElement('div', {
classList: ['shadow', 'left'],
style: { width: toPx(left) },
});
rightShadow = makeHtmlElement('div', {
classList: ['shadow', 'right'],
style: { width: toPx(right) },
});
previewWindow.appendChild(leftShadow);
previewWindow.appendChild(rightShadow);
}
function attachEventHandlers() {
attachEdgesContainerHandlers();
attachLeftEdgeHandlers();
attachRightEdgeHandlers();
}
function attachEdgesContainerHandlers() {
edgesContainer.addEventListener(
'touchmove',
makeEdgeTouchEventHandler(({ clientX, left, width }) => {
const windowOffsetX = clientX - left - width / 2;
moveResizeWindow(windowOffsetX);
}),
);
edgesContainer.addEventListener('dragstart', evt => {
setGhostImage(evt);
edgesContainer.classList.add('dragging');
});
edgesContainer.addEventListener('dragend', () => {
edgesContainer.classList.remove('dragging');
});
edgesContainer.addEventListener('drag', evt => {
if (evt.target !== edgesContainer) {
return;
}
const { offsetX, pageX, clientX } = evt;
if (pageX === 0 || clientX === 0) {
return;
}
const { offsetWidth: resizeWindowWidth } = resizeWindow;
const windowOffsetX = offsetX - resizeWindowWidth / 2;
moveResizeWindow(windowOffsetX);
});
}
function attachLeftEdgeHandlers() {
leftEdge.addEventListener('dragstart', setGhostImage);
leftEdge.addEventListener('drag', makeEdgeDragHandler(moveLeftEdge));
leftEdge.addEventListener(
'touchmove',
makeEdgeTouchEventHandler(({ clientX, left }) =>
moveLeftEdge(clientX - left),
),
);
}
function attachRightEdgeHandlers() {
rightEdge.addEventListener('dragstart', setGhostImage);
rightEdge.addEventListener('drag', makeEdgeDragHandler(moveRightEdge));
rightEdge.addEventListener(
'touchmove',
makeEdgeTouchEventHandler(({ clientX, left, width }) =>
moveRightEdge(clientX - left - width),
),
);
}
function makeEdgeDragHandler(
handler: EdgeDragEventHandler,
): EventHandlerNonNull {
return (evt: DragEvent) => {
evt.preventDefault();
const { offsetX, pageX, clientX } = evt;
if (pageX === 0 || clientX === 0) {
return;
}
handler(offsetX);
};
}
function makeEdgeTouchEventHandler(
handler: EdgeTouchEventHandler,
): EventHandlerNonNull {
return (evt: TouchEvent) => {
evt.preventDefault();
evt.stopPropagation();
if (evt.touches.length !== 1) {
return;
}
const { clientX } = evt.touches[0];
const { left, width } = edgesContainer.getBoundingClientRect();
handler({ clientX, left, width });
};
}
function moveLeftEdge(offsetX: number) {
const {
offsetLeft: resizeWindowLeft,
offsetWidth: resizeWindowWidth,
} = resizeWindow;
const minWidth = 10;
if (offsetX + minWidth >= resizeWindowWidth) {
return;
}
const leftOffset = Math.max(resizeWindowLeft + offsetX, 0);
applyElementConfig(leftShadow, {
style: { width: toPx(leftOffset) },
});
applyElementConfig(resizeWindow, {
style: { left: toPx(leftOffset) },
});
const previewWindowWidth = resizeWindow.offsetWidth;
const xScale = previewWindowWidth / width;
stateController.changeXViewRange(xScale, getXOffset(leftOffset));
}
function moveRightEdge(offsetX: number) {
const totalWidth = resizeWindow.parentElement.offsetWidth;
const {
offsetLeft: resizeWindowLeft,
offsetWidth: resizeWindowWidth,
} = resizeWindow;
const offsetRight = totalWidth - (resizeWindowLeft + resizeWindowWidth);
const newRight = offsetRight - offsetX;
if (resizeWindowLeft + newRight >= totalWidth) {
return;
}
const rightOffset = Math.max(newRight, 0);
applyElementConfig(rightShadow, {
style: { width: toPx(rightOffset) },
});
applyElementConfig(resizeWindow, {
style: { right: toPx(rightOffset) },
});
const xScale = resizeWindowWidth / width;
stateController.changeXViewRange(xScale);
}
function moveResizeWindow(offsetX: number) {
const {
offsetLeft: resizeWindowLeft,
offsetWidth: resizeWindowWidth,
} = resizeWindow;
const totalWidth = resizeWindow.parentElement.offsetWidth;
let newLeft = Math.max(resizeWindowLeft + offsetX, 0);
let newRight = totalWidth - newLeft - resizeWindowWidth;
if (newRight < 0) {
newRight = 0;
newLeft = totalWidth - resizeWindowWidth;
}
applyElementConfig(leftShadow, {
style: { width: toPx(newLeft) },
});
applyElementConfig(resizeWindow, {
style: { left: toPx(newLeft) },
});
applyElementConfig(rightShadow, {
style: { width: toPx(newRight) },
});