mirror of
https://github.com/EDCD/coriolis.git
synced 2025-12-10 15:15:34 +00:00
Continued porting to react, approaching beta
This commit is contained in:
@@ -2,19 +2,31 @@ import React from 'react';
|
||||
import Link from './Link';
|
||||
import cn from 'classnames';
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the current window location equals the link
|
||||
* @return {boolean} If matches
|
||||
*/
|
||||
function isActive(href) {
|
||||
return encodeURI(href) == (window.location.pathname + window.location.search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Active Link - Highlighted when URL matches window location
|
||||
*/
|
||||
export default class ActiveLink extends Link {
|
||||
|
||||
isActive = () => {
|
||||
return encodeURI(this.props.href) == (window.location.pathname + window.location.search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the component
|
||||
* @return {React.Component} The active link
|
||||
*/
|
||||
render() {
|
||||
let className = this.props.className;
|
||||
if (this.isActive()) {
|
||||
if (isActive(this.props.href)) {
|
||||
className = cn(className, 'active');
|
||||
}
|
||||
|
||||
return <a {...this.props} className={className} onClick={this.handler}>{this.props.children}</a>
|
||||
return <a {...this.props} className={className} onClick={this.handler.bind(this)}>{this.props.children}</a>;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,11 +4,15 @@ import TranslatedComponent from './TranslatedComponent';
|
||||
import cn from 'classnames';
|
||||
import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
|
||||
|
||||
/**
|
||||
* Available modules menu
|
||||
*/
|
||||
export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
modules: React.PropTypes.oneOfType([ React.PropTypes.object, React.PropTypes.array ]).isRequired,
|
||||
modules: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.array]).isRequired,
|
||||
onSelect: React.PropTypes.func.isRequired,
|
||||
diffDetails: React.PropTypes.func,
|
||||
m: React.PropTypes.object,
|
||||
shipMass: React.PropTypes.number,
|
||||
warning: React.PropTypes.func
|
||||
@@ -18,23 +22,83 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
shipMass: 0
|
||||
};
|
||||
|
||||
buildGroup(translate, mountedModule, warningFunc, mass, onSelect, grp, modules) {
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
this._hideDiff = this._hideDiff.bind(this);
|
||||
this.state = { list: this._initList(props, context) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate the list of available moduels
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
* @return {Array} Array of React Components
|
||||
*/
|
||||
_initList(props, context) {
|
||||
let translate = context.language.translate;
|
||||
let { m, warning, shipMass, onSelect, modules } = props;
|
||||
let list;
|
||||
let buildGroup = this._buildGroup.bind(
|
||||
this,
|
||||
translate,
|
||||
m,
|
||||
warning,
|
||||
shipMass - (m && m.mass ? m.mass : 0),
|
||||
(m) => {
|
||||
this._hideDiff();
|
||||
onSelect(m);
|
||||
}
|
||||
);
|
||||
|
||||
if (modules instanceof Array) {
|
||||
list = buildGroup(modules[0].grp, modules);
|
||||
} else {
|
||||
list = [];
|
||||
// At present time slots with grouped options (Hardpoints and Internal) can be empty
|
||||
list.push(<div className={'empty-c upp'} key={'empty'} onClick={onSelect.bind(null, null)} >{translate('empty')}</div>);
|
||||
for (let g in modules) {
|
||||
list.push(<div ref={g} key={g} className={'select-group cap'}>{translate(g)}</div>);
|
||||
list.push(buildGroup(g, modules[g]));
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate React Components for Module Group
|
||||
* @param {Function} translate Translate function
|
||||
* @param {Objecy} mountedModule Mounted Module
|
||||
* @param {Funciton} warningFunc Warning function
|
||||
* @param {number} mass Mass
|
||||
* @param {function} onSelect Select/Mount callback
|
||||
* @param {string} grp Group name
|
||||
* @param {Array} modules Available modules
|
||||
* @return {React.Component} Available Module Group contents
|
||||
*/
|
||||
_buildGroup(translate, mountedModule, warningFunc, mass, onSelect, grp, modules) {
|
||||
let prevClass = null, prevRating = null;
|
||||
let elems = [];
|
||||
|
||||
for (let i = 0; i < modules.length; i++) {
|
||||
let m = modules[i];
|
||||
let mount = null;
|
||||
let disabled = m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass;
|
||||
let classes = cn(m.name ? 'lc' : 'c', {
|
||||
active: mountedModule && mountedModule.id === m.id,
|
||||
warning: warningFunc && warningFunc(m),
|
||||
disabled: m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass
|
||||
warning: !disabled && warningFunc && warningFunc(m),
|
||||
disabled
|
||||
});
|
||||
|
||||
switch(m.mount) {
|
||||
case 'F': mount = <MountFixed className={'lg'} />; break;
|
||||
case 'G': mount = <MountGimballed className={'lg'}/>; break;
|
||||
case 'T': mount = <MountTurret className={'lg'}/>; break;
|
||||
case 'F': mount = <MountFixed className={'lg'} />; break;
|
||||
case 'G': mount = <MountGimballed className={'lg'}/>; break;
|
||||
case 'T': mount = <MountTurret className={'lg'}/>; break;
|
||||
}
|
||||
|
||||
if (i > 0 && modules.length > 3 && m.class != prevClass && (m.rating != prevRating || m.mount) && m.grp != 'pa') {
|
||||
@@ -42,7 +106,13 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
}
|
||||
|
||||
elems.push(
|
||||
<li key={m.id} className={classes} onClick={onSelect.bind(null, m)}>
|
||||
<li
|
||||
key={m.id}
|
||||
className={classes}
|
||||
onMouseOver={disabled ? null : this._showDiff.bind(this, mountedModule, m)}
|
||||
onMouseLeave={this._hideDiff}
|
||||
onClick={disabled ? null : onSelect.bind(null, m)}
|
||||
>
|
||||
{mount}
|
||||
<span>{(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')}</span>
|
||||
</li>
|
||||
@@ -54,43 +124,54 @@ export default class AvailableModulesMenu extends TranslatedComponent {
|
||||
return <ul key={'modules' + grp} >{elems}</ul>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate tooltip content for the difference between the
|
||||
* mounted module and the hovered modules
|
||||
* @param {Object} mm The module mounet currently
|
||||
* @param {Object} m The hovered module
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_showDiff(mm, m, event) {
|
||||
if (this.props.diffDetails) {
|
||||
this.context.tooltip(this.props.diffDetails(m, mm), event.currentTarget.getBoundingClientRect());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide diff tooltip
|
||||
*/
|
||||
_hideDiff() {
|
||||
this.context.tooltip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to mounted (if it exists) component on mount
|
||||
*/
|
||||
componentDidMount() {
|
||||
let m = this.props.m
|
||||
let m = this.props.m;
|
||||
|
||||
if (!(this.props.modules instanceof Array) && m && m.grp) {
|
||||
findDOMNode(this).scrollTop = this.refs[m.grp].offsetTop; // Scroll to currently selected group
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on property and context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
this.setState({ list: this._initList(nextProps, nextContext) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the list
|
||||
* @return {React.Component} List
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
let m = this.props.m;
|
||||
let modules = this.props.modules;
|
||||
let list;
|
||||
let buildGroup = this.buildGroup.bind(
|
||||
null,
|
||||
translate,
|
||||
m,
|
||||
this.props.warning,
|
||||
this.props.shipMass - (m && m.mass ? m.mass : 0),
|
||||
this.props.onSelect
|
||||
);
|
||||
|
||||
if (modules instanceof Array) {
|
||||
list = buildGroup(modules[0].grp, modules);
|
||||
} else {
|
||||
list = [];
|
||||
// At present time slots with grouped options (Hardpoints and Internal) can be empty
|
||||
list.push(<div className={'empty-c upp'} key={'empty'} onClick={this.props.onSelect.bind(null, null)} >{translate('empty')}</div>);
|
||||
for (let g in modules) {
|
||||
list.push(<div ref={g} key={g} className={'select-group cap'}>{translate(g)}</div>);
|
||||
list.push(buildGroup(g, modules[g]));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('select', this.props.className)} onClick={(e) => e.stopPropagation() }>
|
||||
{list}
|
||||
<div className={cn('select', this.props.className)} onScroll={this._hideDiff} onClick={(e) => e.stopPropagation() }>
|
||||
{this.state.list}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
211
src/app/components/BarChart.jsx
Normal file
211
src/app/components/BarChart.jsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import React from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import d3 from 'd3';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
|
||||
const MARGIN = { top: 15, right: 20, bottom: 40, left: 150 };
|
||||
const BAR_HEIGHT = 30;
|
||||
|
||||
/**
|
||||
* Get ship and build name
|
||||
* @param {Object} build Ship build
|
||||
* @return {string} name and build name
|
||||
*/
|
||||
function bName(build) {
|
||||
return build.buildName + '\n' + build.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a SVG text element's content with
|
||||
* tspans that wrap on newline
|
||||
* @param {string} d Data point
|
||||
*/
|
||||
function insertLinebreaks(d) {
|
||||
let el = d3.select(this);
|
||||
let lines = d.split('\n');
|
||||
el.text('').attr('y', -6);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let tspan = el.append('tspan').text(lines[i].length > 18 ? lines[i].substring(0, 15) + '...' : lines[i]);
|
||||
if (i > 0) {
|
||||
tspan.attr('x', -9).attr('dy', '1em');
|
||||
} else {
|
||||
tspan.attr('class', 'primary');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bar Chart
|
||||
*/
|
||||
export default class BarChart extends TranslatedComponent {
|
||||
|
||||
static defaultProps = {
|
||||
colors: ['#7b6888', '#6b486b', '#3182bd', '#a05d56', '#d0743c'],
|
||||
unit: ''
|
||||
};
|
||||
|
||||
static PropTypes = {
|
||||
data: React.PropTypes.array.isRequired,
|
||||
width: React.PropTypes.number.isRequired,
|
||||
format: React.PropTypes.string.isRequired,
|
||||
label: React.PropTypes.string.isRequired,
|
||||
unit: React.PropTypes.string.isRequired,
|
||||
colors: React.PropTypes.array,
|
||||
predicate: React.PropTypes.string,
|
||||
desc: React.PropTypes.bool
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
this._updateDimensions = this._updateDimensions.bind(this);
|
||||
this._hideTip = this._hideTip.bind(this);
|
||||
|
||||
let scale = d3.scale.linear();
|
||||
let y0 = d3.scale.ordinal();
|
||||
let y1 = d3.scale.ordinal();
|
||||
|
||||
this.xAxis = d3.svg.axis().scale(scale).ticks(5).outerTickSize(0).orient('bottom').tickFormat(context.language.formats.s2);
|
||||
this.yAxis = d3.svg.axis().scale(y0).outerTickSize(0).orient('left');
|
||||
this.state = { scale, y0, y1, color: d3.scale.ordinal().range(props.colors) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and Show tooltip
|
||||
* @param {Object} build Ship build
|
||||
* @param {string} property Property to display
|
||||
*/
|
||||
_showTip(build, property) {
|
||||
let { unit, format } = this.props;
|
||||
let { scale, y0, y1 } = this.state;
|
||||
let { formats } = this.context.language;
|
||||
let fontSize = parseFloat(window.getComputedStyle(document.getElementById('coriolis')).getPropertyValue('font-size') || 16);
|
||||
let val = build[property];
|
||||
let valStr = formats[format](val) + ' ' + unit;
|
||||
let width = (valStr.length / 1.7) * fontSize;
|
||||
let midPoint = width / 2;
|
||||
let valMidPoint = scale(val) / 2;
|
||||
let y = y0(bName(build)) + y1(property) - fontSize - 5;
|
||||
|
||||
let tooltip = <g>
|
||||
<g transform={`translate(${Math.max(0, valMidPoint - midPoint)},${y})`}>
|
||||
<rect className='primary-disabled' height={fontSize} width={width} />
|
||||
<text x={midPoint} y={fontSize} dy='-0.4em' style={{ textAnchor: 'middle', fontSize: '0.7em' }}>{valStr}</text>
|
||||
</g>
|
||||
<path className='primary-disabled' d='M0,0L5,5L10,0Z' dy='1em' transform={`translate(${Math.max(0, valMidPoint - 5)},${y + fontSize})`} />
|
||||
</g>;
|
||||
this.setState({ tooltip });
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide tooltip
|
||||
*/
|
||||
_hideTip() {
|
||||
this.setState({ tooltip: null });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update dimensions based on properties and scale
|
||||
* @param {Object} props React Component properties
|
||||
* @param {number} scale size ratio / scale
|
||||
*/
|
||||
_updateDimensions(props, scale) {
|
||||
let { width, data, properties } = props;
|
||||
let innerWidth = width - MARGIN.left - MARGIN.right;
|
||||
let barHeight = Math.round(BAR_HEIGHT * scale);
|
||||
let dataSize = data.length;
|
||||
let innerHeight = barHeight * dataSize;
|
||||
let outerHeight = innerHeight + MARGIN.top + MARGIN.bottom;
|
||||
let max = data.reduce((max, build) => (properties.reduce(((m, p) => (m > build[p] ? m : build[p])), max)), 0);
|
||||
|
||||
this.state.scale.range([0, innerWidth]).domain([0, max]);
|
||||
this.state.y0.domain(data.map(bName)).rangeRoundBands([0, innerHeight], 0.3);
|
||||
this.state.y1.domain(properties).rangeRoundBands([0, this.state.y0.rangeBand()]);
|
||||
|
||||
this.setState({
|
||||
barHeight,
|
||||
dataSize,
|
||||
innerWidth,
|
||||
outerHeight,
|
||||
innerHeight
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update dimensions based on props and context.
|
||||
*/
|
||||
componentWillMount() {
|
||||
this._updateDimensions(this.props, this.context.sizeRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on property and context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
let { data, width, predicate, desc } = nextProps;
|
||||
let props = this.props;
|
||||
|
||||
if (width != props.width || this.context.sizeRatio != nextContext.sizeRatio || data != props.data) {
|
||||
this._updateDimensions(nextProps, nextContext.sizeRatio);
|
||||
}
|
||||
|
||||
if (this.context.language != nextContext.language) {
|
||||
this.xAxis.tickFormat(nextContext.language.formats.s2);
|
||||
}
|
||||
|
||||
if (predicate != props.predicate || desc != props.desc) {
|
||||
this.state.y0.domain(data.map(bName));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the chart
|
||||
* @return {React.Component} Chart SVG
|
||||
*/
|
||||
render() {
|
||||
if (!this.props.width) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let { label, unit, width, data, properties } = this.props;
|
||||
let { innerWidth, outerHeight, innerHeight, y0, y1, scale, color, tooltip } = this.state;
|
||||
|
||||
let bars = data.map((build, i) =>
|
||||
<g key={i} transform={`translate(0,${y0(bName(build))})`}>
|
||||
{ properties.map((p) =>
|
||||
<rect
|
||||
key={p}
|
||||
x={0}
|
||||
y={y1(p)}
|
||||
width={scale(build[p])}
|
||||
height={y1.rangeBand()}
|
||||
fill={color(p)}
|
||||
onMouseOver={this._showTip.bind(this, build, p)}
|
||||
onMouseOut={this._hideTip}
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
|
||||
return <svg style={{ width, height: outerHeight }}>
|
||||
<g transform={`translate(${MARGIN.left},${MARGIN.top})`}>
|
||||
{bars}
|
||||
{tooltip}
|
||||
<g className='x axis' ref={(elem) => d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}>
|
||||
<text className='cap' y='30' dy='.1em' x={innerWidth / 2} style={{ textAnchor: 'middle' }}>
|
||||
<tspan>{label}</tspan>
|
||||
{ unit ? <tspan className='metric'>{` (${unit})`}</tspan> : null }
|
||||
</text>
|
||||
</g>
|
||||
<g className='y axis' ref={(elem) => { let e = d3.select(elem); e.call(this.yAxis); e.selectAll('text').each(insertLinebreaks); }} />
|
||||
</g>
|
||||
</svg>;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import cn from 'classnames';
|
||||
import { SizeMap } from '../shipyard/Constants';
|
||||
|
||||
|
||||
/**
|
||||
* Comparison Table
|
||||
*/
|
||||
export default class ComparisonTable extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -12,16 +15,27 @@ export default class ComparisonTable extends TranslatedComponent {
|
||||
builds: React.PropTypes.array.isRequired,
|
||||
onSort: React.PropTypes.func.isRequired,
|
||||
predicate: React.PropTypes.string.isRequired, // Used only to test again prop changes for shouldRender
|
||||
desc: React.PropTypes.bool.isRequired, // Used only to test again prop changes for shouldRender
|
||||
}
|
||||
desc: React.PropTypes.oneOfType([React.PropTypes.bool.isRequired, React.PropTypes.number.isRequired]), // Used only to test again prop changes for shouldRender
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this._buildHeaders = this._buildHeaders.bind(this);
|
||||
|
||||
this.state = this._buildHeaders(props.facets, props.onSort, context.language.translate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build table headers
|
||||
* @param {Array} facets Facets list
|
||||
* @param {Function} onSort Sort callback
|
||||
* @param {Function} translate Translate function
|
||||
* @return {Object} Header Components
|
||||
*/
|
||||
_buildHeaders(facets, onSort, translate) {
|
||||
let header = [
|
||||
<th key='ship' rowSpan='2' className='sortable' onClick={onSort.bind(null, 'name')}>{translate('ship')}</th>,
|
||||
@@ -39,7 +53,7 @@ export default class ComparisonTable extends TranslatedComponent {
|
||||
|
||||
if (pl > 1) {
|
||||
for (let i = 0; i < pl; i++) {
|
||||
subHeader.push(<th key={p[i]} className={cn('sortable', { lft: i === 0 } )} onClick={onSort.bind(null, p[i])} >{translate(f.lbls[i])}</th>);
|
||||
subHeader.push(<th key={p[i]} className={cn('sortable', { lft: i === 0 })} onClick={onSort.bind(null, p[i])} >{translate(f.lbls[i])}</th>);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +62,14 @@ export default class ComparisonTable extends TranslatedComponent {
|
||||
return { header, subHeader };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a table row for the build
|
||||
* @param {Object} build Ship build
|
||||
* @param {Array} facets Facets list
|
||||
* @param {Object} formats Localized formats map
|
||||
* @param {Object} units Localized untis map
|
||||
* @return {React.Component} Table row
|
||||
*/
|
||||
_buildRow(build, facets, formats, units) {
|
||||
let url = `/outfit/${build.id}/${build.toString()}?bn=${build.buildName}`;
|
||||
let cells = [
|
||||
@@ -66,6 +88,11 @@ export default class ComparisonTable extends TranslatedComponent {
|
||||
return <tr key={build.id + build.buildName} className='tr'>{cells}</tr>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on property and context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
// If facets or language has changed re-render header
|
||||
if (nextProps.facets != this.props.facets || nextContext.language != this.context.language) {
|
||||
@@ -73,6 +100,10 @@ export default class ComparisonTable extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the table
|
||||
* @return {React.Component} Comparison table
|
||||
*/
|
||||
render() {
|
||||
let { builds, facets } = this.props;
|
||||
let { header, subHeader } = this.state;
|
||||
@@ -97,6 +128,5 @@ export default class ComparisonTable extends TranslatedComponent {
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,12 @@ import { Ships } from 'coriolis-data';
|
||||
import Persist from '../stores/Persist';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import { Insurance } from '../shipyard/Constants';
|
||||
import { slotName, nameComparator } from '../utils/SlotFunctions';
|
||||
import { slotName, slotComparator } from '../utils/SlotFunctions';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
|
||||
/**
|
||||
* Cost Section
|
||||
*/
|
||||
export default class CostSection extends TranslatedComponent {
|
||||
|
||||
static PropTypes = {
|
||||
@@ -15,6 +18,10 @@ export default class CostSection extends TranslatedComponent {
|
||||
buildName: React.PropTypes.string
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._costsTab = this._costsTab.bind(this);
|
||||
@@ -52,10 +59,17 @@ export default class CostSection extends TranslatedComponent {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ship instance to base/reference retrofit changes from
|
||||
* @param {string} shipId Ship Id
|
||||
* @param {string} name Build name
|
||||
* @param {Ship} retrofitShip Existing retrofit ship
|
||||
* @return {Ship} Retrofit ship
|
||||
*/
|
||||
_buildRetrofitShip(shipId, name, retrofitShip) {
|
||||
let data = Ships[shipId]; // Retrieve the basic ship properties, slots and defaults
|
||||
|
||||
if (!retrofitShip) {
|
||||
if (!retrofitShip) { // Don't create a new instance unless needed
|
||||
retrofitShip = new Ship(shipId, data.properties, data.slots); // Create a new Ship for retrofit comparison
|
||||
}
|
||||
|
||||
@@ -67,15 +81,28 @@ export default class CostSection extends TranslatedComponent {
|
||||
return retrofitShip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default retrofit build name if it exists
|
||||
* @param {string} shipId Ship Id
|
||||
* @param {string} name Build name
|
||||
* @return {string} Build name or null
|
||||
*/
|
||||
_defaultRetrofitName(shipId, name) {
|
||||
return Persist.hasBuild(shipId, name) ? name : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show selected tab
|
||||
* @param {string} tab Tab name
|
||||
*/
|
||||
_showTab(tab) {
|
||||
Persist.setCostTab(tab);
|
||||
this.setState({ tab });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update prices on discount change
|
||||
*/
|
||||
_onDiscountChanged() {
|
||||
let shipDiscount = Persist.getShipDiscount();
|
||||
let moduleDiscount = Persist.getModuleDiscount();
|
||||
@@ -84,13 +111,17 @@ export default class CostSection extends TranslatedComponent {
|
||||
this.setState({ shipDiscount, moduleDiscount });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update insurance on change
|
||||
* @param {string} insuranceName Insurance level name
|
||||
*/
|
||||
_onInsuranceChanged(insuranceName) {
|
||||
this.setState({ insurance: Insurance[insuranceName] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Repopulate modules on retrofit ship from existing build
|
||||
* @param {string} retrofitName Build name to base the retrofit ship on
|
||||
* @param {SyntheticEvent} event Build name to base the retrofit ship on
|
||||
*/
|
||||
_onBaseRetrofitChange(event) {
|
||||
let retrofitName = event.target.value;
|
||||
@@ -105,6 +136,12 @@ export default class CostSection extends TranslatedComponent {
|
||||
this.setState({ retrofitName });
|
||||
}
|
||||
|
||||
/**
|
||||
* On build save
|
||||
* @param {string} shipId Ship Id
|
||||
* @param {string} name Build name
|
||||
* @param {string} code Serialized ship 'code'
|
||||
*/
|
||||
_onBuildSaved(shipId, name, code) {
|
||||
if(this.state.retrofitName == name) {
|
||||
this.state.retrofitShip.buildFrom(code); // Repopulate modules from saved build
|
||||
@@ -114,6 +151,12 @@ export default class CostSection extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On build deleted
|
||||
* @param {string} shipId Ship Id
|
||||
* @param {string} name Build name
|
||||
* @param {string} code Serialized ship 'code'
|
||||
*/
|
||||
_onBuildDeleted(shipId, name, code) {
|
||||
if(this.state.retrofitName == name) {
|
||||
this.state.retrofitShip.buildWith(Ships[shipId].defaults); // Retrofit ship becomes stock build
|
||||
@@ -122,11 +165,19 @@ export default class CostSection extends TranslatedComponent {
|
||||
this.setState({ buildOptions: Persist.getBuildsNamesFor(shipId) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle item cost inclusion in overall total
|
||||
* @param {Object} item Cost item
|
||||
*/
|
||||
_toggleCost(item) {
|
||||
this.props.ship.setCostIncluded(item, !item.incCost);
|
||||
this.setState({ total: this.props.ship.totalCost });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle item cost inclusion in retrofit total
|
||||
* @param {Object} item Cost item
|
||||
*/
|
||||
_toggleRetrofitCost(item) {
|
||||
let retrofitTotal = this.state.retrofitTotal;
|
||||
item.retroItem.incCost = !item.retroItem.incCost;
|
||||
@@ -134,6 +185,10 @@ export default class CostSection extends TranslatedComponent {
|
||||
this.setState({ retrofitTotal });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cost list sort predicate
|
||||
* @param {string} predicate sort predicate
|
||||
*/
|
||||
_sortCostBy(predicate) {
|
||||
let { costPredicate, costDesc } = this.state;
|
||||
|
||||
@@ -144,20 +199,27 @@ export default class CostSection extends TranslatedComponent {
|
||||
this.setState({ costPredicate: predicate, costDesc });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort cost list
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {string} predicate Sort predicate
|
||||
* @param {Boolean} desc Sort descending
|
||||
*/
|
||||
_sortCost(ship, predicate, desc) {
|
||||
let costList = ship.costList;
|
||||
let translate = this.context.language.translate;
|
||||
|
||||
if (predicate == 'm') {
|
||||
costList.sort(nameComparator(this.context.language.translate));
|
||||
costList.sort(slotComparator(translate, null, desc));
|
||||
} else {
|
||||
costList.sort((a, b) => (a.m && a.m.cost ? a.m.cost : 0) - (b.m && b.m.cost ? b.m.cost : 0));
|
||||
}
|
||||
|
||||
if (!desc) {
|
||||
costList.reverse();
|
||||
costList.sort(slotComparator(translate, (a, b) => (a.m.cost || 0) - (b.m.cost || 0), desc));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set ammo list sort predicate
|
||||
* @param {string} predicate sort predicate
|
||||
*/
|
||||
_sortAmmoBy(predicate) {
|
||||
let { ammoPredicate, ammoDesc } = this.state;
|
||||
|
||||
@@ -168,19 +230,26 @@ export default class CostSection extends TranslatedComponent {
|
||||
this.setState({ ammoPredicate: predicate, ammoDesc });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort ammo cost list
|
||||
* @param {Array} ammoCosts Ammo cost list
|
||||
* @param {string} predicate Sort predicate
|
||||
* @param {Boolean} desc Sort descending
|
||||
*/
|
||||
_sortAmmo(ammoCosts, predicate, desc) {
|
||||
let translate = this.context.language.translate;
|
||||
|
||||
if (predicate == 'm') {
|
||||
ammoCosts.sort(nameComparator(this.context.language.translate));
|
||||
ammoCosts.sort(slotComparator(translate, null, desc));
|
||||
} else {
|
||||
ammoCosts.sort((a, b) => a[predicate] - b[predicate]);
|
||||
}
|
||||
|
||||
if (!desc) {
|
||||
ammoCosts.reverse();
|
||||
ammoCosts.sort(slotComparator(translate, (a, b) => a[predicate] - b[predicate], desc));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set retrofit list sort predicate
|
||||
* @param {string} predicate sort predicate
|
||||
*/
|
||||
_sortRetrofitBy(predicate) {
|
||||
let { retroPredicate, retroDesc } = this.state;
|
||||
|
||||
@@ -191,6 +260,12 @@ export default class CostSection extends TranslatedComponent {
|
||||
this.setState({ retroPredicate: predicate, retroDesc });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort retrofit cost list
|
||||
* @param {Array} retrofitCosts Retrofit cost list
|
||||
* @param {string} predicate Sort predicate
|
||||
* @param {Boolean} desc Sort descending
|
||||
*/
|
||||
_sortRetrofit(retrofitCosts, predicate, desc) {
|
||||
let translate = this.context.language.translate;
|
||||
|
||||
@@ -205,6 +280,10 @@ export default class CostSection extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the cost tab
|
||||
* @return {React.Component} Tab contents
|
||||
*/
|
||||
_costsTab() {
|
||||
let { ship } = this.props;
|
||||
let { total, shipDiscount, moduleDiscount, insurance } = this.state;
|
||||
@@ -250,6 +329,10 @@ export default class CostSection extends TranslatedComponent {
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the retofit tab
|
||||
* @return {React.Component} Tab contents
|
||||
*/
|
||||
_retrofitTab() {
|
||||
let { retrofitTotal, retrofitCosts, moduleDiscount, retrofitName } = this.state;
|
||||
let { translate, formats, units } = this.context.language;
|
||||
@@ -268,11 +351,11 @@ export default class CostSection extends TranslatedComponent {
|
||||
<td className='le shorten cap'>{translate(item.sellName)}</td>
|
||||
<td style={{ width: '1em' }}>{item.buyClassRating}</td>
|
||||
<td className='le shorten cap'>{translate(item.buyName)}</td>
|
||||
<td colSpan='2' className={cn('ri', item.retroItem.incCost ? item.netCost > 0 ? 'warning' : 'secondary-disabled' : 'disabled' )}>{int(item.netCost)}{units.CR}</td>
|
||||
<td colSpan='2' className={cn('ri', item.retroItem.incCost ? item.netCost > 0 ? 'warning' : 'secondary-disabled' : 'disabled')}>{int(item.netCost)}{units.CR}</td>
|
||||
</tr>);
|
||||
}
|
||||
} else {
|
||||
rows = <tr><td colSpan='7' style={{ padding: '3em 0' }}>{translate('PHRASE_NO_RETROCH')}</td></tr>
|
||||
rows = <tr><td colSpan='7' style={{ padding: '3em 0' }}>{translate('PHRASE_NO_RETROCH')}</td></tr>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
@@ -284,7 +367,7 @@ export default class CostSection extends TranslatedComponent {
|
||||
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'buyName')}>{translate('buy')}</th>
|
||||
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'cr')}>
|
||||
{translate('net cost')}
|
||||
{moduleDiscount < 1 && <u className='optional-hide'>{`[${translate('modules')} -${formats.rPct(1 - moduleDiscount)}]`}</u>}
|
||||
{moduleDiscount < 1 && <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} -${formats.rPct(1 - moduleDiscount)}]`}</u>}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -301,7 +384,7 @@ export default class CostSection extends TranslatedComponent {
|
||||
<td className='val cen' style={{ borderRight: 'none', width: '1em' }}><u className='primary-disabled'>▾</u></td>
|
||||
<td className='val' style={{ borderLeft:'none', padding: 0 }}>
|
||||
<select style={{ width: '100%', padding: 0 }} value={retrofitName} onChange={this._onBaseRetrofitChange}>
|
||||
{options}
|
||||
{options}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -311,9 +394,15 @@ export default class CostSection extends TranslatedComponent {
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update retrofit costs
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {Ship} retrofitShip Retrofit Ship instance
|
||||
*/
|
||||
_updateRetrofit(ship, retrofitShip) {
|
||||
let retrofitCosts = [];
|
||||
var retrofitTotal = 0, i, l, item;
|
||||
let retrofitTotal = 0, i, l, item;
|
||||
|
||||
if (ship.bulkheads.index != retrofitShip.bulkheads.index) {
|
||||
item = {
|
||||
@@ -330,9 +419,9 @@ export default class CostSection extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
for (var g in { standard: 1, internal: 1, hardpoints: 1 }) {
|
||||
var retroSlotGroup = retrofitShip[g];
|
||||
var slotGroup = ship[g];
|
||||
for (let g in { standard: 1, internal: 1, hardpoints: 1 }) {
|
||||
let retroSlotGroup = retrofitShip[g];
|
||||
let slotGroup = ship[g];
|
||||
for (i = 0, l = slotGroup.length; i < l; i++) {
|
||||
if (slotGroup[i].m != retroSlotGroup[i].m) {
|
||||
item = { netCost: 0, retroItem: retroSlotGroup[i] };
|
||||
@@ -358,6 +447,10 @@ export default class CostSection extends TranslatedComponent {
|
||||
this._sortRetrofit(retrofitCosts, this.state.retroPredicate, this.state.retroDesc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the ammo tab
|
||||
* @return {React.Component} Tab contents
|
||||
*/
|
||||
_ammoTab() {
|
||||
let { ammoTotal, ammoCosts } = this.state;
|
||||
let { translate, formats, units } = this.context.language;
|
||||
@@ -400,6 +493,7 @@ export default class CostSection extends TranslatedComponent {
|
||||
|
||||
/**
|
||||
* Recalculate all ammo costs
|
||||
* @param {Ship} ship Ship instance
|
||||
*/
|
||||
_updateAmmoCosts(ship) {
|
||||
let ammoCosts = [], ammoTotal = 0, item, q, limpets = 0, srvs = 0, scoop = false;
|
||||
@@ -408,10 +502,10 @@ export default class CostSection extends TranslatedComponent {
|
||||
let slotGroup = ship[g];
|
||||
for (let i = 0, l = slotGroup.length; i < l; i++) {
|
||||
if (slotGroup[i].m) {
|
||||
//special cases needed for SCB, AFMU, and limpet controllers since they don't use standard ammo/clip
|
||||
// Special cases needed for SCB, AFMU, and limpet controllers since they don't use standard ammo/clip
|
||||
q = 0;
|
||||
switch (slotGroup[i].m.grp) {
|
||||
case 'fs': //skip fuel calculation if scoop present
|
||||
case 'fs': // Skip fuel calculation if scoop present
|
||||
scoop = true;
|
||||
break;
|
||||
case 'scb':
|
||||
@@ -429,7 +523,7 @@ export default class CostSection extends TranslatedComponent {
|
||||
default:
|
||||
q = slotGroup[i].m.clip + slotGroup[i].m.ammo;
|
||||
}
|
||||
//calculate ammo costs only if a cost is specified
|
||||
// Calculate ammo costs only if a cost is specified
|
||||
if (slotGroup[i].m.ammocost > 0) {
|
||||
item = {
|
||||
m: slotGroup[i].m,
|
||||
@@ -444,7 +538,7 @@ export default class CostSection extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
//limpets if controllers exist and cargo space available
|
||||
// Limpets if controllers exist and cargo space available
|
||||
if (limpets > 0) {
|
||||
item = {
|
||||
m: { name: 'limpets', class: '', rating: '' },
|
||||
@@ -466,7 +560,7 @@ export default class CostSection extends TranslatedComponent {
|
||||
ammoCosts.push(item);
|
||||
ammoTotal += item.total;
|
||||
}
|
||||
//calculate refuel costs if no scoop present
|
||||
// Calculate refuel costs if no scoop present
|
||||
if (!scoop) {
|
||||
item = {
|
||||
m: { name: 'fuel', class: '', rating: '' },
|
||||
@@ -482,7 +576,10 @@ export default class CostSection extends TranslatedComponent {
|
||||
this._sortAmmo(ammoCosts, this.state.ammoPredicate, this.state.ammoDesc);
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
/**
|
||||
* Add listeners on mount and update costs
|
||||
*/
|
||||
componentWillMount() {
|
||||
this.listeners = [
|
||||
Persist.addListener('discounts', this._onDiscountChanged.bind(this)),
|
||||
Persist.addListener('insurance', this._onInsuranceChanged.bind(this)),
|
||||
@@ -494,13 +591,18 @@ export default class CostSection extends TranslatedComponent {
|
||||
this._sortCost(this.props.ship);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on property and context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next context
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
let retrofitShip = this.state.retrofitShip;
|
||||
|
||||
if (nextProps.ship != this.props.ship) { // Ship has changed
|
||||
let nextId = nextProps.ship.id;
|
||||
let retrofitName = this._defaultRetrofitName(nextId, nextProps.buildName);
|
||||
retrofitShip = this._buildRetrofitShip(nextId, retrofitName, nextId == this.props.ship.id ? retrofitShip : null );
|
||||
retrofitShip = this._buildRetrofitShip(nextId, retrofitName, nextId == this.props.ship.id ? retrofitShip : null);
|
||||
this.setState({
|
||||
retrofitShip,
|
||||
retrofitName,
|
||||
@@ -515,6 +617,11 @@ export default class CostSection extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort lists before render
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextState Incoming/Next state
|
||||
*/
|
||||
componentWillUpdate(nextProps, nextState) {
|
||||
let state = this.state;
|
||||
|
||||
@@ -536,10 +643,17 @@ export default class CostSection extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
/**
|
||||
* Remove listeners
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this.listeners.forEach(l => l.remove());
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Cost section
|
||||
* @return {React.Component} Contents
|
||||
*/
|
||||
render() {
|
||||
let tab = this.state.tab;
|
||||
let translate = this.context.language.translate;
|
||||
@@ -558,9 +672,9 @@ export default class CostSection extends TranslatedComponent {
|
||||
<table className='tabs'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width:'33%' }} className={cn({active: tab == 'costs'})} onClick={this._showTab.bind(this, 'costs')} >{translate('costs')}</th>
|
||||
<th style={{ width:'33%' }} className={cn({active: tab == 'retrofit'})} onClick={this._showTab.bind(this, 'retrofit')} >{translate('retrofit costs')}</th>
|
||||
<th style={{ width:'34%' }} className={cn({active: tab == 'ammo'})} onClick={this._showTab.bind(this, 'ammo')} >{translate('reload costs')}</th>
|
||||
<th style={{ width:'33%' }} className={cn({ active: tab == 'costs' })} onClick={this._showTab.bind(this, 'costs')} >{translate('costs')}</th>
|
||||
<th style={{ width:'33%' }} className={cn({ active: tab == 'retrofit' })} onClick={this._showTab.bind(this, 'retrofit')} >{translate('retrofit costs')}</th>
|
||||
<th style={{ width:'34%' }} className={cn({ active: tab == 'ammo' })} onClick={this._showTab.bind(this, 'ammo')} >{translate('reload costs')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
|
||||
@@ -1,36 +1,58 @@
|
||||
import React from 'react';
|
||||
import Slot from './Slot';
|
||||
|
||||
/**
|
||||
* Hardpoint / Utility Slot
|
||||
*/
|
||||
export default class HardpointSlot extends Slot {
|
||||
|
||||
/**
|
||||
* Get the CSS class name for the slot.
|
||||
* @return {string} CSS Class name
|
||||
*/
|
||||
_getClassNames() {
|
||||
return this.props.maxClass > 0 ? 'hardpoint' : null;
|
||||
}
|
||||
|
||||
_getMaxClassLabel(translate){
|
||||
/**
|
||||
* Get the label for the slot
|
||||
* @param {Function} translate Translate function
|
||||
* @return {string} Label
|
||||
*/
|
||||
_getMaxClassLabel(translate) {
|
||||
return translate(['U','S','M','L','H'][this.props.maxClass]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the slot contents
|
||||
* @param {Object} m Mounted Module
|
||||
* @param {Function} translate Translate function
|
||||
* @param {Object} formats Localized Formats map
|
||||
* @param {Object} u Localized Units Map
|
||||
* @return {React.Component} Slot contents
|
||||
*/
|
||||
_getSlotDetails(m, translate, formats, u) {
|
||||
if (m) {
|
||||
let classRating = `${m.class}${m.rating}${m.mount ? '/' + m.mount : ''}${m.missile ? m.missile : ''}`;
|
||||
return (
|
||||
<div>
|
||||
let { drag, drop } = this.props;
|
||||
|
||||
return <div className='details' draggable='true' onDragStart={drag} onDragEnd={drop}>
|
||||
<div className={'cb'}>
|
||||
<div className={'l'}>{classRating + ' ' + translate(m.name || m.grp)}</div>
|
||||
<div className={'r'}>{m.mass}{u.T}</div>
|
||||
<div className={'cb'}>
|
||||
{ m.damage ? <div className={'l'}>{translate('damage')}: {m.damage} { m.ssdam ? <span>({formats.int(m.ssdam)} {u.MJ})</span> : null }</div> : null }
|
||||
{ m.dps ? <div className={'l'}>{translate('DPS')}: {m.dps} { m.mjdps ? <span>({formats.int(m.mjdps)} {u.MJ})</span> : null }</div> : null }
|
||||
{ m.thermload ? <div className={'l'}>{translate('T_LOAD')}: {m.thermload}</div> : null }
|
||||
{ m.type ? <div className={'l'}>{translate('type')}: {m.type}</div> : null }
|
||||
{ m.rof ? <div className={'l'}>{translate('ROF')}: {m.rof}{u.ps}</div> : null }
|
||||
{ m.armourpen ? <div className={'l'}>{translate('pen')}: {m.armourpen}</div> : null }
|
||||
{ m.shieldmul ? <div className={'l'}>+{formats.rPct(m.shieldmul)}</div> : null }
|
||||
{ m.range ? <div className={'l'}>{m.range} <u>km</u></div> : null }
|
||||
{ m.ammo >= 0 ? <div className={'l'}>{translate('ammo')}: {formats.int(m.clip)}+{formats.int(m.ammo)}</div> : null }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className={'cb'}>
|
||||
{ m.damage ? <div className={'l'}>{translate('damage')}: {m.damage} { m.ssdam ? <span>({formats.int(m.ssdam)} {u.MJ})</span> : null }</div> : null }
|
||||
{ m.dps ? <div className={'l'}>{translate('DPS')}: {m.dps} { m.mjdps ? <span>({formats.int(m.mjdps)} {u.MJ})</span> : null }</div> : null }
|
||||
{ m.thermload ? <div className={'l'}>{translate('T_LOAD')}: {m.thermload}</div> : null }
|
||||
{ m.type ? <div className={'l'}>{translate('type')}: {m.type}</div> : null }
|
||||
{ m.rof ? <div className={'l'}>{translate('ROF')}: {m.rof}{u.ps}</div> : null }
|
||||
{ m.armourpen ? <div className={'l'}>{translate('pen')}: {m.armourpen}</div> : null }
|
||||
{ m.shieldmul ? <div className={'l'}>+{formats.rPct(m.shieldmul)}</div> : null }
|
||||
{ m.range ? <div className={'l'}>{m.range} <u>km</u></div> : null }
|
||||
{ m.ammo >= 0 ? <div className={'l'}>{translate('ammo')}: {formats.int(m.clip)}+{formats.int(m.ammo)}</div> : null }
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
return <div className={'empty'}>{translate('empty')}</div>;
|
||||
}
|
||||
|
||||
@@ -4,35 +4,60 @@ import HardpointSlot from './HardpointSlot';
|
||||
import cn from 'classnames';
|
||||
import { MountFixed, MountGimballed, MountTurret } from '../components/SvgIcons';
|
||||
|
||||
/**
|
||||
* Hardpoint slot section
|
||||
*/
|
||||
export default class HardpointsSlotSection extends SlotSection {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context, 'hardpoints', 'hardpoints');
|
||||
|
||||
this._empty = this._empty.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all slots
|
||||
*/
|
||||
_empty() {
|
||||
this.props.ship.emptyWeapons();
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill slots with specified module
|
||||
* @param {string} group Group name
|
||||
* @param {string} mount Mount Type - F, G, T
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fill(group, mount, event) {
|
||||
this.props.ship.useWeapon(group, mount, null, event.getModifierState('Alt'));
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all on section header right click
|
||||
*/
|
||||
_contextMenu() {
|
||||
this._empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the slot React Components
|
||||
* @return {Array} Array of Slots
|
||||
*/
|
||||
_getSlots() {
|
||||
let { ship, currentMenu } = this.props;
|
||||
let { originSlot, targetSlot } = this.state;
|
||||
let slots = [];
|
||||
let hardpoints = this.props.ship.hardpoints;
|
||||
let availableModules = this.props.ship.getAvailableModules();
|
||||
let currentMenu = this.props.currentMenu;
|
||||
let hardpoints = ship.hardpoints;
|
||||
let availableModules = ship.getAvailableModules();
|
||||
|
||||
for (let i = 0, l = hardpoints.length; i < l; i++) {
|
||||
let h = hardpoints[i];
|
||||
@@ -44,6 +69,11 @@ export default class HardpointsSlotSection extends SlotSection {
|
||||
onOpen={this._openMenu.bind(this, h)}
|
||||
onSelect={this._selectModule.bind(this, h)}
|
||||
selected={currentMenu == h}
|
||||
drag={this._drag.bind(this, h)}
|
||||
dragOver={this._dragOverSlot.bind(this, h)}
|
||||
drop={this._drop}
|
||||
dropClass={this._dropClass(h, originSlot, targetSlot)}
|
||||
ship={ship}
|
||||
m={h.m}
|
||||
/>);
|
||||
}
|
||||
@@ -52,6 +82,11 @@ export default class HardpointsSlotSection extends SlotSection {
|
||||
return slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the section drop-down menu
|
||||
* @param {Function} translate Translate function
|
||||
* @return {React.Component} Section menu
|
||||
*/
|
||||
_getSectionMenu(translate) {
|
||||
let _fill = this._fill;
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import ActiveLink from './ActiveLink';
|
||||
import cn from 'classnames';
|
||||
import { Cogs, CoriolisLogo, Hammer, Rocket, StatsBars } from './SvgIcons';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import Persist from '../stores/Persist';
|
||||
import { toDetailedExport } from '../shipyard/Serializer';
|
||||
import ModalDeleteAll from './ModalDeleteAll';
|
||||
@@ -18,8 +17,16 @@ import Slider from './Slider';
|
||||
const SIZE_MIN = 0.65;
|
||||
const SIZE_RANGE = 0.55;
|
||||
|
||||
/**
|
||||
* Coriolis App Header section / menus
|
||||
*/
|
||||
export default class Header extends TranslatedComponent {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
this.shipOrder = Object.keys(Ships).sort();
|
||||
@@ -44,64 +51,114 @@ export default class Header extends TranslatedComponent {
|
||||
for (let name in Discounts) {
|
||||
this.discountOptions.push(<option key={name} value={Discounts[name]}>{name}</option>);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Update insurance level
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_setInsurance(e) {
|
||||
Persist.setInsurance(e.target.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the Module discount
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_setModuleDiscount(e) {
|
||||
Persist.setModuleDiscount(e.target.value * 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the Ship discount
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_setShipDiscount(e) {
|
||||
Persist.setShipDiscount(e.target.value * 1);
|
||||
}
|
||||
|
||||
_setLanguage(e){
|
||||
/**
|
||||
* Update the current language
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_setLanguage(e) {
|
||||
Persist.setLangCode(e.target.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle tooltips setting
|
||||
*/
|
||||
_toggleTooltips() {
|
||||
Persist.showTooltips(!Persist.showTooltips());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show delete all modal
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_showDeleteAll(e) {
|
||||
e.preventDefault();
|
||||
InterfaceEvents.showModal(<ModalDeleteAll />);
|
||||
this.context.showModal(<ModalDeleteAll />);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show export modal with backup data
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_showBackup(e) {
|
||||
let translate = this.context.language.translate;
|
||||
e.preventDefault();
|
||||
InterfaceEvents.showModal(<ModalExport
|
||||
this.context.showModal(<ModalExport
|
||||
title={translate('backup')}
|
||||
description={translate('PHRASE_BACKUP_DESC')}
|
||||
data={Persist.getAll()}
|
||||
/>);
|
||||
};
|
||||
|
||||
_showDetailedExport(e){
|
||||
/**
|
||||
* Show export modal with detailed export
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_showDetailedExport(e) {
|
||||
let translate = this.context.language.translate;
|
||||
e.preventDefault();
|
||||
|
||||
InterfaceEvents.showModal(<ModalExport
|
||||
this.context.showModal(<ModalExport
|
||||
title={translate('detailed export')}
|
||||
description={translate('PHRASE_EXPORT_DESC')}
|
||||
data={toDetailedExport(Persist.getBuilds())}
|
||||
/>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show import modal
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_showImport(e) {
|
||||
e.preventDefault();
|
||||
InterfaceEvents.showModal(<ModalImport/>);
|
||||
this.context.showModal(<ModalImport/>);
|
||||
}
|
||||
|
||||
_setTextSize(size) {
|
||||
Persist.setSizeRatio((size * SIZE_RANGE) + SIZE_MIN);
|
||||
/**
|
||||
* Update the app scale / size ratio
|
||||
* @param {number} scale scale Size Ratio
|
||||
*/
|
||||
_setTextSize(scale) {
|
||||
Persist.setSizeRatio((scale * SIZE_RANGE) + SIZE_MIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the app scale / size ratio
|
||||
*/
|
||||
_resetTextSize() {
|
||||
Persist.setSizeRatio(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a menu
|
||||
* @param {SyntheticEvent} event Event
|
||||
* @param {string} menu Menu name
|
||||
*/
|
||||
_openMenu(event, menu) {
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -109,23 +166,31 @@ export default class Header extends TranslatedComponent {
|
||||
menu = null;
|
||||
}
|
||||
|
||||
InterfaceEvents.openMenu(menu);
|
||||
this.context.openMenu(menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the ships menu
|
||||
* @return {React.Component} Menu
|
||||
*/
|
||||
_getShipsMenu() {
|
||||
let shipList = [];
|
||||
|
||||
for (let s in Ships) {
|
||||
shipList.push(<ActiveLink key={s} href={'/outfit/' + s} className={'block'}>{Ships[s].properties.name}</ActiveLink>);
|
||||
shipList.push(<ActiveLink key={s} href={'/outfit/' + s} className='block'>{Ships[s].properties.name}</ActiveLink>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'menu-list dbl no-wrap'} onClick={ (e) => e.stopPropagation() }>
|
||||
<div className='menu-list dbl no-wrap' onClick={ (e) => e.stopPropagation() }>
|
||||
{shipList}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the builds menu
|
||||
* @return {React.Component} Menu
|
||||
*/
|
||||
_getBuildsMenu() {
|
||||
let builds = Persist.getBuilds();
|
||||
let buildList = [];
|
||||
@@ -135,19 +200,23 @@ export default class Header extends TranslatedComponent {
|
||||
let buildNameOrder = Object.keys(builds[shipId]).sort();
|
||||
for (let buildName of buildNameOrder) {
|
||||
let href = ['/outfit/', shipId, '/', builds[shipId][buildName], '?bn=', buildName].join('');
|
||||
shipBuilds.push(<li key={shipId + '-' + buildName} ><ActiveLink href={href} className={'block'}>{buildName}</ActiveLink></li>);
|
||||
shipBuilds.push(<li key={shipId + '-' + buildName} ><ActiveLink href={href} className='block'>{buildName}</ActiveLink></li>);
|
||||
}
|
||||
buildList.push(<ul key={shipId}>{Ships[shipId].properties.name}{shipBuilds}</ul>);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'menu-list'} onClick={ (e) => e.stopPropagation() }>
|
||||
<div className={'dbl'}>{buildList}</div>
|
||||
<div className='menu-list' onClick={ (e) => e.stopPropagation() }>
|
||||
<div className='dbl'>{buildList}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the comparison menu
|
||||
* @return {React.Component} Menu
|
||||
*/
|
||||
_getComparisonsMenu() {
|
||||
let comparisons;
|
||||
let translate = this.context.language.translate;
|
||||
@@ -157,69 +226,68 @@ export default class Header extends TranslatedComponent {
|
||||
let comps = Object.keys(Persist.getComparisons()).sort();
|
||||
|
||||
for (let name of comps) {
|
||||
comparisons.push(<ActiveLink key={name} href={'/compare/' + name} className={'block name'}>{name}</ActiveLink>);
|
||||
comparisons.push(<ActiveLink key={name} href={'/compare/' + name} className='block name'>{name}</ActiveLink>);
|
||||
}
|
||||
} else {
|
||||
comparisons = <span className={'cap'}>{translate('none created')}</span>;
|
||||
comparisons = <span className='cap'>{translate('none created')}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'menu-list'} onClick={ (e) => e.stopPropagation() } style={{ whiteSpace: 'nowrap' }}>
|
||||
<div className='menu-list' onClick={ (e) => e.stopPropagation() } style={{ whiteSpace: 'nowrap' }}>
|
||||
{comparisons}
|
||||
<hr />
|
||||
<Link href='/compare/all' ui-sref="compare({name: 'all'})" className={'block cap'}>{translate('compare all')}</Link>
|
||||
<Link href='/compare' className={'block cap'}>{translate('create new')}</Link>
|
||||
<Link href='/compare/all' ui-sref="compare({name: 'all'})" className='block cap'>{translate('compare all')}</Link>
|
||||
<Link href='/compare' className='block cap'>{translate('create new')}</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the settings menu
|
||||
* @return {React.Component} Menu
|
||||
*/
|
||||
_getSettingsMenu() {
|
||||
let translate = this.context.language.translate;
|
||||
let tips = Persist.showTooltips();
|
||||
|
||||
return (
|
||||
<div className={'menu-list no-wrap cap'} onClick={ (e) => e.stopPropagation() }>
|
||||
<ul>
|
||||
{translate('language')}
|
||||
<li>
|
||||
<select className={'cap'} value={Persist.getLangCode()} onChange={this._setLanguage}>
|
||||
{this.languageOptions}
|
||||
</select>
|
||||
</li>
|
||||
</ul><br/>
|
||||
<ul>
|
||||
{translate('insurance')}
|
||||
<li>
|
||||
<select className={'cap'} value={Persist.getInsurance()} onChange={this._setInsurance}>
|
||||
{this.insuranceOptions}
|
||||
</select>
|
||||
</li>
|
||||
</ul><br/>
|
||||
<ul>
|
||||
{translate('ship')} {translate('discount')}
|
||||
<li>
|
||||
<select className={'cap'} value={Persist.getShipDiscount()} onChange={this._setShipDiscount}>
|
||||
{this.discountOptions}
|
||||
</select>
|
||||
</li>
|
||||
</ul><br/>
|
||||
<ul>
|
||||
{translate('module')} {translate('discount')}
|
||||
<li>
|
||||
<select className={'cap'} value={Persist.getModuleDiscount()} onChange={this._setModuleDiscount} >
|
||||
{this.discountOptions}
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
<div className='menu-list no-wrap cap' onClick={ (e) => e.stopPropagation() }>
|
||||
<div style={{ lineHeight: '2em' }}>
|
||||
{translate('language')}
|
||||
<select className='cap' value={Persist.getLangCode()} onChange={this._setLanguage}>
|
||||
{this.languageOptions}
|
||||
</select>
|
||||
<br/>
|
||||
<span className='cap ptr' onClick={this._toggleTooltips} >
|
||||
{translate('tooltips')}
|
||||
<div className={cn({ disabled: !tips, 'primary-disabled': tips })} style={{ marginLeft: '0.5em', display: 'inline-block' }}>{(tips ? '✔' : '✖')}</div>
|
||||
</span>
|
||||
<br/>
|
||||
{translate('insurance')}
|
||||
<select className='cap' value={Persist.getInsurance()} onChange={this._setInsurance}>
|
||||
{this.insuranceOptions}
|
||||
</select>
|
||||
<br/>
|
||||
{translate('ship')} {translate('discount')}
|
||||
<select className='cap' value={Persist.getShipDiscount()} onChange={this._setShipDiscount}>
|
||||
{this.discountOptions}
|
||||
</select>
|
||||
<br/>
|
||||
{translate('module')} {translate('discount')}
|
||||
<select className='cap' value={Persist.getModuleDiscount()} onChange={this._setModuleDiscount} >
|
||||
{this.discountOptions}
|
||||
</select>
|
||||
</div>
|
||||
<hr />
|
||||
<ul>
|
||||
{translate('builds')} & {translate('comparisons')}
|
||||
<li><a href="#" className={'block'} onClick={this._showBackup.bind(this)}>{translate('backup')}</a></li>
|
||||
<li><a href="#" className={'block'} onClick={this._showDetailedExport.bind(this)}>{translate('detailed export')}</a></li>
|
||||
<li><a href="#" className={'block'} onClick={this._showImport.bind(this)}>{translate('import')}</a></li>
|
||||
<li><a href="#" className='block' onClick={this._showBackup.bind(this)}>{translate('backup')}</a></li>
|
||||
<li><a href="#" className='block' onClick={this._showDetailedExport.bind(this)}>{translate('detailed export')}</a></li>
|
||||
<li><a href="#" className='block' onClick={this._showImport.bind(this)}>{translate('import')}</a></li>
|
||||
<li><a href="#" onClick={this._showDeleteAll.bind(this)}>{translate('delete all')}</a></li>
|
||||
</ul>
|
||||
<hr />
|
||||
<table style={{width: 300, backgroundColor: 'transparent'}}>
|
||||
<table style={{ width: 300, backgroundColor: 'transparent' }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: '1em', verticalAlign: 'top' }}><u>A</u></td>
|
||||
@@ -227,22 +295,34 @@ export default class Header extends TranslatedComponent {
|
||||
<td style={{ width: 20 }}><span style={{ fontSize: 30 }}>A</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan='3' style={{ textAlign: 'center', cursor: 'pointer' }} className={'primary-disabled cap'} onClick={this._resetTextSize.bind(this)}>{translate('reset')}</td>
|
||||
<td colSpan='3' style={{ textAlign: 'center', cursor: 'pointer' }} className='primary-disabled cap' onClick={this._resetTextSize.bind(this)}>{translate('reset')}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<Link href="/about" className={'block'}>{translate('about')}</Link>
|
||||
<Link href="/about" className='block'>{translate('about')}</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
/**
|
||||
* Add listeners on mount
|
||||
*/
|
||||
componentWillMount() {
|
||||
Persist.addListener('language', () => this.forceUpdate());
|
||||
Persist.addListener('insurance', () => this.forceUpdate());
|
||||
Persist.addListener('discounts', () => this.forceUpdate());
|
||||
Persist.addListener('deletedAll', () => this.forceUpdate());
|
||||
Persist.addListener('buildSaved', () => this.forceUpdate());
|
||||
Persist.addListener('buildDeleted', () => this.forceUpdate());
|
||||
Persist.addListener('tooltips', () => this.forceUpdate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on property and context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
if(this.context.language != nextContext.language) {
|
||||
let translate = nextContext.language.translate;
|
||||
@@ -253,6 +333,10 @@ export default class Header extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the header
|
||||
* @return {React.Component} Header
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
let openedMenu = this.props.currentMenu;
|
||||
@@ -264,32 +348,32 @@ export default class Header extends TranslatedComponent {
|
||||
|
||||
return (
|
||||
<header>
|
||||
<Link className={'l'} href="/" style={{marginRight: '1em'}} title="Home"><CoriolisLogo className={'icon xl'} /></Link>
|
||||
<Link className='l' href="/" style={{ marginRight: '1em' }} title="Home"><CoriolisLogo className='icon xl' /></Link>
|
||||
|
||||
<div className={'l menu'}>
|
||||
<div className={cn('menu-header', {selected: openedMenu == 's'})} onClick={ (e) => this._openMenu(e,'s') } >
|
||||
<Rocket className={'warning'} /><span className={'menu-item-label'}>{' ' + translate('ships')}</span>
|
||||
<div className='l menu'>
|
||||
<div className={cn('menu-header', { selected: openedMenu == 's' })} onClick={ (e) => this._openMenu(e,'s') } >
|
||||
<Rocket className='warning' /><span className='menu-item-label'>{' ' + translate('ships')}</span>
|
||||
</div>
|
||||
{openedMenu == 's' ? this._getShipsMenu() : null}
|
||||
</div>
|
||||
|
||||
<div className={'l menu'}>
|
||||
<div className={cn('menu-header', {selected: openedMenu == 'b', disabled: !hasBuilds})} onClick={ hasBuilds ? (e) => this._openMenu(e,'b') : null }>
|
||||
<Hammer className={cn('warning', { 'warning-disabled': !hasBuilds})} /><span className={'menu-item-label'}>{' ' + translate('builds')}</span>
|
||||
<div className='l menu'>
|
||||
<div className={cn('menu-header', { selected: openedMenu == 'b', disabled: !hasBuilds })} onClick={ hasBuilds ? (e) => this._openMenu(e,'b') : null }>
|
||||
<Hammer className={cn('warning', { 'warning-disabled': !hasBuilds })} /><span className='menu-item-label'>{' ' + translate('builds')}</span>
|
||||
</div>
|
||||
{openedMenu == 'b' ? this._getBuildsMenu() : null}
|
||||
</div>
|
||||
|
||||
<div className={'l menu'}>
|
||||
<div className={cn('menu-header', {selected: openedMenu == 'comp', disabled: !hasBuilds})} onClick={ hasBuilds ? (e) => this._openMenu(e,'comp') : null }>
|
||||
<StatsBars className={cn('warning', { 'warning-disabled': !hasBuilds})} /><span className={'menu-item-label'}>{' ' + translate('compare')}</span>
|
||||
<div className='l menu'>
|
||||
<div className={cn('menu-header', { selected: openedMenu == 'comp', disabled: !hasBuilds })} onClick={ hasBuilds ? (e) => this._openMenu(e,'comp') : null }>
|
||||
<StatsBars className={cn('warning', { 'warning-disabled': !hasBuilds })} /><span className='menu-item-label'>{' ' + translate('compare')}</span>
|
||||
</div>
|
||||
{openedMenu == 'comp' ? this._getComparisonsMenu() : null}
|
||||
</div>
|
||||
|
||||
<div className={'r menu'}>
|
||||
<div className={cn('menu-header', {selected: openedMenu == 'settings'})}onClick={ (e) => this._openMenu(e,'settings') }>
|
||||
<Cogs className={'xl warning'}/><span className={'menu-item-label'}>{translate('settings')}</span>
|
||||
<div className='r menu'>
|
||||
<div className={cn('menu-header', { selected: openedMenu == 'settings' })}onClick={ (e) => this._openMenu(e,'settings') }>
|
||||
<Cogs className='xl warning'/><span className='menu-item-label'>{translate('settings')}</span>
|
||||
</div>
|
||||
{openedMenu == 'settings' ? this._getSettingsMenu() : null}
|
||||
</div>
|
||||
|
||||
@@ -2,36 +2,48 @@ import React from 'react';
|
||||
import Slot from './Slot';
|
||||
import { Infinite } from './SvgIcons';
|
||||
|
||||
/**
|
||||
* Internal Slot
|
||||
*/
|
||||
export default class InternalSlot extends Slot {
|
||||
|
||||
/**
|
||||
* Generate the slot contents
|
||||
* @param {Object} m Mounted Module
|
||||
* @param {Function} translate Translate function
|
||||
* @param {Object} formats Localized Formats map
|
||||
* @param {Object} u Localized Units Map
|
||||
* @return {React.Component} Slot contents
|
||||
*/
|
||||
_getSlotDetails(m, translate, formats, u) {
|
||||
if (m) {
|
||||
let classRating = m.class + m.rating;
|
||||
let { drag, drop } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
return <div className='details' draggable='true' onDragStart={drag} onDragEnd={drop}>
|
||||
<div className={'cb'}>
|
||||
<div className={'l'}>{classRating + ' ' + translate(m.name || m.grp)}</div>
|
||||
<div className={'r'}>{m.mass || m.capacity || 0}{u.T}</div>
|
||||
<div className={'cb'}>
|
||||
{ m.optmass ? <div className={'l'}>{translate('optimal mass') + ': '}{m.optmass}{u.T}</div> : null }
|
||||
{ m.maxmass ? <div className={'l'}>{translate('max mass') + ': '}{m.maxmass}{u.T}</div> : null }
|
||||
{ m.bins ? <div className={'l'}>{m.bins + ' '}<u>{translate('bins')}</u></div> : null }
|
||||
{ m.bays ? <div className={'l'}>{translate('bays') + ': ' + m.bays}</div> : null }
|
||||
{ m.rate ? <div className={'l'}>{translate('rate')}: {m.rate}{u.kgs} {translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}</div> : null }
|
||||
{ m.ammo ? <div className={'l'}>{translate('ammo')}: {formats.gen(m.ammo)}</div> : null }
|
||||
{ m.cells ? <div className={'l'}>{translate('cells')}: {m.cells}</div> : null }
|
||||
{ m.recharge ? <div className={'l'}>{translate('recharge')}: {m.recharge} <u>MJ</u> {translate('total')}: {m.cells * m.recharge}{u.MJ}</div> : null }
|
||||
{ m.repair ? <div className={'l'}>{translate('repair')}: {m.repair}</div> : null }
|
||||
{ m.range ? <div className={'l'}>{translate('range')} {m.range}{u.km}</div> : null }
|
||||
{ m.time ? <div className={'l'}>{translate('time')}: {formats.time(m.time)}</div> : null }
|
||||
{ m.maximum ? <div className={'l'}>{translate('max')}: {(m.maximum)}</div> : null }
|
||||
{ m.rangeLS ? <div className={'l'}>{m.rangeLS}{u.Ls}</div> : null }
|
||||
{ m.rangeLS === null ? <div className={'l'}><Infinite/>{u.Ls}</div> : null }
|
||||
{ m.rangeRating ? <div className={'l'}>{translate('range')}: {m.rangeRating}</div> : null }
|
||||
{ m.armouradd ? <div className={'l'}>+{m.armouradd} <u>{translate('armour')}</u></div> : null }
|
||||
</div>
|
||||
<div className={'r'}>{m.mass || m.cargo || m.fuel || 0}{u.T}</div>
|
||||
</div>
|
||||
);
|
||||
<div className={'cb'}>
|
||||
{ m.optmass ? <div className={'l'}>{translate('optimal mass') + ': '}{m.optmass}{u.T}</div> : null }
|
||||
{ m.maxmass ? <div className={'l'}>{translate('max mass') + ': '}{m.maxmass}{u.T}</div> : null }
|
||||
{ m.bins ? <div className={'l'}>{m.bins + ' '}<u>{translate('bins')}</u></div> : null }
|
||||
{ m.bays ? <div className={'l'}>{translate('bays') + ': ' + m.bays}</div> : null }
|
||||
{ m.rate ? <div className={'l'}>{translate('rate')}: {m.rate}{u.kgs} {translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}</div> : null }
|
||||
{ m.ammo ? <div className={'l'}>{translate('ammo')}: {formats.gen(m.ammo)}</div> : null }
|
||||
{ m.cells ? <div className={'l'}>{translate('cells')}: {m.cells}</div> : null }
|
||||
{ m.recharge ? <div className={'l'}>{translate('recharge')}: {m.recharge} <u>MJ</u> {translate('total')}: {m.cells * m.recharge}{u.MJ}</div> : null }
|
||||
{ m.repair ? <div className={'l'}>{translate('repair')}: {m.repair}</div> : null }
|
||||
{ m.range ? <div className={'l'}>{translate('range')} {m.range}{u.km}</div> : null }
|
||||
{ m.time ? <div className={'l'}>{translate('time')}: {formats.time(m.time)}</div> : null }
|
||||
{ m.maximum ? <div className={'l'}>{translate('max')}: {(m.maximum)}</div> : null }
|
||||
{ m.rangeLS ? <div className={'l'}>{m.rangeLS}{u.Ls}</div> : null }
|
||||
{ m.rangeLS === null ? <div className={'l'}><Infinite/>{u.Ls}</div> : null }
|
||||
{ m.rangeRating ? <div className={'l'}>{translate('range')}: {m.rangeRating}</div> : null }
|
||||
{ m.armouradd ? <div className={'l'}>+{m.armouradd} <u>{translate('armour')}</u></div> : null }
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
return <div className={'empty'}>{translate('empty')}</div>;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,16 @@ import SlotSection from './SlotSection';
|
||||
import InternalSlot from './InternalSlot';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
|
||||
|
||||
/**
|
||||
* Internal slot section
|
||||
*/
|
||||
export default class InternalSlotSection extends SlotSection {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context, 'internal', 'internal compartments');
|
||||
|
||||
@@ -16,12 +23,19 @@ export default class InternalSlotSection extends SlotSection {
|
||||
this._fillWithArmor = this._fillWithArmor.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all slots
|
||||
*/
|
||||
_empty() {
|
||||
this.props.ship.emptyInternal();
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill all slots with cargo racks
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fillWithCargo(event) {
|
||||
let clobber = event.getModifierState('Alt');
|
||||
let ship = this.props.ship;
|
||||
@@ -34,6 +48,10 @@ export default class InternalSlotSection extends SlotSection {
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill all slots with Shield Cell Banks
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fillWithCells(event) {
|
||||
let clobber = event.getModifierState('Alt');
|
||||
let ship = this.props.ship;
|
||||
@@ -49,6 +67,10 @@ export default class InternalSlotSection extends SlotSection {
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill all slots with Hull Reinforcement Packages
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_fillWithArmor(event) {
|
||||
let clobber = event.getModifierState('Alt');
|
||||
let ship = this.props.ship;
|
||||
@@ -61,18 +83,27 @@ export default class InternalSlotSection extends SlotSection {
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all on section header right click
|
||||
*/
|
||||
_contextMenu() {
|
||||
this._empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the slot React Components
|
||||
* @return {Array} Array of Slots
|
||||
*/
|
||||
_getSlots() {
|
||||
let slots = [];
|
||||
let { currentMenu, ship } = this.props;
|
||||
let {internal, fuelCapacity, ladenMass } = ship;
|
||||
let { originSlot, targetSlot } = this.state;
|
||||
let { internal, fuelCapacity, ladenMass } = ship;
|
||||
let availableModules = ship.getAvailableModules();
|
||||
|
||||
for (let i = 0, l = internal.length; i < l; i++) {
|
||||
let s = internal[i];
|
||||
|
||||
slots.push(<InternalSlot
|
||||
key={i}
|
||||
maxClass={s.maxClass}
|
||||
@@ -82,13 +113,23 @@ export default class InternalSlotSection extends SlotSection {
|
||||
selected={currentMenu == s}
|
||||
enabled={s.enabled}
|
||||
m={s.m}
|
||||
drag={this._drag.bind(this, s)}
|
||||
dragOver={this._dragOverSlot.bind(this, s)}
|
||||
drop={this._drop}
|
||||
dropClass={this._dropClass(s, originSlot, targetSlot)}
|
||||
fuel={fuelCapacity}
|
||||
ship={ship}
|
||||
/>);
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the section drop-down menu
|
||||
* @param {Function} translate Translate function
|
||||
* @return {React.Component} Section menu
|
||||
*/
|
||||
_getSectionMenu(translate) {
|
||||
return <div className='select' onClick={e => e.stopPropagation()}>
|
||||
<ul>
|
||||
|
||||
@@ -4,15 +4,18 @@ import d3 from 'd3';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
|
||||
const RENDER_POINTS = 20; // Only render 20 points on the graph
|
||||
const MARGIN = { top: 15, right: 15, bottom: 35, left: 60 }
|
||||
const MARGIN = { top: 15, right: 15, bottom: 35, left: 60 };
|
||||
|
||||
/**
|
||||
* Line Chart
|
||||
*/
|
||||
export default class LineChart extends TranslatedComponent {
|
||||
|
||||
static defaultProps = {
|
||||
xMin: 0,
|
||||
yMin: 0,
|
||||
colors: ['#ff8c0d']
|
||||
}
|
||||
};
|
||||
|
||||
static PropTypes = {
|
||||
width: React.PropTypes.number.isRequired,
|
||||
@@ -29,6 +32,11 @@ export default class LineChart extends TranslatedComponent {
|
||||
colors: React.PropTypes.array,
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
@@ -42,11 +50,12 @@ export default class LineChart extends TranslatedComponent {
|
||||
let markerElems = [];
|
||||
let detailElems = [<text key={'lbl'} className='label x' y='1.25em'/>];
|
||||
let xScale = d3.scale.linear();
|
||||
let xAxisScale = d3.scale.linear();
|
||||
let yScale = d3.scale.linear();
|
||||
let series = props.series;
|
||||
let seriesLines = [];
|
||||
|
||||
this.xAxis = d3.svg.axis().scale(xScale).outerTickSize(0).orient('bottom');
|
||||
this.xAxis = d3.svg.axis().scale(xAxisScale).outerTickSize(0).orient('bottom');
|
||||
this.yAxis = d3.svg.axis().scale(yScale).ticks(6).outerTickSize(0).orient('left');
|
||||
|
||||
for(let i = 0, l = series ? series.length : 1; i < l; i++) {
|
||||
@@ -58,6 +67,7 @@ export default class LineChart extends TranslatedComponent {
|
||||
|
||||
this.state = {
|
||||
xScale,
|
||||
xAxisScale,
|
||||
yScale,
|
||||
seriesLines,
|
||||
detailElems,
|
||||
@@ -66,15 +76,19 @@ export default class LineChart extends TranslatedComponent {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tooltip content
|
||||
* @param {number} xPos x coordinate
|
||||
*/
|
||||
_tooltip(xPos) {
|
||||
let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props;
|
||||
let { xScale, yScale } = this.state;
|
||||
let { xScale, yScale, innerWidth } = this.state;
|
||||
let { formats, translate } = this.context.language;
|
||||
let x0 = xScale.invert(xPos),
|
||||
y0 = func(x0),
|
||||
tips = this.tipContainer,
|
||||
yTotal = 0,
|
||||
flip = (x0 / xScale.domain()[1] > 0.65),
|
||||
flip = (xPos / innerWidth > 0.65),
|
||||
tipWidth = 0,
|
||||
tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height;
|
||||
|
||||
@@ -100,32 +114,53 @@ export default class LineChart extends TranslatedComponent {
|
||||
this.markersContainer.selectAll('circle').attr('cx', xPos).attr('cy', (d, i) => yScale(series ? y0[series[i]] : y0));
|
||||
}
|
||||
|
||||
_updateDimensions(props, sizeRatio) {
|
||||
/**
|
||||
* Update dimensions based on properties and scale
|
||||
* @param {Object} props React Component properties
|
||||
* @param {number} scale size ratio / scale
|
||||
*/
|
||||
_updateDimensions(props, scale) {
|
||||
let { width, xMax, xMin, yMin, yMax } = props;
|
||||
let innerWidth = width - MARGIN.left - MARGIN.right;
|
||||
let outerHeight = Math.round(width * 0.5 * sizeRatio);
|
||||
let outerHeight = Math.round(width * 0.5 * scale);
|
||||
let innerHeight = outerHeight - MARGIN.top - MARGIN.bottom;
|
||||
|
||||
this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true);
|
||||
this.state.xAxisScale.range([0, innerWidth]).domain([xMin, xMax]).clamp(true);
|
||||
this.state.yScale.range([innerHeight, 0]).domain([yMin, yMax]);
|
||||
this.setState({ innerWidth, outerHeight, innerHeight });
|
||||
}
|
||||
|
||||
/**
|
||||
* Show tooltip
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_showTip(e) {
|
||||
this._moveTip(e);
|
||||
this.tipContainer.style('display', null);
|
||||
this.markersContainer.style('display', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move and update tooltip
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_moveTip(e) {
|
||||
this._tooltip(Math.round(e.clientX - e.target.getBoundingClientRect().left));
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide tooltip
|
||||
*/
|
||||
_hideTip() {
|
||||
this.tipContainer.style('display', 'none');
|
||||
this.markersContainer.style('display', 'none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update series data generated from props
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
_updateSeriesData(props) {
|
||||
let { func, xMin, xMax, series } = props;
|
||||
let delta = (xMax - xMin) / RENDER_POINTS;
|
||||
@@ -134,23 +169,31 @@ export default class LineChart extends TranslatedComponent {
|
||||
if (delta) {
|
||||
seriesData = new Array(RENDER_POINTS);
|
||||
for (let i = 0, x = xMin; i < RENDER_POINTS; i++) {
|
||||
seriesData[i] = [ x, func(x) ];
|
||||
seriesData[i] = [x, func(x)];
|
||||
x += delta;
|
||||
}
|
||||
seriesData[RENDER_POINTS - 1] = [ xMax, func(xMax) ];
|
||||
seriesData[RENDER_POINTS - 1] = [xMax, func(xMax)];
|
||||
} else {
|
||||
let yVal = func(xMin);
|
||||
seriesData = [ [0, yVal], [1, yVal]];
|
||||
seriesData = [[0, yVal], [1, yVal]];
|
||||
}
|
||||
|
||||
this.setState({ seriesData });
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
/**
|
||||
* Update dimensions and series data based on props and context.
|
||||
*/
|
||||
componentWillMount() {
|
||||
this._updateDimensions(this.props, this.context.sizeRatio);
|
||||
this._updateSeriesData(this.props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on property and context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next conext
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
let { func, xMin, xMax, yMin, yMax, width } = nextProps;
|
||||
let props = this.props;
|
||||
@@ -166,6 +209,10 @@ export default class LineChart extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the chart
|
||||
* @return {React.Component} Chart SVG
|
||||
*/
|
||||
render() {
|
||||
if (!this.props.width) {
|
||||
return null;
|
||||
@@ -192,7 +239,7 @@ export default class LineChart extends TranslatedComponent {
|
||||
</text>
|
||||
</g>
|
||||
<g ref={(g) => this.tipContainer = d3.select(g)} className='tooltip' style={{ display: 'none' }}>
|
||||
<rect className='tip' style={{height: tipHeight + 'em'}}></rect>
|
||||
<rect className='tip' style={{ height: tipHeight + 'em' }}></rect>
|
||||
{detailElems}
|
||||
</g>
|
||||
<g ref={(g) => this.markersContainer = d3.select(g)} style={{ display: 'none' }}>
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
import React from 'react';
|
||||
import Router from '../Router';
|
||||
import shallowEqual from '../utils/shallowEqual';
|
||||
import { shallowEqual } from '../utils/UtilityFunctions';
|
||||
|
||||
/**
|
||||
* Link wrapper component
|
||||
*/
|
||||
export default class Link extends React.Component {
|
||||
|
||||
/**
|
||||
* Determine if a component should be rerendered
|
||||
* @param {object} nextProps Next properties
|
||||
* @return {boolean} true if update is needed
|
||||
*/
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return !shallowEqual(this.props, nextProps);
|
||||
}
|
||||
|
||||
handler = (event) => {
|
||||
if (event.getModifierState
|
||||
&& ( event.getModifierState('Shift')
|
||||
|| event.getModifierState('Alt')
|
||||
|| event.getModifierState('Control')
|
||||
|| event.getModifierState('Meta')
|
||||
|| event.button > 1)) {
|
||||
/**
|
||||
* Link click handler
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
handler(event) {
|
||||
if (event.getModifierState &&
|
||||
(event.getModifierState('Shift') ||
|
||||
event.getModifierState('Alt') ||
|
||||
event.getModifierState('Control') ||
|
||||
event.getModifierState('Meta') ||
|
||||
event.button > 1)) {
|
||||
return;
|
||||
}
|
||||
event.nativeEvent && event.preventDefault && event.preventDefault();
|
||||
@@ -24,8 +36,12 @@ export default class Link extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the link
|
||||
* @return {React.Component} A href element
|
||||
*/
|
||||
render() {
|
||||
return <a {...this.props} onClick={this.handler}>{this.props.children}</a>
|
||||
return <a {...this.props} onClick={this.handler.bind(this)}>{this.props.children}</a>;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,17 +1,24 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import Persist from '../stores/Persist';
|
||||
|
||||
/**
|
||||
* Build ship and name comparator
|
||||
* @param {Object} a [description]
|
||||
* @param {Object} b [description]
|
||||
* @return {number} 1, 0, -1
|
||||
*/
|
||||
function buildComparator(a, b) {
|
||||
if (a.name == b.name) {
|
||||
return a.buildName > b.buildName;
|
||||
return a.buildName.localeCompare(b.buildName);
|
||||
}
|
||||
return a.name > b.name;
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compare builds modal
|
||||
*/
|
||||
export default class ModalCompare extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -21,52 +28,71 @@ export default class ModalCompare extends TranslatedComponent {
|
||||
|
||||
static defaultProps = {
|
||||
builds: []
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
let builds = props.builds;
|
||||
let allBuilds = Persist.getBuilds();
|
||||
let unusedBuilds = [];
|
||||
let usedBuilds = [];
|
||||
|
||||
for (let id in allBuilds) {
|
||||
for (let buildName in allBuilds[id]) {
|
||||
if (!builds.find((e) => e.buildName == buildName && e.id == id)) {
|
||||
unusedBuilds.push({ id, buildName, name: Ships[id].properties.name })
|
||||
}
|
||||
let b = { id, buildName, name: Ships[id].properties.name };
|
||||
builds.find((e) => e.buildName == buildName && e.id == id) ? usedBuilds.push(b) : unusedBuilds.push(b);
|
||||
}
|
||||
}
|
||||
|
||||
builds.sort(buildComparator);
|
||||
usedBuilds.sort(buildComparator);
|
||||
unusedBuilds.sort(buildComparator);
|
||||
|
||||
this.state = { builds, unusedBuilds };
|
||||
this.state = { usedBuilds, unusedBuilds, used: usedBuilds.length };
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a build to the compare list
|
||||
* @param {number} buildIndex Idnex of build in list
|
||||
*/
|
||||
_addBuild(buildIndex) {
|
||||
let { builds, unusedBuilds } = this.state;
|
||||
builds.push(unusedBuilds[buildIndex]);
|
||||
unusedBuilds = unusedBuilds.splice(buildIndex, 1);
|
||||
builds.sort(buildComparator);
|
||||
let { usedBuilds, unusedBuilds } = this.state;
|
||||
usedBuilds.push(unusedBuilds[buildIndex]);
|
||||
unusedBuilds.splice(buildIndex, 1);
|
||||
usedBuilds.sort(buildComparator);
|
||||
|
||||
this.setState({ builds, unusedBuilds });
|
||||
this.setState({ used: usedBuilds.length });
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a build from the compare list
|
||||
* @param {number} buildIndex Idnex of build in list
|
||||
*/
|
||||
_removeBuild(buildIndex) {
|
||||
let { builds, unusedBuilds } = this.state;
|
||||
unusedBuilds.push(builds[buildIndex]);
|
||||
builds = builds.splice(buildIndex, 1);
|
||||
let { usedBuilds, unusedBuilds } = this.state;
|
||||
unusedBuilds.push(usedBuilds[buildIndex]);
|
||||
usedBuilds.splice(buildIndex, 1);
|
||||
unusedBuilds.sort(buildComparator);
|
||||
|
||||
this.setState({ builds, unusedBuilds });
|
||||
this.setState({ used: usedBuilds.length });
|
||||
}
|
||||
|
||||
/**
|
||||
* OK Action - Use selected builds
|
||||
*/
|
||||
_selectBuilds() {
|
||||
this.props.onSelect(this.state.builds);
|
||||
this.props.onSelect(this.state.usedBuilds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modal
|
||||
* @return {React.Component} Modal Content
|
||||
*/
|
||||
render() {
|
||||
let { builds, unusedBuilds } = this.state;
|
||||
let { usedBuilds, unusedBuilds } = this.state;
|
||||
let translate = this.context.language.translate;
|
||||
|
||||
let availableBuilds = unusedBuilds.map((build, i) =>
|
||||
@@ -76,7 +102,7 @@ export default class ModalCompare extends TranslatedComponent {
|
||||
</tr>
|
||||
);
|
||||
|
||||
let selectedBuilds = builds.map((build, i) =>
|
||||
let selectedBuilds = usedBuilds.map((build, i) =>
|
||||
<tr key={i} onClick={this._removeBuild.bind(this, i)}>
|
||||
<td className='tl'>{build.name}</td><
|
||||
td className='tl'>{build.buildName}</td>
|
||||
@@ -102,7 +128,7 @@ export default class ModalCompare extends TranslatedComponent {
|
||||
</div>
|
||||
<br/>
|
||||
<button className='cap' onClick={this._selectBuilds.bind(this)}>{translate('Ok')}</button>
|
||||
<button className='r cap' onClick={() => InterfaceEvents.hideModal()}>{translate('Cancel')}</button>
|
||||
<button className='r cap' onClick={() => this.context.hideModal()}>{translate('Cancel')}</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import Persist from '../stores/Persist';
|
||||
|
||||
/**
|
||||
* Delete All saved data modal
|
||||
*/
|
||||
export default class ModalDeleteAll extends TranslatedComponent {
|
||||
|
||||
/**
|
||||
* Delete everything and hide the modal
|
||||
*/
|
||||
_deleteAll() {
|
||||
Persist.deleteAll();
|
||||
InterfaceEvents.hideModal();
|
||||
this.context.hideModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the component
|
||||
* @return {React.Component} Modal contents
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
|
||||
return <div className='modal' onClick={(e) => e.stopPropagation()}>
|
||||
<h2>{translate('delete all')}</h2>
|
||||
<p style={{textAlign: 'center'}}>{translate('PHRASE_CONFIRMATION')}</p>
|
||||
<button className='l cap' onClick={this._deleteAll}>{translate('yes')}</button>
|
||||
<button className='r cap' onClick={InterfaceEvents.hideModal}>{translate('no')}</button>
|
||||
<p className='cen'>{translate('PHRASE_CONFIRMATION')}</p>
|
||||
<button className='l cap' onClick={this._deleteAll.bind(this)}>{translate('yes')}</button>
|
||||
<button className='r cap' onClick={this.context.hideModal}>{translate('no')}</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
|
||||
/**
|
||||
* Export Modal
|
||||
*/
|
||||
export default class ModalExport extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
title: React.PropTypes.string,
|
||||
promise: React.PropTypes.func,
|
||||
generator: React.PropTypes.func,
|
||||
data: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object, React.PropTypes.array])
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
let exportJson;
|
||||
|
||||
if (props.promise) {
|
||||
if (props.generator) {
|
||||
exportJson = 'Generating...';
|
||||
} else if(typeof props.data == 'string') {
|
||||
exportJson = props.data;
|
||||
@@ -25,16 +31,25 @@ export default class ModalExport extends TranslatedComponent {
|
||||
this.state = { exportJson };
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
// When promise is done update exportJson accordingly
|
||||
/**
|
||||
* If generator is provided, execute on mount
|
||||
*/
|
||||
componentWillMount() {
|
||||
if (this.props.generator) {
|
||||
this.props.generator((str) => this.setState({ exportJson: str }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modal
|
||||
* @return {React.Component} Modal Content
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
let description;
|
||||
|
||||
if (this.props.description) {
|
||||
description = <div>{translate(this.props.description)}</div>
|
||||
description = <div>{translate(this.props.description)}</div>;
|
||||
}
|
||||
|
||||
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
|
||||
@@ -43,7 +58,7 @@ export default class ModalExport extends TranslatedComponent {
|
||||
<div>
|
||||
<textarea className='cb json' onFocus={ (e) => e.target.select() } readOnly value={this.state.exportJson} />
|
||||
</div>
|
||||
<button className={'r dismiss cap'} onClick={InterfaceEvents.hideModal}>{translate('close')}</button>
|
||||
<button className='r dismiss cap' onClick={this.context.hideModal}>{translate('close')}</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,45 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import Persist from '../stores/Persist';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import { ModuleNameToGroup } from '../shipyard/Constants';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import { fromDetailedBuild } from '../shipyard/Serializer';
|
||||
import { Download } from './SvgIcons';
|
||||
|
||||
|
||||
const textBuildRegex = new RegExp('^\\[([\\w \\-]+)\\]\n');
|
||||
const lineRegex = new RegExp('^([\\dA-Z]{1,2}): (\\d)([A-I])[/]?([FGT])?([SD])? ([\\w\\- ]+)');
|
||||
const mountMap = { 'H': 4, 'L': 3, 'M': 2, 'S': 1, 'U': 0 };
|
||||
const standardMap = { 'RB': 0, 'TM': 1, 'FH': 2, 'EC': 3, 'PC': 4, 'SS': 5, 'FS': 6 };
|
||||
const bhMap = { 'lightweight alloy': 0, 'reinforced alloy': 1, 'military grade composite': 2, 'mirrored surface composite': 3, 'reactive surface composite': 4 };
|
||||
|
||||
/**
|
||||
* Check is slot is empty
|
||||
* @param {Object} slot Slot model
|
||||
* @return {Boolean} True if empty
|
||||
*/
|
||||
function isEmptySlot(slot) {
|
||||
return slot.maxClass == this && slot.m === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Equal ignore case utility function. Must be bound to a string
|
||||
* @param {string} str String
|
||||
* @return {Boolean} True if equal
|
||||
*/
|
||||
function equalsIgnoreCase(str) {
|
||||
return str.toLowerCase() == this.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a build is valid
|
||||
* @param {string} shipId Ship ID
|
||||
* @param {string} code Serialzied ship build 'code'
|
||||
* @param {string} name Build name
|
||||
* @throws {string} If build is not valid
|
||||
*/
|
||||
function validateBuild(shipId, code, name) {
|
||||
let shipData = Ships[shipId];
|
||||
|
||||
@@ -43,6 +60,11 @@ function validateBuild(shipId, code, name) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a ship-loadout JSON object to a Coriolis build
|
||||
* @param {Object} detailedBuild ship-loadout
|
||||
* @return {Object} Coriolis build
|
||||
*/
|
||||
function detailedJsonToBuild(detailedBuild) {
|
||||
let ship;
|
||||
if (!detailedBuild.name) {
|
||||
@@ -54,7 +76,7 @@ function detailedJsonToBuild(detailedBuild) {
|
||||
}
|
||||
|
||||
try {
|
||||
ship = Serializer.fromDetailedBuild(detailedBuild);
|
||||
ship = fromDetailedBuild(detailedBuild);
|
||||
} catch (e) {
|
||||
throw detailedBuild.ship + ' Build "' + detailedBuild.name + '": Invalid data';
|
||||
}
|
||||
@@ -62,8 +84,15 @@ function detailedJsonToBuild(detailedBuild) {
|
||||
return { shipId: ship.id, name: detailedBuild.name, code: ship.toString() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Import Modal
|
||||
*/
|
||||
export default class ModalImport extends TranslatedComponent {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -86,6 +115,11 @@ export default class ModalImport extends TranslatedComponent {
|
||||
this._validateImport = this._validateImport.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a Coriolis backup
|
||||
* @param {Object} importData Backup Data
|
||||
* @throws {string} If import fails
|
||||
*/
|
||||
_importBackup(importData) {
|
||||
if (importData.builds && typeof importData.builds == 'object') {
|
||||
for (let shipId in importData.builds) {
|
||||
@@ -117,6 +151,10 @@ export default class ModalImport extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import an array of ship-loadout objects / builds
|
||||
* @param {Array} importArr Array of ship-loadout JSON Schema builds
|
||||
*/
|
||||
_importDetailedArray(importArr) {
|
||||
let builds = {};
|
||||
for (let i = 0, l = importArr.length; i < l; i++) {
|
||||
@@ -129,6 +167,11 @@ export default class ModalImport extends TranslatedComponent {
|
||||
this.setState({ builds });
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a text build from ED Shipyard
|
||||
* @param {string} buildStr Build string
|
||||
* @throws {string} If parse / import fails
|
||||
*/
|
||||
_importTextBuild(buildStr) {
|
||||
let buildName = textBuildRegex.exec(buildStr)[1].trim();
|
||||
let shipName = buildName.toLowerCase();
|
||||
@@ -173,43 +216,40 @@ export default class ModalImport extends TranslatedComponent {
|
||||
|
||||
if (cl > slotClass) { throw cl + rating + ' ' + name + ' exceeds slot size: "' + line + '"'; }
|
||||
|
||||
slot = _.find(ship.hardpoints, isEmptySlot, slotClass);
|
||||
slot = ship.hardpoints.find(isEmptySlot, slotClass);
|
||||
|
||||
if (!slot) { throw 'No hardpoint slot available for: "' + line + '"'; }
|
||||
|
||||
group = _.find(GroupMap, equalsIgnoreCase, name);
|
||||
group = ModuleNameToGroup[name.trim()];
|
||||
|
||||
let hp = ModuleUtils.findHardpoint(group, cl, rating, group ? null : name, mount, missile);
|
||||
|
||||
if (!hp) { throw 'Unknown component: "' + line + '"'; }
|
||||
|
||||
ship.use(slot, hp, true);
|
||||
|
||||
} else if (typeSize == 'BH') {
|
||||
let bhId = bhMap[name.toLowerCase()];
|
||||
|
||||
if (bhId === undefined) { throw 'Unknown bulkhead: "' + line + '"'; }
|
||||
|
||||
ship.useBulkhead(bhId, true);
|
||||
|
||||
} else if (standardMap[typeSize] != undefined) {
|
||||
let standardIndex = standardMap[typeSize];
|
||||
|
||||
if (ship.standard[standardIndex].maxClass < cl) { throw name + ' exceeds max class for the ' + ship.name; }
|
||||
|
||||
ship.use(ship.standard[standardIndex], cl + rating, ModuleUtils.standard(standardIndex, cl + rating), true);
|
||||
|
||||
} else {
|
||||
throw 'Unknown component: "' + line + '"';
|
||||
}
|
||||
} else {
|
||||
if (cl > typeSize) { throw cl + rating + ' ' + name + ' exceeds slot size: "' + line + '"'; }
|
||||
|
||||
slot = _.find(ship.internal, isEmptySlot, typeSize);
|
||||
slot = ship.internal.find(isEmptySlot, typeSize);
|
||||
|
||||
if (!slot) { throw 'No internal slot available for: "' + line + '"'; }
|
||||
|
||||
group = _.find(GroupMap, equalsIgnoreCase, name);
|
||||
group = ModuleNameToGroup[name.trim()];
|
||||
|
||||
let intComp = ModuleUtils.findInternal(group, cl, rating, group ? null : name);
|
||||
|
||||
@@ -221,13 +261,18 @@ export default class ModalImport extends TranslatedComponent {
|
||||
|
||||
let builds = {};
|
||||
builds[shipId] = {};
|
||||
builds[shipId]['Imported ' + buildName] = Serializer.fromShip(ship);
|
||||
builds[shipId]['Imported ' + buildName] = ship.toString();
|
||||
this.setState({ builds });
|
||||
}
|
||||
|
||||
_validateImport(e) {
|
||||
/**
|
||||
* Validate the import string / text box contents
|
||||
* @param {SyntheticEvent} event Event
|
||||
* @throws {string} If validation fails
|
||||
*/
|
||||
_validateImport(event) {
|
||||
let importData = null;
|
||||
let importString = e.target.value;
|
||||
let importString = event.target.value;
|
||||
this.setState({
|
||||
builds: null,
|
||||
comparisons: null,
|
||||
@@ -244,7 +289,7 @@ export default class ModalImport extends TranslatedComponent {
|
||||
|
||||
try {
|
||||
if (textBuildRegex.test(importString)) { // E:D Shipyard build text
|
||||
importTextBuild(importString);
|
||||
this._importTextBuild(importString);
|
||||
} else { // JSON Build data
|
||||
importData = JSON.parse(importString);
|
||||
|
||||
@@ -268,6 +313,9 @@ export default class ModalImport extends TranslatedComponent {
|
||||
this.setState({ importValid: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Process imported data
|
||||
*/
|
||||
_process() {
|
||||
let builds = null, comparisons = null;
|
||||
|
||||
@@ -278,7 +326,7 @@ export default class ModalImport extends TranslatedComponent {
|
||||
let code = builds[shipId][buildName];
|
||||
// Update builds object such that orginal name retained, but can be renamed
|
||||
builds[shipId][buildName] = {
|
||||
code: code,
|
||||
code,
|
||||
useName: buildName
|
||||
};
|
||||
}
|
||||
@@ -295,8 +343,10 @@ export default class ModalImport extends TranslatedComponent {
|
||||
this.setState({ processed: true, builds, comparisons });
|
||||
};
|
||||
|
||||
/**
|
||||
* Import parsed, processed data and save
|
||||
*/
|
||||
_import() {
|
||||
|
||||
if (this.state.builds) {
|
||||
let builds = this.state.builds;
|
||||
for (let shipId in builds) {
|
||||
@@ -329,16 +379,33 @@ export default class ModalImport extends TranslatedComponent {
|
||||
Persist.setInsurance(this.state.insurance);
|
||||
}
|
||||
|
||||
InterfaceEvents.hideModal();
|
||||
this.context.hideModal();
|
||||
};
|
||||
|
||||
/**
|
||||
* Capture build name changes
|
||||
* @param {Object} build Build import object
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
_changeBuildName(build, e) {
|
||||
build.useName = e.target.value;
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* If imported data is already provided process immediately on mount
|
||||
*/
|
||||
componentWillMount() {
|
||||
if (this.props.importingBuilds) {
|
||||
this.setState({ builds: this.props.importingBuilds, canEdit : false});
|
||||
this.setState({ builds: this.props.importingBuilds, canEdit : false });
|
||||
this._process();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the import modal
|
||||
* @return {React.Component} Modal contents
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
let state = this.state;
|
||||
@@ -357,27 +424,27 @@ export default class ModalImport extends TranslatedComponent {
|
||||
if (state.comparisons) {
|
||||
let comparisonRows = [];
|
||||
|
||||
for (let name in comparisons) {
|
||||
let comparison = comparisons[name];
|
||||
for (let name in state.comparisons) {
|
||||
let comparison = state.comparisons[name];
|
||||
let hasComparison = Persist.hasComparison(name);
|
||||
comparisonRows.push(
|
||||
<tr key={name} className='cb'>
|
||||
<td>
|
||||
<input type='text' value={comparison.useName}/>
|
||||
</td>
|
||||
<td style={{ textAlign:'center' }} className={ cn({ warning: hasComparison, disabled: comparison.useName == '' }) }>
|
||||
<span>{translate(comparison.useName == '' ? 'skip' : (hasComparison ? 'overwrite' : 'create'))}></span>
|
||||
<td style={{ textAlign:'center' }} className={ cn('cap', { warning: hasComparison, disabled: comparison.useName == '' }) }>
|
||||
{translate(comparison.useName == '' ? 'skip' : (hasComparison ? 'overwrite' : 'create'))}>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
comparisonTable = (
|
||||
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%'}} >
|
||||
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%' }} >
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign:'left' }}>{translate('comparison')}</th>
|
||||
<th >{translate('action')}</th>
|
||||
<th>{translate('action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -388,7 +455,7 @@ export default class ModalImport extends TranslatedComponent {
|
||||
}
|
||||
|
||||
if(this.state.canEdit) {
|
||||
edit = <button className='l cap' style={{ marginLeft: '2em' }} onClick={() => this.setState({processed: false})}>{translate('edit data')}</button>
|
||||
edit = <button className='l cap' style={{ marginLeft: '2em' }} onClick={() => this.setState({ processed: false })}>{translate('edit data')}</button>;
|
||||
}
|
||||
|
||||
let builds = this.state.builds;
|
||||
@@ -398,11 +465,11 @@ export default class ModalImport extends TranslatedComponent {
|
||||
let b = shipBuilds[buildName];
|
||||
let hasBuild = Persist.hasBuild(shipId, b.useName);
|
||||
buildRows.push(
|
||||
<tr className='cb'>
|
||||
<tr key={shipId + buildName} className='cb'>
|
||||
<td>{Ships[shipId].properties.name}</td>
|
||||
<td><input type='text' value={b.useName}/></td>
|
||||
<td style={{ textAlign: 'center' }} className={cn({ warning: hasBuild, disabled: b.useName == ''})}>
|
||||
<span>{translate(b.useName == '' ? 'skip' : (hasBuild ? 'overwrite' : 'create'))}></span>
|
||||
<td><input type='text' onChange={this._changeBuildName.bind(this, b)} value={b.useName}/></td>
|
||||
<td style={{ textAlign: 'center' }} className={cn('cap', { warning: hasBuild, disabled: b.useName == '' })}>
|
||||
{translate(b.useName == '' ? 'skip' : (hasBuild ? 'overwrite' : 'create'))}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -411,12 +478,12 @@ export default class ModalImport extends TranslatedComponent {
|
||||
|
||||
importStage = (
|
||||
<div>
|
||||
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%'}}>
|
||||
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left' }} >{translate('ship')}</th>
|
||||
<th style={{ textAlign: 'left' }} >{translate('build name')}</th>
|
||||
<th >{translate('action')}</th>
|
||||
<th>{translate('action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -433,7 +500,7 @@ export default class ModalImport extends TranslatedComponent {
|
||||
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
|
||||
<h2 >{translate('import')}</h2>
|
||||
{importStage}
|
||||
<button className={'r dismiss cap'} onClick={InterfaceEvents.hideModal}>{translate('close')}</button>
|
||||
<button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import ShortenUrl from '../utils/ShortenUrl';
|
||||
|
||||
/**
|
||||
* Permalink modal
|
||||
*/
|
||||
export default class ModalPermalink extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
url: React.propTypes.string.isRequired
|
||||
url: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -16,13 +23,20 @@ export default class ModalPermalink extends TranslatedComponent {
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
/**
|
||||
* Shorten URL on mount
|
||||
*/
|
||||
componentWillMount() {
|
||||
ShortenUrl(this.props.url,
|
||||
(shortenedUrl) => this.setState({ shortenedUrl }),
|
||||
(error) => this.setState({ shortenedUrl: 'Error - ' + e.statusText })
|
||||
(error) => this.setState({ shortenedUrl: 'Error - ' + error })
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modal
|
||||
* @return {React.Component} Modal Content
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
|
||||
@@ -30,12 +44,12 @@ export default class ModalPermalink extends TranslatedComponent {
|
||||
<h2>{translate('permalink')}</h2>
|
||||
<br/>
|
||||
<h3>{translate('URL')}</h3>
|
||||
<input value={this.props.url} size={40} onFocus={ (e) => e.target.select() }/>
|
||||
<input value={this.props.url} size={40} readOnly onFocus={ (e) => e.target.select() }/>
|
||||
<br/><br/>
|
||||
<h3 >{translate('shortened')}</h3>
|
||||
<input value={this.state.shortenedUrl} size={25} onFocus={ (e) => e.target.select() }/>
|
||||
<input value={this.state.shortenedUrl} readOnly size={25} onFocus={ (e) => e.target.select() }/>
|
||||
<br/><br/>
|
||||
<button className={'r dismiss cap'} onClick={InterfaceEvents.hideModal}>{translate('close')}</button>
|
||||
<button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,34 @@ import React from 'react';
|
||||
import d3 from 'd3';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { wrapCtxMenu } from '../utils/InterfaceEvents';
|
||||
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
||||
|
||||
/**
|
||||
* Round to avoid floating point precision errors
|
||||
* @param {[type]} selected [description]
|
||||
* @param {[type]} sum [description]
|
||||
* @param {[type]} avail [description]
|
||||
* @return {[type]} [description]
|
||||
* @param {Boolean} selected Band selected
|
||||
* @param {number} sum Band power sum
|
||||
* @param {number} avail Total available power
|
||||
* @return {string} CSS Class name
|
||||
*/
|
||||
function getClass(selected, sum, avail) {
|
||||
return selected ? 'secondary' : ((Math.round(sum * 100) / 100) >= avail) ? 'warning' : 'primary';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the # label for a Priority band
|
||||
* @param {number} val Priority Band Watt value
|
||||
* @param {number} index Priority Band index
|
||||
* @param {Function} wattScale Watt Scale function
|
||||
* @return {number} label / text
|
||||
*/
|
||||
function bandText(val, index, wattScale) {
|
||||
return (val > 0 && wattScale(val) > 13) ? index + 1 : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Power Bands Component
|
||||
* Renders the SVG to simulate in-game power bands
|
||||
*/
|
||||
export default class PowerBands extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -28,6 +39,11 @@ export default class PowerBands extends TranslatedComponent {
|
||||
code: React.PropTypes.string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
this.wattScale = d3.scale.linear();
|
||||
@@ -38,6 +54,7 @@ export default class PowerBands extends TranslatedComponent {
|
||||
this._updateDimensions = this._updateDimensions.bind(this);
|
||||
this._updateScales = this._updateScales.bind(this);
|
||||
this._selectNone = this._selectNone.bind(this);
|
||||
this._hidetip = () => this.context.tooltip();
|
||||
|
||||
let maxBand = props.bands[props.bands.length - 1];
|
||||
|
||||
@@ -52,13 +69,18 @@ export default class PowerBands extends TranslatedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
_updateDimensions(props, size) {
|
||||
let barHeight = Math.round(20 * size);
|
||||
/**
|
||||
* Update dimensions based on properties and scale
|
||||
* @param {Object} props React Component properties
|
||||
* @param {number} scale size ratio / scale
|
||||
*/
|
||||
_updateDimensions(props, scale) {
|
||||
let barHeight = Math.round(20 * scale);
|
||||
let innerHeight = (barHeight * 2) + 2;
|
||||
let mTop = Math.round(25 * size);
|
||||
let mBottom = Math.round(25 * size);
|
||||
let mLeft = Math.round(45 * size);
|
||||
let mRight = Math.round(140 * size);
|
||||
let mTop = Math.round(25 * scale);
|
||||
let mBottom = Math.round(25 * scale);
|
||||
let mLeft = Math.round(45 * scale);
|
||||
let mRight = Math.round(140 * scale);
|
||||
let innerWidth = props.width - mLeft - mRight;
|
||||
|
||||
this._updateScales(innerWidth, this.state.maxPwr, props.available);
|
||||
@@ -77,6 +99,9 @@ export default class PowerBands extends TranslatedComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Select no bands
|
||||
*/
|
||||
_selectNone() {
|
||||
this.setState({
|
||||
ret : {},
|
||||
@@ -84,6 +109,10 @@ export default class PowerBands extends TranslatedComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a retracted band
|
||||
* @param {number} index Band index
|
||||
*/
|
||||
_selectRet(index) {
|
||||
let ret = this.state.ret;
|
||||
if(ret[index]) {
|
||||
@@ -95,6 +124,10 @@ export default class PowerBands extends TranslatedComponent {
|
||||
this.setState({ ret: Object.assign({}, ret) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a deployed band
|
||||
* @param {number} index Band index
|
||||
*/
|
||||
_selectDep(index) {
|
||||
let dep = this.state.dep;
|
||||
|
||||
@@ -107,31 +140,45 @@ export default class PowerBands extends TranslatedComponent {
|
||||
this.setState({ dep: Object.assign({}, dep) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update scale
|
||||
* @param {number} innerWidth SVG innerwidth
|
||||
* @param {number} maxPwr Maximum power level MJ (deployed or available)
|
||||
* @param {number} available Available power MJ
|
||||
*/
|
||||
_updateScales(innerWidth, maxPwr, available) {
|
||||
this.wattScale.range([0, innerWidth]).domain([0,maxPwr]).clamp(true);
|
||||
this.wattScale.range([0, innerWidth]).domain([0, maxPwr]).clamp(true);
|
||||
this.pctScale.range([0, innerWidth]).domain([0, maxPwr / available]).clamp(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on property and context changes
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextContext Incoming/Next context
|
||||
*/
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
let { innerWidth, maxPwr } = this.state;
|
||||
let { language, sizeRatio } = this.context;
|
||||
let maxBand = nextProps.bands[nextProps.bands.length - 1];
|
||||
let nextMaxPwr = Math.max(nextProps.available, maxBand.retractedSum, maxBand.deployedSum);
|
||||
|
||||
if (maxPwr != nextMaxPwr) { // Update Axes if max power has changed
|
||||
this._updateScales(innerWidth, nextMaxPwr, nextProps.available);
|
||||
this.setState({ maxPwr: nextMaxPwr });
|
||||
}
|
||||
|
||||
if (this.context !== nextContext) {
|
||||
if (language !== nextContext.language) {
|
||||
this.wattAxis.tickFormat(nextContext.language.formats.r2);
|
||||
this.pctAxis.tickFormat(nextContext.language.formats.rPct);
|
||||
}
|
||||
|
||||
if (nextProps.width != this.props.width || this.context !== nextContext) {
|
||||
if (maxPwr != nextMaxPwr) { // Update Axes if max power has changed
|
||||
this._updateScales(innerWidth, nextMaxPwr, nextProps.available);
|
||||
this.setState({ maxPwr: nextMaxPwr });
|
||||
} else if (nextProps.width != this.props.width || sizeRatio != nextContext.sizeRatio) {
|
||||
this._updateDimensions(nextProps, nextContext.sizeRatio);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the power bands
|
||||
* @return {React.Component} Power bands
|
||||
*/
|
||||
render() {
|
||||
if (!this.props.width) {
|
||||
return null;
|
||||
@@ -142,7 +189,7 @@ export default class PowerBands extends TranslatedComponent {
|
||||
let { f2, pct1, rPct, r2 } = formats; // wattFmt, pctFmt, pctAxis, wattAxis
|
||||
let { available, bands, width } = props;
|
||||
let { innerWidth, maxPwr, ret, dep } = state;
|
||||
let pwrWarningClass = cn('threshold', {exceeded: bands[0].retractedSum * 2 >= available });
|
||||
let pwrWarningClass = cn('threshold', { exceeded: bands[0].retractedSum * 2 >= available });
|
||||
let deployed = [];
|
||||
let retracted = [];
|
||||
let retSelected = Object.keys(ret).length > 0;
|
||||
@@ -150,7 +197,7 @@ export default class PowerBands extends TranslatedComponent {
|
||||
let retSum = 0;
|
||||
let depSum = 0;
|
||||
|
||||
for (var i = 0; i < bands.length; i++) {
|
||||
for (let i = 0; i < bands.length; i++) {
|
||||
let b = bands[i];
|
||||
retSum += (!retSelected || ret[i]) ? b.retracted : 0;
|
||||
depSum += (!depSelected || dep[i]) ? b.deployed + b.retracted : 0;
|
||||
@@ -217,14 +264,13 @@ export default class PowerBands extends TranslatedComponent {
|
||||
<g className='power-band'>{deployed}</g>
|
||||
<g ref={ (elem) => d3.select(elem).call(this.wattAxis) } className='watt axis'></g>
|
||||
<g ref={ (elem) => {
|
||||
let axis = d3.select(elem);
|
||||
axis.call(this.pctAxis);
|
||||
axis.select('g:nth-child(6)').selectAll('line, text').attr('class', pwrWarningClass);
|
||||
}}
|
||||
className='pct axis' transform={`translate(0,${state.innerHeight})`}></g>
|
||||
let axis = d3.select(elem);
|
||||
axis.call(this.pctAxis);
|
||||
axis.select('g:nth-child(6)').selectAll('line, text').attr('class', pwrWarningClass);
|
||||
}} className='pct axis' transform={`translate(0,${state.innerHeight})`}></g>
|
||||
<line x1={pctScale(0.5)} x2={pctScale(0.5)} y1='0' y2={state.innerHeight} className={pwrWarningClass} />
|
||||
<text dy='0.5em' x='-3' y={state.retY} className='primary upp' textAnchor='end'>{translate('ret')}</text>
|
||||
<text dy='0.5em' x='-3' y={state.depY} className='primary upp' textAnchor='end'>{translate('dep')}</text>
|
||||
<text dy='0.5em' x='-3' y={state.retY} className='primary upp' textAnchor='end' onMouseOver={this.context.termtip.bind(null, 'retracted')} onMouseLeave={this._hidetip}>{translate('ret')}</text>
|
||||
<text dy='0.5em' x='-3' y={state.depY} className='primary upp' textAnchor='end' onMouseOver={this.context.termtip.bind(null, 'deployed', 's')} onMouseLeave={this._hidetip}>{translate('dep')}</text>
|
||||
<text dy='0.5em' x={innerWidth + 5} y={state.retY} className={getClass(retSelected, retSum, available)}>{f2(Math.max(0, retSum)) + ' (' + pct1(Math.max(0, retSum / available)) + ')'}</text>
|
||||
<text dy='0.5em' x={innerWidth + 5} y={state.depY} className={getClass(depSelected, depSum, available)}>{f2(Math.max(0, depSum)) + ' (' + pct1(Math.max(0, depSum / available)) + ')'}</text>
|
||||
</g>
|
||||
|
||||
@@ -2,9 +2,8 @@ import React from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import PowerBands from './PowerBands';
|
||||
import { slotName, nameComparator } from '../utils/SlotFunctions';
|
||||
import { slotName, slotComparator } from '../utils/SlotFunctions';
|
||||
import { Power, NoPower } from './SvgIcons';
|
||||
|
||||
const POWER = [
|
||||
@@ -14,6 +13,9 @@ const POWER = [
|
||||
<Power className='secondary-disabled' />
|
||||
];
|
||||
|
||||
/**
|
||||
* Power Management Section
|
||||
*/
|
||||
export default class PowerManagement extends TranslatedComponent {
|
||||
static PropTypes = {
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
@@ -21,6 +23,10 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._renderPowerRows = this._renderPowerRows.bind(this);
|
||||
@@ -34,51 +40,77 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sort order and sort
|
||||
* @param {string} predicate Sort predicate
|
||||
*/
|
||||
_sortOrder(predicate) {
|
||||
let desc = this.state.desc;
|
||||
|
||||
if (predicate == this.state.predicate) {
|
||||
desc = !desc;
|
||||
|
||||
} else {
|
||||
desc = true;
|
||||
}
|
||||
|
||||
this._sort(this.props.ship, predicate, desc);
|
||||
this.setState({ predicate, desc });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the power list
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {string} predicate Sort predicate
|
||||
* @param {Boolean} desc Sort order descending
|
||||
*/
|
||||
_sort(ship, predicate, desc) {
|
||||
let powerList = ship.powerList;
|
||||
let comp = slotComparator.bind(null, this.context.language.translate);
|
||||
|
||||
switch (predicate) {
|
||||
case 'n': powerList.sort(nameComparator(this.context.language.translate)); break;
|
||||
case 't': powerList.sort((a, b) => a.type.localeCompare(b.type)); break;
|
||||
case 'pri': powerList.sort((a, b) => a.priority - b.priority);break;
|
||||
case 'pwr': powerList.sort((a, b) => (a.m ? a.m.power : 0) - (b.m ? b.m.power : 0)); break;
|
||||
case 'r': powerList.sort((a, b) => ship.getSlotStatus(a) - ship.getSlotStatus(b)); break;
|
||||
case 'd': powerList.sort((a, b) => ship.getSlotStatus(a, true) - ship.getSlotStatus(b, true)); break;
|
||||
case 'n': comp = comp(null, desc); break;
|
||||
case 't': comp = comp((a, b) => a.type.localeCompare(b.type), desc); break;
|
||||
case 'pri': comp = comp((a, b) => a.priority - b.priority, desc); break;
|
||||
case 'pwr': comp = comp((a, b) => a.m.power - b.m.power, desc); break;
|
||||
case 'r': comp = comp((a, b) => ship.getSlotStatus(a) - ship.getSlotStatus(b), desc); break;
|
||||
case 'd': comp = comp((a, b) => ship.getSlotStatus(a, true) - ship.getSlotStatus(b, true), desc); break;
|
||||
}
|
||||
|
||||
if (!desc) {
|
||||
powerList.reverse();
|
||||
}
|
||||
powerList.sort(comp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update slot priority
|
||||
* @param {Object} slot Slot model
|
||||
* @param {number} inc increment / decrement
|
||||
*/
|
||||
_priority(slot, inc) {
|
||||
if (this.props.ship.setSlotPriority(slot, slot.priority + inc)) {
|
||||
this.props.onChange();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle slot active/inactive
|
||||
* @param {Object} slot Slot model
|
||||
*/
|
||||
_toggleEnabled(slot) {
|
||||
this.props.ship.setSlotEnabled(slot, !slot.enabled);
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate/Render table rows
|
||||
* @param {Ship} ship Ship instance
|
||||
* @param {Function} translate Translate function
|
||||
* @param {Function} pwr Localized Power formatter
|
||||
* @param {Function} pct Localized Percent formatter
|
||||
* @return {Array} Array of React.Component table rows
|
||||
*/
|
||||
_renderPowerRows(ship, translate, pwr, pct) {
|
||||
|
||||
let powerRows = [];
|
||||
|
||||
for (var i = 0, l = ship.powerList.length; i < l; i++) {
|
||||
for (let i = 0, l = ship.powerList.length; i < l; i++) {
|
||||
let slot = ship.powerList[i];
|
||||
|
||||
if (slot.m && slot.m.power) {
|
||||
@@ -102,7 +134,7 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
{' ' + (slot.priority + 1) + ' '}
|
||||
<span className='ptr' onClick={this._priority.bind(this, slot, 1)}>►</span>
|
||||
</td>
|
||||
<td className='ri ptr' style={{ width: '3.25em'}} onClick={toggleEnabled}>{pwr(m.power)}</td>
|
||||
<td className='ri ptr' style={{ width: '3.25em' }} onClick={toggleEnabled}>{pwr(m.power)}</td>
|
||||
<td className='ri ptr' style={{ width: '3em' }} onClick={toggleEnabled}><u>{pct(m.power / ship.powerAvailable)}</u></td>
|
||||
{retractedElem}
|
||||
{deployedElem}
|
||||
@@ -112,32 +144,50 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
return powerRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update power bands width from DOM
|
||||
*/
|
||||
_updateWidth() {
|
||||
this.setState({ width: findDOMNode(this).offsetWidth });
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
/**
|
||||
* Add listeners when about to mount and sort power list
|
||||
*/
|
||||
componentWillMount() {
|
||||
this._sort(this.props.ship, this.state.predicate, this.state.desc);
|
||||
this.resizeListener = InterfaceEvents.addListener('windowResized', this._updateWidth);
|
||||
this.resizeListener = this.context.onWindowResize(this._updateWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger DOM updates on mount
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._updateWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort power list if the ship instance has changed
|
||||
* @param {Object} nextProps Incoming/Next properties
|
||||
* @param {Object} nextState Incoming/Next state
|
||||
*/
|
||||
componentWillUpdate(nextProps, nextState) {
|
||||
// Can optimize this later: only sort when
|
||||
// - predicate/desc changes
|
||||
// - modules/language change AND sorting by type, name
|
||||
// - power changes and sorting by pwr
|
||||
// - enabled/disabled changes and sorting by priority
|
||||
this._sort(nextProps.ship, nextState.predicate, nextState.desc);
|
||||
if (this.props.ship != nextProps.ship) {
|
||||
this._sort(nextProps.ship, nextState.predicate, nextState.desc);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
/**
|
||||
* Remove listeners on unmount
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this.resizeListener.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render power management section
|
||||
* @return {React.Component} contents
|
||||
*/
|
||||
render() {
|
||||
let { ship, code } = this.props;
|
||||
let { translate, formats } = this.context.language;
|
||||
|
||||
@@ -4,86 +4,91 @@ import cn from 'classnames';
|
||||
import { SizeMap } from '../shipyard/Constants';
|
||||
import { Warning } from './SvgIcons';
|
||||
|
||||
/**
|
||||
* Ship Summary Table / Stats
|
||||
*/
|
||||
export default class ShipSummaryTable extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
ship: React.PropTypes.object.isRequired
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the table
|
||||
* @return {React.Component} Summary table
|
||||
*/
|
||||
render() {
|
||||
let ship = this.props.ship;
|
||||
let language = this.context.language;
|
||||
let { language, tooltip, termtip } = this.context;
|
||||
let translate = language.translate;
|
||||
let u = language.units;
|
||||
let formats = language.formats;
|
||||
let round = formats.round;
|
||||
let int = formats.int;
|
||||
let armourDetails = null;
|
||||
let hide = tooltip.bind(null, null);
|
||||
|
||||
if (ship.armourMultiplier > 1 || ship.armourAdded) {
|
||||
armourDetails = <u>({
|
||||
(ship.armourMultiplier > 1 ? formats.rPct(ship.armourMultiplier) : '')
|
||||
+ (ship.armourAdded ? ' + ' + ship.armourAdded : '')
|
||||
(ship.armourMultiplier > 1 ? formats.rPct(ship.armourMultiplier) : '') +
|
||||
(ship.armourAdded ? ' + ' + ship.armourAdded : '')
|
||||
})</u>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='summary'>
|
||||
<table id='summaryTable'>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<th rowSpan={2}>{translate('size')}</th>
|
||||
<th rowSpan={2}>{translate('agility')}</th>
|
||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canThrust() }) }>{translate('speed')}</th>
|
||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canBoost() }) }>{translate('boost')}</th>
|
||||
<th rowSpan={2}>{translate('DPS')}</th>
|
||||
<th rowSpan={2}>{translate('armour')}</th>
|
||||
<th rowSpan={2}>{translate('shields')}</th>
|
||||
<th colSpan={3}>{translate('mass')}</th>
|
||||
<th rowSpan={2}>{translate('cargo')}</th>
|
||||
<th rowSpan={2}>{translate('fuel')}</th>
|
||||
<th colSpan={3}>{translate('jump range')}</th>
|
||||
<th colSpan={3}>{translate('total range')}</th>
|
||||
<th rowSpan={2}>{translate('lock factor')}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className='lft'>{translate('hull')}</th>
|
||||
<th>{translate('unladen')}</th>
|
||||
<th>{translate('laden')}</th>
|
||||
<th className='lft'>{translate('max')}</th>
|
||||
<th>{translate('full tank')}</th>
|
||||
<th>{translate('laden')}</th>
|
||||
<th className='lft'>{translate('jumps')}</th>
|
||||
<th>{translate('unladen')}</th>
|
||||
<th>{translate('laden')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className='cap'>{translate(SizeMap[ship.class])}</td>
|
||||
<td>{ship.agility}/10</td>
|
||||
<td>{ ship.canThrust() ? <span>{int(ship.topSpeed)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td>{ ship.canBoost() ? <span>{int(ship.topBoost)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td>{round(ship.totalDps)}</td>
|
||||
<td>{int(ship.armour)} {armourDetails}</td>
|
||||
<td>{int(ship.shieldStrength)} {u.MJ} { ship.shieldMultiplier > 1 && ship.shieldStrength > 0 ? <u>({formats.rPct(ship.shieldMultiplier)})</u> : null }</td>
|
||||
<td>{ship.hullMass} {u.T}</td>
|
||||
<td>{round(ship.unladenMass)} {u.T}</td>
|
||||
<td>{round(ship.ladenMass)} {u.T}</td>
|
||||
<td>{round(ship.cargoCapacity)} {u.T}</td>
|
||||
<td>{round(ship.fuelCapacity)} {u.T}</td>
|
||||
<td>{round(ship.unladenRange)} {u.LY}</td>
|
||||
<td>{round(ship.fullTankRange)} {u.LY}</td>
|
||||
<td>{round(ship.ladenRange)} {u.LY}</td>
|
||||
<td>{round(ship.maxJumpCount)}</td>
|
||||
<td>{round(ship.unladenTotalRange)} {u.LY}</td>
|
||||
<td>{round(ship.ladenTotalRange)} {u.LY}</td>
|
||||
<td>{ship.masslock}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
return <div id='summary'>
|
||||
<table id='summaryTable'>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<th rowSpan={2}>{translate('size')}</th>
|
||||
<th onMouseEnter={termtip.bind(null, 'maneuverability')} onMouseLeave={hide} rowSpan={2}>{translate('MNV')}</th>
|
||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canThrust() }) }>{translate('speed')}</th>
|
||||
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canBoost() }) }>{translate('boost')}</th>
|
||||
<th onMouseOver={termtip.bind(null, 'damage per second')} onMouseOut={hide} rowSpan={2}>{translate('DPS')}</th>
|
||||
<th rowSpan={2}>{translate('armour')}</th>
|
||||
<th rowSpan={2}>{translate('shields')}</th>
|
||||
<th colSpan={3}>{translate('mass')}</th>
|
||||
<th rowSpan={2}>{translate('cargo')}</th>
|
||||
<th rowSpan={2}>{translate('fuel')}</th>
|
||||
<th colSpan={3}>{translate('jump range')}</th>
|
||||
<th colSpan={3}>{translate('total range')}</th>
|
||||
<th onMouseOver={termtip.bind(null, 'mass lock factor')} onMouseOut={hide} rowSpan={2}>{translate('MLF')}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className='lft'>{translate('hull')}</th>
|
||||
<th>{translate('unladen')}</th>
|
||||
<th>{translate('laden')}</th>
|
||||
<th className='lft'>{translate('max')}</th>
|
||||
<th>{translate('full tank')}</th>
|
||||
<th>{translate('laden')}</th>
|
||||
<th className='lft'>{translate('jumps')}</th>
|
||||
<th>{translate('unladen')}</th>
|
||||
<th>{translate('laden')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className='cap'>{translate(SizeMap[ship.class])}</td>
|
||||
<td>{ship.agility}/10</td>
|
||||
<td>{ ship.canThrust() ? <span>{int(ship.topSpeed)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td>{ ship.canBoost() ? <span>{int(ship.topBoost)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td>{round(ship.totalDps)}</td>
|
||||
<td>{int(ship.armour)} {armourDetails}</td>
|
||||
<td>{int(ship.shieldStrength)} {u.MJ} { ship.shieldMultiplier > 1 && ship.shieldStrength > 0 ? <u>({formats.rPct(ship.shieldMultiplier)})</u> : null }</td>
|
||||
<td>{ship.hullMass} {u.T}</td>
|
||||
<td>{round(ship.unladenMass)} {u.T}</td>
|
||||
<td>{round(ship.ladenMass)} {u.T}</td>
|
||||
<td>{round(ship.cargoCapacity)} {u.T}</td>
|
||||
<td>{round(ship.fuelCapacity)} {u.T}</td>
|
||||
<td>{round(ship.unladenRange)} {u.LY}</td>
|
||||
<td>{round(ship.fullTankRange)} {u.LY}</td>
|
||||
<td>{round(ship.ladenRange)} {u.LY}</td>
|
||||
<td>{round(ship.maxJumpCount)}</td>
|
||||
<td>{round(ship.unladenTotalRange)} {u.LY}</td>
|
||||
<td>{round(ship.ladenTotalRange)} {u.LY}</td>
|
||||
<td>{ship.masslock}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Horizontal Slider
|
||||
*/
|
||||
export default class Slider extends React.Component {
|
||||
|
||||
static defaultProps = {
|
||||
@@ -16,6 +19,10 @@ export default class Slider extends React.Component {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -23,27 +30,47 @@ export default class Slider extends React.Component {
|
||||
this.up = this.up.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* On Mouse down handler
|
||||
* @param {SyntheticEvent} e Event
|
||||
*/
|
||||
down(e) {
|
||||
let rect = e.currentTarget.getBoundingClientRect();
|
||||
this.move = this.updatePercent.bind(this, rect.left, rect.width);
|
||||
this.move = this._updatePercent.bind(this, rect.left, rect.width);
|
||||
this.move(e);
|
||||
document.addEventListener("mousemove", this.move);
|
||||
document.addEventListener("mouseup", this.up);
|
||||
document.addEventListener('mousemove', this.move);
|
||||
document.addEventListener('mouseup', this.up);
|
||||
}
|
||||
|
||||
/**
|
||||
* On Mouse up handler
|
||||
*/
|
||||
up() {
|
||||
document.removeEventListener("mousemove", this.move);
|
||||
document.removeEventListener("mouseup", this.up);
|
||||
document.removeEventListener('mousemove', this.move);
|
||||
document.removeEventListener('mouseup', this.up);
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
this.up();
|
||||
}
|
||||
|
||||
updatePercent(left, width, event) {
|
||||
/**
|
||||
* Update the slider percentage
|
||||
* @param {number} left Slider left position
|
||||
* @param {number} width Slider width
|
||||
* @param {Event} event Event
|
||||
*/
|
||||
_updatePercent(left, width, event) {
|
||||
this.props.onChange(Math.min(Math.max((event.clientX - left) / width, 0), 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove listeners on unmount
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the slider
|
||||
* @return {React.Component} The slider
|
||||
*/
|
||||
render() {
|
||||
let pctStr = (this.props.percent * 100) + '%';
|
||||
let { axis, axisUnit, min, max } = this.props;
|
||||
@@ -51,9 +78,9 @@ export default class Slider extends React.Component {
|
||||
|
||||
if (axis) {
|
||||
axisGroup = <g style={{ fontSize: '.7em' }}>
|
||||
<text className='primary-disabled' y="3em" x="0" style={{ textAnchor: 'middle' }}>{min + axisUnit}</text>
|
||||
<text className='primary-disabled' y="3em" x="50%" style={{ textAnchor: 'middle' }}>{(min + max / 2) + axisUnit}</text>
|
||||
<text className='primary-disabled' y="3em" x="99%" style={{ textAnchor: 'middle' }}>{max + axisUnit}</text>
|
||||
<text className='primary-disabled' y='3em' x='0' style={{ textAnchor: 'middle' }}>{min + axisUnit}</text>
|
||||
<text className='primary-disabled' y='3em' x='50%' style={{ textAnchor: 'middle' }}>{(min + max / 2) + axisUnit}</text>
|
||||
<text className='primary-disabled' y='3em' x='99%' style={{ textAnchor: 'middle' }}>{max + axisUnit}</text>
|
||||
</g>;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,12 @@ import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import cn from 'classnames';
|
||||
import AvailableModulesMenu from './AvailableModulesMenu';
|
||||
import { wrapCtxMenu } from '../utils/InterfaceEvents';
|
||||
import { diffDetails } from '../utils/SlotFunctions';
|
||||
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
||||
|
||||
/**
|
||||
* Abstract Slot
|
||||
*/
|
||||
export default class Slot extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -13,10 +17,17 @@ export default class Slot extends TranslatedComponent {
|
||||
maxClass: React.PropTypes.number.isRequired,
|
||||
selected: React.PropTypes.bool,
|
||||
m: React.PropTypes.object,
|
||||
shipMass: React.PropTypes.number,
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
warning: React.PropTypes.func,
|
||||
drag: React.PropTypes.func,
|
||||
drop: React.PropTypes.func,
|
||||
dropClass: React.PropTypes.string
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -27,6 +38,11 @@ export default class Slot extends TranslatedComponent {
|
||||
// Must be implemented by subclasses:
|
||||
// _getSlotDetails()
|
||||
|
||||
/**
|
||||
* Get the CSS class name for the slot. Can/should be overriden
|
||||
* as necessary.
|
||||
* @return {string} CSS Class name
|
||||
*/
|
||||
_getClassNames() {
|
||||
return null;
|
||||
}
|
||||
@@ -40,14 +56,22 @@ export default class Slot extends TranslatedComponent {
|
||||
return this.props.maxClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty slot on right-click
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_contextMenu(event) {
|
||||
this.props.onSelect(null,null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the slot
|
||||
* @return {React.Component} The slot
|
||||
*/
|
||||
render() {
|
||||
let language = this.context.language;
|
||||
let translate = language.translate;
|
||||
let m = this.props.m;
|
||||
let { ship, m, dropClass, dragOver, onOpen, selected, onSelect, warning, shipMass, availableModules } = this.props;
|
||||
let slotDetails, menu;
|
||||
|
||||
if (m) {
|
||||
@@ -59,18 +83,19 @@ export default class Slot extends TranslatedComponent {
|
||||
if (this.props.selected) {
|
||||
menu = <AvailableModulesMenu
|
||||
className={this._getClassNames()}
|
||||
modules={this.props.availableModules()}
|
||||
shipMass={this.props.shipMass}
|
||||
modules={availableModules()}
|
||||
shipMass={ship.hullMass}
|
||||
m={m}
|
||||
onSelect={this.props.onSelect}
|
||||
warning={this.props.warning}
|
||||
onSelect={onSelect}
|
||||
warning={warning}
|
||||
diffDetails={diffDetails.bind(ship, this.context.language)}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('slot', {selected: this.props.selected})} onClick={this.props.onOpen} onContextMenu={this._contextMenu}>
|
||||
<div className={'details'}>
|
||||
<div className={'sz'}>{this._getMaxClassLabel(translate)}</div>
|
||||
<div className={cn('slot', dropClass, { selected })} onClick={onOpen} onContextMenu={this._contextMenu} onDragOver={dragOver}>
|
||||
<div className='details-container'>
|
||||
<div className='sz'>{this._getMaxClassLabel(translate)}</div>
|
||||
{slotDetails}
|
||||
</div>
|
||||
{menu}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import { wrapCtxMenu } from '../utils/InterfaceEvents';
|
||||
import { wrapCtxMenu } from '../utils/UtilityFunctions';
|
||||
import { Equalizer } from '../components/SvgIcons';
|
||||
import cn from 'classnames';
|
||||
|
||||
/**
|
||||
* Abstract Slot Section
|
||||
*/
|
||||
export default class SlotSection extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -14,6 +16,13 @@ export default class SlotSection extends TranslatedComponent {
|
||||
togglePwr: React.PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
* @param {string} sectionId Section DOM Id
|
||||
* @param {string} sectionName Section name
|
||||
*/
|
||||
constructor(props, context, sectionId, sectionName) {
|
||||
super(props);
|
||||
this.sectionId = sectionId;
|
||||
@@ -23,7 +32,10 @@ export default class SlotSection extends TranslatedComponent {
|
||||
this._selectModule = this._selectModule.bind(this);
|
||||
this._getSectionMenu = this._getSectionMenu.bind(this);
|
||||
this._contextMenu = this._contextMenu.bind(this);
|
||||
this._drop = this._drop.bind(this);
|
||||
this._dragOverNone = this._dragOverNone.bind(this);
|
||||
this._close = this._close.bind(this);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
// Must be implemented by subclasses:
|
||||
@@ -31,33 +43,143 @@ export default class SlotSection extends TranslatedComponent {
|
||||
// _getSectionMenu()
|
||||
// _contextMenu()
|
||||
|
||||
/**
|
||||
* Open a menu
|
||||
* @param {string} menu Menu name
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_openMenu(menu, event) {
|
||||
event.stopPropagation();
|
||||
if (this.props.currentMenu === menu) {
|
||||
menu = null;
|
||||
}
|
||||
|
||||
InterfaceEvents.openMenu(menu);
|
||||
this.context.openMenu(menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount/Use the specified module in the slot
|
||||
* @param {Object} slot Slot
|
||||
* @param {Object} m Selected module
|
||||
*/
|
||||
_selectModule(slot, m) {
|
||||
this.props.ship.use(slot, m);
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Slot Drag Handler
|
||||
* @param {object} originSlot Origin slot model
|
||||
* @param {Event} e Drag Event
|
||||
*/
|
||||
_drag(originSlot, e) {
|
||||
e.dataTransfer.setData('text/html', e.currentTarget);
|
||||
e.dataTransfer.effectAllowed = 'all';
|
||||
this.setState({ originSlot });
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Slot Drag Over Handler
|
||||
* @param {object} targetSlot Potential drop target
|
||||
* @param {Event} e Drag Event
|
||||
*/
|
||||
_dragOverSlot(targetSlot, e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
let os = this.state.originSlot;
|
||||
if (os) {
|
||||
console.log('has origin');
|
||||
e.dataTransfer.dropEffect = os != targetSlot && targetSlot.maxClass >= os.m.class ? 'copyMove' : 'none';
|
||||
this.setState({ targetSlot });
|
||||
} else {
|
||||
e.dataTransfer.dropEffect = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag over non-droppable target/element
|
||||
* @param {Event} e Drag Event
|
||||
*/
|
||||
_dragOverNone(e) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'none';
|
||||
this.setState({ targetSlot: null });
|
||||
}
|
||||
|
||||
/**
|
||||
* Slot drop handler. If the target is eligible swap the origin and target modules.
|
||||
* If the target slot's current module cannot be mounted in the origin slot then
|
||||
* the origin slot will be empty.
|
||||
*/
|
||||
_drop() {
|
||||
let { originSlot, targetSlot } = this.state;
|
||||
let m = originSlot.m;
|
||||
|
||||
if (targetSlot && m && targetSlot.maxClass >= m.class) {
|
||||
// Swap modules if possible
|
||||
if (targetSlot.m && originSlot.maxClass >= targetSlot.m.class) {
|
||||
this.props.ship.use(originSlot, targetSlot.m, true);
|
||||
} else { // Otherwise empty the origin slot
|
||||
this.props.ship.use(originSlot, null, true); // Empty but prevent summary update
|
||||
}
|
||||
this.props.ship.use(targetSlot, m); // update target slot
|
||||
this.props.onChange();
|
||||
}
|
||||
this.setState({ originSlot: null, targetSlot: null });
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine drop eligibilty CSS class
|
||||
* @param {Object} slot Current slot
|
||||
* @param {Object} originSlot Origin slot
|
||||
* @param {Object} targetSlot Target slot
|
||||
* @return {string} CSS Class name
|
||||
*/
|
||||
_dropClass(slot, originSlot, targetSlot) {
|
||||
if (!originSlot) {
|
||||
return null;
|
||||
}
|
||||
if (slot === originSlot) {
|
||||
if (targetSlot && targetSlot.m && originSlot.maxClass < targetSlot.m.class) {
|
||||
return 'dropEmpty'; // Origin slot will be emptied
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (originSlot.m && slot.maxClass >= originSlot.m.class) { // Eligble drop slot
|
||||
if (slot === targetSlot) {
|
||||
return 'drop'; // Can drop
|
||||
}
|
||||
return 'eligible'; // Potential drop slot
|
||||
}
|
||||
|
||||
return 'ineligible'; // Cannot be dropped / invalid drop slot
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle slot Active/Inactive
|
||||
* @param {Object} slot Slot
|
||||
*/
|
||||
_togglePwr(slot) {
|
||||
this.props.ship.setSlotEnabled(slot, !slot.enabled);
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close current menu
|
||||
*/
|
||||
_close() {
|
||||
if (this.props.currentMenu) {
|
||||
InterfaceEvents.closeMenu();
|
||||
this.context.closeMenu();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the slot section
|
||||
* @return {React.Component} Slot section
|
||||
*/
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
let sectionMenuOpened = this.props.currentMenu === this.sectionName;
|
||||
@@ -65,8 +187,8 @@ export default class SlotSection extends TranslatedComponent {
|
||||
let ctx = wrapCtxMenu(this._contextMenu);
|
||||
|
||||
return (
|
||||
<div id={this.sectionId} className={'group'}>
|
||||
<div className={cn('section-menu', {selected: sectionMenuOpened})} onClick={open} onContextMenu={ctx}>
|
||||
<div id={this.sectionId} className={'group'} onDragLeave={this._dragOverNone}>
|
||||
<div className={cn('section-menu', { selected: sectionMenuOpened })} onClick={open} onContextMenu={ctx}>
|
||||
<h1>{translate(this.sectionName)} <Equalizer/></h1>
|
||||
{sectionMenuOpened ? this._getSectionMenu(translate) : null }
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { jumpRange } from '../shipyard/Calculations';
|
||||
import { diffDetails } from '../utils/SlotFunctions';
|
||||
import AvailableModulesMenu from './AvailableModulesMenu';
|
||||
|
||||
/**
|
||||
* Standard Slot
|
||||
*/
|
||||
export default class StandardSlot extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -10,46 +15,51 @@ export default class StandardSlot extends TranslatedComponent {
|
||||
modules: React.PropTypes.array.isRequired,
|
||||
onSelect: React.PropTypes.func.isRequired,
|
||||
onOpen: React.PropTypes.func.isRequired,
|
||||
ship: React.PropTypes.object.isRequired,
|
||||
selected: React.PropTypes.bool,
|
||||
shipMass: React.PropTypes.number,
|
||||
warning: React.PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the slot
|
||||
* @return {React.Component} Slot component
|
||||
*/
|
||||
render() {
|
||||
let { translate, formats, units } = this.context.language;
|
||||
let { slot, warning } = this.props;
|
||||
let { modules, slot, warning, onSelect, ladenMass, ship } = this.props;
|
||||
let m = slot.m;
|
||||
let classRating = m.class + m.rating;
|
||||
let menu;
|
||||
|
||||
if (this.props.selected) {
|
||||
menu = <AvailableModulesMenu
|
||||
modules={this.props.modules}
|
||||
shipMass={this.props.shipMass}
|
||||
modules={modules}
|
||||
shipMass={ship.ladenMass}
|
||||
m={m}
|
||||
onSelect={this.props.onSelect}
|
||||
warning={this.props.warning}
|
||||
onSelect={onSelect}
|
||||
warning={warning}
|
||||
diffDetails={diffDetails.bind(ship, this.context.language)}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('slot', {selected: this.props.selected})} onClick={this.props.onOpen}>
|
||||
<div className={cn('details', {warning: warning && warning(slot.m)})}>
|
||||
<div className={cn('slot', { selected: this.props.selected })} onClick={this.props.onOpen}>
|
||||
<div className={cn('details-container', { warning: warning && warning(slot.m) })}>
|
||||
<div className={'sz'}>{slot.maxClass}</div>
|
||||
<div>
|
||||
<div className={'l'}>{classRating + ' ' + translate(m.grp)}</div>
|
||||
<div className={'r'}>{m.mass || m.capacity}{units.T}</div>
|
||||
<div className='l'>{classRating + ' ' + translate(m.grp)}</div>
|
||||
<div className={'r'}>{m.mass || m.fuel}{units.T}</div>
|
||||
<div className={'cb'}>
|
||||
{ m.optmass ? <div className={'l'}>{translate('optimal mass') + ': '}{m.optmass}{units.T}</div> : null }
|
||||
{ m.maxmass ? <div className={'l'}>{translate('max mass') + ': '}{m.maxmass}{units.T}</div> : null }
|
||||
{ m.range ? <div className={'l'}>{translate('range')}: {m.range}{units.km}</div> : null }
|
||||
{ m.time ? <div className={'l'}>{translate('time')}: {formats.time(m.time)}</div> : null }
|
||||
{ m.eff ? <div className={'l'}>{translate('efficiency')}: {m.eff}</div> : null }
|
||||
{ m.pGen ? <div className={'l'}>{translate('power')}: {m.pGen}{units.MW}</div> : null }
|
||||
{ m.maxfuel ? <div className={'l'}>{translate('max') + ' ' + translate('fuel') + ': '}{m.maxfuel}{units.T}</div> : null }
|
||||
{ m.weaponcapacity ? <div className={'l'}>{translate('WEP')}: {m.weaponcapacity}{units.MJ} / {m.weaponrecharge}{units.MW}</div> : null }
|
||||
{ m.systemcapacity ? <div className={'l'}>{translate('SYS')}: {m.systemcapacity}{units.MJ} / {m.systemrecharge}{units.MW}</div> : null }
|
||||
{ m.enginecapacity ? <div className={'l'}>{translate('ENG')}: {m.enginecapacity}{units.MJ} / {m.enginerecharge}{units.MW}</div> : null }
|
||||
{ m.optmass ? <div className='l'>{translate('optimal mass') + ': '}{m.optmass}{units.T}</div> : null }
|
||||
{ m.maxmass ? <div className='l'>{translate('max mass') + ': '}{m.maxmass}{units.T}</div> : null }
|
||||
{ m.range ? <div className='l'>{translate('range')}: {m.range}{units.km}</div> : null }
|
||||
{ m.time ? <div className='l'>{translate('time')}: {formats.time(m.time)}</div> : null }
|
||||
{ m.eff ? <div className='l'>{translate('efficiency')}: {m.eff}</div> : null }
|
||||
{ m.pGen ? <div className='l'>{translate('power')}: {m.pGen}{units.MW}</div> : null }
|
||||
{ m.maxfuel ? <div className='l'>{translate('max') + ' ' + translate('fuel') + ': '}{m.maxfuel}{units.T}</div> : null }
|
||||
{ m.weaponcapacity ? <div className='l'>{translate('WEP')}: {m.weaponcapacity}{units.MJ} / {m.weaponrecharge}{units.MW}</div> : null }
|
||||
{ m.systemcapacity ? <div className='l'>{translate('SYS')}: {m.systemcapacity}{units.MJ} / {m.systemrecharge}{units.MW}</div> : null }
|
||||
{ m.enginecapacity ? <div className='l'>{translate('ENG')}: {m.enginecapacity}{units.MJ} / {m.enginerecharge}{units.MW}</div> : null }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,41 +2,61 @@ import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import SlotSection from './SlotSection';
|
||||
import StandardSlot from './StandardSlot';
|
||||
import { diffDetails } from '../utils/SlotFunctions';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
|
||||
/**
|
||||
* Standard Slot section
|
||||
*/
|
||||
export default class StandardSlotSection extends SlotSection {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context, 'standard', 'standard');
|
||||
|
||||
this._optimizeStandard = this._optimizeStandard.bind(this);
|
||||
this._optimizeCargo = this._optimizeCargo.bind(this);
|
||||
this._optimizeExplorer = this._optimizeExplorer.bind(this);
|
||||
this._hideDiff = this._hideDiff.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill all standard slots with the specificed rating (using max class)
|
||||
* @param {String} rating [A-E]
|
||||
*/
|
||||
_fill(rating) {
|
||||
this.props.ship.useStandard(rating);
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the lightest/optimal available standard modules
|
||||
*/
|
||||
_optimizeStandard() {
|
||||
this.props.ship.useLightestStandard();
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trader build
|
||||
*/
|
||||
_optimizeCargo() {
|
||||
let ship = this.props.ship;
|
||||
ship.internal.forEach((slot) => {
|
||||
var id = ModuleUtils.findInternalId('cr', slot.maxClass, 'E');
|
||||
ship.use(slot, ModuleUtils.internal(id));
|
||||
});
|
||||
ship.internal.forEach((slot) => ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E')));
|
||||
ship.useLightestStandard();
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Explorer build
|
||||
*/
|
||||
_optimizeExplorer() {
|
||||
let ship = this.props.ship,
|
||||
intLength = ship.internal.length,
|
||||
@@ -68,7 +88,6 @@ export default class StandardSlotSection extends SlotSection {
|
||||
let am = ModuleUtils.findInternal('am', slot.maxClass, afmUnitCount ? 'B' : 'A');
|
||||
ship.use(slot, am);
|
||||
ship.setSlotEnabled(slot, false); // Disabled power for AFM Unit
|
||||
|
||||
} else {
|
||||
ship.use(slot, null);
|
||||
}
|
||||
@@ -98,46 +117,89 @@ export default class StandardSlotSection extends SlotSection {
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the specified bulkhead
|
||||
* @param {number} bulkheadIndex 0 - 4
|
||||
*/
|
||||
_selectBulkhead(bulkheadIndex) {
|
||||
this.props.ship.useBulkhead(bulkheadIndex);
|
||||
this.context.tooltip();
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* On right click optimize the standard modules
|
||||
*/
|
||||
_contextMenu() {
|
||||
this._optimizeStandard();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the bulkhead diff tooltip
|
||||
* @param {number} bhIndex Potential Bulkhead alternative
|
||||
* @param {SyntheticEvent} event Event
|
||||
*/
|
||||
_bhDiff(bhIndex, event) {
|
||||
let ship = this.props.ship;
|
||||
this.context.tooltip(
|
||||
diffDetails.call(ship, this.context.language, ModuleUtils.bulkheads(ship.id, bhIndex), ship.bulkheads.m),
|
||||
event.currentTarget.getBoundingClientRect()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the diff tooltip
|
||||
*/
|
||||
_hideDiff() {
|
||||
this.context.tooltip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the slot React Components
|
||||
* @return {Array} Array of Slots
|
||||
*/
|
||||
_getSlots() {
|
||||
let { translate, units } = this.context.language;
|
||||
let { ship, currentMenu } = this.props;
|
||||
let slots = new Array(8);
|
||||
let open = this._openMenu;
|
||||
let select = this._selectModule;
|
||||
let selBulkhead = this._selectBulkhead;
|
||||
let ship = this.props.ship
|
||||
let st = ship.standard;
|
||||
let avail = ship.getAvailableModules().standard;
|
||||
let bulkheads = ship.bulkheads;
|
||||
let currentMenu = this.props.currentMenu;
|
||||
let bh = ship.bulkheads;
|
||||
|
||||
slots[0] = (
|
||||
<div key='bh' className={cn('slot', {selected: currentMenu === bulkheads})} onClick={open.bind(this, bulkheads)}>
|
||||
<div className={'details'}>
|
||||
<div className={'sz'}>8</div>
|
||||
<div>
|
||||
<div className={'l'}>{translate('bh')}</div>
|
||||
<div className={'r'}>{bulkheads.m.mass}{units.T}</div>
|
||||
<div className={'cl l'}>{translate(bulkheads.m.name)}</div>
|
||||
<div key='bh' className={cn('slot', { selected: currentMenu === bh })} onClick={open.bind(this, bh)}>
|
||||
<div className={'details-container'}>
|
||||
<div className={'details'}>
|
||||
<div className={'sz'}>8</div>
|
||||
<div>
|
||||
<div className={'l'}>{translate('bh')}</div>
|
||||
<div className={'r'}>{bh.m.mass}{units.T}</div>
|
||||
<div className={'cl l'}>{translate(bh.m.name)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{currentMenu === bulkheads &&
|
||||
{currentMenu === bh &&
|
||||
<div className='select' onClick={ e => e.stopPropagation() }>
|
||||
<ul>
|
||||
<li onClick={selBulkhead.bind(this, 0)} className={cn('lc', { active: bulkheads.id == '0' })}>{translate('Lightweight Alloy')}</li>
|
||||
<li onClick={selBulkhead.bind(this, 1)} className={cn('lc', { active: bulkheads.id == '1' })}>{translate('Reinforced Alloy')}</li>
|
||||
<li onClick={selBulkhead.bind(this, 2)} className={cn('lc', { active: bulkheads.id == '2' })}>{translate('Military Grade Composite')}</li>
|
||||
<li onClick={selBulkhead.bind(this, 3)} className={cn('lc', { active: bulkheads.id == '3' })}>{translate('Mirrored Surface Composite')}</li>
|
||||
<li onClick={selBulkhead.bind(this, 4)} className={cn('lc', { active: bulkheads.id == '4' })}>{translate('Reactive Surface Composite')}</li>
|
||||
<li onClick={selBulkhead.bind(this, 0)} onMouseOver={this._bhDiff.bind(this, 0)} onMouseLeave={this._hideDiff} className={cn('lc', { active: bh.index == 0 })}>
|
||||
{translate('Lightweight Alloy')}
|
||||
</li>
|
||||
<li onClick={selBulkhead.bind(this, 1)} onMouseOver={this._bhDiff.bind(this, 1)} onMouseLeave={this._hideDiff} className={cn('lc', { active: bh.index == 1 })}>
|
||||
{translate('Reinforced Alloy')}
|
||||
</li>
|
||||
<li onClick={selBulkhead.bind(this, 2)} onMouseOver={this._bhDiff.bind(this, 2)} onMouseLeave={this._hideDiff} className={cn('lc', { active: bh.index == 2 })}>
|
||||
{translate('Military Grade Composite')}
|
||||
</li>
|
||||
<li onClick={selBulkhead.bind(this, 3)} onMouseOver={this._bhDiff.bind(this, 3)} onMouseLeave={this._hideDiff} className={cn('lc', { active: bh.index == 3 })}>
|
||||
{translate('Mirrored Surface Composite')}
|
||||
</li>
|
||||
<li onClick={selBulkhead.bind(this, 4)} onMouseOver={this._bhDiff.bind(this, 4)} onMouseLeave={this._hideDiff} className={cn('lc', { active: bh.index == 4 })}>
|
||||
{translate('Reactive Surface Composite')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
@@ -151,6 +213,7 @@ export default class StandardSlotSection extends SlotSection {
|
||||
onOpen={open.bind(this, st[0])}
|
||||
onSelect={select.bind(this, st[0])}
|
||||
selected={currentMenu == st[0]}
|
||||
ship={ship}
|
||||
warning={m => m.pGen < ship.powerRetracted}
|
||||
/>;
|
||||
|
||||
@@ -161,6 +224,7 @@ export default class StandardSlotSection extends SlotSection {
|
||||
onOpen={open.bind(this, st[1])}
|
||||
onSelect={select.bind(this, st[1])}
|
||||
selected={currentMenu == st[1]}
|
||||
ship={ship}
|
||||
warning={m => m.maxmass < ship.ladenMass}
|
||||
/>;
|
||||
|
||||
@@ -171,6 +235,7 @@ export default class StandardSlotSection extends SlotSection {
|
||||
modules={avail[2]}
|
||||
onOpen={open.bind(this, st[2])}
|
||||
onSelect={select.bind(this, st[2])}
|
||||
ship={ship}
|
||||
selected={currentMenu == st[2]}
|
||||
/>;
|
||||
|
||||
@@ -180,6 +245,7 @@ export default class StandardSlotSection extends SlotSection {
|
||||
modules={avail[3]}
|
||||
onOpen={open.bind(this, st[3])}
|
||||
onSelect={select.bind(this, st[3])}
|
||||
ship={ship}
|
||||
selected={currentMenu == st[3]}
|
||||
/>;
|
||||
|
||||
@@ -190,6 +256,7 @@ export default class StandardSlotSection extends SlotSection {
|
||||
onOpen={open.bind(this, st[4])}
|
||||
onSelect={select.bind(this, st[4])}
|
||||
selected={currentMenu == st[4]}
|
||||
ship={ship}
|
||||
warning= {m => m.enginecapacity < ship.boostEnergy}
|
||||
/>;
|
||||
|
||||
@@ -200,6 +267,7 @@ export default class StandardSlotSection extends SlotSection {
|
||||
onOpen={open.bind(this, st[5])}
|
||||
onSelect={select.bind(this, st[5])}
|
||||
selected={currentMenu == st[5]}
|
||||
ship={ship}
|
||||
warning= {m => m.enginecapacity < ship.boostEnergy}
|
||||
/>;
|
||||
|
||||
@@ -210,12 +278,18 @@ export default class StandardSlotSection extends SlotSection {
|
||||
onOpen={open.bind(this, st[6])}
|
||||
onSelect={select.bind(this, st[6])}
|
||||
selected={currentMenu == st[6]}
|
||||
warning= {m => m.capacity < st[2].m.maxfuel} // Show warning when fuel tank is smaller than FSD Max Fuel
|
||||
ship={ship}
|
||||
warning= {m => m.fuel < st[2].m.maxfuel} // Show warning when fuel tank is smaller than FSD Max Fuel
|
||||
/>;
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the section drop-down menu
|
||||
* @param {Function} translate Translate function
|
||||
* @return {React.Component} Section menu
|
||||
*/
|
||||
_getSectionMenu(translate) {
|
||||
let _fill = this._fill;
|
||||
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import shallowEqual from '../utils/shallowEqual';
|
||||
import { shallowEqual } from '../utils/UtilityFunctions';
|
||||
|
||||
/**
|
||||
* Base SVG Icon Component
|
||||
*/
|
||||
class SvgIcon extends React.Component {
|
||||
|
||||
/**
|
||||
* Only rerender an SVG Icon if properties have changed
|
||||
* @param {Object} nextProps Next/Incoming properties
|
||||
* @return {Boolean} True if properties have changed
|
||||
*/
|
||||
shouldComponentUpdate(nextProps) { return !shallowEqual(this.props, nextProps); }
|
||||
|
||||
svg() { return null; }
|
||||
|
||||
/**
|
||||
* Standard SVG view box, can/should be overriden by sub-classes as necessary
|
||||
* @return {string} view box string
|
||||
*/
|
||||
viewBox() { return '0 0 32 32'; }
|
||||
|
||||
/**
|
||||
* Render the Icon
|
||||
* @return {React.Component} SVG Icon
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<svg className={cn('icon', this.props.className)} style={this.props.style} viewBox={this.viewBox()}>
|
||||
@@ -19,7 +33,14 @@ class SvgIcon extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bin Icon - Delete
|
||||
*/
|
||||
export class Bin extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M4 10v20c0 1.1 0.9 2 2 2h18c1.1 0 2-0.9 2-2v-20h-22zM10 28h-2v-14h2v14zM14 28h-2v-14h2v14zM18 28h-2v-14h2v14zM22 28h-2v-14h2v14z'/>
|
||||
@@ -28,7 +49,14 @@ export class Bin extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Coriolis Logo
|
||||
*/
|
||||
export class CoriolisLogo extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g transform='translate(1,1)'>
|
||||
<path stroke='#ff3b00' transform='rotate(45 15 15)' d='m4,4 l 11,-4 l 11,4 l 4,11 l -4,11 l -11,4 l -11,-4 l -4,-11 l 4,-11 l 22,0 l 0,22 l -22,0 z' strokeWidth='1' fill='#000000'/>
|
||||
@@ -37,13 +65,27 @@ export class CoriolisLogo extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download / To Inbox
|
||||
*/
|
||||
export class Download extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M16 18l8-8h-6v-8h-4v8h-6zM23.273 14.727l-2.242 2.242 8.128 3.031-13.158 4.907-13.158-4.907 8.127-3.031-2.242-2.242-8.727 3.273v8l16 6 16-6v-8z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Eddb.io Logo
|
||||
*/
|
||||
export class Eddb extends SvgIcon {
|
||||
/**
|
||||
* Render the Icon
|
||||
* @return {React.Component} SVG Icon
|
||||
*/
|
||||
render() {
|
||||
return <svg className={cn(this.props.className)} style={this.props.style} viewBox='0 0 90 32'>
|
||||
<path d='M19.1,25.2c0.3,0,0.6,0.1,0.7,0.2c0.2,0.1,0.3,0.3,0.4,0.4c0.1,0.2,0.2,0.4,0.2,0.6v3.3c0,0.3-0.1,0.6-0.2,0.7c-0.1,0.2-0.3,0.3-0.4,0.3c-0.2,0.1-0.4,0.2-0.6,0.1H3.6c-0.3,0-0.6-0.1-0.7-0.2c-0.2-0.1-0.3-0.3-0.3-0.4c-0.1-0.2-0.2-0.4-0.1-0.6V10.2c0-0.3,0.1-0.5,0.2-0.7C2.7,9.4,2.9,9.3,3,9.2C3.2,9.1,3.4,9,3.6,9h15.5c0.3,0,0.6,0.1,0.7,0.2c0.2,0.1,0.3,0.3,0.4,0.4c0.1,0.2,0.2,0.4,0.2,0.6V22c0,0.3-0.1,0.6-0.2,0.7c-0.1,0.2-0.3,0.3-0.4,0.3c-0.2,0.1-0.4,0.2-0.6,0.1h-6.8v-6.8c0.3-0.2,0.6-0.4,0.8-0.7c0.2-0.3,0.3-0.7,0.3-1c0-0.6-0.2-1.1-0.6-1.4c-0.4-0.4-0.9-0.6-1.4-0.6c-0.5,0-1,0.2-1.4,0.6c-0.4,0.4-0.6,0.9-0.6,1.4c0,0.8,0.3,1.4,1,1.8v8.7H19.1z'/>
|
||||
@@ -54,7 +96,14 @@ export class Eddb extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed - <>
|
||||
*/
|
||||
export class Embed extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M18 23l3 3 10-10-10-10-3 3 7 7z'/>
|
||||
@@ -63,8 +112,19 @@ export class Embed extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Equalizer
|
||||
*/
|
||||
export class Equalizer extends SvgIcon {
|
||||
viewBox () { return '0 0 1024 1024'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 1024 1024'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M448 128v-16c0-26.4-21.6-48-48-48h-160c-26.4 0-48 21.6-48 48v16h-192v128h192v16c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-16h576v-128h-576zM256 256v-128h128v128h-128zM832 432c0-26.4-21.6-48-48-48h-160c-26.4 0-48 21.6-48 48v16h-576v128h576v16c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-16h192v-128h-192v-16zM640 576v-128h128v128h-128zM448 752c0-26.4-21.6-48-48-48h-160c-26.4 0-48 21.6-48 48v16h-192v128h192v16c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-16h576v-128h-576v-16zM256 896v-128h128v128h-128z'/>
|
||||
@@ -72,32 +132,71 @@ export class Equalizer extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Floppy disk - save
|
||||
*/
|
||||
export class FloppyDisk extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M28 0h-28v32h32v-28l-4-4zM16 4h4v8h-4v-8zM28 28h-24v-24h2v10h18v-10h2.343l1.657 1.657v22.343z' />;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuel Gauge
|
||||
*/
|
||||
export class Fuel extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M16 0c-8.837 0-16 7.163-16 16s7.163 16 16 16 16-7.163 16-16-7.163-16-16-16zM9.464 26.067c0.347-0.957 0.536-1.99 0.536-3.067 0-3.886-2.463-7.197-5.913-8.456 0.319-2.654 1.508-5.109 3.427-7.029 2.267-2.266 5.28-3.515 8.485-3.515s6.219 1.248 8.485 3.515c1.92 1.92 3.108 4.375 3.428 7.029-3.45 1.26-5.913 4.57-5.913 8.456 0 1.077 0.189 2.11 0.536 3.067-1.928 1.258-4.18 1.933-6.536 1.933s-4.608-0.675-6.536-1.933zM17.242 20.031c0.434 0.109 0.758 0.503 0.758 0.969v2c0 0.55-0.45 1-1 1h-2c-0.55 0-1-0.45-1-1v-2c0-0.466 0.324-0.86 0.758-0.969l0.742-14.031h1l0.742 14.031z' />;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Github Logo
|
||||
*/
|
||||
export class GitHub extends SvgIcon {
|
||||
viewBox() { return '0 0 1024 1024' };
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 1024 1024'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M512 0C229.252 0 0 229.25199999999995 0 512c0 226.251 146.688 418.126 350.155 485.813 25.593 4.686 34.937-11.125 34.937-24.626 0-12.188-0.469-52.562-0.718-95.314-128.708 23.46-161.707-31.541-172.469-60.373-5.525-14.809-30.407-60.249-52.398-72.263-17.988-9.828-43.26-33.237-0.917-33.735 40.434-0.476 69.348 37.308 78.471 52.75 45.938 77.749 119.876 55.627 148.999 42.5 4.654-32.999 17.902-55.627 32.501-68.373-113.657-12.939-233.22-56.875-233.22-253.063 0-55.94 19.968-101.561 52.658-137.404-5.22-12.999-22.844-65.095 5.063-135.563 0 0 42.937-13.749 140.811 52.501 40.811-11.406 84.594-17.031 128.124-17.22 43.499 0.188 87.314 5.874 128.188 17.28 97.689-66.311 140.686-52.501 140.686-52.501 28 70.532 10.375 122.564 5.124 135.499 32.811 35.844 52.626 81.468 52.626 137.404 0 196.686-119.751 240-233.813 252.686 18.439 15.876 34.748 47.001 34.748 94.748 0 68.437-0.686 123.627-0.686 140.501 0 13.625 9.312 29.561 35.25 24.562C877.436 929.998 1024 738.126 1024 512 1024 229.25199999999995 794.748 0 512 0z' />;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Infinite / Infinity
|
||||
*/
|
||||
export class Infinite extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M24.5 23.5c-2.003 0-3.887-0.78-5.303-2.197l-3.197-3.196-3.196 3.196c-1.417 1.417-3.3 2.197-5.303 2.197s-3.887-0.78-5.304-2.197c-1.417-1.417-2.197-3.3-2.197-5.303s0.78-3.887 2.197-5.304c1.417-1.417 3.3-2.197 5.304-2.197s3.887 0.78 5.303 2.197l3.196 3.196 3.196-3.196c1.417-1.417 3.3-2.197 5.303-2.197s3.887 0.78 5.303 2.197c1.417 1.417 2.197 3.3 2.197 5.304s-0.78 3.887-2.197 5.303c-1.416 1.417-3.3 2.197-5.303 2.197zM21.304 19.197c0.854 0.853 1.989 1.324 3.196 1.323s2.342-0.47 3.196-1.324c0.854-0.854 1.324-1.989 1.324-3.196s-0.47-2.342-1.324-3.196c-0.854-0.854-1.989-1.324-3.196-1.324s-2.342 0.47-3.196 1.324l-3.196 3.196 3.196 3.197zM7.5 11.48c-1.207 0-2.342 0.47-3.196 1.324s-1.324 1.989-1.324 3.196c0 1.207 0.47 2.342 1.324 3.196s1.989 1.324 3.196 1.324c1.207 0 2.342-0.47 3.196-1.324l3.196-3.196-3.196-3.196c-0.854-0.854-1.989-1.324-3.196-1.324v0z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Info - i within circle
|
||||
*/
|
||||
export class Info extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M14 9.5c0-0.825 0.675-1.5 1.5-1.5h1c0.825 0 1.5 0.675 1.5 1.5v1c0 0.825-0.675 1.5-1.5 1.5h-1c-0.825 0-1.5-0.675-1.5-1.5v-1z'/>
|
||||
@@ -107,7 +206,14 @@ export class Info extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Link / Permalink / Chain
|
||||
*/
|
||||
export class LinkIcon extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M13.757 19.868c-0.416 0-0.832-0.159-1.149-0.476-2.973-2.973-2.973-7.81 0-10.783l6-6c1.44-1.44 3.355-2.233 5.392-2.233s3.951 0.793 5.392 2.233c2.973 2.973 2.973 7.81 0 10.783l-2.743 2.743c-0.635 0.635-1.663 0.635-2.298 0s-0.635-1.663 0-2.298l2.743-2.743c1.706-1.706 1.706-4.481 0-6.187-0.826-0.826-1.925-1.281-3.094-1.281s-2.267 0.455-3.094 1.281l-6 6c-1.706 1.706-1.706 4.481 0 6.187 0.635 0.635 0.635 1.663 0 2.298-0.317 0.317-0.733 0.476-1.149 0.476z'/>
|
||||
@@ -116,39 +222,89 @@ export class LinkIcon extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* No Power - Lightning bolt + no entry
|
||||
*/
|
||||
export class NoPower extends SvgIcon {
|
||||
viewBox() { return '0 0 512 512' };
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 512 512'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M437.020 74.98c-48.353-48.351-112.64-74.98-181.020-74.98s-132.667 26.629-181.020 74.98c-48.351 48.353-74.98 112.64-74.98 181.020s26.629 132.667 74.98 181.020c48.353 48.351 112.64 74.98 181.020 74.98s132.667-26.629 181.020-74.98c48.351-48.353 74.98-112.64 74.98-181.020s-26.629-132.667-74.98-181.020zM448 256c0 41.407-13.177 79.794-35.556 111.19l-267.633-267.634c31.396-22.379 69.782-35.556 111.189-35.556 105.869 0 192 86.131 192 192zM64 256c0-41.407 13.177-79.793 35.556-111.189l267.635 267.634c-31.397 22.378-69.784 35.555-111.191 35.555-105.869 0-192-86.131-192-192z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification - Exclamation mark within circle
|
||||
*/
|
||||
export class Notification extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M16 3c-3.472 0-6.737 1.352-9.192 3.808s-3.808 5.72-3.808 9.192c0 3.472 1.352 6.737 3.808 9.192s5.72 3.808 9.192 3.808c3.472 0 6.737-1.352 9.192-3.808s3.808-5.72 3.808-9.192c0-3.472-1.352-6.737-3.808-9.192s-5.72-3.808-9.192-3.808zM16 0v0c8.837 0 16 7.163 16 16s-7.163 16-16 16c-8.837 0-16-7.163-16-16s7.163-16 16-16zM14 22h4v4h-4zM14 6h4v12h-4z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Power - Lightning Bolt
|
||||
*/
|
||||
export class Power extends SvgIcon {
|
||||
viewBox() { return '0 0 512 512' };
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 512 512'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M192 0l-192 256h192l-128 256 448-320h-256l192-192z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Question mark within Circle
|
||||
*/
|
||||
export class Question extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M14 22h4v4h-4zM22 8c1.105 0 2 0.895 2 2v6l-6 4h-4v-2l6-4v-2h-10v-4h12zM16 3c-3.472 0-6.737 1.352-9.192 3.808s-3.808 5.72-3.808 9.192c0 3.472 1.352 6.737 3.808 9.192s5.72 3.808 9.192 3.808c3.472 0 6.737-1.352 9.192-3.808s3.808-5.72 3.808-9.192c0-3.472-1.352-6.737-3.808-9.192s-5.72-3.808-9.192-3.808zM16 0v0c8.837 0 16 7.163 16 16s-7.163 16-16 16c-8.837 0-16-7.163-16-16s7.163-16 16-16z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload - Clockwise circular arrow
|
||||
*/
|
||||
export class Reload extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M32 12h-12l4.485-4.485c-2.267-2.266-5.28-3.515-8.485-3.515s-6.219 1.248-8.485 3.515c-2.266 2.267-3.515 5.28-3.515 8.485s1.248 6.219 3.515 8.485c2.267 2.266 5.28 3.515 8.485 3.515s6.219-1.248 8.485-3.515c0.189-0.189 0.371-0.384 0.546-0.583l3.010 2.634c-2.933 3.349-7.239 5.464-12.041 5.464-8.837 0-16-7.163-16-16s7.163-16 16-16c4.418 0 8.418 1.791 11.313 4.687l4.687-4.687v12z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning - Exclamation point witin triangle
|
||||
*/
|
||||
export class Warning extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M16 2.899l13.409 26.726h-26.819l13.409-26.726zM16 0c-0.69 0-1.379 0.465-1.903 1.395l-13.659 27.222c-1.046 1.86-0.156 3.383 1.978 3.383h27.166c2.134 0 3.025-1.522 1.978-3.383h0l-13.659-27.222c-0.523-0.93-1.213-1.395-1.903-1.395v0z'/>
|
||||
@@ -158,8 +314,19 @@ export class Warning extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed mount hardpoint
|
||||
*/
|
||||
export class MountFixed extends SvgIcon {
|
||||
viewBox () { return '0 0 200 200'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<circle fillOpacity='0' r='70' cy='100' cx='100' strokeWidth='5' />
|
||||
@@ -171,8 +338,19 @@ export class MountFixed extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gimballed mount hardpoint
|
||||
*/
|
||||
export class MountGimballed extends SvgIcon {
|
||||
viewBox () { return '0 0 200 200'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<ellipse ry='25' rx='95' cy='100' cx='100' fillOpacity='0' strokeWidth='5' />
|
||||
@@ -181,8 +359,19 @@ export class MountGimballed extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turrent mount hardpoint
|
||||
*/
|
||||
export class MountTurret extends SvgIcon {
|
||||
viewBox () { return '0 0 200 200'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<line y2='170' x2='162' y1='170' x1='8' strokeWidth='6' />
|
||||
@@ -192,39 +381,84 @@ export class MountTurret extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rocket ship
|
||||
*/
|
||||
export class Rocket extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M22 2l-10 10h-6l-6 8c0 0 6.357-1.77 10.065-0.94l-10.065 12.94 13.184-10.255c1.839 4.208-1.184 10.255-1.184 10.255l8-6v-6l10-10 2-10-10 2z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hammer
|
||||
*/
|
||||
export class Hammer extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M31.562 25.905l-9.423-9.423c-0.583-0.583-1.538-0.583-2.121 0l-0.707 0.707-5.75-5.75 9.439-9.439h-10l-4.439 4.439-0.439-0.439h-2.121v2.121l0.439 0.439-6.439 6.439 5 5 6.439-6.439 5.75 5.75-0.707 0.707c-0.583 0.583-0.583 1.538 0 2.121l9.423 9.423c0.583 0.583 1.538 0.583 2.121 0l3.535-3.535c0.583-0.583 0.583-1.538 0-2.121z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stats bars / Histogram / Compare
|
||||
*/
|
||||
export class StatsBars extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M0 26h32v4h-32zM4 18h4v6h-4zM10 10h4v14h-4zM16 16h4v8h-4zM22 4h4v20h-4z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cogs / Settings
|
||||
*/
|
||||
export class Cogs extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M11.366 22.564l1.291-1.807-1.414-1.414-1.807 1.291c-0.335-0.187-0.694-0.337-1.071-0.444l-0.365-2.19h-2l-0.365 2.19c-0.377 0.107-0.736 0.256-1.071 0.444l-1.807-1.291-1.414 1.414 1.291 1.807c-0.187 0.335-0.337 0.694-0.443 1.071l-2.19 0.365v2l2.19 0.365c0.107 0.377 0.256 0.736 0.444 1.071l-1.291 1.807 1.414 1.414 1.807-1.291c0.335 0.187 0.694 0.337 1.071 0.444l0.365 2.19h2l0.365-2.19c0.377-0.107 0.736-0.256 1.071-0.444l1.807 1.291 1.414-1.414-1.291-1.807c0.187-0.335 0.337-0.694 0.444-1.071l2.19-0.365v-2l-2.19-0.365c-0.107-0.377-0.256-0.736-0.444-1.071zM7 27c-1.105 0-2-0.895-2-2s0.895-2 2-2 2 0.895 2 2-0.895 2-2 2zM32 12v-2l-2.106-0.383c-0.039-0.251-0.088-0.499-0.148-0.743l1.799-1.159-0.765-1.848-2.092 0.452c-0.132-0.216-0.273-0.426-0.422-0.629l1.219-1.761-1.414-1.414-1.761 1.219c-0.203-0.149-0.413-0.29-0.629-0.422l0.452-2.092-1.848-0.765-1.159 1.799c-0.244-0.059-0.492-0.109-0.743-0.148l-0.383-2.106h-2l-0.383 2.106c-0.251 0.039-0.499 0.088-0.743 0.148l-1.159-1.799-1.848 0.765 0.452 2.092c-0.216 0.132-0.426 0.273-0.629 0.422l-1.761-1.219-1.414 1.414 1.219 1.761c-0.149 0.203-0.29 0.413-0.422 0.629l-2.092-0.452-0.765 1.848 1.799 1.159c-0.059 0.244-0.109 0.492-0.148 0.743l-2.106 0.383v2l2.106 0.383c0.039 0.251 0.088 0.499 0.148 0.743l-1.799 1.159 0.765 1.848 2.092-0.452c0.132 0.216 0.273 0.426 0.422 0.629l-1.219 1.761 1.414 1.414 1.761-1.219c0.203 0.149 0.413 0.29 0.629 0.422l-0.452 2.092 1.848 0.765 1.159-1.799c0.244 0.059 0.492 0.109 0.743 0.148l0.383 2.106h2l0.383-2.106c0.251-0.039 0.499-0.088 0.743-0.148l1.159 1.799 1.848-0.765-0.452-2.092c0.216-0.132 0.426-0.273 0.629-0.422l1.761 1.219 1.414-1.414-1.219-1.761c0.149-0.203 0.29-0.413 0.422-0.629l2.092 0.452 0.765-1.848-1.799-1.159c0.059-0.244 0.109-0.492 0.148-0.743l2.106-0.383zM21 15.35c-2.402 0-4.35-1.948-4.35-4.35s1.948-4.35 4.35-4.35 4.35 1.948 4.35 4.35c0 2.402-1.948 4.35-4.35 4.35z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Power Switch - Reset
|
||||
*/
|
||||
export class Switch extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M20 4.581v4.249c1.131 0.494 2.172 1.2 3.071 2.099 1.889 1.889 2.929 4.4 2.929 7.071s-1.040 5.182-2.929 7.071c-1.889 1.889-4.4 2.929-7.071 2.929s-5.182-1.040-7.071-2.929c-1.889-1.889-2.929-4.4-2.929-7.071s1.040-5.182 2.929-7.071c0.899-0.899 1.94-1.606 3.071-2.099v-4.249c-5.783 1.721-10 7.077-10 13.419 0 7.732 6.268 14 14 14s14-6.268 14-14c0-6.342-4.217-11.698-10-13.419zM14 0h4v16h-4z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-game Coriolis Station logo
|
||||
*/
|
||||
export class StationCoriolis extends SvgIcon {
|
||||
viewBox () { return '0 0 200 200'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<rect x='73.001' y='94.017' width='53.997' height='11.945'/>
|
||||
@@ -233,8 +467,19 @@ export class StationCoriolis extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-game Ocellus Station logo
|
||||
*/
|
||||
export class StationOcellus extends SvgIcon {
|
||||
viewBox () { return '0 0 200 200'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M100.002,200C155.139,200,200,155.142,200,100.001c0-55.143-44.861-100.002-99.998-100.002C44.86-0.001-0.002,44.857-0.002,100.001C-0.001,155.142,44.86,200,100.002,200z M100.002,5.574c52.063,0,94.423,42.359,94.423,94.427c0,52.067-42.361,94.422-94.423,94.422c-52.07,0-94.428-42.358-94.428-94.422C5.574,47.933,47.933,5.574,100.002,5.574z'/>
|
||||
@@ -244,8 +489,19 @@ export class StationOcellus extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-game Orbis Station logo
|
||||
*/
|
||||
export class StationOrbis extends SvgIcon {
|
||||
viewBox () { return '0 0 200 200'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M100.002,200c55.138,0,99.996-44.861,99.996-100c0-55.141-44.858-100-99.996-100C44.861,0-0.001,44.857-0.001,100C0,155.139,44.861,200,100.002,200z M100.002,194.424c-35.465,0-66.413-19.663-82.552-48.651l44.426-23.388c7.704,13.067,21.888,21.884,38.127,21.884c16.054,0,30.096-8.621,37.853-21.446l44.441,23.389C166.092,174.961,135.282,194.424,100.002,194.424zM100.002,61.306c21.335,0,38.691,17.356,38.691,38.694c0,21.338-17.364,38.691-38.691,38.691c-21.339,0-38.696-17.354-38.696-38.691C61.307,78.662,78.663,61.306,100.002,61.306zM194.422,100c0,14.802-3.427,28.808-9.521,41.287l-44.447-23.4c2.433-5.477,3.812-11.521,3.812-17.89c0-23.578-18.539-42.852-41.8-44.145V5.636C153.392,6.956,194.422,48.762,194.422,100z M96.895,5.655v50.233C73.938,57.491,55.73,76.635,55.73,100c0,6.187,1.286,12.081,3.592,17.434l-44.455,23.402C8.911,128.472,5.571,114.619,5.571,100C5.577,48.972,46.261,7.297,96.895,5.655z'/>
|
||||
@@ -254,8 +510,19 @@ export class StationOrbis extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-game Outpost Station logo
|
||||
*/
|
||||
export class StationOutpost extends SvgIcon {
|
||||
viewBox () { return '0 0 200 200'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 200 200'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g>
|
||||
<path d='M145.137,59.126h4.498v6.995h5.576V46.556h-5.576v6.994h-4.498V16.328h-5.574v57.667h-15.411v14.824h-7.63v-14.58h-13.044v14.58h-8.295v-14.58H82.138v14.58h-6.573v-14.58H59.072v14.58h-6.573v-14.58H39.458v36.338h13.041V94.391h6.573v16.186h16.493V94.391h6.573v16.186h13.044V94.391h8.295v16.186h13.044V94.391h7.63v40.457l17.634,17.637h13.185v31.182h5.577V73.996H145.14v-14.87H145.137z M154.97,146.907h-10.871l-14.376-14.376V79.57h25.247V146.907z'/>
|
||||
@@ -265,14 +532,32 @@ export class StationOutpost extends SvgIcon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload - From inbox
|
||||
*/
|
||||
export class Upload extends SvgIcon {
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <path d='M14 18h4v-8h6l-8-8-8 8h6zM20 13.5v3.085l9.158 3.415-13.158 4.907-13.158-4.907 9.158-3.415v-3.085l-12 4.5v8l16 6 16-6v-8z'/>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Elite Dangerous Loader / Spinner
|
||||
*/
|
||||
export class Loader extends SvgIcon {
|
||||
viewBox () { return '0 0 40 40'; }
|
||||
/**
|
||||
* Overriden view box
|
||||
* @return {string} view box
|
||||
*/
|
||||
viewBox() { return '0 0 40 40'; }
|
||||
/**
|
||||
* Generate the SVG
|
||||
* @return {React.Component} SVG Contents
|
||||
*/
|
||||
svg() {
|
||||
return <g className={'loader'}>
|
||||
<path d='m5,8l5,8l5,-8z' className={'l1 d1'} />
|
||||
|
||||
89
src/app/components/Tooltip.jsx
Normal file
89
src/app/components/Tooltip.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { shallowEqual } from '../utils/UtilityFunctions';
|
||||
|
||||
/**
|
||||
* Document Root Tooltip
|
||||
*/
|
||||
export default class Tooltip extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
rect: React.PropTypes.object.isRequired,
|
||||
options: React.PropTypes.object
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
options: {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adjusts the position of the tooltip if its content
|
||||
* appear outside of the windows left or right border
|
||||
* @param {DomElement} elem Tooltip contents container
|
||||
*/
|
||||
_adjustPosition(elem) {
|
||||
if (elem) {
|
||||
let o = this.props.options.orientation || 'n';
|
||||
let rect = elem.getBoundingClientRect();
|
||||
|
||||
if (o == 'n' || o == 's') {
|
||||
let docWidth = document.documentElement.clientWidth;
|
||||
|
||||
if (rect.left < 0) {
|
||||
elem.style.left = rect.width / 2 + 'px';
|
||||
} else if ((rect.left + rect.width) > docWidth) {
|
||||
elem.style.left = docWidth - (rect.width / 2) + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a component should be rerendered
|
||||
* @param {object} nextProps Next properties
|
||||
* @return {boolean} true if update is needed
|
||||
*/
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return !shallowEqual(this.props, nextProps);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders the component
|
||||
* @return {React.Component} Tooltip
|
||||
*/
|
||||
render() {
|
||||
if (!this.props.children) { // If no content is provided
|
||||
return null;
|
||||
}
|
||||
|
||||
let { children, options, rect } = this.props;
|
||||
let o = options.orientation || 'n';
|
||||
let style = options.style || {};
|
||||
|
||||
switch (o) {
|
||||
case 's':
|
||||
style.top = rect.top + rect.height;
|
||||
style.left = rect.left + (rect.width / 2);
|
||||
break;
|
||||
case 'n':
|
||||
style.top = rect.top;
|
||||
style.left = rect.left + (rect.width / 2);
|
||||
break;
|
||||
case 'e':
|
||||
style.left = rect.left + rect.width;
|
||||
style.top = rect.top + (rect.height / 2);
|
||||
break;
|
||||
case 'w':
|
||||
style.left = rect.left;
|
||||
style.top = rect.top + (rect.height / 2);
|
||||
}
|
||||
|
||||
return <div>
|
||||
<div className={ 'arr ' + o} style={style} />
|
||||
<div className={ 'tip ' + o} style={style} ref={this._adjustPosition.bind(this)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +1,22 @@
|
||||
import React from 'react';
|
||||
import shallowEqual from '../utils/shallowEqual';
|
||||
import { shallowEqual } from '../utils/UtilityFunctions';
|
||||
|
||||
/**
|
||||
* @class Abstract TranslatedComponent
|
||||
* Abstract Translated Component
|
||||
*/
|
||||
export default class TranslatedComponent extends React.Component {
|
||||
|
||||
static contextTypes = {
|
||||
language: React.PropTypes.object.isRequired,
|
||||
sizeRatio: React.PropTypes.number.isRequired
|
||||
}
|
||||
sizeRatio: React.PropTypes.number.isRequired,
|
||||
openMenu: React.PropTypes.func.isRequired,
|
||||
closeMenu: React.PropTypes.func.isRequired,
|
||||
showModal: React.PropTypes.func.isRequired,
|
||||
hideModal: React.PropTypes.func.isRequired,
|
||||
tooltip: React.PropTypes.func.isRequired,
|
||||
termtip: React.PropTypes.func.isRequired,
|
||||
onWindowResize: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
/**
|
||||
* Created an instance of a Translated Component. This is an abstract class.
|
||||
@@ -23,9 +30,9 @@ export default class TranslatedComponent extends React.Component {
|
||||
/**
|
||||
* Determine if the context change incldues a language or size change
|
||||
* @param {object} nextContext The incoming / next context
|
||||
* @return {boolean} true if the language has changed
|
||||
* @return {boolean} true if the language has changed
|
||||
*/
|
||||
didContextChange(nextContext){
|
||||
didContextChange(nextContext) {
|
||||
return nextContext.language !== this.context.language || nextContext.sizeRatio != this.context.sizeRatio;
|
||||
}
|
||||
|
||||
@@ -34,14 +41,12 @@ export default class TranslatedComponent extends React.Component {
|
||||
* props, state, or context changes. This method performs a shallow comparison to
|
||||
* determine change.
|
||||
*
|
||||
* @param {object} nextProps
|
||||
* @param {objec} nextState
|
||||
* @param {objec} nextContext
|
||||
* @param {object} nextProps Next/Incoming Properties
|
||||
* @param {objec} nextState Next/Incoming State
|
||||
* @param {objec} nextContext Next/Incoming Context
|
||||
* @return {boolean} True if props, state, or context has changed
|
||||
*/
|
||||
shouldComponentUpdate(nextProps, nextState, nextContext) {
|
||||
return !shallowEqual(this.props, nextProps)
|
||||
|| !shallowEqual(this.state, nextState)
|
||||
|| this.didContextChange(nextContext);
|
||||
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState) || this.didContextChange(nextContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,35 +3,60 @@ import SlotSection from './SlotSection';
|
||||
import HardpointSlot from './HardpointSlot';
|
||||
import cn from 'classnames';
|
||||
|
||||
/**
|
||||
* Utility Slot Section
|
||||
*/
|
||||
export default class UtilitySlotSection extends SlotSection {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {Object} props React Component properties
|
||||
* @param {Object} context React Component context
|
||||
*/
|
||||
constructor(props, context) {
|
||||
super(props, context, 'utility', 'utility mounts');
|
||||
|
||||
this._empty = this._empty.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all utility slots and close the menu
|
||||
*/
|
||||
_empty() {
|
||||
this.props.ship.emptyUtility();
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount module in utility slot, replace all if Alt is held
|
||||
* @param {string} group Module Group name
|
||||
* @param {string} rating Module Rating
|
||||
* @param {string} name Module name
|
||||
* @param {Synthetic} event Event
|
||||
*/
|
||||
_use(group, rating, name, event) {
|
||||
this.props.ship.useUtility(group, rating, name, event.getModifierState('Alt'));
|
||||
this.props.onChange();
|
||||
this._close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty all utility slots on right-click
|
||||
*/
|
||||
_contextMenu() {
|
||||
this._empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create all HardpointSlots (React component) for the slots
|
||||
* @return {Array} Array of HardpointSlots
|
||||
*/
|
||||
_getSlots() {
|
||||
let slots = [];
|
||||
let hardpoints = this.props.ship.hardpoints;
|
||||
let availableModules = this.props.ship.getAvailableModules();
|
||||
let currentMenu = this.props.currentMenu;
|
||||
let { ship, currentMenu } = this.props;
|
||||
let hardpoints = ship.hardpoints;
|
||||
let { originSlot, targetSlot } = this.state;
|
||||
let availableModules = ship.getAvailableModules();
|
||||
|
||||
for (let i = 0, l = hardpoints.length; i < l; i++) {
|
||||
let h = hardpoints[i];
|
||||
@@ -43,7 +68,12 @@ export default class UtilitySlotSection extends SlotSection {
|
||||
onOpen={this._openMenu.bind(this,h)}
|
||||
onSelect={this._selectModule.bind(this, h)}
|
||||
selected={currentMenu == h}
|
||||
drag={this._drag.bind(this, h)}
|
||||
dragOver={this._dragOverSlot.bind(this, h)}
|
||||
drop={this._drop}
|
||||
dropClass={this._dropClass(h, originSlot, targetSlot)}
|
||||
enabled={h.enabled}
|
||||
ship={ship}
|
||||
m={h.m}
|
||||
/>);
|
||||
}
|
||||
@@ -52,6 +82,11 @@ export default class UtilitySlotSection extends SlotSection {
|
||||
return slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the section menu
|
||||
* @param {Function} translate Translate function
|
||||
* @return {React.Component} Section menu
|
||||
*/
|
||||
_getSectionMenu(translate) {
|
||||
let _use = this._use;
|
||||
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
angular.module('app').directive('barChart', ['$window', '$translate', '$rootScope', function($window, $translate, $rootScope) {
|
||||
|
||||
function bName(build) {
|
||||
return build.buildName + '\n' + build.name;
|
||||
}
|
||||
|
||||
function insertLinebreaks(d) {
|
||||
var el = d3.select(this);
|
||||
var lines = d.split('\n');
|
||||
el.text('').attr('y', -6);
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var tspan = el.append('tspan').text(lines[i].length > 18 ? lines[i].substring(0, 15) + '...' : lines[i]);
|
||||
if (i > 0) {
|
||||
tspan.attr('x', -9).attr('dy', '1em');
|
||||
} else {
|
||||
tspan.attr('class', 'primary');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
data: '=',
|
||||
facet: '='
|
||||
},
|
||||
link: function(scope, element) {
|
||||
var color = d3.scale.ordinal().range([ '#7b6888', '#6b486b', '#3182bd', '#a05d56', '#d0743c']),
|
||||
labels = scope.facet.lbls,
|
||||
fmt = null,
|
||||
unit = null,
|
||||
properties = scope.facet.props,
|
||||
margin = { top: 10, right: 20, bottom: 40, left: 150 },
|
||||
y0 = d3.scale.ordinal(),
|
||||
y1 = d3.scale.ordinal(),
|
||||
x = d3.scale.linear(),
|
||||
yAxis = d3.svg.axis().scale(y0).outerTickSize(0).orient('left'),
|
||||
xAxis = d3.svg.axis().scale(x).ticks(5).outerTickSize(0).orient('bottom');
|
||||
|
||||
// Create chart
|
||||
var svg = d3.select(element[0]).append('svg');
|
||||
var vis = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
|
||||
|
||||
// Create and Add tooltip
|
||||
var tip = d3.tip()
|
||||
.attr('class', 'd3-tip')
|
||||
.html(function(property, propertyIndex) {
|
||||
return (labels ? ($translate.instant(labels[propertyIndex]) + ': ') : '') + fmt(property.value) + ' ' + unit;
|
||||
});
|
||||
|
||||
vis.call(tip);
|
||||
|
||||
// Create Y Axis SVG Elements
|
||||
vis.append('g').attr('class', 'y axis');
|
||||
vis.selectAll('g.y.axis g text').each(insertLinebreaks);
|
||||
// Create X Axis SVG Elements
|
||||
var xAxisLbl = vis.append('g')
|
||||
.attr('class', 'x axis cap')
|
||||
.append('text')
|
||||
.attr('y', 33)
|
||||
.attr('dy', '.1em')
|
||||
.style('text-anchor', 'middle');
|
||||
|
||||
updateFormats();
|
||||
|
||||
function render() {
|
||||
var data = scope.data,
|
||||
width = element[0].offsetWidth,
|
||||
w = width - margin.left - margin.right,
|
||||
height = 50 + (30 * data.length * $rootScope.sizeRatio),
|
||||
h = height - margin.top - margin.bottom,
|
||||
maxVal = d3.max(data, function(d) { return d3.max(properties, function(p) {return d[p]; }); });
|
||||
|
||||
// Update chart size
|
||||
svg.attr('width', width).attr('height', height);
|
||||
|
||||
// Remove existing elements
|
||||
vis.selectAll('.ship').remove();
|
||||
vis.selectAll('rect').remove();
|
||||
|
||||
// Update X & Y Axis
|
||||
x.range([0, w]).domain([0, maxVal]);
|
||||
y0.domain(data.map(bName)).rangeRoundBands([0, h], 0.3);
|
||||
y1.domain(properties).rangeRoundBands([0, y0.rangeBand()]);
|
||||
vis.selectAll('.y.axis').call(yAxis);
|
||||
vis.selectAll('.x.axis').attr('transform', 'translate(0,' + h + ')').call(xAxis);
|
||||
xAxisLbl.attr('x', w / 2);
|
||||
// Update Y-Axis labels
|
||||
vis.selectAll('g.y.axis g text').each(insertLinebreaks);
|
||||
|
||||
var group = vis.selectAll('.ship')
|
||||
.data(scope.data, bName)
|
||||
.enter().append('g')
|
||||
.attr('class', 'g')
|
||||
.attr('transform', function(build) { return 'translate(0,' + y0(bName(build)) + ')'; });
|
||||
|
||||
group.selectAll('rect')
|
||||
.data(function(build) {
|
||||
var o = [];
|
||||
for (var i = 0; i < properties.length; i++) {
|
||||
o.push({ name: properties[i], value: build[properties[i]] });
|
||||
}
|
||||
return o;
|
||||
})
|
||||
.enter().append('rect')
|
||||
.attr('height', y1.rangeBand())
|
||||
.attr('x', 0)
|
||||
.attr('y', function(d) {return y1(d.name); })
|
||||
.attr('width', function(d) { return x(d.value); })
|
||||
.on('mouseover', tip.show)
|
||||
.on('mouseout', tip.hide)
|
||||
.style('fill', function(d) { return color(d.name); });
|
||||
|
||||
}
|
||||
|
||||
function updateFormats() {
|
||||
fmt = $rootScope[scope.facet.fmt];
|
||||
unit = $translate.instant(scope.facet.unit);
|
||||
xAxisLbl.text($translate.instant(scope.facet.title) + (unit ? (' (' + $translate.instant(unit) + ')') : ''));
|
||||
xAxis.tickFormat($rootScope.localeFormat.numberFormat('.2s'));
|
||||
render();
|
||||
}
|
||||
|
||||
angular.element($window).bind('orientationchange resize render', render);
|
||||
scope.$watchCollection('data', render); // Watch for changes in the comparison array
|
||||
scope.$on('languageChanged', updateFormats);
|
||||
scope.$on('$destroy', function() {
|
||||
angular.element($window).unbind('orientationchange resize render', render);
|
||||
tip.destroy(); // Remove the tooltip from the DOM
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
}]);
|
||||
Reference in New Issue
Block a user