MediaWiki:Gadget-fa-bid-calculator.js: Difference between revisions
Drop cap note, Salary label, new-topic link |
The league is NBA RealGM |
||
| (9 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
/** | /** | ||
* | * NBA RealGM — free agency bid calculator. | ||
* Implements the points system on [[NBA:Free Agency]]. If the two ever disagree, | * Implements the points system on [[NBA:Free Agency]]. If the two ever disagree, | ||
* the wiki page is the rule and this script is the bug. | * the wiki page is the rule and this script is the bug. | ||
| Line 11: | Line 11: | ||
var MIN_OFFER = 500000, | var MIN_OFFER = 500000, | ||
MIN_RAISE = 200000, | MIN_RAISE = 200000, | ||
MIN_RAISE_POINTS = 0.2, | |||
MAX_YEARS = 4, | MAX_YEARS = 4, | ||
MAX_YEARS_35 = 2, | MAX_YEARS_35 = 2, | ||
YEAR_POINTS = 1.5; | YEAR_POINTS = 1.5; | ||
function money( n ) { | function money( n ) { | ||
return '$' + n.toLocaleString( 'en-US' ); | return '$' + n.toLocaleString( 'en-US' ); | ||
} | |||
/** Digits only, so "4,000,000" and "4000000" both read as 4000000. */ | |||
function parseMoney( v ) { | |||
var digits = String( v ).replace( /[^0-9]/g, '' ); | |||
return digits === '' ? NaN : Number( digits ); | |||
} | |||
/** | |||
* Reformat a text input with thousands separators, keeping the caret where | |||
* the typist left it. A plain reformat would send it to the end, which makes | |||
* editing the middle of a figure unusable. | |||
*/ | |||
function formatWithCaret( el ) { | |||
var before = el.value.slice( 0, el.selectionStart ), | |||
digitsBefore = before.replace( /[^0-9]/g, '' ).length, | |||
n = parseMoney( el.value ); | |||
el.value = isNaN( n ) ? '' : n.toLocaleString( 'en-US' ); | |||
// Walk forward past the same number of digits to place the caret. | |||
var pos = 0, seen = 0; | |||
while ( pos < el.value.length && seen < digitsBefore ) { | |||
if ( /[0-9]/.test( el.value[ pos ] ) ) { | |||
seen++; | |||
} | |||
pos++; | |||
} | |||
el.setSelectionRange( pos, pos ); | |||
} | |||
function pointWord( n ) { | |||
return '= ' + pts( n ) + ( pts( n ) === 1 ? ' point' : ' points' ); | |||
} | } | ||
| Line 64: | Line 99: | ||
link.target = '_blank'; | link.target = '_blank'; | ||
link.rel = 'noopener'; | link.rel = 'noopener'; | ||
link.textContent = ' | link.textContent = 'Copy and start a new topic in NBA: Free Agency'; | ||
// IPB has no parameter for seeding the post body, so the next best thing | |||
// is to put it on the clipboard on the way out. | |||
link.addEventListener( 'click', function () { | |||
if ( navigator.clipboard && navigator.clipboard.writeText ) { | |||
navigator.clipboard.writeText( text ); | |||
} | |||
} ); | |||
actions.appendChild( link ); | actions.appendChild( link ); | ||
| Line 86: | Line 128: | ||
form.className = 'tc-calc'; | form.className = 'tc-calc'; | ||
/** | |||
* One labelled row. Controls live in their own flex wrapper so that on a | |||
* narrow screen the label can stack above them while they stay in a row. | |||
* Returns the wrapper, so callers can append a readout beside the control. | |||
*/ | |||
function row( labelText, el ) { | function row( labelText, el ) { | ||
var r = document.createElement( 'div' ); | var r = document.createElement( 'div' ); | ||
| Line 91: | Line 138: | ||
var l = document.createElement( 'label' ); | var l = document.createElement( 'label' ); | ||
l.textContent = labelText; | l.textContent = labelText; | ||
var c = document.createElement( 'div' ); | |||
c.className = 'tc-calc-controls'; | |||
c.appendChild( el ); | |||
r.appendChild( l ); | r.appendChild( l ); | ||
r.appendChild( | r.appendChild( c ); | ||
form.appendChild( r ); | form.appendChild( r ); | ||
return | return c; | ||
} | } | ||
var years = document.createElement( 'select' ); | var years = document.createElement( 'select' ); | ||
// A number input cannot display separators — browsers reject a comma as | |||
// invalid number syntax — so this is a text input formatted by hand. | |||
var salary = document.createElement( 'input' ); | var salary = document.createElement( 'input' ); | ||
salary.type = ' | salary.type = 'text'; | ||
salary. | salary.inputMode = 'numeric'; | ||
salary. | salary.autocomplete = 'off'; | ||
salary. | salary.className = 'tc-calc-money'; | ||
salary.value = MIN_OFFER.toLocaleString( 'en-US' ); | |||
salary.addEventListener( 'input', function () { | |||
formatWithCaret( salary ); | |||
} ); | |||
var age35 = document.createElement( 'input' ); | var age35 = document.createElement( 'input' ); | ||
| Line 115: | Line 171: | ||
name.type = 'text'; | name.type = 'text'; | ||
name.placeholder = 'Caldwell Jones'; | name.placeholder = 'Caldwell Jones'; | ||
name. | name.className = 'tc-calc-wide'; | ||
var team = document.createElement( 'input' ); | var team = document.createElement( 'input' ); | ||
team.type = 'text'; | team.type = 'text'; | ||
team.placeholder = ' | team.placeholder = 'Sonics'; | ||
team. | team.className = 'tc-calc-wide'; | ||
function fillYears() { | function fillYears() { | ||
| Line 136: | Line 192: | ||
fillYears(); | fillYears(); | ||
row( ' | var yearPts = document.createElement( 'span' ); | ||
row( 'Salary', salary ); | yearPts.className = 'tc-calc-inline'; | ||
row( 'Contract length', years ).appendChild( yearPts ); | |||
var salaryPts = document.createElement( 'span' ); | |||
salaryPts.className = 'tc-calc-inline'; | |||
row( 'Salary', salary ).appendChild( salaryPts ); | |||
row( 'Player is 35+', age35 ).parentNode | |||
.querySelector( 'label' ).style.fontWeight = 'normal'; | |||
row( 'Bid to beat (optional)', beat ); | row( 'Bid to beat (optional)', beat ); | ||
row( 'Your team (optional)', team ); | row( 'Your team (optional)', team ); | ||
| Line 150: | Line 211: | ||
function render() { | function render() { | ||
var y = Number( years.value ), | var y = Number( years.value ), | ||
s = | s = parseMoney( salary.value ), | ||
max = age35.checked ? MAX_YEARS_35 : MAX_YEARS; | max = age35.checked ? MAX_YEARS_35 : MAX_YEARS; | ||
| Line 156: | Line 217: | ||
if ( !s || isNaN( s ) ) { | if ( !s || isNaN( s ) ) { | ||
yearPts.textContent = ''; | |||
salaryPts.textContent = ''; | |||
var empty = document.createElement( 'p' ); | var empty = document.createElement( 'p' ); | ||
empty.className = 'tc-calc-note'; | empty.className = 'tc-calc-note'; | ||
| Line 164: | Line 227: | ||
var total = points( y, s ); | var total = points( y, s ); | ||
yearPts.textContent = pointWord( y * YEAR_POINTS ); | |||
salaryPts.textContent = pointWord( s / 1000000 ); | |||
var big = document.createElement( 'div' ); | var big = document.createElement( 'div' ); | ||
| Line 170: | Line 235: | ||
out.appendChild( big ); | out.appendChild( big ); | ||
// --- validity --- | // --- validity --- | ||
| Line 190: | Line 243: | ||
if ( y > max ) { | if ( y > max ) { | ||
problems.push( 'Maximum length is ' + max + ' years' + | problems.push( 'Maximum length is ' + max + ' years' + | ||
( age35.checked ? ' for a player 35 | ( age35.checked ? ' for a player 35+' : '' ) + '.' ); | ||
} | } | ||
if ( problems.length ) { | if ( problems.length ) { | ||
| Line 205: | Line 258: | ||
box.className = 'tc-calc-note'; | box.className = 'tc-calc-note'; | ||
if ( total > | // A bid must clear the standing one by at least MIN_RAISE_POINTS, so | ||
// the bar is target + 0.2, not merely more than target. Everything | |||
// here is counted in tenths of a point: 0.1 points is exactly | |||
// $100,000 and a year is exactly 15 tenths, so integer arithmetic | |||
// gets the answer that 0.2 + 2.0 in binary floating point does not. | |||
var requiredTenths = Math.round( target * 10 ) + ( MIN_RAISE_POINTS * 10 ); | |||
if ( Math.round( total * 10 ) >= requiredTenths ) { | |||
box.textContent = 'This already beats ' + pts( target ) + ' points.'; | box.textContent = 'This already beats ' + pts( target ) + ' points.'; | ||
} else { | } else { | ||
var salaryTenths = requiredTenths - y * ( YEAR_POINTS * 10 ), | |||
var | newSalary = Math.max( salaryTenths * 100000, MIN_OFFER ); | ||
box.textContent = 'To beat ' + pts( target ) + ' points at ' + y + | |||
( y === 1 ? ' year' : ' years' ) + ': ' + money( newSalary ) + | ( y === 1 ? ' year' : ' years' ) + ': ' + money( newSalary ) + | ||
' (' + pts( points( y, newSalary ) ) + ' points).' | ' (' + pts( points( y, newSalary ) ) + ' points). '; | ||
var use = document.createElement( 'button' ); | |||
use.type = 'button'; | |||
use.className = 'tc-calc-use'; | |||
use.textContent = 'Use ' + money( newSalary ); | |||
use.addEventListener( 'click', function () { | |||
} | salary.value = newSalary.toLocaleString( 'en-US' ); | ||
box. | render(); | ||
} ); | |||
box.appendChild( use ); | |||
} | } | ||
out.appendChild( box ); | out.appendChild( box ); | ||
| Line 232: | Line 288: | ||
// --- ready-to-paste post --- | // --- ready-to-paste post --- | ||
// " | // "Sonics offer Caldwell Jones 2 years @ $500,000 = 3.5 points". | ||
// Whatever of team/player is missing simply drops out. | // Whatever of team/player is missing simply drops out. | ||
var tm = team.value.trim(), | var tm = team.value.trim(), | ||
pl = name.value.trim(), | pl = name.value.trim(), | ||
prefix = tm && pl ? tm + ' offer ' + pl : ( pl || ( tm ? tm + ' offer' : '' ) ), | prefix = tm && pl ? tm + ' offer ' + pl : ( pl || ( tm ? tm + ' offer' : '' ) ), | ||
offer = y + ( y === 1 ? ' | offer = y + ( y === 1 ? ' year' : ' years' ) + ' @ ' + money( s ) + | ||
' = ' + pts( total ) + ' | ' = ' + pts( total ) + ' points'; | ||
out.appendChild( postBlock( prefix ? prefix + ' ' + offer : offer ) ); | out.appendChild( postBlock( prefix ? prefix + ' ' + offer : offer ) ); | ||