Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions asset/images/counter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions asset/images/eye-off.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions asset/images/eye.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 81 additions & 0 deletions asset/javascript/TimeController.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ function TimerController(reference, hooks = {}) {
const stopButton = actionButtonsContainer.querySelector('.js-stop-button');
const pauseButton = actionButtonsContainer.querySelector('.js-pause-button');
const editButton = actionButtonsContainer.querySelector('.js-edit-button');
const counterToggleButton = actionButtonsContainer.querySelector('.js-counter-toggle-button');

const counterContainer = reference.querySelector('.js-timer-counter');
const counterInput = reference.querySelector('.js-counter-value');
const counterDecrementButton = reference.querySelector('.js-counter-decrement-button');
const counterIncrementButton = reference.querySelector('.js-counter-increment-button');

const editActionButtonsContainer = reference.querySelector('.js-edit-container-stopwatch');
const cancelEditButton = editActionButtonsContainer.querySelector('.js-cancel-edit-button');
Expand All @@ -39,10 +45,13 @@ function TimerController(reference, hooks = {}) {
let deadline = null;
let previousTimerValue = DEFAULT_SECONDS;
let resetTimeoutId = null;
let counterEnabled = false;
let counterValue = 0;

function init() {
bindInputs();
bindButtons();
bindCounter();
setRemaining(DEFAULT_SECONDS);
}

Expand Down Expand Up @@ -118,6 +127,55 @@ function TimerController(reference, hooks = {}) {
pauseButton.addEventListener('click', pause);
}

function bindCounter() {
if (!counterContainer) return;

if (counterToggleButton) counterToggleButton.addEventListener('click', toggleCounter);
if (counterDecrementButton) counterDecrementButton.addEventListener('click', decrementCounter);
if (counterIncrementButton) counterIncrementButton.addEventListener('click', incrementCounter);

if (counterInput) {
counterInput.addEventListener('change', () => setCounter(counterInput.value));
counterInput.addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
counterInput.blur();
}
});
}
}

function toggleCounter() {
setCounterEnabled(!counterEnabled);
notifyChange();
}

function setCounterEnabled(enabled) {
counterEnabled = !!enabled;

if (counterContainer) counterContainer.classList.toggle('hide', !counterEnabled);
if (counterToggleButton) counterToggleButton.classList.toggle('is-active', counterEnabled);
reference.classList.toggle('has-counter', counterEnabled);
}

function setCounter(value) {
const parsed = parseInt(value, 10);

counterValue = Number.isFinite(parsed) ? parsed : 0;

if (counterInput) counterInput.value = counterValue;

notifyChange();
}

function incrementCounter() {
setCounter(counterValue + 1);
}

function decrementCounter() {
setCounter(counterValue - 1);
}

function openEditInput() {
if (TimerStatus.isRunning(status) || TimerStatus.isCountdown(status)) return;

Expand Down Expand Up @@ -344,15 +402,31 @@ function TimerController(reference, hooks = {}) {
remaining,
status,
endsAt: isActive() ? deadline : null,
counter: {
enabled: counterEnabled,
value: counterValue,
},
};
}

function restoreCounter(counter) {
const enabled = !!(counter && counter.enabled);
const parsed = counter ? parseInt(counter.value, 10) : 0;

counterEnabled = enabled;
counterValue = Number.isFinite(parsed) ? parsed : 0;

if (counterInput) counterInput.value = counterValue;
setCounterEnabled(counterEnabled);
}

function restore(data = {}) {
clearTimeout(resetTimeoutId);
setInverted(false);
hideCountdownNumber();

setName(data.name || '');
restoreCounter(data.counter);
duration = data.duration > 0 ? data.duration : DEFAULT_SECONDS;

const wasActive = data.status === TimerStatus.RUNNING || data.status === TimerStatus.COUNTDOWN;
Expand Down Expand Up @@ -411,6 +485,13 @@ function TimerController(reference, hooks = {}) {
getStatus: () => status,
getRemaining: () => remaining,
getDuration: () => duration,
getCounter: () => counterValue,
setCounter,
incrementCounter,
decrementCounter,
isCounterEnabled: () => counterEnabled,
setCounterEnabled,
toggleCounter,
serialize,
restore,
destroy,
Expand Down
30 changes: 27 additions & 3 deletions asset/javascript/TimerApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export function TimerApp(reference) {
const stopAllButton = globalControls.querySelector('.js-stop-all-button');
const enterFullscreenButton = globalControls.querySelector('.js-enter-fullscreen-button');
const exitFullscreenButton = globalControls.querySelector('.js-exit-fullscreen-button');
const hideControlsButton = globalControls.querySelector('.js-hide-controls-button');
const showControlsButton = globalControls.querySelector('.js-show-controls-button');

const layoutButtons = Array.from(reference.querySelectorAll('.js-layout-button'));
const themeMenuContainer = reference.querySelector('.js-theme-menu-container');
Expand All @@ -31,6 +33,7 @@ export function TimerApp(reference) {
let timers = [];
let layout = DEFAULT_LAYOUT;
let tickIntervalId = null;
let controlsHidden = false;

function init() {
bindGlobalButtons();
Expand All @@ -48,6 +51,7 @@ export function TimerApp(reference) {

if (state && AVAILABLE_LAYOUTS.includes(state.layout)) {
theme.setTheme(state.theme);
setControlsHidden(!!state.controlsHidden);
applyLayout(state.layout, state.timers || []);
return;
}
Expand All @@ -66,6 +70,23 @@ export function TimerApp(reference) {

enterFullscreenButton.addEventListener('click', handleFullscreen);
exitFullscreenButton.addEventListener('click', handleFullscreen);

hideControlsButton.addEventListener('click', () => {
setControlsHidden(true);
persist();
});
showControlsButton.addEventListener('click', () => {
setControlsHidden(false);
persist();
});
}

function setControlsHidden(hidden) {
controlsHidden = !!hidden;

reference.classList.toggle('controls-hidden', controlsHidden);
hideControlsButton.classList.toggle('hide', controlsHidden);
showControlsButton.classList.toggle('hide', !controlsHidden);
}

function bindLayoutButtons() {
Expand Down Expand Up @@ -208,7 +229,7 @@ export function TimerApp(reference) {

let shouldPlayTick = false;
let shouldPlayStop = false;
let countdownTimer = null;
const countdownTimers = [];

timers.forEach(timer => {
const result = timer.tick(now);
Expand All @@ -222,14 +243,14 @@ export function TimerApp(reference) {

if (result.second <= COUNTDOWN_THRESHOLD) {
shouldPlayTick = true;
if (!countdownTimer) countdownTimer = timer;
countdownTimers.push(timer);
}
});

if (shouldPlayStop) sound.playStop();
if (shouldPlayTick && !shouldPlayStop) sound.playTick();

if (countdownTimer) renderCountdown(countdownTimer);
countdownTimers.forEach(renderCountdown);
updatePageTitle();
persist();
ensureTicking();
Expand Down Expand Up @@ -306,6 +327,7 @@ export function TimerApp(reference) {
saveState({
layout,
theme: theme.getTheme(),
controlsHidden,
timers: serializeTimers(),
});
}
Expand All @@ -316,6 +338,8 @@ export function TimerApp(reference) {
getTimers: () => timers,
getLayout: () => layout,
setLayout: total => applyLayout(total, serializeTimers()),
isControlsHidden: () => controlsHidden,
setControlsHidden,
};
}

Expand Down
9 changes: 9 additions & 0 deletions asset/javascript/__mocks__/timerDom.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const TIMER_CARD_MARKUP = `
</div>
<span class="timer-countdown js-timer-countdown hide"></span>
<div class="action-button-container js-stopwatch-action-buttons">
<button class="as-icon js-counter-toggle-button"></button>
<button class="as-icon js-edit-button"></button>
<button class="as-icon js-stop-button hide"></button>
<button class="as-icon js-start-button"></button>
Expand All @@ -19,6 +20,12 @@ export const TIMER_CARD_MARKUP = `
<button class="as-icon js-cancel-edit-button"></button>
<button class="as-icon js-finish-edit-button"></button>
</div>

<div class="timer-counter js-timer-counter hide">
<button class="as-icon js-counter-decrement-button">−</button>
<input type="number" class="timer-counter-value js-counter-value" value="0" />
<button class="as-icon js-counter-increment-button">+</button>
</div>
</div>
`;

Expand All @@ -30,6 +37,8 @@ export const APP_MARKUP = `
<div class="action-button-container global-controls js-global-controls">
<button class="as-icon js-enter-fullscreen-button"></button>
<button class="as-icon js-exit-fullscreen-button hide"></button>
<button class="as-icon js-hide-controls-button"></button>
<button class="as-icon js-show-controls-button hide"></button>
<button class="as-icon js-stop-all-button hide"></button>
<button class="as-icon js-start-all-button hide"></button>
<button class="as-icon js-pause-all-button hide"></button>
Expand Down
59 changes: 59 additions & 0 deletions asset/javascript/__tests__/TimeController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,63 @@ describe('TimerController', () => {
expect(timer.getStatus()).toBe(TimerStatus.STOPPED);
expect(timer.getRemaining()).toBe(300);
});

it('liga e desliga o contador pelo botão do card', () => {
const counter = card.querySelector('.js-timer-counter');

expect(timer.isCounterEnabled()).toBe(false);
expect(counter.classList.contains('hide')).toBe(true);

card.querySelector('.js-counter-toggle-button').click();

expect(timer.isCounterEnabled()).toBe(true);
expect(counter.classList.contains('hide')).toBe(false);

card.querySelector('.js-counter-toggle-button').click();

expect(timer.isCounterEnabled()).toBe(false);
expect(counter.classList.contains('hide')).toBe(true);
});

it('incrementa, decrementa e aceita valor digitado no contador', () => {
const input = card.querySelector('.js-counter-value');

card.querySelector('.js-counter-increment-button').click();
card.querySelector('.js-counter-increment-button').click();
expect(timer.getCounter()).toBe(2);
expect(input.value).toBe('2');

card.querySelector('.js-counter-decrement-button').click();
expect(timer.getCounter()).toBe(1);

input.value = '15';
input.dispatchEvent(new Event('change'));
expect(timer.getCounter()).toBe(15);

input.value = 'abc';
input.dispatchEvent(new Event('change'));
expect(timer.getCounter()).toBe(0);
});

it('serializa e restaura o contador', () => {
timer.toggleCounter();
timer.setCounter(7);

expect(timer.serialize().counter).toEqual({ enabled: true, value: 7 });

const otherCard = mountTimerCard();
const other = TimerController(otherCard);
other.restore({ duration: 30, remaining: 30, status: 'STOPPED', counter: { enabled: true, value: 7 } });

expect(other.isCounterEnabled()).toBe(true);
expect(other.getCounter()).toBe(7);
expect(otherCard.querySelector('.js-counter-value').value).toBe('7');

other.restore({ duration: 30, remaining: 30, status: 'STOPPED' });

expect(other.isCounterEnabled()).toBe(false);
expect(other.getCounter()).toBe(0);

other.destroy();
});
});
44 changes: 44 additions & 0 deletions asset/javascript/__tests__/TimerApp.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,48 @@ describe('TimerApp', () => {

expect(restored.getLayout()).toBe(1);
});

it('esconde e mostra os controles individuais pelo botão de olho', () => {
const hideButton = body.querySelector('.js-hide-controls-button');
const showButton = body.querySelector('.js-show-controls-button');

expect(app.isControlsHidden()).toBe(false);

hideButton.click();

expect(app.isControlsHidden()).toBe(true);
expect(body.classList.contains('controls-hidden')).toBe(true);
expect(hideButton.classList.contains('hide')).toBe(true);
expect(showButton.classList.contains('hide')).toBe(false);

showButton.click();

expect(app.isControlsHidden()).toBe(false);
expect(body.classList.contains('controls-hidden')).toBe(false);
});

it('restaura o estado de controles escondidos', () => {
body.querySelector('.js-hide-controls-button').click();

const restored = TimerApp(mountApp());

expect(restored.isControlsHidden()).toBe(true);
expect(document.body.classList.contains('controls-hidden')).toBe(true);
});

it('mostra a contagem regressiva em todos os timers que chegam nos segundos finais juntos', () => {
selectLayout(2);
const [first, second] = app.getTimers();

first.restore({ duration: 30, remaining: 12, status: 'PAUSED' });
second.restore({ duration: 30, remaining: 12, status: 'PAUSED' });
body.querySelector('.js-start-all-button').click();

jest.advanceTimersByTime(3000);

const numbers = Array.from(body.querySelectorAll('.js-timer-countdown'));

expect(numbers.every(number => !number.classList.contains('hide'))).toBe(true);
expect(numbers.map(number => number.textContent)).toEqual(['9', '9']);
});
});
Loading
Loading