Skip to content

Commit

Permalink
feat(events): dispatch show events (show and shown)
Browse files Browse the repository at this point in the history
  • Loading branch information
Johann-S committed Mar 20, 2019
1 parent eb7b41c commit 792a30b
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 47 deletions.
25 changes: 11 additions & 14 deletions src/js/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { showContent, showStep, Selectors, ClassName, customProperty, detectAnimation } from './util'
import { show, Selectors, ClassName, customProperty, detectAnimation } from './util'
import { clickStepLinearListener, clickStepNonLinearListener } from './listeners'

const DEFAULT_OPTIONS = {
Expand Down Expand Up @@ -30,16 +30,15 @@ class Stepper {
}

detectAnimation(this._stepsContents, this.options.animation)
if (this._steps.length) {
showStep(this._steps[this._currentIndex], this._steps)
showContent(this._stepsContents[this._currentIndex], this._stepsContents)
}

this._setLinkListeners()
Object.defineProperty(this._element, customProperty, {
value: this,
writable: true
})

if (this._steps.length) {
show(this._element, this._currentIndex)
}
}

// Private
Expand All @@ -60,36 +59,34 @@ class Stepper {
next () {
this._currentIndex = (this._currentIndex + 1) <= this._steps.length - 1 ? this._currentIndex + 1 : (this._steps.length - 1)

showStep(this._steps[this._currentIndex], this._steps)
showContent(this._stepsContents[this._currentIndex], this._stepsContents)
show(this._element, this._currentIndex)
}

previous () {
this._currentIndex = (this._currentIndex - 1) >= 0 ? this._currentIndex - 1 : 0

showStep(this._steps[this._currentIndex], this._steps)
showContent(this._stepsContents[this._currentIndex], this._stepsContents)
show(this._element, this._currentIndex)
}

to (stepNumber) {
const tempIndex = stepNumber - 1

this._currentIndex = tempIndex >= 0 && tempIndex < this._steps.length
? tempIndex
: 0

showStep(this._steps[this._currentIndex], this._steps)
showContent(this._stepsContents[this._currentIndex], this._stepsContents)
show(this._element, this._currentIndex)
}

reset () {
this._currentIndex = 0
showStep(this._steps[this._currentIndex], this._steps)
showContent(this._stepsContents[this._currentIndex], this._stepsContents)
show(this._element, this._currentIndex)
}

destroy () {
this._steps.forEach(step => {
const trigger = step.querySelector(Selectors.TRIGGER)

if (this.options.linear) {
trigger.removeEventListener('click', clickStepLinearListener)
} else {
Expand Down
7 changes: 3 additions & 4 deletions src/js/listeners.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { closest } from './polyfill'
import { Selectors, customProperty, showStep, showContent } from './util'
import { Selectors, customProperty, show } from './util'

function clickStepLinearListener (event) {
event.preventDefault()
Expand All @@ -11,11 +11,10 @@ function clickStepNonLinearListener (event) {
const step = closest(event.target, Selectors.STEPS)
const stepperNode = closest(step, Selectors.STEPPER)
const stepper = stepperNode[customProperty]

const stepIndex = stepper._steps.indexOf(step)

stepper._currentIndex = stepIndex
showStep(step, stepper._steps)
showContent(stepper._stepsContents[stepIndex], stepper._stepsContents)
show(stepperNode, stepIndex)
}

export {
Expand Down
31 changes: 30 additions & 1 deletion src/js/polyfill.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
let matches = window.Element.prototype.matches
let closest = (element, selector) => element.closest(selector)
let WinEvent = (inType, params) => new window.Event(inType, params)
let createCustomEvent = (eventName, params) => {
const cEvent = new window.CustomEvent(eventName, params)

return cEvent
}

/* istanbul ignore next */
function polyfill () {
Expand Down Expand Up @@ -35,11 +40,35 @@ function polyfill () {
return e
}
}

if (typeof window.CustomEvent !== 'function') {
const originPreventDefault = window.Event.prototype.preventDefault

createCustomEvent = (eventName, params) => {
const evt = document.createEvent('CustomEvent')

params = params || { bubbles: false, cancelable: false, detail: null }
evt.initCustomEvent(eventName, params.bubbles, params.cancelable, params.detail)
evt.preventDefault = function () {
if (!this.cancelable) {
return
}

originPreventDefault.call(this)
Object.defineProperty(this, 'defaultPrevented', {
get: function () { return true }
})
}

return evt
}
}
}

polyfill()

export {
closest,
WinEvent
WinEvent,
createCustomEvent
}
60 changes: 43 additions & 17 deletions src/js/util.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WinEvent, closest } from './polyfill'
import { WinEvent, createCustomEvent } from './polyfill'

const MILLISECONDS_MULTIPLIER = 1000
const Selectors = {
Expand All @@ -18,18 +18,44 @@ const ClassName = {
const transitionEndEvent = 'transitionend'
const customProperty = 'bsStepper'

const showStep = (step, stepList) => {
if (step.classList.contains(ClassName.ACTIVE)) {
const show = (stepperNode, indexStep) => {
const stepper = stepperNode[customProperty]

if (stepper._steps[indexStep].classList.contains(ClassName.ACTIVE) || stepper._stepsContents[indexStep].classList.contains(ClassName.ACTIVE)) {
return
}

const showEvent = createCustomEvent('show.bs-stepper', {
cancelable: true,
detail: {
indexStep
}
})
stepperNode.dispatchEvent(showEvent)

const activeStep = stepper._steps.filter(step => step.classList.contains(ClassName.ACTIVE))
const activeContent = stepper._stepsContents.filter(content => content.classList.contains(ClassName.ACTIVE))

if (showEvent.defaultPrevented) {
return
}

const stepperNode = closest(step, Selectors.STEPPER)
const activeStep = stepList.filter(step => step.classList.contains(ClassName.ACTIVE))
if (activeStep.length) {
activeStep[0].classList.remove(ClassName.ACTIVE)
}
if (activeContent.length) {
activeContent[0].classList.remove(ClassName.ACTIVE)
activeContent[0].classList.remove(ClassName.BLOCK)
}

showStep(stepperNode, stepper._steps[indexStep], stepper._steps)
showContent(stepperNode, stepper._stepsContents[indexStep], stepper._stepsContents, activeContent)
}

const showStep = (stepperNode, step, stepList) => {
stepList.forEach(step => {
const trigger = step.querySelector(Selectors.TRIGGER)

trigger.setAttribute('aria-selected', 'false')
// if stepper is in linear mode, set disabled attribute on the trigger
if (stepperNode.classList.contains(ClassName.LINEAR)) {
Expand All @@ -39,32 +65,32 @@ const showStep = (step, stepList) => {

step.classList.add(ClassName.ACTIVE)
const currentTrigger = step.querySelector(Selectors.TRIGGER)

currentTrigger.setAttribute('aria-selected', 'true')
// if stepper is in linear mode, remove disabled attribute on current
if (stepperNode.classList.contains(ClassName.LINEAR)) {
currentTrigger.removeAttribute('disabled')
}
}

const showContent = (content, contentList) => {
if (content.classList.contains(ClassName.ACTIVE)) {
return
}
const showContent = (stepperNode, content, contentList, activeContent) => {
const shownEvent = createCustomEvent('shown.bs-stepper', {
cancelable: true,
detail: {
indexStep: contentList.indexOf(content)
}
})

function complete () {
content.classList.add(ClassName.BLOCK)
content.removeEventListener(transitionEndEvent, complete)
}

const activeContent = contentList.filter(content => content.classList.contains(ClassName.ACTIVE))
if (activeContent.length) {
activeContent[0].classList.remove(ClassName.ACTIVE)
activeContent[0].classList.remove(ClassName.BLOCK)
stepperNode.dispatchEvent(shownEvent)
}

if (content.classList.contains(ClassName.FADE)) {
content.classList.remove(ClassName.NONE)
const duration = getTransitionDurationFromElement(content)

content.addEventListener(transitionEndEvent, complete)
if (activeContent.length) {
activeContent[0].classList.add(ClassName.NONE)
Expand All @@ -74,6 +100,7 @@ const showContent = (content, contentList) => {
emulateTransitionEnd(content, duration)
} else {
content.classList.add(ClassName.ACTIVE)
stepperNode.dispatchEvent(shownEvent)
}
}

Expand Down Expand Up @@ -126,8 +153,7 @@ const detectAnimation = (contentList, animation) => {
}

export {
showContent,
showStep,
show,
Selectors,
ClassName,
customProperty,
Expand Down
19 changes: 11 additions & 8 deletions tests/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -143,20 +143,23 @@ <h2>Vertical stepper</h2>
</div>
<script src="dist/js/bs-stepper.js"></script>
<script>
var stepper1 = null
var stepper2 = null
var stepper3 = null
var stepper1Node = document.querySelector('#stepper1')
var stepper1 = new Stepper(document.querySelector('#stepper1'))

document.addEventListener('DOMContentLoaded', function () {
stepper1 = new Stepper(document.querySelector('#stepper1'))
stepper2 = new Stepper(document.querySelector('#stepper2'), {
stepper1Node.addEventListener('show.bs-stepper', function (event) {
console.warn('show.bs-stepper', event)
})
stepper1Node.addEventListener('shown.bs-stepper', function (event) {
console.warn('shown.bs-stepper', event)
})

var stepper2 = new Stepper(document.querySelector('#stepper2'), {
linear: false,
animation: true
})
stepper3 = new Stepper(document.querySelector('#stepper3'), {
var stepper3 = new Stepper(document.querySelector('#stepper3'), {
animation: true
})
})
</script>
</body>
</html>
52 changes: 49 additions & 3 deletions tests/units/main.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ describe('Stepper', function () {
})

describe('next', function () {
it('should go to the next step', function () {
it('should go to the next step', function (done) {
fixture.innerHTML = [
'<div id="myStepper" class="bs-stepper">',
' <div class="step" data-target="#test1">',
Expand All @@ -222,10 +222,56 @@ describe('Stepper', function () {
var stepperNode = document.getElementById('myStepper')
var stepper = new Stepper(stepperNode)

stepperNode.addEventListener('show.bs-stepper', function (event) {
expect(event.detail.indexStep).toEqual(1)
})
stepperNode.addEventListener('shown.bs-stepper', function (event) {
expect(event.detail.indexStep).toEqual(1)
expect(document.querySelector('#test1').classList.contains('active')).toBe(false)
expect(document.querySelector('#test2').classList.contains('active')).toBe(true)
done()
})

stepper.next()
})

expect(document.querySelector('#test1').classList.contains('active')).toBe(false)
expect(document.querySelector('#test2').classList.contains('active')).toBe(true)
it('should not go to the next step if the show event is default prevented', function (done) {
fixture.innerHTML = [
'<div id="myStepper" class="bs-stepper">',
' <div class="step" data-target="#test1">',
' <button class="step-trigger">1</button>',
' </div>',
' <div class="step" data-target="#test2">',
' <button class="step-trigger">2</button>',
' </div>',
' <div id="test1">1</div>',
' <div id="test2">2</div>',
'</div>'
].join('')

var stepperNode = document.getElementById('myStepper')
var stepper = new Stepper(stepperNode)
var listeners = {
show: function (event) {
event.preventDefault()
expect(event.detail.indexStep).toEqual(1)

setTimeout(function () {
expect(listeners.shown).not.toHaveBeenCalled()
done()
}, 10)
},
shown: function (event) {
console.warn('shown called but it should not be the case')
}
}

spyOn(listeners, 'shown')

stepperNode.addEventListener('show.bs-stepper', listeners.show)
stepperNode.addEventListener('shown.bs-stepper', listeners.shown)

stepper.next()
})

it('should stay at the end if we call next', function () {
Expand Down

0 comments on commit 792a30b

Please sign in to comment.