diff --git a/README.md b/README.md index 9847f30..c641caa 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ The alternative text for this image. Assign [alt text](https://github.com/adaptl
---------------------------- -**Version number:** 5.4.1 +**Version number:** 5.5.0 **Framework versions:** 5.17.2+ **Author / maintainer:** Adapt Core Team with [contributors](https://github.com/adaptlearning/adapt-contrib-hotgraphic/graphs/contributors) **Accessibility support:** WAI AA diff --git a/bower.json b/bower.json index d9615fe..62725ac 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "adapt-contrib-hotgraphic", - "version": "5.4.1", + "version": "5.5.0", "framework": ">=5.17.2", "homepage": "https://github.com/adaptlearning/adapt-contrib-hotgraphic", "bugs": "https://github.com/adaptlearning/adapt-contrib-hotgraphic/issues", diff --git a/js/adapt-contrib-hotgraphic.js b/js/adapt-contrib-hotgraphic.js index 45882e9..eb4b9fa 100644 --- a/js/adapt-contrib-hotgraphic.js +++ b/js/adapt-contrib-hotgraphic.js @@ -1,12 +1,8 @@ -define([ - 'core/js/adapt', - './hotgraphicView', - 'core/js/models/itemsComponentModel' -], function(Adapt, HotgraphicView, ItemsComponentModel) { - - return Adapt.register('hotgraphic', { - model: ItemsComponentModel.extend({}), - view: HotgraphicView - }); +import Adapt from 'core/js/adapt'; +import HotgraphicView from './hotgraphicView'; +import ItemsComponentModel from 'core/js/models/itemsComponentModel'; +export default Adapt.register('hotgraphic', { + model: ItemsComponentModel.extend({}), + view: HotgraphicView }); diff --git a/js/hotgraphicPopupView.js b/js/hotgraphicPopupView.js index f267937..1e7f7ef 100644 --- a/js/hotgraphicPopupView.js +++ b/js/hotgraphicPopupView.js @@ -1,144 +1,140 @@ -define([ - 'core/js/adapt' -], function(Adapt) { - - class HotgraphicPopupView extends Backbone.View { - - className() { - return 'hotgraphic-popup'; - } - - events() { - return { - 'click .js-hotgraphic-popup-close': 'closePopup', - 'click .js-hotgraphic-controls-click': 'onControlClick' - }; - } - - initialize(...args) { - super.initialize(...args); - // Debounce required as a second (bad) click event is dispatched on iOS causing a jump of two items. - this.onControlClick = _.debounce(this.onControlClick.bind(this), 100); - this.listenToOnce(Adapt, 'notify:opened', this.onOpened); - this.listenTo(this.model.get('_children'), { - 'change:_isActive': this.onItemsActiveChange, - 'change:_isVisited': this.onItemsVisitedChange - }); - this.render(); - } - - onOpened() { - this.applyNavigationClasses(this.model.getActiveItem().get('_index')); - this.updatePageCount(); - this.handleTabs(); - } - - applyNavigationClasses (index) { - const itemCount = this.model.get('_items').length; - const canCycleThroughPagination = this.model.get('_canCycleThroughPagination'); - - const shouldEnableBack = index > 0 || canCycleThroughPagination; - const shouldEnableNext = index < itemCount - 1 || canCycleThroughPagination; - const $controls = this.$('.hotgraphic-popup__controls'); - - this.$('hotgraphic-popup__nav') - .toggleClass('first', !shouldEnableBack) - .toggleClass('last', !shouldEnableNext); - - Adapt.a11y.toggleAccessibleEnabled($controls.filter('.back'), shouldEnableBack); - Adapt.a11y.toggleAccessibleEnabled($controls.filter('.next'), shouldEnableNext); - } - - updatePageCount() { - const template = Adapt.course.get('_globals')._components._hotgraphic.popupPagination || '{{itemNumber}} / {{totalItems}}'; - const labelText = Handlebars.compile(template)({ - itemNumber: this.model.getActiveItem().get('_index') + 1, - totalItems: this.model.get('_items').length - }); - this.$('.hotgraphic-popup__count').html(labelText); - } - - handleTabs() { - Adapt.a11y.toggleHidden(this.$('.hotgraphic-popup__item:not(.is-active)'), true); - Adapt.a11y.toggleHidden(this.$('.hotgraphic-popup__item.is-active'), false); - } - - onItemsActiveChange(item, _isActive) { - if (!_isActive) return; - const index = item.get('_index'); - this.updatePageCount(); - this.applyItemClasses(index); - this.handleTabs(); - this.handleFocus(index); - } - - applyItemClasses(index) { - this.$(`.hotgraphic-popup__item[data-index="${index}"]`).addClass('is-active').removeAttr('aria-hidden'); - this.$(`.hotgraphic-popup__item[data-index="${index}"] .hotgraphic-popup__item-title`).attr('id', 'notify-heading'); - this.$(`.hotgraphic-popup__item:not([data-index="${index}"])`).removeClass('is-active').attr('aria-hidden', 'true'); - this.$(`.hotgraphic-popup__item:not([data-index="${index}"]) .hotgraphic-popup__item-title`).removeAttr('id'); - } - - handleFocus(index) { - Adapt.a11y.focusFirst(this.$('.hotgraphic-popup__inner .is-active')); - this.applyNavigationClasses(index); - } - - onItemsVisitedChange(item, _isVisited) { - if (!_isVisited) return; - - this.$('.hotgraphic-popup__item') - .filter(`[data-index="${item.get('_index')}"]`) - .addClass('is-visited'); - } - - render() { - const data = this.model.toJSON(); - data.view = this; - const template = Handlebars.templates[this.constructor.template]; - this.$el.html(template(data)); - } - - closePopup() { - Adapt.trigger('notify:close'); - } - - onControlClick(event) { - const direction = $(event.currentTarget).data('direction'); - const index = this.getNextIndex(direction); - if (index === -1) return; - - this.setItemState(index); - } - - getNextIndex(direction) { - let index = this.model.getActiveItem().get('_index'); - const lastIndex = this.model.get('_items').length - 1; - - switch (direction) { - case 'back': - if (index > 0) return --index; - if (this.model.get('_canCycleThroughPagination')) return lastIndex; - break; - case 'next': - if (index < lastIndex) return ++index; - if (this.model.get('_canCycleThroughPagination')) return 0; - } - return -1; - } - - setItemState(index) { - this.model.getActiveItem().toggleActive(); - - const nextItem = this.model.getItem(index); - nextItem.toggleActive(); - nextItem.toggleVisited(true); - } - - }; - - HotgraphicPopupView.template = 'hotgraphicPopup'; - - return HotgraphicPopupView; - -}); +import Adapt from 'core/js/adapt'; + +class HotgraphicPopupView extends Backbone.View { + + className() { + return 'hotgraphic-popup'; + } + + events() { + return { + 'click .js-hotgraphic-popup-close': 'closePopup', + 'click .js-hotgraphic-controls-click': 'onControlClick' + }; + } + + initialize(...args) { + super.initialize(...args); + // Debounce required as a second (bad) click event is dispatched on iOS causing a jump of two items. + this.onControlClick = _.debounce(this.onControlClick.bind(this), 100); + this.listenToOnce(Adapt, 'notify:opened', this.onOpened); + this.listenTo(this.model.get('_children'), { + 'change:_isActive': this.onItemsActiveChange, + 'change:_isVisited': this.onItemsVisitedChange + }); + this.render(); + } + + onOpened() { + this.applyNavigationClasses(this.model.getActiveItem().get('_index')); + this.updatePageCount(); + this.handleTabs(); + } + + applyNavigationClasses (index) { + const itemCount = this.model.get('_items').length; + const canCycleThroughPagination = this.model.get('_canCycleThroughPagination'); + + const shouldEnableBack = index > 0 || canCycleThroughPagination; + const shouldEnableNext = index < itemCount - 1 || canCycleThroughPagination; + const $controls = this.$('.hotgraphic-popup__controls'); + + this.$('hotgraphic-popup__nav') + .toggleClass('first', !shouldEnableBack) + .toggleClass('last', !shouldEnableNext); + + Adapt.a11y.toggleAccessibleEnabled($controls.filter('.back'), shouldEnableBack); + Adapt.a11y.toggleAccessibleEnabled($controls.filter('.next'), shouldEnableNext); + } + + updatePageCount() { + const template = Adapt.course.get('_globals')._components._hotgraphic.popupPagination || '{{itemNumber}} / {{totalItems}}'; + const labelText = Handlebars.compile(template)({ + itemNumber: this.model.getActiveItem().get('_index') + 1, + totalItems: this.model.get('_items').length + }); + this.$('.hotgraphic-popup__count').html(labelText); + } + + handleTabs() { + Adapt.a11y.toggleHidden(this.$('.hotgraphic-popup__item:not(.is-active)'), true); + Adapt.a11y.toggleHidden(this.$('.hotgraphic-popup__item.is-active'), false); + } + + onItemsActiveChange(item, _isActive) { + if (!_isActive) return; + const index = item.get('_index'); + this.updatePageCount(); + this.applyItemClasses(index); + this.handleTabs(); + this.handleFocus(index); + } + + applyItemClasses(index) { + this.$(`.hotgraphic-popup__item[data-index="${index}"]`).addClass('is-active').removeAttr('aria-hidden'); + this.$(`.hotgraphic-popup__item[data-index="${index}"] .hotgraphic-popup__item-title`).attr('id', 'notify-heading'); + this.$(`.hotgraphic-popup__item:not([data-index="${index}"])`).removeClass('is-active').attr('aria-hidden', 'true'); + this.$(`.hotgraphic-popup__item:not([data-index="${index}"]) .hotgraphic-popup__item-title`).removeAttr('id'); + } + + handleFocus(index) { + Adapt.a11y.focusFirst(this.$('.hotgraphic-popup__inner .is-active')); + this.applyNavigationClasses(index); + } + + onItemsVisitedChange(item, _isVisited) { + if (!_isVisited) return; + + this.$('.hotgraphic-popup__item') + .filter(`[data-index="${item.get('_index')}"]`) + .addClass('is-visited'); + } + + render() { + const data = this.model.toJSON(); + data.view = this; + const template = Handlebars.templates[this.constructor.template]; + this.$el.html(template(data)); + } + + closePopup() { + Adapt.trigger('notify:close'); + } + + onControlClick(event) { + const direction = $(event.currentTarget).data('direction'); + const index = this.getNextIndex(direction); + if (index === -1) return; + + this.setItemState(index); + } + + getNextIndex(direction) { + let index = this.model.getActiveItem().get('_index'); + const lastIndex = this.model.get('_items').length - 1; + + switch (direction) { + case 'back': + if (index > 0) return --index; + if (this.model.get('_canCycleThroughPagination')) return lastIndex; + break; + case 'next': + if (index < lastIndex) return ++index; + if (this.model.get('_canCycleThroughPagination')) return 0; + } + return -1; + } + + setItemState(index) { + this.model.getActiveItem().toggleActive(); + + const nextItem = this.model.getItem(index); + nextItem.toggleActive(); + nextItem.toggleVisited(true); + } + +}; + +HotgraphicPopupView.template = 'hotgraphicPopup'; + +export default HotgraphicPopupView; diff --git a/js/hotgraphicView.js b/js/hotgraphicView.js index 99c3dc7..fc61c21 100644 --- a/js/hotgraphicView.js +++ b/js/hotgraphicView.js @@ -1,171 +1,165 @@ -define([ - 'core/js/adapt', - 'core/js/views/componentView', - './hotgraphicPopupView' -], function(Adapt, ComponentView, HotgraphicPopupView) { - - class HotGraphicView extends ComponentView { - - events () { - return { - 'click .js-hotgraphic-item-click': 'onPinClicked' - }; - } +import Adapt from 'core/js/adapt'; +import ComponentView from 'core/js/views/componentView'; +import HotgraphicPopupView from './hotgraphicPopupView'; - initialize(...args) { - super.initialize(...args); +class HotGraphicView extends ComponentView { - this.setUpViewData(); - this.setUpModelData(); - this.setUpEventListeners(); - } + events () { + return { + 'click .js-hotgraphic-item-click': 'onPinClicked' + }; + } - setUpViewData() { - this.popupView = null; - this._isPopupOpen = false; - } + initialize(...args) { + super.initialize(...args); - setUpModelData() { - if (this.model.get('_canCycleThroughPagination') === undefined) { - this.model.set('_canCycleThroughPagination', false); - } - } + this.setUpViewData(); + this.setUpModelData(); + this.setUpEventListeners(); + } - setUpEventListeners() { - this.listenTo(Adapt, 'device:changed', this.reRender); + setUpViewData() { + this.popupView = null; + this._isPopupOpen = false; + } - this.listenTo(this.model.get('_children'), { - 'change:_isActive': this.onItemsActiveChange, - 'change:_isVisited': this.onItemsVisitedChange - }); - } + setUpModelData() { + if (this.model.get('_canCycleThroughPagination') !== undefined) return; + this.model.set('_canCycleThroughPagination', false); + } - reRender() { - if (Adapt.device.screenSize === 'large' || this.model.get('_isNarrativeOnMobile') === false) return; + setUpEventListeners() { + this.listenTo(Adapt, 'device:changed', this.reRender); - this.replaceWithNarrative(); - } + this.listenTo(this.model.get('_children'), { + 'change:_isActive': this.onItemsActiveChange, + 'change:_isVisited': this.onItemsVisitedChange + }); + } - replaceWithNarrative() { - const NarrativeView = Adapt.getViewClass('narrative'); - if (!NarrativeView) return; - - const model = this.prepareNarrativeModel(); - const newNarrative = new NarrativeView({ model }); - // NOTE: if this component is doing its inital render in 'narrative mode', - // this.$el.parents() won't exist at this point - which is why the following is - // written the way it is, instead of (what would appear to be) the more efficient - // this.$el.parents('.component__container') - const $container = Adapt.findViewByModelId(model.get('_parentId')).$el.find('.component__container'); - $container.append(newNarrative.$el); - - this.remove(); - _.defer(() => { - Adapt.trigger('device:resize'); - }); - } + reRender() { + if (Adapt.device.screenSize === 'large' || this.model.get('_isNarrativeOnMobile') === false) return; - prepareNarrativeModel() { - this.model.set({ - _component: 'narrative', - _wasHotgraphic: true, - originalBody: this.model.get('body'), - originalInstruction: this.model.get('instruction') - }); - - // Check if active item exists, default to 0 - const activeItem = this.model.getActiveItem(); - if (!activeItem) { - this.model.getItem(0).toggleActive(true); - } - - // Swap mobile body and instructions for desktop variants. - if (this.model.get('mobileBody')) { - this.model.set('body', this.model.get('mobileBody')); - } - if (this.model.get('mobileInstruction')) { - this.model.set('instruction', this.model.get('mobileInstruction')); - } - - return this.model; - } + this.replaceWithNarrative(); + } + + replaceWithNarrative() { + const NarrativeView = Adapt.getViewClass('narrative'); + if (!NarrativeView) return; + + const model = this.prepareNarrativeModel(); + const newNarrative = new NarrativeView({ model }); + // NOTE: if this component is doing its inital render in 'narrative mode', + // this.$el.parents() won't exist at this point - which is why the following is + // written the way it is, instead of (what would appear to be) the more efficient + // this.$el.parents('.component__container') + const $container = Adapt.findViewByModelId(model.get('_parentId')).$el.find('.component__container'); + $container.append(newNarrative.$el); + + this.remove(); + _.defer(() => { + Adapt.trigger('device:resize'); + }); + } + + prepareNarrativeModel() { + this.model.set({ + _component: 'narrative', + _wasHotgraphic: true, + originalBody: this.model.get('body'), + originalInstruction: this.model.get('instruction') + }); - onItemsActiveChange(model, _isActive) { - this.getItemElement(model).toggleClass('is-active', _isActive); + // Check if active item exists, default to 0 + const activeItem = this.model.getActiveItem(); + if (!activeItem) { + this.model.getItem(0).toggleActive(true); } - getItemElement(model) { - const index = model.get('_index'); - return this.$('.js-hotgraphic-item-click').filter(`[data-index="${index}"]`); + // Swap mobile body and instructions for desktop variants. + if (this.model.get('mobileBody')) { + this.model.set('body', this.model.get('mobileBody')); + } + if (this.model.get('mobileInstruction')) { + this.model.set('instruction', this.model.get('mobileInstruction')); } - onItemsVisitedChange(model, _isVisited) { - if (!_isVisited) return; + return this.model; + } - const $pin = this.getItemElement(model); - // Append the word 'visited.' to the pin's aria-label - const visitedLabel = ` ${this.model.get('_globals')._accessibility._ariaLabels.visited}.`; - $pin.find('.aria-label').each(function(index, el) { - el.innerHTML += visitedLabel; - }); + onItemsActiveChange(model, _isActive) { + this.getItemElement(model).toggleClass('is-active', _isActive); + } - $pin.addClass('is-visited'); - } + getItemElement(model) { + const index = model.get('_index'); + return this.$('.js-hotgraphic-item-click').filter(`[data-index="${index}"]`); + } - preRender() { - if (Adapt.device.screenSize === 'large') { - this.render(); - return; - } + onItemsVisitedChange(model, _isVisited) { + if (!_isVisited) return; - this.reRender(); - } + const $pin = this.getItemElement(model); + // Append the word 'visited.' to the pin's aria-label + const visitedLabel = ` ${this.model.get('_globals')._accessibility._ariaLabels.visited}.`; + $pin.find('.aria-label').each((index, el) => { + el.innerHTML += visitedLabel; + }); + + $pin.addClass('is-visited'); + } - postRender() { - this.$('.hotgraphic__widget').imageready(this.setReadyStatus.bind(this)); - if (this.model.get('_setCompletionOn') === 'inview') { - this.setupInviewCompletion('.component__widget'); - } + preRender() { + if (Adapt.device.screenSize === 'large') { + this.render(); + return; } - onPinClicked (event) { - const item = this.model.getItem($(event.currentTarget).data('index')); - item.toggleActive(true); - item.toggleVisited(true); + this.reRender(); + } - this.openPopup(); - } + postRender() { + this.$('.hotgraphic__widget').imageready(this.setReadyStatus.bind(this)); + if (this.model.get('_setCompletionOn') !== 'inview') return; + this.setupInviewCompletion('.component__widget'); + } + + onPinClicked (event) { + const item = this.model.getItem($(event.currentTarget).data('index')); + item.toggleActive(true); + item.toggleVisited(true); - openPopup() { - if (this._isPopupOpen) return; + this.openPopup(); + } - this._isPopupOpen = true; + openPopup() { + if (this._isPopupOpen) return; - this.popupView = new HotgraphicPopupView({ - model: this.model - }); + this._isPopupOpen = true; - Adapt.notify.popup({ - _view: this.popupView, - _isCancellable: true, - _showCloseButton: false, - _classes: 'hotgraphic ' + this.model.get('_classes') - }); + this.popupView = new HotgraphicPopupView({ + model: this.model + }); - this.listenToOnce(Adapt, { - 'popup:closed': this.onPopupClosed - }); - } + Adapt.notify.popup({ + _view: this.popupView, + _isCancellable: true, + _showCloseButton: false, + _classes: 'hotgraphic ' + this.model.get('_classes') + }); - onPopupClosed() { - this.model.getActiveItem().toggleActive(); - this._isPopupOpen = false; - } + this.listenToOnce(Adapt, { + 'popup:closed': this.onPopupClosed + }); + } + onPopupClosed() { + this.model.getActiveItem().toggleActive(); + this._isPopupOpen = false; } - HotGraphicView.template = 'hotgraphic'; +} - return HotGraphicView; +HotGraphicView.template = 'hotgraphic'; -}); +export default HotGraphicView;