diff --git a/app/assets/javascripts/stick-to-window-when-scrolling.js b/app/assets/javascripts/stick-to-window-when-scrolling.js index 9eeaa65ed..b804fe5e9 100644 --- a/app/assets/javascripts/stick-to-window-when-scrolling.js +++ b/app/assets/javascripts/stick-to-window-when-scrolling.js @@ -5,6 +5,153 @@ var GOVUK = global.GOVUK || {}; var _mode = 'default'; + // Constructor to make objects representing the area sticky elements can scroll in + var ScrollArea = function (el, edge) { + var $el = el.$fixedEl; + var $scrollArea = $el.closest('.sticky-scroll-area'); + + $scrollArea = $scrollArea.length ? $scrollArea : $el.parent(); + scrollArea = $scrollArea.get(0); + + this._els = [el]; + this.edge = edge; + this.node = scrollArea; + this.setEvents(); + }; + ScrollArea.prototype.addEl = function (el) { + this._els.push(el); + }; + ScrollArea.prototype.hasEl = function (el) { + return $.inArray(el, this._els) !== -1; + }; + ScrollArea.prototype.updateEls = function (usedEls) { + this._els = usedEls; + }; + ScrollArea.prototype.setEvents = function () { + this.node.addEventListener('focus', this.focusHandler.bind(this), true); + }; + ScrollArea.prototype.removeEvents = function () { + this.node.removeEventListener('focus', this.focusHandler.bind(this)); + }; + ScrollArea.prototype.focusHandler = function (e) { + var $focusedElement = $(document.activeElement); + var endOfFurthestEl = focusOverlap.endOfFurthestEl(this._els, this.edge); + var overlap = focusOverlap.getOverlap($focusedElement, this.edge, endOfFurthestEl); + + if (overlap > 0) { + $(window).scrollTop($(window).scrollTop() + overlap); + } + }; + ScrollArea.prototype.destroy = function () { + this.removeEvents(); + }; + + // Object collecting together methods for interacting with scrollareas + var scrollAreas = { + _scrollAreas: [], + getAreaForEl: function (el) { + var loopIdx = this._scrollAreas.length; + + while(loopIdx--) { + if (this._scrollAreas[loopIdx].hasEl(el)) { + return this._scrollAreas[loopIdx]; + } + } + + return false; + }, + getAreaByEl: function (el) { + var matches = $.grep(this._scrollAreas, function (area) { + return $.inArray(el, area.els) !== -1; + }); + + return matches[0] || false; + }, + addEl: function (el, edge) { + var scrollArea = this.getAreaForEl(el); + + if (!scrollArea) { + this._scrollAreas.push(new ScrollArea(el, edge)); + } else { + scrollArea.addEl(el); + } + }, + syncEls: function (elsInDOM) { + var self = this; + var unusedAreas = []; + + var getUsed = function (area) { + var used = []; + + $.each(elsInDOM, function (elIdx, el) { + if (area.hasEl(el)) { + used.push(el); + } + }); + + return used; + }; + + var deleteUnused = function (idx, areaIdx) { + // remove any events for overlap checking bound to the scrollArea + self._scrollAreas[areaIdx].destroy(); + self._scrollAreas.splice(areaIdx, 1); + }; + + // update any scroll areas with els still in the DOM and track any with none + $.each(this._scrollAreas, function (areaIdx, area) { + var used = getUsed(area); + + if (!used.length) { + unusedAreas.push(areaIdx); + } + + area.updateEls(used); + }); + + // delete any scroll areas with no els still in DOM + $.each(unusedAreas, deleteUnused); + } + }; + + // Object collecting together methods for stopping sticky overlapping focused elements + var focusOverlap = { + getOverlap: function ($focusedElement, edge, endOfFurthestEl) { + var topOfFocusedElement = $focusedElement.offset().top; + + if (!endOfFurthestEl) { return 0; } + + if (edge === 'top') { + return endOfFurthestEl - topOfFocusedElement; + } else { + return (topOfFocusedElement + $focusedElement.outerHeight()) - endOfFurthestEl; + } + }, + endOfFurthestEl: function (els, edge) { + var stuckEls = $.grep(els, function (el) { return el.isStuck(); }); + var edgeOfEl; + var offsets; + + if (edge === 'bottom') { + edgeOfEl = function (el) { + return el.$fixedEl.offset().top; + }; + } else { + edgeOfEl = function (el) { + return el.$fixedEl.offset().top + el.height; + }; + } + + if (!stuckEls.length) { return false; } + + offsets = $.map(stuckEls, function (el) { return edgeOfEl(el); }); + + return offsets.reduce(function (accumulator, offset) { + return (accumulator < offset) ? offset: accumulator; + }); + } + }; + // Object collecting together methods for dealing with marking the edge of a sticky, or group of // sticky elements (as seen in dialog mode) var oppositeEdge = { @@ -376,6 +523,7 @@ Sticky.prototype.recalculate = function () { var self = this; var onSyncComplete = function () { + scrollAreas.syncEls(self._els); self.setEvents(); if (_mode === 'dialog') { dialog.fitToHeight(self); @@ -390,7 +538,8 @@ }; Sticky.prototype.setElWidth = function (el) { var $el = el.$fixedEl; - var width = $el.parent().width(); + var scrollArea = scrollAreas.getAreaByEl(el); + var width = $(scrollArea.node).width(); el.horizontalSpace = width; // if stuck, element won't inherit width from parent so set explicitly @@ -457,6 +606,7 @@ if (!exists) { elObj = new StickyElement($el, self); + scrollAreas.addEl(elObj, self.edge); } self.setElementDimensions(elObj, onDimensionsSet); diff --git a/app/assets/javascripts/templateFolderForm.js b/app/assets/javascripts/templateFolderForm.js index 0db522984..6aa8f6f74 100644 --- a/app/assets/javascripts/templateFolderForm.js +++ b/app/assets/javascripts/templateFolderForm.js @@ -33,10 +33,11 @@ this.activateStickyElements(); // first off show the new template / new folder buttons - this.currentState = this.$form.data('prev-state') || 'unknown'; - if (this.currentState === 'unknown') { + this._lastState = this.$form.data('prev-state'); + if (this._lastState === undefined) { this.selectActionButtons(); } else { + this.currentState = this._lastState; this.render(); } @@ -144,11 +145,21 @@ } }; + // method that checks the state against the last one, used prior to render() to see if needed + this.stateChanged = function() { + let changed = this.currentState !== this._lastState; + + this._lastState = this.currentState; + return changed; + }; + this.actionButtonClicked = function(event) { event.preventDefault(); this.currentState = $(event.currentTarget).val(); - this.render(); + if (this.stateChanged()) { + this.render(); + } }; this.selectionStatus = { @@ -173,7 +184,9 @@ this.currentState = 'nothing-selected-buttons'; } - this.render(); + if (this.stateChanged()) { + this.render(); + } this.selectionStatus.update(numSelected); diff --git a/app/templates/views/templates/choose.html b/app/templates/views/templates/choose.html index ee4cc105d..4f999bb23 100644 --- a/app/templates/views/templates/choose.html +++ b/app/templates/views/templates/choose.html @@ -74,6 +74,7 @@ {% if current_user.has_permissions('manage_templates') %} {% call form_wrapper( + class='sticky-scroll-area', module='template-folder-form', data_kwargs={'prev-state': templates_and_folders_form.op or None} ) %}