2
\$\begingroup\$

Context

This software is a countdown/visual timer. It was initially developed to display the time left before the end of an exam.


Question

My JavaScript is working but when I want to validate it by some code quality tool it keeps throwing me this error: SyntaxError: Unexpected keyword 'const'. Const declarations are not supported in strict mode.

If any of you can improve this code and help me to improve it(even the design), I would be very grateful.

Here's the code:

'use strict'
 
const load = () => {
    const data = {}
 
    init(data)
 
    setInterval(() => {
        update(data)
        render(data)
    }, 100)
 
    document.querySelector('#start').onclick = event => {
        start(data)
    }
 
    document.querySelector('#reset').onclick = event => {
        reset(data)
    }
 
    document.querySelector('#license').onclick = showLicense
}
 
const init = data => {
    data.canvas = document.querySelector('#countdown')
    data.ctx = data.canvas.getContext('2d')
 
    data.endTime = Date.now()
    data.start = false
    data.reset = false
 
    data.canvas.width = data.canvas.height = 500
}
 
const update = data => {
    if (data.start || data.reset) {
        data.startAngle = timeToAngle(data.endTime)
        data.reset = false
    }
    if (data.start) {
        let difference = Math.abs(Math.floor((data.endTime - Date.now()) / 1000))
        let seconds = pad(difference % 60, '00')
        difference = Math.floor(difference / 60)
        let minutes = pad(difference % 60, '00')
        difference = Math.floor(difference / 60)
        let hours = difference
 
        if (data.endTime < Date.now()) {
            if (hours != 0) {
                hours = -hours
            } else if (minutes != 0) {
                minutes = -minutes
            } else {
                seconds = -seconds
            }
        }
 
        document.querySelector('#hours').value = hours
        document.querySelector('#minutes').value = minutes
        document.querySelector('#seconds').value = seconds
    }
}
 
const render = data => {
    const {
        canvas,
        ctx,
        startAngle,
        start
    } = data
 
    ctx.fillStyle = '#fff'
    ctx.fillRect(0, 0, canvas.width, canvas.height)
 
    let angle = Math.abs(startAngle)
 
    ctx.save()
 
    if (startAngle > 0) {
        ctx.translate(canvas.width / 2, canvas.height / 2)
        ctx.rotate(-Math.PI / 2)
        ctx.scale(1, -1)
        ctx.translate(-canvas.width / 2, -canvas.height / 2)
        ctx.fillStyle = 'rgba(0, 0, 255, 0.5)'
    } else {
        ctx.translate(canvas.width / 2, canvas.height / 2)
        ctx.rotate(-Math.PI / 2)
        ctx.translate(-canvas.width / 2, -canvas.height / 2)
        ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'
    }
 
    while (angle > 2 * Math.PI) {
        ctx.beginPath()
        ctx.arc(canvas.width / 2, canvas.width / 2, canvas.width / 2, 0, 2 * Math.PI)
        ctx.fill()
 
        angle -= 2 * Math.PI
    }
 
    ctx.beginPath()
    ctx.arc(canvas.width / 2, canvas.width / 2, canvas.width / 2, 0, angle)
    ctx.lineTo(canvas.width / 2, canvas.height / 2)
    ctx.fill()
 
    ctx.restore()
}
 
const start = data => {
    const button = document.querySelector('#start')
    if (data.start) {
        button.innerText = 'Start'
    } else {
        let hours = document.querySelector('#hours').value
        let minutes = document.querySelector('#minutes').value
        let seconds = document.querySelector('#seconds').value
        if (hours.trim() == '') hours = 0
        if (minutes.trim() == '') minutes = 0
        if (seconds.trim() == '') seconds = 0
 
        let sign
        if (hours != 0) {
            sign = hours < 0 ? -1 : 1
        } else if (minutes != 0) {
            sign = minutes < 0 ? -1 : 1
        } else {
            sign = seconds < 0 ? -1 : 1
        }
 
        minutes = sign * Math.abs(minutes)
        seconds = sign * Math.abs(seconds)
 
        data.endTime = Date.now() + 3600000 * hours + 60000 * minutes + 1000 * seconds
 
        button.innerText = 'Pause'
    }
    data.start ^= true
}
 
const reset = data => {
    data.endTime = Date.now()
    data.start = false
    document.querySelector('#hours').value = ''
    document.querySelector('#minutes').value = ''
    document.querySelector('#seconds').value = ''
    document.querySelector('#start').innerText = 'Start'
    data.reset = true
}
 
const showLicense = () => {
    const mask = document.createElement('div')
    mask.style = 'position:fixed;left:0;right:0;top:0;bottom:0;background:rgba(0, 0, 0, 0.5);z-index:1;'
    mask.onclick = () => {
        document.body.removeChild(mask)
        document.body.removeChild(message)
    }
    document.body.appendChild(mask)
 
    const message = document.createElement('div')
    message.style = 'position:fixed;left:50%;top:50%;transform:translate(-50%, -50%);z-index:2;background:#fff;padding:10px;border-radius:10px;overflow:auto;max-width:600px;max-height:500px;'
    message.innerText = 'License unavailable, please report as a bug.'
    message.onclick = () => {
        document.body.removeChild(mask)
        document.body.removeChild(message)
    }
 
    const ajax = new XMLHttpRequest()
    ajax.open('GET', 'LICENSE')
    ajax.onreadystatechange = () => {
        console.log('AJAX');
        message.innerText = ajax.responseText
    }
    ajax.send()
    document.body.append(message)
}
 
const timeToAngle = endTime => {
    const difference = endTime - Date.now()
    return difference / 3600000 * 2 * Math.PI
}
 
const pad = (value, format) => (format + value).slice(-format.length)
 
load()
* {
    box-sizing: border-box;
}

body {
    background: #eee;
    margin: 0;
}

main {
    background: #fff;
    max-width: 700px;
    margin: auto;
    padding: 10px;
    display: flex;
    flex-flow: column;
    align-items: center;
}

h1 {
    text-align: center;
}

menu {
    display: flex;
    justify-content: space-between;
    width: 500px;
    padding: 0;
}

#hours, #minutes, #seconds {
    width: 30px;
    text-align: center;
}

footer {
    max-width: 700px;
    margin: auto;
    background: #ddd;
    padding: 10px;
    display: flex;
    justify-content: space-between;
}

footer a {
    display: inline-block;
    background: #ccc;
    padding: 10px;
    border-radius: 10px;
    color: black;
    text-decoration: none;
    cursor: pointer;
}

footer a:hover {
    background: #888;
    color: #fff;
}
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Countdown</title>
    <script src="countdown.js" defer></script>
    <link rel="stylesheet" href="style.css" type="text/css">
</head>

<body>
    <main>
        <h1 contenteditable>Countdown</h1>
        <canvas id="countdown"></canvas>
        <menu>
            <button id="reset">Reset</button>
            <div class="group">
                <input id="hours" placeholder="HH" autofocus>:<input id="minutes" placeholder="MM">:<input id="seconds" placeholder="SS">
            </div>
            <button id="start">Start</button>
        </menu>
    </main>
    <footer>
        <a href="https://github.com/SteeveDroz/countdown">Find the project on GitHub</a>
        <a href="https://github.com/SteeveDroz/countdown/issues">Report bugs</a>
        <a id="license">MIT license</a>
    </footer>
</body>

</html>

\$\endgroup\$
  • 1
    \$\begingroup\$ You're using a browser version that doesn't support const when in strict mode, most likely a Safari version prior to 10. Simple fix is to just use var or transpile with babel. See this StackOverflow question. I'd say this question is also better placed over at the Stack Overflow site. \$\endgroup\$ – Sven Mar 22 '18 at 17:59
  • \$\begingroup\$ I've edited the title and I don't know why it can't pass jslint.com or other validators will keep throwing the same error(I'm pretty lost.) @Svenskunganka \$\endgroup\$ – DIDIx13 Mar 22 '18 at 18:33
  • 1
    \$\begingroup\$ jslint.com does not support latest JavaScript syntax - it's a pretty outdated site. Try ESLint instead. \$\endgroup\$ – Rene Saarsoo May 5 '18 at 11:39
2
\$\begingroup\$

You're passing your app state as the parameter data to almost all the functions.

This suggests you're better off combining these functions into a class, where you could assign these values to this which would be automatically accessible from all methods in class.

Alternatively, if this all is a tiny app, you could just use global variables, which too would be accessible from all functions.

\$\endgroup\$
1
\$\begingroup\$

Review points

  • Fetching DOM elements is not cheap, so cache those in a variable (perhaps in data) instead of accessing them each time.
  • When fetching DOM elements by id attribute, use document.getElementById() instead of document.querySelector('#') refer to answers to this SO question and the relevant jsPerf test

  • An event delegate can reduce the number of click handlers needed - the example snippet below uses Function.bind() to create partially applied functions for a simple event delegate - if you didn't use an event delegate, the click handlers like:

    document.querySelector('#start').onclick = event => {
        start(data)
    }
    

    Could be simplified like this:

    document.querySelector('#start').onclick = start.bind(null, data)
    
  • Unless you fully understand the ways Automatic semicolon insertion can be broken by certain statements, add semi-colons to terminate the lines.
  • The AJAX statechange handler (i.e. registered via onreadystatechange) doesn't check the readyState and status properties of the request, and thus the innerText property of message will likely be updated before desired (e.g. during request open, transmission, loading, etc.)

Rewrite

Below is what one could do with the advice above.

'use strict'

const load = () => {
  const data = {};

  init(data);

  setInterval(() => {
    update(data);
    render(data);
  }, 100);
  const mapping = {
    "start": start.bind(null, data),
    "reset": reset.bind(null, data),
    "license": showLicense
  };
  const clickHandler = event => event.target.id in mapping && mapping[event.target.id]();
  document.body.addEventListener('click', clickHandler);
}

const init = data => {
  data.canvas = document.getElementById('countdown');
  data.ctx = data.canvas.getContext('2d');

  data.endTime = Date.now();
  data.start = false;
  data.reset = false;

  data.canvas.width = data.canvas.height = 500;

  const inputs = ['hours', 'minutes', 'seconds'];
  inputs.forEach(inputName => data[inputName + 'Input'] = document.getElementById(inputName));
  data.startButton = document.getElementById('start');
}

const update = data => {
  if (data.start || data.reset) {
    data.startAngle = timeToAngle(data.endTime);
    data.reset = false;
  }
  if (data.start) {
    let difference = Math.abs(Math.floor((data.endTime - Date.now()) / 1000));
    let seconds = pad(difference % 60, '00');
    difference = Math.floor(difference / 60);
    let minutes = pad(difference % 60, '00');
    difference = Math.floor(difference / 60);
    let hours = difference;

    if (data.endTime < Date.now()) {
      if (hours != 0) {
        hours = -hours;
      } else if (minutes != 0) {
        minutes = -minutes;
      } else {
        seconds = -seconds;
      }
    }

    data.hoursInput.value = hours;
    data.minutesInput.value = minutes;
    data.secondsInput.value = seconds;
  }
}

const render = data => {
  const {
    canvas,
    ctx,
    startAngle,
    start
  } = data;

  ctx.fillStyle = '#fff';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  let angle = Math.abs(startAngle);

  ctx.save();

  if (startAngle > 0) {
    ctx.translate(canvas.width / 2, canvas.height / 2);
    ctx.rotate(-Math.PI / 2);
    ctx.scale(1, -1);
    ctx.translate(-canvas.width / 2, -canvas.height / 2);
    ctx.fillStyle = 'rgba(0, 0, 255, 0.5)';
  } else {
    ctx.translate(canvas.width / 2, canvas.height / 2);
    ctx.rotate(-Math.PI / 2);
    ctx.translate(-canvas.width / 2, -canvas.height / 2);
    ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
  }

  while (angle > 2 * Math.PI) {
    ctx.beginPath();
    ctx.arc(canvas.width / 2, canvas.width / 2, canvas.width / 2, 0, 2 * Math.PI);
    ctx.fill();

    angle -= 2 * Math.PI;
  }

  ctx.beginPath();
  ctx.arc(canvas.width / 2, canvas.width / 2, canvas.width / 2, 0, angle);
  ctx.lineTo(canvas.width / 2, canvas.height / 2);
  ctx.fill();

  ctx.restore();
}

const start = data => {
  if (data.start) {
    data.startButton.innerText = 'Start';
  } else {
    let hours = data.hoursInput.value;
    let minutes = data.minutesInput.value;
    let seconds = data.secondsInput.value;
    if (hours.trim() == '') hours = 0;
    if (minutes.trim() == '') minutes = 0;
    if (seconds.trim() == '') seconds = 0;

    let sign;
    if (hours != 0) {
      sign = hours < 0 ? -1 : 1;
    } else if (minutes != 0) {
      sign = minutes < 0 ? -1 : 1;
    } else {
      sign = seconds < 0 ? -1 : 1;
    }

    minutes = sign * Math.abs(minutes);
    seconds = sign * Math.abs(seconds);

    data.endTime = Date.now() + 3600000 * hours + 60000 * minutes + 1000 * seconds;

    data.startButton.innerText = 'Pause';
  }
  data.start ^= true;
}

const reset = data => {
  data.endTime = Date.now();
  data.start = false;
  data.hoursInput.value = '';
  data.minutesInput.value = '';
  data.secondsInput.value = '';
  data.startButton.innerText = 'Start';
  data.reset = true;
}

const showLicense = () => {
  const mask = document.createElement('div');
  mask.id = 'mask';
  mask.onclick = () => {
    document.body.removeChild(mask);
    document.body.removeChild(message);
  }
  document.body.appendChild(mask);

  const message = document.createElement('div');
  message.id = 'messageContainer';
  message.innerText = 'License unavailable, please report as a bug.';
  message.onclick = () => {
    document.body.removeChild(mask);
    document.body.removeChild(message);
  }

  const ajax = new XMLHttpRequest();
  ajax.open('GET', '/');
  ajax.onreadystatechange = () => {
    //check state, this doesn't make sense here in this snippet
    if (ajax.readyState === 4 && ajax.status === 200) {
      //message.innerText = ajax.responseText
    }
  };
  ajax.send();
  document.body.append(message);
}

const timeToAngle = endTime => {
  const difference = endTime - Date.now();
  return difference / 3600000 * 2 * Math.PI;
}

const pad = (value, format) => (format + value).slice(-format.length);

load();
* {
  box-sizing: border-box;
}

body {
  background: #eee;
  margin: 0;
}

main {
  background: #fff;
  max-width: 700px;
  margin: auto;
  padding: 10px;
  display: flex;
  flex-flow: column;
  align-items: center;
}

h1 {
  text-align: center;
}

menu {
  display: flex;
  justify-content: space-between;
  width: 500px;
  padding: 0;
}

#hours,
#minutes,
#seconds {
  width: 30px;
  text-align: center;
}

footer {
  max-width: 700px;
  margin: auto;
  background: #ddd;
  padding: 10px;
  display: flex;
  justify-content: space-between;
}

footer a {
  display: inline-block;
  background: #ccc;
  padding: 10px;
  border-radius: 10px;
  color: black;
  text-decoration: none;
  cursor: pointer;
}

footer a:hover {
  background: #888;
  color: #fff;
}

#mask {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 1;
}

#messageContainer {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  background: #fff;
  padding: 10px;
  border-radius: 10px;
  overflow: auto;
  max-width: 600px;
  max-height: 500px;
}
<main>
  <h1 contenteditable>Countdown</h1>
  <canvas id="countdown"></canvas>
  <menu>
    <button id="reset">Reset</button>
    <div class="group">
      <input id="hours" placeholder="HH" autofocus>:<input id="minutes" placeholder="MM">:<input id="seconds" placeholder="SS">
    </div>
    <button id="start">Start</button>
  </menu>
</main>
<footer>
  <a href="https://github.com/SteeveDroz/countdown">Find the project on GitHub</a>
  <a href="https://github.com/SteeveDroz/countdown/issues">Report bugs</a>
  <a id="license">MIT license</a>
</footer>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.