Working tooltips!
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<div id="tooltip-root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
@@ -64,4 +64,12 @@ footer {
|
||||
padding: 2em;
|
||||
font-size: 12px;
|
||||
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 { Icon } from './Icon'
|
||||
import classNames from 'classnames'
|
||||
import { Controller, Trigger, Tooltip } from './Tooltip';
|
||||
|
||||
interface Props {
|
||||
/** Name of the selected class, lowercase */
|
||||
@@ -31,13 +32,18 @@ export class ClassPicker extends React.PureComponent<Props> {
|
||||
return (
|
||||
<ul className={cn}>
|
||||
{Object.values(classByName).map((c) =>
|
||||
<li key={c.id} className={classNameForItem(c, selected)}>
|
||||
<Link to={`/${c.name.toLowerCase()}`} title={c.name}>
|
||||
<Icon
|
||||
name={c.icon}
|
||||
golden={selected === c.name.toLowerCase()}
|
||||
/>
|
||||
</Link>
|
||||
<li key={c.id} className={classNameForItem(c, selected)}>
|
||||
<Controller>
|
||||
<Trigger>
|
||||
<Link to={`/${c.name.toLowerCase()}`}>
|
||||
<Icon
|
||||
name={c.icon}
|
||||
golden={selected === c.name.toLowerCase()}
|
||||
/>
|
||||
</Link>
|
||||
</Trigger>
|
||||
<Tooltip>{c.name}</Tooltip>
|
||||
</Controller>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import './Icon.scss'
|
||||
|
||||
@@ -11,8 +11,8 @@ interface Props {
|
||||
|
||||
const NOT_FOUND_ICON = 'inv_misc_questionmark'
|
||||
|
||||
export const Icon: FC<Props> = (props) => {
|
||||
const { name: defaultName, size = 'medium', golden = false, children } = props
|
||||
export const Icon = React.forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const { name: defaultName, size = 'medium', golden = false, children, ...rest } = props
|
||||
const [hasLoadedImage, setLoadedImage] = useState(false)
|
||||
const [fadeIn, setFadeIn] = useState(false)
|
||||
const [name, setName] = useState(defaultName)
|
||||
@@ -43,7 +43,7 @@ export const Icon: FC<Props> = (props) => {
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={className} ref={ref} {...rest}>
|
||||
{url &&
|
||||
<div className="icon__bg" style={{ backgroundImage: `url(${url})` }} />
|
||||
}
|
||||
@@ -51,4 +51,4 @@ export const Icon: FC<Props> = (props) => {
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,21 +1,23 @@
|
||||
import React, { FC } from 'react'
|
||||
import { Tooltip } from './Tooltip'
|
||||
import React from 'react'
|
||||
import { Tooltip, Props as BaseProps } from './Tooltip'
|
||||
import spells from '../data/spells.json'
|
||||
|
||||
interface Props {
|
||||
interface Props extends BaseProps {
|
||||
id: number
|
||||
}
|
||||
|
||||
export const SpellTooltip: FC<Props> = ({ id }) => {
|
||||
const spell: SpellData = spells[id.toString()]
|
||||
if (!spell) {
|
||||
return <Tooltip fixed>Spell not found :(</Tooltip>
|
||||
}
|
||||
|
||||
return <Tooltip fixed title={spell.name} icon={spell.icon}>
|
||||
{spell.rank &&
|
||||
<p className="tight">Rank {spell.rank}</p>
|
||||
export class SpellTooltip extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const spell: SpellData = spells[this.props.id.toString()]
|
||||
if (!spell) {
|
||||
return <Tooltip fixed {...this.props}>Spell not found :(</Tooltip>
|
||||
}
|
||||
<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 React, { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { Icon } from './Icon'
|
||||
import classNames from 'classnames'
|
||||
import { Controller, Trigger } from './Tooltip'
|
||||
import { TalentTooltip } from './TalentTooltip'
|
||||
|
||||
interface Props {
|
||||
talent: TalentData
|
||||
specId?: number
|
||||
points?: number
|
||||
disabled?: boolean
|
||||
errors?: string
|
||||
onClick?: (talentId: number) => void
|
||||
onRightClick?: (talentId: number) => void
|
||||
}
|
||||
|
||||
const defaultProps: Partial<Props> = {
|
||||
points: 0,
|
||||
disabled: false,
|
||||
onClick: () => undefined,
|
||||
onRightClick: () => undefined
|
||||
}
|
||||
export class Talent extends React.PureComponent<Props> {
|
||||
static whyDidYouRender = true
|
||||
|
||||
export const Talent: FC<Props> = (props) => {
|
||||
const { talent, points, disabled } = props
|
||||
const showPoints = !disabled || points > 0
|
||||
|
||||
const containerClassNames = classNames('talent', {
|
||||
'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
|
||||
static defaultProps = {
|
||||
points: 0,
|
||||
disabled: false,
|
||||
onClick: () => undefined,
|
||||
onRightClick: () => undefined
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={containerClassNames}
|
||||
title={talent.ranks[0].toString()}
|
||||
data-row={talent.row}
|
||||
data-col={talent.col}
|
||||
onClick={!disabled ? () => 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>
|
||||
)
|
||||
render() {
|
||||
const { talent, points, errors, disabled } = this.props
|
||||
const showPoints = !disabled || points > 0
|
||||
|
||||
const containerClassNames = classNames('talent', {
|
||||
'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 (this.props.onRightClick) this.props.onRightClick(talent.id)
|
||||
e.preventDefault()
|
||||
return false
|
||||
}
|
||||
|
||||
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 { Map } from 'immutable'
|
||||
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 { Arrow } from './Arrow'
|
||||
|
||||
@@ -15,6 +15,7 @@ interface Props {
|
||||
|
||||
export const TalentTree: React.FC<Props> = ({ specId, knownTalents, availablePoints, onTalentPress }) => {
|
||||
const talents = Object.values(talentsBySpec[specId]).sort(SORT_TALENTS_DESC)
|
||||
const pointsInSpec = getPointsInSpec(specId, knownTalents)
|
||||
|
||||
const handleClick = useCallback(
|
||||
(talentId) => onTalentPress(specId, talentId, 1),
|
||||
@@ -28,18 +29,20 @@ export const TalentTree: React.FC<Props> = ({ specId, knownTalents, availablePoi
|
||||
return (
|
||||
<div className="tree">
|
||||
<div className="tree__header">
|
||||
<h3>{specNames[specId]} ({getPointsInSpec(specId, knownTalents)})</h3>
|
||||
<h3>{specNames[specId]} ({pointsInSpec})</h3>
|
||||
</div>
|
||||
|
||||
<div className="tree__body" style={{ backgroundImage: `url(${require(`../images/specs/${specId}.jpg`)})` }}>
|
||||
{talents.map((talent) => {
|
||||
const points = knownTalents.get(talent.id, 0)
|
||||
const canLearn = canLearnTalent(knownTalents, talent)
|
||||
const unmetRequirements = getUnmetRequirements(talent, knownTalents, pointsInSpec, specNames[specId])
|
||||
|
||||
return <React.Fragment key={talent.id}>
|
||||
<Talent
|
||||
talent={talent}
|
||||
points={points}
|
||||
errors={unmetRequirements}
|
||||
onClick={handleClick}
|
||||
onRightClick={handleRightClick}
|
||||
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 {
|
||||
content: '';
|
||||
padding: 3px;
|
||||
background: url('../images/tooltip-background.png');
|
||||
background: url('../../images/tooltip-background.png');
|
||||
background-position: top right;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
&__body {
|
||||
min-height: 2px;
|
||||
padding: 8px 3px 2px 9px;
|
||||
background: url('../images/tooltip-background.png');
|
||||
background: url('../../images/tooltip-background.png');
|
||||
background-position: top left;
|
||||
}
|
||||
|
||||
@@ -35,14 +35,14 @@
|
||||
content: '';
|
||||
flex: 1;
|
||||
padding: 3px;
|
||||
background: url('../images/tooltip-background.png');
|
||||
background: url('../../images/tooltip-background.png');
|
||||
background-position: bottom left;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
padding: 3px;
|
||||
background: url('../images/tooltip-background.png');
|
||||
background: url('../../images/tooltip-background.png');
|
||||
background-position: bottom right;
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,6 @@
|
||||
}
|
||||
|
||||
&--fixed {
|
||||
|
||||
.tooltip__inner {
|
||||
width: 100%;
|
||||
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 {
|
||||
talentsBySpec,
|
||||
talentToSpec,
|
||||
talentsBySpecArray
|
||||
talentsBySpecArray,
|
||||
talentsById
|
||||
} from '../data/talents';
|
||||
import { classByName } from '../data/classes'
|
||||
import spells from '../data/spells.json'
|
||||
|
||||
export const MAX_POINTS = 51
|
||||
export const MAX_ROWS = 7
|
||||
@@ -59,6 +61,31 @@ export function calcMeetsRequirements(talent: TalentData, known: Map<number, num
|
||||
}, 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 => {
|
||||
// Reached the max rank?
|
||||
if (known.get(talent.id, 0) >= talent.ranks.length) {
|
||||
|
||||
@@ -7,6 +7,8 @@ $col-offset: 44px;
|
||||
$col-gutter: 16px;
|
||||
$col-distance: $icon-size + $col-gutter;
|
||||
|
||||
$z-index-tooltips: 5;
|
||||
|
||||
// Item quality colours
|
||||
$quality-0: #9d9d9d;
|
||||
$quality-1: #fff;
|
||||
|
||||
Vendored
+2
@@ -50,3 +50,5 @@ interface Talent {
|
||||
}
|
||||
|
||||
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