Working tooltips!
This commit is contained in:
@@ -24,6 +24,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<div id="tooltip-root"></div>
|
||||||
<!--
|
<!--
|
||||||
This HTML file is a template.
|
This HTML file is a template.
|
||||||
If you open it directly in the browser, you will see an empty page.
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|||||||
@@ -64,4 +64,12 @@ footer {
|
|||||||
padding: 2em;
|
padding: 2em;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tooltip-root {
|
||||||
|
position: absolute;
|
||||||
|
z-index: $z-index-tooltips;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import { Link } from 'react-router-dom'
|
|||||||
import { classByName } from '../data/classes'
|
import { classByName } from '../data/classes'
|
||||||
import { Icon } from './Icon'
|
import { Icon } from './Icon'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import { Controller, Trigger, Tooltip } from './Tooltip';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** Name of the selected class, lowercase */
|
/** Name of the selected class, lowercase */
|
||||||
@@ -31,13 +32,18 @@ export class ClassPicker extends React.PureComponent<Props> {
|
|||||||
return (
|
return (
|
||||||
<ul className={cn}>
|
<ul className={cn}>
|
||||||
{Object.values(classByName).map((c) =>
|
{Object.values(classByName).map((c) =>
|
||||||
<li key={c.id} className={classNameForItem(c, selected)}>
|
<li key={c.id} className={classNameForItem(c, selected)}>
|
||||||
<Link to={`/${c.name.toLowerCase()}`} title={c.name}>
|
<Controller>
|
||||||
<Icon
|
<Trigger>
|
||||||
name={c.icon}
|
<Link to={`/${c.name.toLowerCase()}`}>
|
||||||
golden={selected === c.name.toLowerCase()}
|
<Icon
|
||||||
/>
|
name={c.icon}
|
||||||
</Link>
|
golden={selected === c.name.toLowerCase()}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</Trigger>
|
||||||
|
<Tooltip>{c.name}</Tooltip>
|
||||||
|
</Controller>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { FC, useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import './Icon.scss'
|
import './Icon.scss'
|
||||||
|
|
||||||
@@ -11,8 +11,8 @@ interface Props {
|
|||||||
|
|
||||||
const NOT_FOUND_ICON = 'inv_misc_questionmark'
|
const NOT_FOUND_ICON = 'inv_misc_questionmark'
|
||||||
|
|
||||||
export const Icon: FC<Props> = (props) => {
|
export const Icon = React.forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||||
const { name: defaultName, size = 'medium', golden = false, children } = props
|
const { name: defaultName, size = 'medium', golden = false, children, ...rest } = props
|
||||||
const [hasLoadedImage, setLoadedImage] = useState(false)
|
const [hasLoadedImage, setLoadedImage] = useState(false)
|
||||||
const [fadeIn, setFadeIn] = useState(false)
|
const [fadeIn, setFadeIn] = useState(false)
|
||||||
const [name, setName] = useState(defaultName)
|
const [name, setName] = useState(defaultName)
|
||||||
@@ -43,7 +43,7 @@ export const Icon: FC<Props> = (props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className} ref={ref} {...rest}>
|
||||||
{url &&
|
{url &&
|
||||||
<div className="icon__bg" style={{ backgroundImage: `url(${url})` }} />
|
<div className="icon__bg" style={{ backgroundImage: `url(${url})` }} />
|
||||||
}
|
}
|
||||||
@@ -51,4 +51,4 @@ export const Icon: FC<Props> = (props) => {
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
@@ -1,21 +1,23 @@
|
|||||||
import React, { FC } from 'react'
|
import React from 'react'
|
||||||
import { Tooltip } from './Tooltip'
|
import { Tooltip, Props as BaseProps } from './Tooltip'
|
||||||
import spells from '../data/spells.json'
|
import spells from '../data/spells.json'
|
||||||
|
|
||||||
interface Props {
|
interface Props extends BaseProps {
|
||||||
id: number
|
id: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpellTooltip: FC<Props> = ({ id }) => {
|
export class SpellTooltip extends React.PureComponent<Props> {
|
||||||
const spell: SpellData = spells[id.toString()]
|
render() {
|
||||||
if (!spell) {
|
const spell: SpellData = spells[this.props.id.toString()]
|
||||||
return <Tooltip fixed>Spell not found :(</Tooltip>
|
if (!spell) {
|
||||||
}
|
return <Tooltip fixed {...this.props}>Spell not found :(</Tooltip>
|
||||||
|
|
||||||
return <Tooltip fixed title={spell.name} icon={spell.icon}>
|
|
||||||
{spell.rank &&
|
|
||||||
<p className="tight">Rank {spell.rank}</p>
|
|
||||||
}
|
}
|
||||||
<p className="yellow">{spell.description}</p>
|
|
||||||
</Tooltip>
|
return <Tooltip fixed title={spell.name} icon={spell.icon} {...this.props}>
|
||||||
|
{spell.rank &&
|
||||||
|
<p className="tight">Rank {spell.rank}</p>
|
||||||
|
}
|
||||||
|
<p className="yellow">{spell.description}</p>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+59
-48
@@ -1,66 +1,77 @@
|
|||||||
import './Talent.scss'
|
import './Talent.scss'
|
||||||
import React, { FC } from 'react'
|
import React from 'react'
|
||||||
import { Icon } from './Icon'
|
import { Icon } from './Icon'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import { Controller, Trigger } from './Tooltip'
|
||||||
|
import { TalentTooltip } from './TalentTooltip'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
talent: TalentData
|
talent: TalentData
|
||||||
specId?: number
|
specId?: number
|
||||||
points?: number
|
points?: number
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
errors?: string
|
||||||
onClick?: (talentId: number) => void
|
onClick?: (talentId: number) => void
|
||||||
onRightClick?: (talentId: number) => void
|
onRightClick?: (talentId: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultProps: Partial<Props> = {
|
export class Talent extends React.PureComponent<Props> {
|
||||||
points: 0,
|
static whyDidYouRender = true
|
||||||
disabled: false,
|
|
||||||
onClick: () => undefined,
|
|
||||||
onRightClick: () => undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Talent: FC<Props> = (props) => {
|
static defaultProps = {
|
||||||
const { talent, points, disabled } = props
|
points: 0,
|
||||||
const showPoints = !disabled || points > 0
|
disabled: false,
|
||||||
|
onClick: () => undefined,
|
||||||
const containerClassNames = classNames('talent', {
|
onRightClick: () => undefined
|
||||||
'talent--disabled': disabled && points === 0,
|
|
||||||
'talent--available': !disabled && points < talent.ranks.length,
|
|
||||||
'talent--disabled-with-points': points >= talent.ranks.length || (points > 0 && disabled)
|
|
||||||
})
|
|
||||||
|
|
||||||
const pointsClassNames = classNames('point-label', {
|
|
||||||
'point-label--enabled': !disabled
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleContextMenu = (e) => {
|
|
||||||
if (props.onRightClick) props.onRightClick(talent.id)
|
|
||||||
e.preventDefault()
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
render() {
|
||||||
<div
|
const { talent, points, errors, disabled } = this.props
|
||||||
className={containerClassNames}
|
const showPoints = !disabled || points > 0
|
||||||
title={talent.ranks[0].toString()}
|
|
||||||
data-row={talent.row}
|
const containerClassNames = classNames('talent', {
|
||||||
data-col={talent.col}
|
'talent--disabled': disabled && points === 0,
|
||||||
onClick={!disabled ? () => props.onClick(talent.id) : () => {}}
|
'talent--available': !disabled && points < talent.ranks.length,
|
||||||
onContextMenu={handleContextMenu}
|
'talent--disabled-with-points': points >= talent.ranks.length || (points > 0 && disabled)
|
||||||
>
|
})
|
||||||
<div className="talent__status" />
|
|
||||||
<Icon name={talent.icon} size="medium" />
|
const pointsClassNames = classNames('point-label', {
|
||||||
|
'point-label--enabled': !disabled
|
||||||
{showPoints &&
|
})
|
||||||
<div className={pointsClassNames}>
|
|
||||||
{points}
|
const handleContextMenu = (e) => {
|
||||||
/{talent.ranks.length}
|
if (this.props.onRightClick) this.props.onRightClick(talent.id)
|
||||||
</div>
|
e.preventDefault()
|
||||||
}
|
return false
|
||||||
</div>
|
}
|
||||||
)
|
|
||||||
|
return (
|
||||||
|
<Controller>
|
||||||
|
<Trigger>
|
||||||
|
<div
|
||||||
|
className={containerClassNames}
|
||||||
|
data-row={talent.row}
|
||||||
|
data-col={talent.col}
|
||||||
|
onClick={!disabled ? () => this.props.onClick(talent.id) : () => {}}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
|
>
|
||||||
|
<div className="talent__status" />
|
||||||
|
<Icon name={talent.icon} size="medium" />
|
||||||
|
|
||||||
|
{showPoints &&
|
||||||
|
<div className={pointsClassNames}>
|
||||||
|
{points}/{talent.ranks.length}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Trigger>
|
||||||
|
<TalentTooltip talent={talent} points={points} errors={errors}>
|
||||||
|
{!disabled && points < talent.ranks.length && <p className="green tight">Click to learn</p>}
|
||||||
|
{points > 0 && <p className="green">Right-click to unlearn</p>}
|
||||||
|
</TalentTooltip>
|
||||||
|
</Controller>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Talent.defaultProps = defaultProps
|
// ;(Talent as any).whyDidYouRender = true
|
||||||
|
|
||||||
;(Talent as any).whyDidYouRender = true
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Props as BaseProps, Tooltip } from './Tooltip'
|
||||||
|
import spells from '../data/spells.json'
|
||||||
|
|
||||||
|
interface Props extends BaseProps {
|
||||||
|
talent: TalentData
|
||||||
|
points: number
|
||||||
|
errors?: string
|
||||||
|
currentSpellId?: number
|
||||||
|
nextSpellId?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TalentTooltip extends React.PureComponent<Props> {
|
||||||
|
render() {
|
||||||
|
const { talent, points, errors } = this.props
|
||||||
|
const currentSpellId = talent.ranks[Math.max(0, points - 1)]
|
||||||
|
const nextSpellId = points > 0 && points < talent.ranks.length && talent.ranks[Math.max(0, points)]
|
||||||
|
|
||||||
|
const currentSpell: SpellData = spells[currentSpellId.toString()]
|
||||||
|
const nextSpell: SpellData = spells[nextSpellId.toString()]
|
||||||
|
|
||||||
|
const title = (currentSpell || nextSpell).name
|
||||||
|
const rank = (currentSpell || nextSpell).rank
|
||||||
|
const description = (currentSpell || nextSpell).description
|
||||||
|
|
||||||
|
return <Tooltip fixed title={title} icon={false}>
|
||||||
|
{rank &&
|
||||||
|
<p className="tight">Rank {points}/{talent.ranks.length}</p>
|
||||||
|
}
|
||||||
|
{errors && errors.split('_').map((err, index) =>
|
||||||
|
<p key={index} className="tight" style={{ color: 'red' }}>{err}</p>
|
||||||
|
)}
|
||||||
|
<p className="yellow">{description}</p>
|
||||||
|
|
||||||
|
{nextSpell && <React.Fragment>
|
||||||
|
<p className="tight">Next rank:</p>
|
||||||
|
<p className="yellow">{nextSpell.description}</p>
|
||||||
|
</React.Fragment>}
|
||||||
|
|
||||||
|
{this.props.children}
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import './TalentTree.scss'
|
|||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { Map } from 'immutable'
|
import { Map } from 'immutable'
|
||||||
import { Talent } from './Talent';
|
import { Talent } from './Talent';
|
||||||
import { getPointsInSpec, canLearnTalent, SORT_TALENTS_DESC } from '../lib/tree';
|
import { getPointsInSpec, canLearnTalent, SORT_TALENTS_DESC, getUnmetRequirements } from '../lib/tree';
|
||||||
import { talentsBySpec, specNames, talentsById } from '../data/talents'
|
import { talentsBySpec, specNames, talentsById } from '../data/talents'
|
||||||
import { Arrow } from './Arrow'
|
import { Arrow } from './Arrow'
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ interface Props {
|
|||||||
|
|
||||||
export const TalentTree: React.FC<Props> = ({ specId, knownTalents, availablePoints, onTalentPress }) => {
|
export const TalentTree: React.FC<Props> = ({ specId, knownTalents, availablePoints, onTalentPress }) => {
|
||||||
const talents = Object.values(talentsBySpec[specId]).sort(SORT_TALENTS_DESC)
|
const talents = Object.values(talentsBySpec[specId]).sort(SORT_TALENTS_DESC)
|
||||||
|
const pointsInSpec = getPointsInSpec(specId, knownTalents)
|
||||||
|
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(talentId) => onTalentPress(specId, talentId, 1),
|
(talentId) => onTalentPress(specId, talentId, 1),
|
||||||
@@ -28,18 +29,20 @@ export const TalentTree: React.FC<Props> = ({ specId, knownTalents, availablePoi
|
|||||||
return (
|
return (
|
||||||
<div className="tree">
|
<div className="tree">
|
||||||
<div className="tree__header">
|
<div className="tree__header">
|
||||||
<h3>{specNames[specId]} ({getPointsInSpec(specId, knownTalents)})</h3>
|
<h3>{specNames[specId]} ({pointsInSpec})</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="tree__body" style={{ backgroundImage: `url(${require(`../images/specs/${specId}.jpg`)})` }}>
|
<div className="tree__body" style={{ backgroundImage: `url(${require(`../images/specs/${specId}.jpg`)})` }}>
|
||||||
{talents.map((talent) => {
|
{talents.map((talent) => {
|
||||||
const points = knownTalents.get(talent.id, 0)
|
const points = knownTalents.get(talent.id, 0)
|
||||||
const canLearn = canLearnTalent(knownTalents, talent)
|
const canLearn = canLearnTalent(knownTalents, talent)
|
||||||
|
const unmetRequirements = getUnmetRequirements(talent, knownTalents, pointsInSpec, specNames[specId])
|
||||||
|
|
||||||
return <React.Fragment key={talent.id}>
|
return <React.Fragment key={talent.id}>
|
||||||
<Talent
|
<Talent
|
||||||
talent={talent}
|
talent={talent}
|
||||||
points={points}
|
points={points}
|
||||||
|
errors={unmetRequirements}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onRightClick={handleRightClick}
|
onRightClick={handleRightClick}
|
||||||
disabled={availablePoints === 0 || !canLearn}
|
disabled={availablePoints === 0 || !canLearn}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import './Tooltip.scss'
|
|
||||||
import React, { FC } from 'react'
|
|
||||||
import classNames from 'classnames'
|
|
||||||
import { Icon } from './Icon'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title?: string
|
|
||||||
/** Override width of tooltip. Needs `fixed` to be true to have effect. */
|
|
||||||
width?: string
|
|
||||||
/** Fixed width */
|
|
||||||
fixed?: boolean
|
|
||||||
/** Display tooltip inline */
|
|
||||||
inline?: boolean
|
|
||||||
/** Icon to show next to tooltip */
|
|
||||||
icon?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Tooltip: FC<Props> = (props) => {
|
|
||||||
const { title, children } = props
|
|
||||||
|
|
||||||
const cn = classNames('tooltip', {
|
|
||||||
'tooltip--fixed': props.fixed,
|
|
||||||
'tooltip--inline': props.inline,
|
|
||||||
})
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
width: props.width
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={cn}>
|
|
||||||
{props.icon &&
|
|
||||||
<Icon className="tooltip__icon" name={props.icon} />
|
|
||||||
}
|
|
||||||
<div className="tooltip__inner" style={style}>
|
|
||||||
<div className="tooltip__top">
|
|
||||||
<div className="tooltip__body">
|
|
||||||
{title && <div className="tooltip__title tight">{title}</div>}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="tooltip__footer" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import { Map } from 'immutable'
|
||||||
|
|
||||||
|
const TOOLTIP_ROOT = document.getElementById('tooltip-root')
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
position?: TooltipPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Controller extends React.PureComponent<Props> {
|
||||||
|
static defaultProps = {
|
||||||
|
position: 'top-right'
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip = React.createRef<HTMLSpanElement>()
|
||||||
|
trigger = React.createRef<React.ReactInstance>()
|
||||||
|
|
||||||
|
state = {
|
||||||
|
isVisible: false,
|
||||||
|
tooltipDimensions: Map({
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
}),
|
||||||
|
style: Map({
|
||||||
|
position: 'absolute' as 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
}),
|
||||||
|
triggerRect: Map({
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseEnter = () => {
|
||||||
|
this.setState({
|
||||||
|
isVisible: true,
|
||||||
|
style: this.state.style.merge(this.getPosition())
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseLeave = () => {
|
||||||
|
this.setState({ isVisible: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (this.state.isVisible) {
|
||||||
|
const { width, height } = (ReactDOM.findDOMNode(this.tooltip.current) as HTMLElement).getBoundingClientRect()
|
||||||
|
const { style, tooltipDimensions } = this.state
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
tooltipDimensions: tooltipDimensions.merge({ width, height }),
|
||||||
|
style: style.merge(this.getPosition())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPosition = (): { top: number, left: number } => {
|
||||||
|
const { tooltipDimensions, triggerRect } = this.state
|
||||||
|
return calculatePosition(this.props.position, triggerRect, tooltipDimensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTriggerRect = (triggerRect: { left: number, top: number, width: number, height: number }) => {
|
||||||
|
this.setState({
|
||||||
|
triggerRect: this.state.triggerRect.merge(triggerRect)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { children } = this.props
|
||||||
|
const { isVisible, style } = this.state
|
||||||
|
|
||||||
|
return React.Children.map(children, (child: React.ReactElement) => {
|
||||||
|
const name = (child.type as any).name
|
||||||
|
if (name === 'Trigger') {
|
||||||
|
return React.cloneElement(child, {
|
||||||
|
ref: this.trigger,
|
||||||
|
resize: this.updateTriggerRect,
|
||||||
|
onMouseEnter: this.handleMouseEnter,
|
||||||
|
onMouseLeave: this.handleMouseLeave,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Tooltip
|
||||||
|
return isVisible && createPortal(
|
||||||
|
<span style={style.toJS()} ref={this.tooltip}>
|
||||||
|
{React.cloneElement(child)}
|
||||||
|
</span>,
|
||||||
|
TOOLTIP_ROOT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOOLTIP_BOUNDING_PADDING = 10
|
||||||
|
|
||||||
|
const calculatePosition = (pos: TooltipPosition, trigger: Map<string, number>, tooltip: Map<string, number>) => {
|
||||||
|
const { innerWidth: windowWidth } = window
|
||||||
|
let top = 0
|
||||||
|
let left = 0
|
||||||
|
|
||||||
|
const triggerTop = trigger.get('top') + window.pageYOffset
|
||||||
|
|
||||||
|
// Top
|
||||||
|
switch (pos) {
|
||||||
|
case 'top-left':
|
||||||
|
case 'top-right': {
|
||||||
|
top = triggerTop - (tooltip.get('height') + 5)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'bottom-left':
|
||||||
|
case 'bottom-right': {
|
||||||
|
top = triggerTop + trigger.get('height') + 5
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left
|
||||||
|
switch (pos) {
|
||||||
|
case 'left':
|
||||||
|
case 'bottom-left':
|
||||||
|
case 'top-left': {
|
||||||
|
left = (trigger.get('left')) - tooltip.get('width')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'right':
|
||||||
|
case 'bottom-right':
|
||||||
|
case 'top-right': {
|
||||||
|
left = (trigger.get('left')) + trigger.get('width')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const overflowsRight = left + tooltip.get('width') + TOOLTIP_BOUNDING_PADDING > windowWidth
|
||||||
|
const overflowsTop = top - (window.scrollY + TOOLTIP_BOUNDING_PADDING) < 0
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
switch (pos) {
|
||||||
|
case 'top-right': {
|
||||||
|
if (overflowsRight && !overflowsTop) return calculatePosition('top-left', trigger, tooltip)
|
||||||
|
if (!overflowsRight && overflowsTop) return calculatePosition('bottom-right', trigger, tooltip)
|
||||||
|
if (overflowsRight && overflowsTop) return calculatePosition('bottom-left', trigger, tooltip)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'top-left': {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { top, left }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
&:after {
|
&:after {
|
||||||
content: '';
|
content: '';
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
background: url('../images/tooltip-background.png');
|
background: url('../../images/tooltip-background.png');
|
||||||
background-position: top right;
|
background-position: top right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
&__body {
|
&__body {
|
||||||
min-height: 2px;
|
min-height: 2px;
|
||||||
padding: 8px 3px 2px 9px;
|
padding: 8px 3px 2px 9px;
|
||||||
background: url('../images/tooltip-background.png');
|
background: url('../../images/tooltip-background.png');
|
||||||
background-position: top left;
|
background-position: top left;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,14 +35,14 @@
|
|||||||
content: '';
|
content: '';
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
background: url('../images/tooltip-background.png');
|
background: url('../../images/tooltip-background.png');
|
||||||
background-position: bottom left;
|
background-position: bottom left;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: '';
|
content: '';
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
background: url('../images/tooltip-background.png');
|
background: url('../../images/tooltip-background.png');
|
||||||
background-position: bottom right;
|
background-position: bottom right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--fixed {
|
&--fixed {
|
||||||
|
|
||||||
.tooltip__inner {
|
.tooltip__inner {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import './Tooltip.scss'
|
||||||
|
import React from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import { Icon } from '../Icon'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
title?: string
|
||||||
|
/** Override width of tooltip. Needs `fixed` to be true to have effect. */
|
||||||
|
width?: string
|
||||||
|
/** Fixed width */
|
||||||
|
fixed?: boolean
|
||||||
|
/** Display tooltip inline */
|
||||||
|
inline?: boolean
|
||||||
|
/** Icon to show next to tooltip */
|
||||||
|
icon?: string | false
|
||||||
|
anchor?: HTMLElement
|
||||||
|
style?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Tooltip extends React.PureComponent<Props> {
|
||||||
|
static defaultProps = {
|
||||||
|
style: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { title, icon, children } = this.props
|
||||||
|
|
||||||
|
const cn = classNames('tooltip', {
|
||||||
|
'tooltip--fixed': this.props.fixed,
|
||||||
|
'tooltip--inline': this.props.inline,
|
||||||
|
})
|
||||||
|
|
||||||
|
const innerStyle = {
|
||||||
|
width: this.props.width
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
opacity: 1,
|
||||||
|
...this.props.style,
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={cn} style={style}>
|
||||||
|
{icon &&
|
||||||
|
<Icon className="tooltip__icon" name={icon} />
|
||||||
|
}
|
||||||
|
<div className="tooltip__inner" style={innerStyle}>
|
||||||
|
<div className="tooltip__top">
|
||||||
|
<div className="tooltip__body">
|
||||||
|
{title && <div className="tooltip__title tight">{title}</div>}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tooltip__footer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import { debounce } from '../../lib/helpers'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
resize?: (rect: { left: number, top: number, width: number, height: number }) => void
|
||||||
|
children: React.ReactElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Trigger extends React.PureComponent<Props> {
|
||||||
|
trigger = React.createRef<HTMLElement>()
|
||||||
|
|
||||||
|
get boundingRect() {
|
||||||
|
if (!this.trigger.current) {
|
||||||
|
throw new Error('Trigger does not have reference to itself')
|
||||||
|
}
|
||||||
|
const { width, height, top, left } = (ReactDOM.findDOMNode(this.trigger.current) as HTMLElement).getBoundingClientRect()
|
||||||
|
return { width, height, top, left }
|
||||||
|
}
|
||||||
|
|
||||||
|
resize = debounce(() => this.props.resize(this.boundingRect), 250)
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.resize()
|
||||||
|
window.addEventListener('scroll', this.resize)
|
||||||
|
window.addEventListener('resize', this.resize)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(){
|
||||||
|
window.removeEventListener('scroll', this.resize)
|
||||||
|
window.removeEventListener('resize', this.resize)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.resize()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { resize, children, ...props } = this.props
|
||||||
|
return React.cloneElement(children, {
|
||||||
|
...props,
|
||||||
|
ref: this.trigger
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './Tooltip'
|
||||||
|
export { Controller } from './Controller'
|
||||||
|
export { Trigger } from './Trigger'
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a function, that, as long as it continues to be invoked, will not
|
||||||
|
* be triggered. The function will be called after it stops being called for
|
||||||
|
* N milliseconds. If `immediate` is passed, trigger the function on the
|
||||||
|
* leading edge, instead of the trailing.
|
||||||
|
*
|
||||||
|
* @see https://davidwalsh.name/javascript-debounce-function
|
||||||
|
*/
|
||||||
|
export function debounce(func: (...args: any[]) => void, wait: number, immediate: boolean = false) {
|
||||||
|
let timeout = null
|
||||||
|
return function () {
|
||||||
|
const context = this, args = arguments
|
||||||
|
const later = function () {
|
||||||
|
timeout = null
|
||||||
|
if (!immediate) func.apply(context, args)
|
||||||
|
}
|
||||||
|
const callNow = immediate && !timeout
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(later, wait)
|
||||||
|
if (callNow) func.apply(context, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
-1
@@ -2,9 +2,11 @@ import { Map } from 'immutable'
|
|||||||
import {
|
import {
|
||||||
talentsBySpec,
|
talentsBySpec,
|
||||||
talentToSpec,
|
talentToSpec,
|
||||||
talentsBySpecArray
|
talentsBySpecArray,
|
||||||
|
talentsById
|
||||||
} from '../data/talents';
|
} from '../data/talents';
|
||||||
import { classByName } from '../data/classes'
|
import { classByName } from '../data/classes'
|
||||||
|
import spells from '../data/spells.json'
|
||||||
|
|
||||||
export const MAX_POINTS = 51
|
export const MAX_POINTS = 51
|
||||||
export const MAX_ROWS = 7
|
export const MAX_ROWS = 7
|
||||||
@@ -59,6 +61,31 @@ export function calcMeetsRequirements(talent: TalentData, known: Map<number, num
|
|||||||
}, true)
|
}, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNameForTalent(talentId: number): string {
|
||||||
|
const spell: SpellData = spells[talentsById[talentId].ranks[0]]
|
||||||
|
if (spell) return spell.name
|
||||||
|
return 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUnmetRequirements(talent: TalentData, known: Map<number, number>, pointsInSpec: number, specName: string): string {
|
||||||
|
const missing = []
|
||||||
|
const dependency = talent.requires[0]
|
||||||
|
if (dependency && known.get(dependency.id, 0) < dependency.qty) {
|
||||||
|
missing.push(
|
||||||
|
`Requires ${dependency.qty} point${dependency.qty !== 1 ? 's' : ''} in ${getNameForTalent(dependency.id)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (talent.row * 5 > pointsInSpec) {
|
||||||
|
missing.push(`Requires ${talent.row * 5} points in ${specName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hackfix: Returning an Array will cause the prop to change everytime, causing re-renders on all
|
||||||
|
// of the components. Returning a string makes the shallow compare easier and prevents re-renders.
|
||||||
|
// Could add Memoization to this function, but not sure of another "better" more "React" way..
|
||||||
|
return missing.join('_')
|
||||||
|
}
|
||||||
|
|
||||||
export const canLearnTalent = (known: Map<number, number>, talent: TalentData): boolean => {
|
export const canLearnTalent = (known: Map<number, number>, talent: TalentData): boolean => {
|
||||||
// Reached the max rank?
|
// Reached the max rank?
|
||||||
if (known.get(talent.id, 0) >= talent.ranks.length) {
|
if (known.get(talent.id, 0) >= talent.ranks.length) {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ $col-offset: 44px;
|
|||||||
$col-gutter: 16px;
|
$col-gutter: 16px;
|
||||||
$col-distance: $icon-size + $col-gutter;
|
$col-distance: $icon-size + $col-gutter;
|
||||||
|
|
||||||
|
$z-index-tooltips: 5;
|
||||||
|
|
||||||
// Item quality colours
|
// Item quality colours
|
||||||
$quality-0: #9d9d9d;
|
$quality-0: #9d9d9d;
|
||||||
$quality-1: #fff;
|
$quality-1: #fff;
|
||||||
|
|||||||
Vendored
+2
@@ -50,3 +50,5 @@ interface Talent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TalentClickHandler = (specId: number, talentId: number, modifier: 1 | -1) => void
|
type TalentClickHandler = (specId: number, talentId: number, modifier: 1 | -1) => void
|
||||||
|
|
||||||
|
type TooltipPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'left' | 'right'
|
||||||
Reference in New Issue
Block a user