UNPKG

@diplodoc/transform

Version:

A simple transformer of text in YFM (Yandex Flavored Markdown) to HTML

248 lines (196 loc) 7.28 kB
import type { Alignment, Coords, ElementRect, OffsetValues, Placement, Side, SideObject, } from './types'; import {DEFAULT_SIDE_OBJECT, OPPOSITE_SIDES} from './constant'; function isVerticalSide(side: Side): boolean { return side === 'top' || side === 'bottom'; } function createSideObject(value: Partial<SideObject> | number = 0): SideObject { if (typeof value === 'number') { return {top: value, bottom: value, left: value, right: value}; } return {...DEFAULT_SIDE_OBJECT, ...value}; } function parsePlacement(placement: Placement): {side: Side; alignment?: Alignment} { const [side, alignment] = placement.split('-') as [Side, Alignment | undefined]; return {side, alignment}; } function getOppositeSide(side: Side): Side { return OPPOSITE_SIDES[side]; } function flipPlacement(placement: Placement): Placement { const {side, alignment} = parsePlacement(placement); const opposite = getOppositeSide(side); return (alignment ? `${opposite}-${alignment}` : opposite) as Placement; } function getOverflow(tooltip: ElementRect, coords: Coords, viewport: ElementRect): SideObject { const rect = updateRect(tooltip, {top: coords.y, left: coords.x}); return detectOverflow(viewport, rect, 5); } function shouldFlip(overflow: SideObject, flippedOverflow: SideObject, side: Side): boolean { const opposite = getOppositeSide(side); return overflow[side] > 0 && flippedOverflow[opposite] < overflow[side]; } export function computePosition( reference: ElementRect, tooltip: ElementRect, viewport: ElementRect, placement: Placement, offset: OffsetValues, isRtl: boolean, flip = true, ): {coords: Coords; placement: Placement} { const coords = computeCoordsFromPlacement(reference, tooltip, offset, placement, isRtl); if (!flip) { return {coords, placement}; } const overflow = getOverflow(tooltip, coords, viewport); const {side} = parsePlacement(placement); if (overflow[side] <= 0) { return {coords, placement}; } const flipped = flipPlacement(placement); const flippedCoords = computeCoordsFromPlacement(reference, tooltip, offset, flipped, isRtl); const flippedOverflow = getOverflow(tooltip, flippedCoords, viewport); if (shouldFlip(overflow, flippedOverflow, side)) { return {coords: flippedCoords, placement: flipped}; } return {coords, placement}; } export function generateId() { const random = Math.random().toString(36).substring(2, 6); const now = Date.now().toString(36); return `${random}${now}`; } export function createRect( params: Pick<ElementRect, 'top' | 'left' | 'width' | 'height'>, ): ElementRect { return { ...params, right: params.left + params.width, bottom: params.top + params.height, }; } export function updateRect(rect: ElementRect, params: Partial<ElementRect>): ElementRect { return createRect({ top: params.top ?? rect.top, left: params.left ?? rect.left, width: params.width ?? rect.width, height: params.height ?? rect.height, }); } export function getViewportRect(): ElementRect { const {documentElement, body} = document; const scrollTop = window.scrollY || documentElement.scrollTop || body.scrollTop; const scrollLeft = window.scrollX || documentElement.scrollLeft || body.scrollLeft; const clientTop = documentElement.clientTop || body.clientTop || 0; const clientLeft = documentElement.clientLeft || body.clientLeft || 0; return createRect({ top: Math.round(scrollTop - clientTop), left: Math.round(scrollLeft - clientLeft), width: document.body.clientWidth, height: document.body.clientHeight, }); } export function getElementRect(element: HTMLElement): ElementRect { const viewport = getViewportRect(); const box = element.getBoundingClientRect(); return createRect({ top: Math.round(box.top + viewport.top), left: Math.round(box.left + viewport.left), width: box.width, height: box.height, }); } export function computeAxisOffset(offset: OffsetValues, side: Side, isRtl?: boolean): Coords { const {mainAxis = 0, crossAxis = 0} = offset; const isVertical = isVerticalSide(side); const mainDirection = side === 'top' || side === 'left' ? -1 : 1; const crossDirection = isRtl && isVertical ? -1 : 1; const mainOffset = mainAxis * mainDirection; const crossOffset = crossAxis * crossDirection; if (isVertical) { return {x: crossOffset, y: mainOffset}; } return {x: mainOffset, y: crossOffset}; } export function computeCoordsFromPlacement( reference: ElementRect, tooltip: ElementRect, offset: OffsetValues, placement: Placement, isRtl: boolean, ) { const {side, alignment} = parsePlacement(placement); const isVertical = isVerticalSide(side); const alignmentAxis = isVertical ? 'x' : 'y'; const alignLength = alignmentAxis === 'y' ? 'height' : 'width'; const centerX = reference.left + reference.width / 2 - tooltip.width / 2; const centerY = reference.top + reference.height / 2 - tooltip.height / 2; const alignmentOffset = reference[alignLength] / 2 - tooltip[alignLength] / 2; const coords: Coords = {x: reference.left, y: reference.top}; switch (side) { case 'top': { coords.x = centerX; coords.y = reference.top - tooltip.height; break; } case 'bottom': { coords.x = centerX; coords.y = reference.top + reference.height; break; } case 'right': { coords.x = reference.left + reference.width; coords.y = centerY; break; } case 'left': { coords.x = reference.left - tooltip.width; coords.y = centerY; break; } } switch (alignment) { case 'start': { coords[alignmentAxis] -= alignmentOffset * (isRtl && isVertical ? -1 : 1); break; } case 'end': { coords[alignmentAxis] += alignmentOffset * (isRtl && isVertical ? -1 : 1); break; } } const axisOffset = computeAxisOffset(offset, side, isRtl); coords.x += axisOffset.x; coords.y += axisOffset.y; return coords; } export function convertToRelativeToOffsetParentRect(rect: ElementRect, offsetParent: HTMLElement) { const offsetRect = getElementRect(offsetParent); return createRect({ top: rect.top - offsetRect.top + offsetParent.offsetTop, left: rect.left - offsetRect.left + offsetParent.offsetLeft, width: rect.width, height: rect.height, }); } export function detectOverflow( boundary: ElementRect, element: ElementRect, padding: Partial<SideObject> | number = 0, ): SideObject { const {top, bottom, left, right} = createSideObject(padding); return { top: boundary.top - element.top + top, bottom: element.bottom - boundary.bottom + bottom, left: boundary.left - element.left + left, right: element.right - boundary.right + right, }; }