/* * This file is part of ORY Editor. * * ORY Editor is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ORY Editor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with ORY Editor. If not, see . * * @license LGPL-3.0 * @copyright 2016-2018 Aeneas Rekkas * @author Aeneas Rekkas * */ import deepEqual from 'deep-equal'; import { ComponetizedCell, ComponetizedRow } from '../../types/editable'; import { Room, Matrix, Vector, MatrixIndex, Callbacks } from '../../types/hover'; import logger from '../logger'; export type MatrixList = { [key: string]: Matrix }; export type CallbackList = { [key: number]: Function }; /** * NO (None): No drop zone. * * Corners are counted clockwise, beginning top left * C1 (Corner top left): Position decided by top left corner function * C2 (Corner top right): Position decided by top right corner function * C3 (Corner bottom right): Position decided by bottom right corner function * C4 (Corner bottom left): Position decided by bottom left corner function * * Above: * AH (Above here): above, same level * AA (Above of self or some ancestor): Above, compute active level using classification functions, e.g. log, sin, mx + t * * Below: * BH (Below here) * BA (Below of self or some ancestor) * * Left of: * LH (Left of here) * LA (Left of self or some ancestor) * * Right of: * RH (Right of here) * RA (Right of self or some ancestor) * * Inside / inline * IL (Inline left) * IR (Inline right) */ export const classes: { [key: string]: number } = { NO: 0, C1: 10, C2: 11, C3: 12, C4: 13, AH: 200, AA: 201, BH: 210, BA: 211, LH: 220, LA: 221, RH: 230, RA: 231, IL: 300, IR: 301, }; const c = classes; /** * A list of matrices that are used to define the callback function. * * @type {{6x6: *[], 10x10: *[], 10x10-no-inline: *[]}} */ export const defaultMatrices: MatrixList = { '6x6': [ [c.C1, c.AA, c.AA, c.AA, c.AA, c.C2], [c.LA, c.IL, c.AH, c.AH, c.IR, c.RA], [c.LA, c.LH, c.NO, c.NO, c.RH, c.RA], [c.LA, c.LH, c.NO, c.NO, c.RH, c.RA], [c.LA, c.C4, c.BH, c.BH, c.C3, c.RA], [c.C4, c.BA, c.BA, c.BA, c.BA, c.C3], ], '10x10': [ [c.C1, c.AA, c.AA, c.AA, c.AA, c.AA, c.AA, c.AA, c.AA, c.C2], [c.LA, c.IL, c.IL, c.IL, c.AH, c.AH, c.IR, c.IR, c.IR, c.RA], [c.LA, c.IL, c.IL, c.IL, c.AH, c.AH, c.IR, c.IR, c.IR, c.RA], [c.LA, c.IL, c.IL, c.IL, c.AH, c.AH, c.IR, c.IR, c.IR, c.RA], [c.LA, c.LH, c.LH, c.LH, c.C1, c.C2, c.RH, c.RH, c.RH, c.RA], [c.LA, c.LH, c.LH, c.LH, c.C4, c.C3, c.RH, c.RH, c.RH, c.RA], [c.LA, c.LH, c.LH, c.C4, c.BH, c.BH, c.C3, c.IR, c.RH, c.RA], [c.LA, c.LH, c.C4, c.BH, c.BH, c.BH, c.BH, c.C3, c.RH, c.RA], [c.LA, c.C4, c.BH, c.BH, c.BH, c.BH, c.BH, c.BH, c.C3, c.RA], [c.C4, c.BA, c.BA, c.BA, c.BA, c.BA, c.BA, c.BA, c.BA, c.C3], ], '10x10-no-inline': [ [c.C1, c.AA, c.AA, c.AA, c.AA, c.AA, c.AA, c.AA, c.AA, c.C2], [c.LA, c.C1, c.AH, c.AH, c.AH, c.AH, c.AH, c.AH, c.C2, c.RA], [c.LA, c.LH, c.C1, c.AH, c.AH, c.AH, c.AH, c.C2, c.RH, c.RA], [c.LA, c.LH, c.LH, c.C1, c.AH, c.AH, c.C2, c.RH, c.RH, c.RA], [c.LA, c.LH, c.LH, c.LH, c.C1, c.C2, c.RH, c.RH, c.RH, c.RA], [c.LA, c.LH, c.LH, c.LH, c.C4, c.C3, c.RH, c.RH, c.RH, c.RA], [c.LA, c.LH, c.LH, c.C4, c.BH, c.BH, c.C3, c.RH, c.RH, c.RA], [c.LA, c.LH, c.C4, c.BH, c.BH, c.BH, c.BH, c.C3, c.RH, c.RA], [c.LA, c.C4, c.BH, c.BH, c.BH, c.BH, c.BH, c.BH, c.C3, c.RA], [c.C4, c.BA, c.BA, c.BA, c.BA, c.BA, c.BA, c.BA, c.BA, c.C3], ], }; /** * Computes the average width and height for cells in a room. * * @param room * @param matrix * @returns {{x: number, y: number}} */ export const getRoomScale = ({ room, matrix, }: { room: Room; matrix: Matrix; }): Vector => { const rows = matrix.length; const cells = matrix[0].length; const scalingX = room.width / cells; const scalingY = room.height / rows; return { x: scalingX, y: scalingY, }; }; /** * Returns the index of the hover cell. * * @param mouse * @param scale */ export const getMouseHoverCell = ({ mouse, scale, }: { mouse: Vector; scale: Vector; }): MatrixIndex => ({ cell: Math.floor(mouse.x / scale.x), row: Math.floor(mouse.y / scale.y), }); /** * Used for caching. */ const last = { '10x10': null, '10x10-no-inline': null }; export const computeHover = ( drag: ComponetizedCell, hover: ComponetizedCell, actions: Callbacks, { room, mouse, matrix, callbacks, }: { room: Room; mouse: Vector; callbacks: CallbackList; matrix: Matrix; }, m: string // tslint:disable-next-line:no-any ): any => { const scale = getRoomScale({ room, matrix }); const hoverCell = getMouseHoverCell({ mouse, scale }); const rows = matrix.length; const cells = matrix[0].length; if (hoverCell.row >= rows) { hoverCell.row = rows - 1; } else if (hoverCell.row < 0) { hoverCell.row = 0; } if (hoverCell.cell >= cells) { hoverCell.cell = cells - 1; } else if (hoverCell.cell < 0) { hoverCell.cell = 0; } const cell = matrix[hoverCell.row][hoverCell.cell]; if (!callbacks[cell]) { logger.error('Matrix callback not found.', { room, mouse, matrix, scale, hoverCell, rows, cells, }); return; } const all = { item: drag.id, hover: hover.id, actions, ctx: { room, mouse, position: hoverCell, size: { rows, cells }, scale, }, }; if (deepEqual(all, last[m])) { return; } last[m] = all; return callbacks[cell](drag, hover, actions, { room, mouse, position: hoverCell, size: { rows, cells }, scale, }); }; /** * Return the mouse position relative to the cell. */ export const relativeMousePosition = ({ mouse, position, scale, }: { mouse: Vector; scale: Vector; position: MatrixIndex; }) => ({ x: Math.round(mouse.x - position.cell * scale.x), y: Math.round(mouse.y - position.row * scale.y), }); /** * Computes the drop level based on the mouse position and the cell width. */ export const computeLevel = ({ size, levels, position, }: { size: number; levels: number; position: number; }) => { if (size <= (levels + 1) * 2) { return Math.round(position / (size / levels)); } const spare = size - (levels + 1) * 2; const steps = [0]; let current = spare; for (let i = 0; i <= levels; i++) { steps.push(steps[i] + current / 2); current /= 2; if (position >= steps[i] + i * 2 && position < steps[i + 1] + (i + 1) * 2) { return i; } } return levels; }; /** * Computes the horizontal drop level based on the mouse position. * * @param mouse * @param position * @param hover * @param scale * @param level * @param inv returns the inverse drop level. Usually true for left and above drop level computation. * @returns number */ export const computeHorizontal = ( { mouse, position, hover, scale, level, }: { mouse: Vector; position: MatrixIndex; scale: Vector; level: number; hover: ComponetizedRow; }, inv: boolean = false ) => { const { node: { cells = [] }, } = hover; const x = relativeMousePosition({ mouse, position, scale }).x; let at = computeLevel({ size: scale.x, position: x, levels: level }); if (cells.length) { // Is row, always opt for lowest level return level; } // If the hovered element is an inline element, level 0 would be directly besides it which doesn't work. // Set it to 1 instead. if (hover.node.inline && at === 0) { at = 1; } return inv ? level - at : at; }; /** * Computes the vertical drop level based on the mouse position. * * @returns number */ export const computeVertical = ( { level, mouse, hover, position, scale, }: { level: number; mouse: Vector; hover: ComponetizedRow; position: MatrixIndex; scale: Vector; }, inv: boolean = false ) => { const { node: { cells = [] }, } = hover; const y = relativeMousePosition({ mouse, position, scale }).y; let at = computeLevel({ size: scale.y, position: y, levels: level }); if (cells.length) { // Is row, always opt for lowest level return level; } // If the hovered element is an inline element, level 0 would be directly besides it which doesn't work. // Set it to 1 instead. if (hover.node.inline && at === 0) { at = 1; } return inv ? level - at : at; }; const getDropLevel = (hover: ComponetizedCell) => (hover.node.inline ? 1 : 0); /** * A list of callbacks. */ export const defaultCallbacks: CallbackList = { [c.NO]: ( item: ComponetizedCell, hover: ComponetizedCell, { clear }: Callbacks ) => clear(item.id), /* corners */ [c.C1]: ( item: ComponetizedCell, hover: ComponetizedCell, { leftOf, above }: Callbacks, // tslint:disable-next-line:no-any ctx: any ) => { const mouse = relativeMousePosition(ctx); const level = getDropLevel(hover); if (mouse.x < mouse.y) { return leftOf(item.rawNode(), hover.rawNode(), level); } above(item.rawNode(), hover.rawNode(), level); }, [c.C2]: ( item: ComponetizedCell, hover: ComponetizedCell, { rightOf, above }: Callbacks, // tslint:disable-next-line:no-any ctx: any ) => { const mouse = relativeMousePosition(ctx); const level = getDropLevel(hover); if (mouse.x > mouse.y) { return rightOf(item.rawNode(), hover.rawNode(), level); } above(item.rawNode(), hover.rawNode(), level); }, [c.C3]: ( item: ComponetizedCell, hover: ComponetizedCell, { rightOf, below }: Callbacks, // tslint:disable-next-line:no-any ctx: any ) => { const mouse = relativeMousePosition(ctx); const level = getDropLevel(hover); if (mouse.x > mouse.y) { return rightOf(item.rawNode(), hover.rawNode(), level); } below(item.rawNode(), hover.rawNode(), level); }, [c.C4]: ( item: ComponetizedCell, hover: ComponetizedCell, { leftOf, below }: Callbacks, // tslint:disable-next-line:no-any ctx: any ) => { const mouse = relativeMousePosition(ctx); const level = getDropLevel(hover); if (mouse.x < mouse.y) { return leftOf(item.rawNode(), hover.rawNode(), level); } below(item.rawNode(), hover.rawNode(), level); }, /* heres */ [c.AH]: ( item: ComponetizedCell, hover: ComponetizedCell, { above }: Callbacks ) => { const level = getDropLevel(hover); above( item.rawNode(), { ...hover.rawNode(), }, level ); }, [c.BH]: ( item: ComponetizedCell, hover: ComponetizedCell, { below }: Callbacks ) => { const level = getDropLevel(hover); below( item.rawNode(), { ...hover.rawNode(), }, level ); }, [c.LH]: ( item: ComponetizedCell, hover: ComponetizedCell, { leftOf }: Callbacks ) => { const level = getDropLevel(hover); leftOf( item.rawNode(), { ...hover.rawNode(), }, level ); }, [c.RH]: ( item: ComponetizedCell, hover: ComponetizedCell, { rightOf }: Callbacks ) => { const level = getDropLevel(hover); rightOf( item.rawNode(), { ...hover.rawNode(), }, level ); }, /* ancestors */ [c.AA]: ( item: ComponetizedCell, hover: ComponetizedCell, { above }: Callbacks, ctx: Object ) => above( item.rawNode(), hover.rawNode(), computeVertical( { ...ctx, hover: hover, level: hover.node.levels.above, // tslint:disable-next-line:no-any } as any, true ) ), [c.BA]: ( item: ComponetizedCell, hover: ComponetizedCell, { below }: Callbacks, ctx: Object ) => below( item.rawNode(), hover.rawNode(), computeVertical({ ...ctx, hover, level: hover.node.levels.below, // tslint:disable-next-line:no-any } as any) ), [c.LA]: ( item: ComponetizedCell, hover: ComponetizedCell, { leftOf }: Callbacks, ctx: Object ) => leftOf( item.rawNode(), hover.rawNode(), computeHorizontal( { ...ctx, hover, level: hover.node.levels.left, // tslint:disable-next-line:no-any } as any, true ) ), [c.RA]: ( item: ComponetizedCell, hover: ComponetizedCell, { rightOf }: Callbacks, ctx: Object ) => rightOf( item.rawNode(), hover.rawNode(), computeHorizontal({ ...ctx, hover, level: hover.node.levels.right, // tslint:disable-next-line:no-any } as any) ), /* inline */ [c.IL]: ( item: ComponetizedCell, hover: ComponetizedCell, { inlineLeft, leftOf }: Callbacks ) => { const { node: { inline, hasInlineNeighbour }, } = hover; const { node: { content: { plugin: { isInlineable = false } = {} } = {} }, } = item; if (inline || !isInlineable) { return leftOf(item.rawNode(), hover.rawNode(), 2); } if (hasInlineNeighbour && hasInlineNeighbour !== item.id) { return leftOf(item.rawNode(), hover.rawNode(), 2); } if ( hasInlineNeighbour && hasInlineNeighbour === item.id && item.node.inline === 'left' ) { return leftOf(item.rawNode(), hover.rawNode(), 2); } inlineLeft(item.rawNode(), hover.rawNode()); }, [c.IR]: ( item: ComponetizedCell, hover: ComponetizedCell, { inlineRight, rightOf }: Callbacks ) => { const { node: { inline, hasInlineNeighbour }, } = hover; const { node: { content: { plugin: { isInlineable = false } = {} } = {} }, } = item; if (inline || !isInlineable) { return rightOf(item.rawNode(), hover.rawNode(), 2); } if (hasInlineNeighbour && hasInlineNeighbour !== item.id) { return rightOf(item.rawNode(), hover.rawNode(), 2); } if ( hasInlineNeighbour && hasInlineNeighbour === item.id && item.node.inline === 'right' ) { return rightOf(item.rawNode(), hover.rawNode(), 2); } inlineRight(item.rawNode(), hover.rawNode()); }, }; export type HoverServiceProps = { matrices?: MatrixList; callbacks?: CallbackList; }; /** * The HoverService uses callbacks and matrices to compute hover logic. * * @class HoverService */ export default class HoverService { callbacks: CallbackList = defaultCallbacks; matrices: MatrixList = defaultMatrices; constructor( { matrices, callbacks }: HoverServiceProps = {} as HoverServiceProps ) { this.matrices = matrices || this.matrices; this.callbacks = callbacks || this.callbacks; } hover( drag: ComponetizedCell, hover: ComponetizedCell, actions: Callbacks, { room, mouse, matrix: use = '10x10', }: { room: Room; mouse: Vector; matrix: string } ) { return computeHover( drag, hover, actions, { room, mouse, matrix: this.matrices[use], callbacks: this.callbacks, }, use ); } }