From d538613b60ea2c5bff4f8e124ecbb8dedb528aea Mon Sep 17 00:00:00 2001 From: Bernhard Posselt Date: Mon, 18 Apr 2016 21:18:13 +0200 Subject: Fix #791 --- js/.jshintrc | 3 +- js/controller/ContentController.js | 334 ++++++++++++---------- js/directive/NewsOnActive.js | 23 ++ js/gui/KeyboardShortcuts.js | 107 +++---- js/tests/unit/controller/ContentControllerSpec.js | 22 +- 5 files changed, 269 insertions(+), 220 deletions(-) create mode 100644 js/directive/NewsOnActive.js (limited to 'js') diff --git a/js/.jshintrc b/js/.jshintrc index 19894c5ea..dedacd590 100644 --- a/js/.jshintrc +++ b/js/.jshintrc @@ -48,6 +48,7 @@ "t": true, "url": true, "navigator": true, - "oc_requesttoken": true + "oc_requesttoken": true, + "_": true } } diff --git a/js/controller/ContentController.js b/js/controller/ContentController.js index dfee837d5..c63fc65e8 100644 --- a/js/controller/ContentController.js +++ b/js/controller/ContentController.js @@ -8,197 +8,219 @@ * @copyright Bernhard Posselt 2014 */ app.controller('ContentController', -function (Publisher, FeedResource, ItemResource, SettingsResource, data, - $route, $routeParams, $location, FEED_TYPE, ITEM_AUTO_PAGE_SIZE, Loading) { - 'use strict'; - - ItemResource.clear(); - - // distribute data to models based on key - Publisher.publishAll(data); + function (Publisher, FeedResource, ItemResource, SettingsResource, data, + $route, $routeParams, $location, FEED_TYPE, ITEM_AUTO_PAGE_SIZE, + Loading, $filter) { + 'use strict'; + var self = this; + ItemResource.clear(); - this.isAutoPagingEnabled = true; + // distribute data to models based on key + Publisher.publishAll(data); - // the interface should show a hint if there are not enough items sent so - // it's assumed that theres nothing to autpage - if (ItemResource.size() >= ITEM_AUTO_PAGE_SIZE) { - this.isNothingMoreToAutoPage = false; - } else { - this.isNothingMoreToAutoPage = true; - } + this.getFirstItem = function () { + var orderFilter = $filter('orderBy'); + var orderedItems = orderFilter(this.getItems(), this.orderBy()); + var firstItem = orderedItems[0]; + if (firstItem === undefined) { + return undefined; + } else { + return firstItem.id; + } + }; - this.getItems = function () { - return ItemResource.getAll(); - }; - this.toggleStar = function (itemId) { - ItemResource.toggleStar(itemId); - }; + this.isAutoPagingEnabled = true; + // the interface should show a hint if there are not enough items sent + // it's assumed that theres nothing to autpage - this.toggleItem = function (item) { - // TODO: unittest - if (this.isCompactView()) { - item.show = !item.show; + if (ItemResource.size() >= ITEM_AUTO_PAGE_SIZE) { + this.isNothingMoreToAutoPage = false; + } else { + this.isNothingMoreToAutoPage = true; } - }; - this.isShowAll = function () { - return SettingsResource.get('showAll'); - }; + this.getItems = function () { + return ItemResource.getAll(); + }; - this.markRead = function (itemId) { - var item = ItemResource.get(itemId); + this.isItemActive = function (id) { + return this.activeItem === id; + }; - if (!item.keepUnread && item.unread === true) { - ItemResource.markItemRead(itemId); - FeedResource.markItemOfFeedRead(item.feedId); - } - }; + this.setItemActive = function (id) { + this.activeItem = id; + }; - this.getFeed = function (feedId) { - return FeedResource.getById(feedId); - }; + this.toggleStar = function (itemId) { + ItemResource.toggleStar(itemId); + }; - this.toggleKeepUnread = function (itemId) { - var item = ItemResource.get(itemId); - if (!item.unread) { - FeedResource.markItemOfFeedUnread(item.feedId); - ItemResource.markItemRead(itemId, false); - } + this.toggleItem = function (item) { + // TODO: unittest + if (this.isCompactView()) { + item.show = !item.show; + } + }; - item.keepUnread = !item.keepUnread; - }; + this.isShowAll = function () { + return SettingsResource.get('showAll'); + }; - var self = this; - var getOrdering = function () { - var ordering = SettingsResource.get('oldestFirst'); + this.markRead = function (itemId) { + var item = ItemResource.get(itemId); - if (self.isFeed()) { - var feed = FeedResource.getById($routeParams.id); - if (feed && feed.ordering === 1) { - ordering = true; - } else if (feed && feed.ordering === 2) { - ordering = false; + if (!item.keepUnread && item.unread === true) { + ItemResource.markItemRead(itemId); + FeedResource.markItemOfFeedRead(item.feedId); } - } - - return ordering; - }; + }; - this.orderBy = function () { - if (getOrdering()) { - return 'id'; - } else { - return '-id'; - } - }; + this.getFeed = function (feedId) { + return FeedResource.getById(feedId); + }; - this.isCompactView = function () { - return SettingsResource.get('compact'); - }; + this.toggleKeepUnread = function (itemId) { + var item = ItemResource.get(itemId); + if (!item.unread) { + FeedResource.markItemOfFeedUnread(item.feedId); + ItemResource.markItemRead(itemId, false); + } - this.isCompactExpand = function () { - return SettingsResource.get('compactExpand'); - }; + item.keepUnread = !item.keepUnread; + }; - this.autoPagingEnabled = function () { - return this.isAutoPagingEnabled; - }; + var getOrdering = function () { + var ordering = SettingsResource.get('oldestFirst'); - this.markReadEnabled = function () { - return !SettingsResource.get('preventReadOnScroll'); - }; + if (self.isFeed()) { + var feed = FeedResource.getById($routeParams.id); + if (feed && feed.ordering === 1) { + ordering = true; + } else if (feed && feed.ordering === 2) { + ordering = false; + } + } - this.scrollRead = function (itemIds) { - var ids = []; - var feedIds = []; + return ordering; + }; - itemIds.forEach(function (itemId) { - var item = ItemResource.get(itemId); - if (!item.keepUnread) { - ids.push(itemId); - feedIds.push(item.feedId); + this.orderBy = function () { + if (getOrdering()) { + return 'id'; + } else { + return '-id'; + } + }; + + this.isCompactView = function () { + return SettingsResource.get('compact'); + }; + + this.isCompactExpand = function () { + return SettingsResource.get('compactExpand'); + }; + + this.autoPagingEnabled = function () { + return this.isAutoPagingEnabled; + }; + + this.markReadEnabled = function () { + return !SettingsResource.get('preventReadOnScroll'); + }; + + this.scrollRead = function (itemIds) { + var ids = []; + var feedIds = []; + + itemIds.forEach(function (itemId) { + var item = ItemResource.get(itemId); + if (!item.keepUnread) { + ids.push(itemId); + feedIds.push(item.feedId); + } + }); + + if (ids.length > 0) { + FeedResource.markItemsOfFeedsRead(feedIds); + ItemResource.markItemsRead(ids); } - }); + }; - if (ids.length > 0) { - FeedResource.markItemsOfFeedsRead(feedIds); - ItemResource.markItemsRead(ids); - } - }; + this.isFeed = function () { + return $route.current.$$route.type === FEED_TYPE.FEED; + }; - this.isFeed = function () { - return $route.current.$$route.type === FEED_TYPE.FEED; - }; + this.autoPage = function () { + if (this.isNothingMoreToAutoPage) { + return; + } - this.autoPage = function () { - if (this.isNothingMoreToAutoPage) { - return; - } + // in case a subsequent autopage request comes in wait until + // the current one finished and execute a request immediately + // afterwards + if (!this.isAutoPagingEnabled) { + this.autoPageAgain = true; + return; + } - // in case a subsequent autopage request comes in wait until - // the current one finished and execute a request immediately afterwards - if (!this.isAutoPagingEnabled) { - this.autoPageAgain = true; - return; - } + this.isAutoPagingEnabled = false; + this.autoPageAgain = false; - this.isAutoPagingEnabled = false; - this.autoPageAgain = false; + var type = $route.current.$$route.type; + var id = $routeParams.id; + var oldestFirst = getOrdering(); + var showAll = SettingsResource.get('showAll'); + var self = this; + var search = $location.search().search; - var type = $route.current.$$route.type; - var id = $routeParams.id; - var oldestFirst = getOrdering(); - var showAll = SettingsResource.get('showAll'); - var self = this; - var search = $location.search().search; + Loading.setLoading('autopaging', true); - Loading.setLoading('autopaging', true); + ItemResource.autoPage(type, id, oldestFirst, showAll, search) + .success(function (data) { + Publisher.publishAll(data); - ItemResource.autoPage(type, id, oldestFirst, showAll, search) - .success(function (data) { - Publisher.publishAll(data); + if (data.items.length >= ITEM_AUTO_PAGE_SIZE) { + self.isAutoPagingEnabled = true; + } else { + self.isNothingMoreToAutoPage = true; + } - if (data.items.length >= ITEM_AUTO_PAGE_SIZE) { + if (self.isAutoPagingEnabled && self.autoPageAgain) { + self.autoPage(); + } + }).error(function () { self.isAutoPagingEnabled = true; + }).finally(function () { + Loading.setLoading('autopaging', false); + }); + }; + + this.getRelativeDate = function (timestamp) { + if (timestamp !== undefined && timestamp !== '') { + var languageCode = SettingsResource.get('language'); + var date = + moment.unix(timestamp).locale(languageCode).fromNow() + ''; + return date; } else { - self.isNothingMoreToAutoPage = true; + return ''; } + }; - if (self.isAutoPagingEnabled && self.autoPageAgain) { - self.autoPage(); - } - }).error(function () { - self.isAutoPagingEnabled = true; - }).finally(function () { - Loading.setLoading('autopaging', false); - }); - }; - - this.getRelativeDate = function (timestamp) { - if (timestamp !== undefined && timestamp !== '') { - var languageCode = SettingsResource.get('language'); - var date = - moment.unix(timestamp).locale(languageCode).fromNow() + ''; - return date; - } else { - return ''; - } - }; - - this.refresh = function () { - $route.reload(); - }; - - this.getMediaType = function (type) { - if (type && type.indexOf('audio') === 0) { - return 'audio'; - } else if (type && type.indexOf('video') === 0) { - return 'video'; - } else { - return undefined; - } - }; + this.refresh = function () { + $route.reload(); + }; -}); \ No newline at end of file + this.getMediaType = function (type) { + if (type && type.indexOf('audio') === 0) { + return 'audio'; + } else if (type && type.indexOf('video') === 0) { + return 'video'; + } else { + return undefined; + } + }; + + this.activeItem = this.getFirstItem(); + }); \ No newline at end of file diff --git a/js/directive/NewsOnActive.js b/js/directive/NewsOnActive.js new file mode 100644 index 000000000..ba1047c60 --- /dev/null +++ b/js/directive/NewsOnActive.js @@ -0,0 +1,23 @@ +/** + * ownCloud - News + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Bernhard Posselt + * @copyright Bernhard Posselt 2016 + */ + +app.directive('newsOnActive', function ($parse) { + 'use strict'; + return { + restrict: 'A', + link: function (scope, elem, attrs) { + elem.on('set-active', function () { + var callback = $parse(attrs.newsOnActive); + scope.$apply(callback); + }); + + } + }; +}); \ No newline at end of file diff --git a/js/gui/KeyboardShortcuts.js b/js/gui/KeyboardShortcuts.js index d05856152..c70510035 100644 --- a/js/gui/KeyboardShortcuts.js +++ b/js/gui/KeyboardShortcuts.js @@ -234,20 +234,12 @@ } }; - var onActiveItem = function (scrollArea, callback) { - var items = scrollArea.find('.item'); - - items.each(function (index, item) { - item = $(item); - - // 130px of the item should be visible - if ((item.height() + item.position().top) > 30) { - callback(item); - - return false; - } - }); + var getActiveElement = function (scrollArea) { + return scrollArea.find('.item.active:first'); + }; + var onActiveItem = function (scrollArea, callback) { + callback(getActiveElement(scrollArea)); }; var toggleUnread = function (scrollArea) { @@ -275,6 +267,10 @@ }); }; + var setItemActive = function (element) { + element.dispatchEvent(new CustomEvent('set-active')); + }; + var scrollToItem = function (scrollArea, item, expandItemInCompact) { // if you go to the next article in compact view, it should // expand the current one @@ -282,6 +278,8 @@ item.offset().top - scrollArea.offset().top + scrollArea.scrollTop() ); + setItemActive(item[0]); + if (expandItemInCompact) { onActiveItem(scrollArea, function (item) { if (!item.hasClass('open')) { @@ -292,64 +290,49 @@ }; var scrollToNextItem = function (scrollArea, expandItemInCompact) { - var items = scrollArea.find('.item'); - var jumped = false; - - items.each(function (index, item) { - item = $(item); - - // special treatment for items that have expand enabled: - // if you click next and the first item has not been expaned and - // is on the top, it should be expanded instead of the next one - if ((item.position().top === 0 && expandItemInCompact && - !item.hasClass('open')) || - item.position().top > 10) { - scrollToItem(scrollArea, item, expandItemInCompact); - - jumped = true; - - return false; - } - }); - - // in case this is the last item it should still scroll below the top - if (!jumped) { + var activeElement = getActiveElement(scrollArea); + var nextElement = activeElement.next(); + if (nextElement.length > 0) { + scrollToItem(scrollArea, nextElement, expandItemInCompact); + } else { + // in case this is the last item it should still scroll below the scrollArea.scrollTop(scrollArea.prop('scrollHeight')); } - }; var scrollToPreviousItem = function (navigationArea, scrollArea, expandItemInCompact) { - var items = scrollArea.find('.item'); - var jumped = false; - - items.each(function (index, item) { - item = $(item); - - if ((item.position().top + 10) >= 0) { - var previous = item.prev(); - - // if there are no items before the current one - if (previous.length > 0) { - scrollToItem(scrollArea, previous, expandItemInCompact); - } else { - tryReload(navigationArea, scrollArea); - scrollArea.scrollTop(0); - } - - jumped = true; - - return false; - } - }); - - // if there was no jump jump to the last element - if (!jumped && items.length > 0) { - scrollToItem(scrollArea, items.last()); + var activeElement = getActiveElement(scrollArea); + var previousElement = activeElement.prev(); + + // if the active element has been scrolled, the previous element + // should be the active one + if (activeElement.position().top + 20 <= 0) { + scrollToItem(scrollArea, activeElement, expandItemInCompact); + } else if (previousElement.length > 0) { + scrollToItem(scrollArea, previousElement, expandItemInCompact); + } else { + tryReload(navigationArea, scrollArea); + scrollArea.scrollTop(0); } }; + // mark current item as active when scrolling + $(document).ready(function () { + var detectAndSetActiveItem = function () { + var items = $('#app-content').find('.item'); + items.each(function (index, item) { + var $item = $(item); + var bottom = $item.position().top + $item.outerHeight(true); + console.log(bottom); + if ((bottom - 20) >= 0){ + setItemActive(item); + return false; + } + }); + }; + $('#app-content').scroll(_.debounce(detectAndSetActiveItem, 250)); + }); $(document).keyup(function (event) { var keyCode = event.keyCode; diff --git a/js/tests/unit/controller/ContentControllerSpec.js b/js/tests/unit/controller/ContentControllerSpec.js index 9138db0ed..297ed61cc 100644 --- a/js/tests/unit/controller/ContentControllerSpec.js +++ b/js/tests/unit/controller/ContentControllerSpec.js @@ -22,6 +22,13 @@ describe('ContentController', function () { SUBSCRIPTIONS: 3, SHARED: 4 }); + $provide.constant('$route', { + current: { + $$route: { + type: 3 + } + } + }) })); @@ -50,7 +57,7 @@ describe('ContentController', function () { ItemResource.clear = jasmine.createSpy('clear'); $controller('ContentController', { - data: {}, + data: {} }); expect(ItemResource.clear).toHaveBeenCalled(); @@ -395,6 +402,14 @@ describe('ContentController', function () { })); + it ('should toggle active item', function ($controller) { + var ctrl = $controller('ContentController'); + expect(ctrl.isItemActive(3)).toBe(undefined); + ctrl.setItemActive(3); + expect(ctrl.isItemActive(4)).toBe(false); + expect(ctrl.isItemActive(3)).toBe(true); + }); + it('should autopage if more than 0 elements', inject(function ($controller, ItemResource, Publisher) { @@ -538,6 +553,11 @@ describe('ContentController', function () { it('should refresh the page', inject(function ($controller) { var route = { + current: { + $$route: { + type: 3 + } + }, reload: jasmine.createSpy('reload') }; var ctrl = $controller('ContentController', { -- cgit v1.2.3