aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-16 16:35:38 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-16 16:35:38 -0800
commit72e131f9c273d15e8d3b5c8a9320ab7fb1d533d4 (patch)
tree8d02878300210d412f88d2890e42f3de8f1e8b80
parentf04d8fbd9900d5326cdd65a725e57f4b08bbd655 (diff)
downloadneko-72e131f9c273d15e8d3b5c8a9320ab7fb1d533d4.tar.gz
neko-72e131f9c273d15e8d3b5c8a9320ab7fb1d533d4.tar.bz2
neko-72e131f9c273d15e8d3b5c8a9320ab7fb1d533d4.zip
Fix scroll-to-read functionality across all UIs (V1, V2, V3)
-rw-r--r--frontend-vanilla/src/main.test.ts37
-rw-r--r--frontend-vanilla/src/main.ts50
-rw-r--r--frontend/src/components/FeedItems.test.tsx62
-rw-r--r--frontend/src/components/FeedItems.tsx75
-rw-r--r--web/static/ui.js295
5 files changed, 298 insertions, 221 deletions
diff --git a/frontend-vanilla/src/main.test.ts b/frontend-vanilla/src/main.test.ts
index 436db14..8eb537e 100644
--- a/frontend-vanilla/src/main.test.ts
+++ b/frontend-vanilla/src/main.test.ts
@@ -22,10 +22,9 @@ vi.mock('./api', () => ({
}));
// Mock IntersectionObserver as a constructor
-let observerCallback: IntersectionObserverCallback;
class MockIntersectionObserver {
constructor(callback: IntersectionObserverCallback) {
- observerCallback = callback;
+ // unused
}
observe = vi.fn();
unobserve = vi.fn();
@@ -259,7 +258,8 @@ describe('main application logic', () => {
expect(store.sidebarVisible).toBe(!initialVisible);
});
- it('should mark item as read when scrolled past', () => {
+ it('should mark item as read when scrolled past', async () => {
+ vi.useRealTimers();
const mockItem = {
_id: 123,
title: 'Scroll Test Item',
@@ -277,18 +277,25 @@ describe('main application logic', () => {
vi.mocked(apiFetch).mockResolvedValue({ ok: true } as Response);
- // Simulate item scrolled above viewport (no longer intersecting, bottom above root top)
- const entry = {
- target: itemEl,
- isIntersecting: false,
- boundingClientRect: { bottom: -10 } as DOMRectReadOnly,
- rootBounds: { top: 0 } as DOMRectReadOnly,
- } as IntersectionObserverEntry;
-
- // This relies on the LAST created observer's callback being captured.
- expect(observerCallback).toBeDefined();
- // @ts-ignore
- observerCallback([entry], {} as IntersectionObserver);
+ // Mock getBoundingClientRect
+ const mainContent = document.getElementById('main-content');
+ if (mainContent) {
+ mainContent.getBoundingClientRect = vi.fn(() => ({
+ top: 0, bottom: 500, height: 500, left: 0, right: 0, width: 0, x: 0, y: 0, toJSON: () => { }
+ }));
+ }
+
+ if (itemEl) {
+ itemEl.getBoundingClientRect = vi.fn(() => ({
+ top: -50, bottom: 50, height: 100, left: 0, right: 0, width: 0, x: 0, y: 0, toJSON: () => { }
+ }));
+ }
+
+ // Trigger scroll
+ mainContent?.dispatchEvent(new Event('scroll'));
+
+ // Wait for throttle (250ms)
+ await new Promise(resolve => setTimeout(resolve, 300));
expect(apiFetch).toHaveBeenCalledWith(expect.stringContaining('/api/item/123'), expect.objectContaining({
method: 'PUT',
diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts
index c310144..a8606e3 100644
--- a/frontend-vanilla/src/main.ts
+++ b/frontend-vanilla/src/main.ts
@@ -276,24 +276,40 @@ export function renderItems() {
observer.observe(sentinel);
}
- // Setup item observer for marking read when items scroll past (above viewport)
- itemObserver = new IntersectionObserver((entries) => {
- entries.forEach(entry => {
- if (!entry.isIntersecting && entry.boundingClientRect.bottom < (entry.rootBounds?.top ?? 0)) {
- const target = entry.target as HTMLElement;
- const id = parseInt(target.getAttribute('data-id') || '0');
- if (id) {
- const item = store.items.find(i => i._id === id);
- if (item && !item.read) {
- updateItem(id, { read: true });
- itemObserver?.unobserve(target);
- }
- }
+ // Scroll listener for reading items
+ // We attach this to the scrollable container: #main-content
+ if (scrollRoot) {
+ let timeoutId: number | null = null;
+ const onScroll = () => {
+ if (timeoutId === null) {
+ timeoutId = window.setTimeout(() => {
+ const containerRect = scrollRoot.getBoundingClientRect();
+
+ store.items.forEach((item) => {
+ if (item.read) return;
+
+ const el = document.querySelector(`.feed-item[data-id="${item._id}"]`);
+ if (el) {
+ const rect = el.getBoundingClientRect();
+ // Mark as read if the top of the item is above the top of the container
+ if (rect.top < containerRect.top) {
+ updateItem(item._id, { read: true });
+ }
+ }
+ });
+ timeoutId = null;
+ }, 250);
}
- });
- }, { root: scrollRoot, threshold: 0 });
-
- contentArea.querySelectorAll('.feed-item').forEach(el => itemObserver!.observe(el));
+ };
+ // Remove existing listener if any (simplistic approach, ideally we track and remove)
+ // Since renderItems is called multiple times, we might be adding multiple listeners?
+ // attachLayoutListeners is called once, but renderItems is called on updates.
+ // We should probably attaching the scroll listener in the layout setup, NOT here.
+ // But we need access to 'items' which is in store.
+ // Let's attach it here but be careful.
+ // Actually, attaching to 'onscroll' property handles replacement automatically.
+ scrollRoot.onscroll = onScroll;
+ }
}
export function renderSettings() {
diff --git a/frontend/src/components/FeedItems.test.tsx b/frontend/src/components/FeedItems.test.tsx
index fc95948..ad8bf4f 100644
--- a/frontend/src/components/FeedItems.test.tsx
+++ b/frontend/src/components/FeedItems.test.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import '@testing-library/jest-dom';
import { render, screen, waitFor, fireEvent, act } from '@testing-library/react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
-import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import FeedItems from './FeedItems';
describe('FeedItems Component', () => {
@@ -126,6 +126,11 @@ describe('FeedItems Component', () => {
});
});
+ afterEach(() => {
+ vi.useRealTimers();
+ vi.restoreAllMocks();
+ });
+
it('marks items as read when scrolled past', async () => {
const mockItems = [{ _id: 101, title: 'Item 1', url: 'u1', read: false, starred: false }];
vi.mocked(global.fetch).mockResolvedValue({
@@ -133,44 +138,53 @@ describe('FeedItems Component', () => {
json: async () => mockItems,
} as Response);
- const observerCallbacks: IntersectionObserverCallback[] = [];
- class MockIntersectionObserver {
- constructor(callback: IntersectionObserverCallback) {
- observerCallbacks.push(callback);
+ // Mock getBoundingClientRect
+ const getBoundingClientRectMock = vi.spyOn(Element.prototype, 'getBoundingClientRect');
+ getBoundingClientRectMock.mockImplementation(function (this: Element) {
+ if (this.classList && this.classList.contains('dashboard-main')) {
+ return {
+ top: 0, bottom: 500, height: 500, left: 0, right: 1000, width: 1000, x: 0, y: 0,
+ toJSON: () => { }
+ } as DOMRect;
}
- observe = vi.fn();
- unobserve = vi.fn();
- disconnect = vi.fn();
- }
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- window.IntersectionObserver = MockIntersectionObserver as any;
+ if (this.id && this.id.startsWith('item-')) {
+ // Item top is -50 (above container top 0)
+ return {
+ top: -50, bottom: 50, height: 100, left: 0, right: 1000, width: 1000, x: 0, y: 0,
+ toJSON: () => { }
+ } as DOMRect;
+ }
+ return {
+ top: 0, bottom: 0, height: 0, left: 0, right: 0, width: 0, x: 0, y: 0,
+ toJSON: () => { }
+ } as DOMRect;
+ });
render(
<MemoryRouter>
- <FeedItems />
+ <div className="dashboard-main">
+ <FeedItems />
+ </div>
</MemoryRouter>
);
+ // Initial load fetch
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeVisible();
});
- // Simulate item leaving viewport
- const entry = {
- isIntersecting: false,
- boundingClientRect: { top: -50 } as DOMRectReadOnly,
- target: { getAttribute: () => '0' } as unknown as Element, // data-index="0"
- intersectionRatio: 0,
- time: 0,
- rootBounds: null,
- intersectionRect: {} as DOMRectReadOnly,
- } as IntersectionObserverEntry;
+ // Trigger scroll
+ const container = document.querySelector('.dashboard-main');
+ expect(container).not.toBeNull();
act(() => {
- // Trigger ALL registered observers
- observerCallbacks.forEach(cb => cb([entry], {} as IntersectionObserver));
+ // Dispatch scroll event
+ fireEvent.scroll(container!);
});
+ // Wait for throttle (500ms) + buffer
+ await new Promise(r => setTimeout(r, 600));
+
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
'/api/item/101',
diff --git a/frontend/src/components/FeedItems.tsx b/frontend/src/components/FeedItems.tsx
index ea5d8fd..e2df011 100644
--- a/frontend/src/components/FeedItems.tsx
+++ b/frontend/src/components/FeedItems.tsx
@@ -198,39 +198,62 @@ export default function FeedItems() {
}, [markAsRead, scrollToItem, toggleStar, fetchItems]);
- // Stable Observer
- const observerRef = useRef<IntersectionObserver | null>(null);
+ // Scroll listener to mark items as read
const sentinelObserverRef = useRef<IntersectionObserver | null>(null);
- useEffect(() => {
- if (observerRef.current) observerRef.current.disconnect();
-
- observerRef.current = new IntersectionObserver(
- (entries) => {
- entries.forEach((entry) => {
- if (!entry.isIntersecting && entry.boundingClientRect.top < 0) {
- const index = Number(entry.target.getAttribute('data-index'));
- const currentItems = itemsRef.current;
- if (!isNaN(index) && index >= 0 && index < currentItems.length) {
- const item = currentItems[index];
- if (!item.read) {
- markAsRead(item);
- }
- }
- }
- });
- },
- { root: null, threshold: 0 }
- );
+ const checkReadStatus = useCallback(() => {
+ const container = document.querySelector('.dashboard-main');
+ if (!container) return;
+ const containerRect = container.getBoundingClientRect();
const currentItems = itemsRef.current;
- currentItems.forEach((_, index) => {
+
+ currentItems.forEach((item, index) => {
+ if (item.read) return;
+
const el = document.getElementById(`item-${index}`);
- if (el) observerRef.current?.observe(el);
+ if (!el) return;
+
+ const rect = el.getBoundingClientRect();
+
+ // Mark as read if the top of the item is above the top of the container
+ if (rect.top < containerRect.top) {
+ markAsRead(item);
+ }
});
+ }, [markAsRead]);
+
+ // Setup scroll listener
+ useEffect(() => {
+ const container = document.querySelector('.dashboard-main');
+ if (!container) return;
+
+ let timeoutId: number | null = null;
+ const onScroll = () => {
+ if (timeoutId === null) {
+ timeoutId = window.setTimeout(() => {
+ checkReadStatus();
+ timeoutId = null;
+ }, 250);
+ }
+ };
+
+ container.addEventListener('scroll', onScroll);
+
+ // Initial check
+ checkReadStatus();
+
+ return () => {
+ if (timeoutId) clearTimeout(timeoutId);
+ container.removeEventListener('scroll', onScroll);
+ };
+ }, [checkReadStatus]);
+
+ // Re-check when items change (e.g. initial load or load more)
+ useEffect(() => {
+ checkReadStatus();
+ }, [items, checkReadStatus]);
- return () => observerRef.current?.disconnect();
- }, [items.length, markAsRead]); // Only re-setup if item count changes
useEffect(() => {
diff --git a/web/static/ui.js b/web/static/ui.js
index cc7ad8d..2a74a27 100644
--- a/web/static/ui.js
+++ b/web/static/ui.js
@@ -1,14 +1,31 @@
var templates = {};
-$(document).ready(function() {
- if ( $(window).width() < 1024 ) {
+function getCookie(name) {
+ var value = "; " + document.cookie;
+ var parts = value.split("; " + name + "=");
+ if (parts.length == 2) return parts.pop().split(";").shift();
+}
+
+$.ajaxSetup({
+ beforeSend: function (xhr, settings) {
+ if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
+ var token = getCookie("csrf_token");
+ if (token) {
+ xhr.setRequestHeader("X-CSRF-Token", token);
+ }
+ }
+ }
+});
+
+$(document).ready(function () {
+ if ($(window).width() < 1024) {
$('#filters').addClass('hidden');
}
document.body.className = localStorage.getItem('theme');
boot();
});
-var AppModel = Backbone.Model.extend({
+var AppModel = Backbone.Model.extend({
defaults: {
'selectedIndex': 0,
'starredFilter': false,
@@ -18,29 +35,29 @@ var AppModel = Backbone.Model.extend({
'searchFilter': undefined
},
- initialize: function() {
+ initialize: function () {
this.bind('change:selectedIndex', this.scroll_to_selected);
-// this.bind('change:selectedIndex', this.scroll_to_selected)
+ // this.bind('change:selectedIndex', this.scroll_to_selected)
},
- boot: function() {
+ boot: function () {
this.items.boot();
this.tags.boot();
- this.feeds.fetch({set: true, remove: false})
- window.setInterval(function() { App.update_read_status() }, 5000);
+ this.feeds.fetch({ set: true, remove: false })
+ window.setInterval(function () { App.update_read_status() }, 5000);
},
- filterToFeed: function(feed) {
+ filterToFeed: function (feed) {
if (feed.get('selected')) {
feed.set('selected', false);
this.set('feedFilter', undefined);
}
else {
- App.tags.models.forEach ( function (t) {
+ App.tags.models.forEach(function (t) {
t.set('selected', false);
});
App.tag = null;
- App.feeds.models.forEach ( function (f) {
+ App.feeds.models.forEach(function (f) {
f.set('selected', false);
});
@@ -50,16 +67,16 @@ var AppModel = Backbone.Model.extend({
this.items.reboot();
},
- filterToTag: function(tag) {
+ filterToTag: function (tag) {
App.tag = null;
if (tag.get('selected')) {
tag.set('selected', false);
}
else {
- App.tags.models.forEach ( function (t) {
+ App.tags.models.forEach(function (t) {
t.set('selected', false);
});
- App.feeds.models.forEach ( function (f) {
+ App.feeds.models.forEach(function (f) {
f.set('selected', false);
});
this.set('feedFilter', undefined);
@@ -69,14 +86,14 @@ var AppModel = Backbone.Model.extend({
App.items.reboot();
},
- filterToStarred: function() {
+ filterToStarred: function () {
this.set('starredFilter', true);
this.set('allFilter', false);
this.set('unreadFilter', false);
this.items.reboot();
},
- filterToAll: function() {
+ filterToAll: function () {
this.set('searchFilter', undefined);
this.set('starredFilter', false);
this.set('allFilter', true);
@@ -84,14 +101,14 @@ var AppModel = Backbone.Model.extend({
this.items.reboot();
},
- filterToUnread: function() {
+ filterToUnread: function () {
this.set('starredFilter', false);
this.set('allFilter', false);
this.set('unreadFilter', true);
this.items.reboot();
},
- filterToSearch: function() {
+ filterToSearch: function () {
this.set('searchFilter', $('#search').val());
this.set('starredFilter', false);
this.set('allFilter', true);
@@ -99,62 +116,62 @@ var AppModel = Backbone.Model.extend({
this.items.reboot();
},
- update_read_status: function() {
+ update_read_status: function () {
var screen_top = $(window).scrollTop();
- var screen_bottom = $(window).scrollTop() + $(window).height();
+ var screen_bottom = $(window).scrollTop() + $(window).height();
// // mark all visible items as read
- $.each($('.item'), function(i,v) {
+ $.each($('.item'), function (i, v) {
var item_top = $(v).offset().top;
// console.log("i ", i, "item_top ", item_top, "screen_top ", screen_top, "screen_bottom ", screen_bottom);
- if( (item_top < screen_top)) {
+ if ((item_top < screen_top)) {
App.items.at(i).markRead();
- // console.log('marking as read: ', i);
+ // console.log('marking as read: ', i);
}
});
-// window.setTimeout(App.update_read_status, 5000);
+ // window.setTimeout(App.update_read_status, 5000);
},
- scroll_to_selected: function() {
+ scroll_to_selected: function () {
var item = $('.item').eq(this.get('selectedIndex'));
- if(item.offset()) {
+ if (item.offset()) {
var item_top = item.offset().top;
$('.item').removeClass('selected');
item.addClass('selected');
$(window).scrollTop(item_top);
}
App.items.at(this.get('selectedIndex')).markRead();
- if(App.items.models.length>1) {
- if(this.get('selected')>=App.items.models.length-1) {
+ if (App.items.models.length > 1) {
+ if (this.get('selected') >= App.items.models.length - 1) {
App.items.boot();
}
}
},
- next: function() {
- if(this.get('selectedIndex') < this.items.models.length-1) {
- this.set('selectedIndex', this.get('selectedIndex')+1);
+ next: function () {
+ if (this.get('selectedIndex') < this.items.models.length - 1) {
+ this.set('selectedIndex', this.get('selectedIndex') + 1);
}
- if(this.get('selectedIndex') == this.items.models.length-1) {
+ if (this.get('selectedIndex') == this.items.models.length - 1) {
App.items.boot();
}
},
- previous: function() {
- if(this.get('selectedIndex') > 0) {
- this.set('selectedIndex', this.get('selectedIndex')-1);
+ previous: function () {
+ if (this.get('selectedIndex') > 0) {
+ this.set('selectedIndex', this.get('selectedIndex') - 1);
}
},
- star: function() {
- if(this.get('selectedIndex') >= 0) {
+ star: function () {
+ if (this.get('selectedIndex') >= 0) {
App.items.at(this.get('selectedIndex')).toggleStar();
}
},
- full: function() {
- if(this.get('selectedIndex') >= 0) {
+ full: function () {
+ if (this.get('selectedIndex') >= 0) {
App.items.at(this.get('selectedIndex')).full();
}
}
@@ -176,55 +193,55 @@ var ControlsView = Backbone.View.extend({
'click .black_theme': 'blackTheme',
},
- initialize: function() {
+ initialize: function () {
_.bindAll(this, 'render');
this.model.bind('change', this.render);
},
- filterToStarred: function() {
+ filterToStarred: function () {
App.filterToStarred();
},
- filterToAll: function() {
+ filterToAll: function () {
App.filterToAll();
},
- filterToUnread: function() {
+ filterToUnread: function () {
App.filterToUnread();
},
- filterToSearch: function() {
+ filterToSearch: function () {
App.filterToSearch();
},
- newFeed: function() {
+ newFeed: function () {
var feed_url = prompt('New url to subscribe to');
- var feed = new Feed({'url': feed_url});
+ var feed = new Feed({ 'url': feed_url });
App.feeds.add(feed);
feed.save();
},
- render: function() {
+ render: function () {
var h = $.tmpl(templates.controls_template, { 'app': this.model.toJSON() });
$(this.el).html(h);
return this;
},
- lightTheme: function() {
+ lightTheme: function () {
document.body.className = "light";
localStorage.setItem("theme", "light");
},
- darkTheme: function() {
+ darkTheme: function () {
document.body.className = "dark";
localStorage.setItem("theme", "dark");
},
- blackTheme: function() {
+ blackTheme: function () {
document.body.className = "black";
localStorage.setItem("theme", "black");
},
-
+
});
@@ -233,7 +250,7 @@ var Item = Backbone.Model.extend({
idAttribute: "_id",
url: '/item/',
- initialize: function() {
+ initialize: function () {
var p_url = this.get('url');
p_url = p_url.replace('https://', '');
p_url = p_url.replace('http://', '');
@@ -241,15 +258,15 @@ var Item = Backbone.Model.extend({
this.bind('change', this.maybeSave);
},
- maybeSave: function() {
- if(this.hasChanged()) {
+ maybeSave: function () {
+ if (this.hasChanged()) {
this.save();
}
},
- markRead: function() {
+ markRead: function () {
// recover if not tag
- if(this.get('read')) {
+ if (this.get('read')) {
return;
}
@@ -261,26 +278,26 @@ var Item = Backbone.Model.extend({
// }
},
- toggleStar: function() {
- this.set({'starred': !(this.get('starred'))} );
+ toggleStar: function () {
+ this.set({ 'starred': !(this.get('starred')) });
},
- star: function() {
- this.set({'starred': true});
+ star: function () {
+ this.set({ 'starred': true });
},
- unstar: function() {
- this.set({'starred': false});
+ unstar: function () {
+ this.set({ 'starred': false });
},
- full: function() {
- this.set({'full': !(this.get('full'))} );
+ full: function () {
+ this.set({ 'full': !(this.get('full')) });
// this should just use this.fetch() but
// it kept GETing from /item instead of /item/id
// so just hacking this in for now
- if(this.get('full_content') == "") {
- $.getJSON('/item/' + this.get('_id'), function(data) {
+ if (this.get('full_content') == "") {
+ $.getJSON('/item/' + this.get('_id'), function (data) {
var i = App.items.get(data['_id'])
i.set('full_content', data['full_content']);
});
@@ -293,55 +310,55 @@ var Item = Backbone.Model.extend({
var ItemCollection = Backbone.Collection.extend({
model: Item,
- initialize: function() {
+ initialize: function () {
_.bindAll(this, 'boot', 'reboot');
},
- boot: function() {
- if(App.loading) {
+ boot: function () {
+ if (App.loading) {
return;
}
- if(App.noMore) {
+ if (App.noMore) {
return;
}
App.loading = true;
url = '/stream/';
- url=url+'?foo=bar'
- if(App.get('searchFilter')) {
+ url = url + '?foo=bar'
+ if (App.get('searchFilter')) {
url = url + '&q=' + App.get('searchFilter');
}
- if(App.get('feedFilter')) {
+ if (App.get('feedFilter')) {
url = url + '&feed_url=' + App.get('feedFilter').get('url');
}
- if(App.get('starredFilter')) {
+ if (App.get('starredFilter')) {
url = url + '&starred=1';
}
- if(App.tag != undefined) {
+ if (App.tag != undefined) {
url = url + '&tag=' + App.tag;
}
- if(App.items.last()) {
- url = url + '&max_id=' + App.items.last().get('_id');
+ if (App.items.last()) {
+ url = url + '&max_id=' + App.items.last().get('_id');
}
- if(App.get('allFilter') || App.get('starredFilter')) {
+ if (App.get('allFilter') || App.get('starredFilter')) {
url = url + '&read_filter=all';
}
console.log('fetching from ', url);
var t = this;
- $.getJSON(url, function(data) {
+ $.getJSON(url, function (data) {
var items = [];
- $.each(data, function(i,v) {
+ $.each(data, function (i, v) {
var item = new Item(v);
t.add(item);
items.push(item);
- if(t.models.length==1){
+ if (t.models.length == 1) {
App.set('selectedIndex', 0);
}
});
// console.log("items ", items)
- if(items.length == 0) {
+ if (items.length == 0) {
// console.log("no more items");
App.noMore = true;
// App.loading = true;
@@ -355,7 +372,7 @@ var ItemCollection = Backbone.Collection.extend({
});
},
- reboot: function() {
+ reboot: function () {
App.noMore = false;
App.loading = false;
this.reset();
@@ -377,56 +394,56 @@ var ItemView = Backbone.View.extend({
"click .full": "full",
},
- initialize: function() {
+ initialize: function () {
_.bindAll(this, 'render', 'star');
this.model.bind('change', this.render);
},
- star: function() {
+ star: function () {
this.model.star();
this.render();
},
- unstar: function() {
+ unstar: function () {
this.model.unstar();
this.render();
},
- full: function() {
+ full: function () {
this.model.full();
this.render();
},
- render: function() {
+ render: function () {
var h = $.tmpl(templates.item_template, { 'item': this.model.toJSON() });
$(this.el).html(h);
return this;
},
});
-var ItemListView = Backbone.View.extend( {
- initialize: function() {
+var ItemListView = Backbone.View.extend({
+ initialize: function () {
_.bindAll(this, 'addOne', 'addAll', 'change', 'render', 'reset');
// App.items.bind('add', this.addOne);
App.items.bind('reset', this.reset);
},
- addOne: function(item) {
- var view = new ItemView({'model': item});
+ addOne: function (item) {
+ var view = new ItemView({ 'model': item });
this.$el.append(view.render().el);
},
- addAll: function(items) {
+ addAll: function (items) {
// Posts.each(this.addOne);
- for(i in items) {
+ for (i in items) {
item = items[i];
- var view = new ItemView({'model': item});
+ var view = new ItemView({ 'model': item });
this.$el.append(view.render().el);
};
},
- change: function() {
+ change: function () {
},
- render: function() {
+ render: function () {
},
- reset: function() {
+ reset: function () {
this.$el.children().remove();
}
});
@@ -436,14 +453,14 @@ var Tag = Backbone.Model.extend({
var TagCollection = Backbone.Collection.extend({
model: Tag,
- initialize: function() {
+ initialize: function () {
_.bindAll(this, 'boot');
},
- boot: function() {
+ boot: function () {
var t = this;
- $.getJSON('/tag/', function(data) {
- $.each(data, function(i,v) {
+ $.getJSON('/tag/', function (data) {
+ $.each(data, function (i, v) {
var tag = new Tag(v);
t.add(tag);
});
@@ -454,44 +471,44 @@ App.tags = new TagCollection();
var TagView = Backbone.View.extend({
- tagName: "li",
+ tagName: "li",
className: "tag",
events: {
"click": "filterTo",
},
- initialize: function() {
+ initialize: function () {
_.bindAll(this, 'render', 'filterTo');
this.model.bind('change', this.render);
},
- render: function() {
+ render: function () {
var h = $.tmpl(templates.tag_template, { 'tag': this.model.toJSON() });
$(this.el).html(h);
return this;
},
- filterTo: function() {
+ filterTo: function () {
App.filterToTag(this.model);
}
});
-var TagListView = Backbone.View.extend( {
+var TagListView = Backbone.View.extend({
- initialize: function() {
+ initialize: function () {
_.bindAll(this, 'addOne', 'addAll', 'change', 'render');
App.tags.bind('add', this.addOne);
App.tags.bind('refresh', this.addAll);
App.tags.bind('change', this.render);
},
- addOne: function(tag) {
- var view = new TagView({'model': tag});
+ addOne: function (tag) {
+ var view = new TagView({ 'model': tag });
this.$el.append(view.render().el);
},
- addAll: function() {
+ addAll: function () {
App.tags.each(this.addOne);
},
- change: function() {
+ change: function () {
},
- render: function() {
+ render: function () {
},
});
@@ -508,7 +525,7 @@ var FeedCollection = Backbone.Collection.extend({
model: Feed,
url: '/feed/',
- initialize: function() {
+ initialize: function () {
/// _.bindAll(this, 'boot');
//console.log('initialized');
},
@@ -523,26 +540,26 @@ var FeedView = Backbone.View.extend({
"click .delete": "del",
"click .edit": "edit",
},
- initialize: function() {
+ initialize: function () {
_.bindAll(this, 'render', 'filterTo', "del");
this.model.bind('change', this.render);
},
- render: function() {
+ render: function () {
var h = $.tmpl(templates.feed_template, { 'feed': this.model.toJSON() });
$(this.el).html(h);
return this;
},
- filterTo: function() {
+ filterTo: function () {
// console.log('filtering to feed ', this.model);
App.filterToFeed(this.model);
},
- del: function() {
- if( window.confirm("Unsubscribe from " + this.model.get("url") + "?" ) ) {
+ del: function () {
+ if (window.confirm("Unsubscribe from " + this.model.get("url") + "?")) {
this.model.destroy();
this.$el.remove();
}
},
- edit: function() {
+ edit: function () {
var cat = window.prompt("Category for this feed?", this.model.get("category"));
if (cat != null) {
this.model.set("category", cat);
@@ -552,26 +569,26 @@ var FeedView = Backbone.View.extend({
});
-var FeedListView = Backbone.View.extend( {
- initialize: function() {
+var FeedListView = Backbone.View.extend({
+ initialize: function () {
_.bindAll(this, 'addOne', 'addAll', 'change', 'render');
App.feeds.bind('add', this.addOne);
App.feeds.bind('refresh', this.addAll);
App.feeds.bind('change', this.render);
},
- addOne: function(feed) {
+ addOne: function (feed) {
// console.log('adding a feed...', feed);
- var view = new FeedView({'model': feed});
+ var view = new FeedView({ 'model': feed });
this.$el.append(view.render().el);
},
- addAll: function() {
+ addAll: function () {
// console.log('feed add all...');
App.feeds.each(this.addOne);
},
- change: function() {
+ change: function () {
// console.log('feeds changed add all...');
},
- render: function() {
+ render: function () {
},
});
@@ -593,31 +610,31 @@ function boot() {
App.tagListView.setElement($('#tags'));
App.feedListView = new FeedListView();
App.feedListView.setElement($('#feeds'));
- App.controlsView = new ControlsView({model: App});
+ App.controlsView = new ControlsView({ model: App });
App.controlsView.setElement($('#controls'));
App.controlsView.render();
infini_scroll();
- $('#unread_filter').on('click', function() {
+ $('#unread_filter').on('click', function () {
App.read_filter = 'unread';
App.items.reboot();
});
- $('#all_filter').on('click', function() {
+ $('#all_filter').on('click', function () {
App.read_filter = 'all';
App.items.reboot();
});
-// $('.logo').on('click', function() {
- // App.set('feedFilter', undefined);
- // App.items.reboot();
+ // $('.logo').on('click', function() {
+ // App.set('feedFilter', undefined);
+ // App.items.reboot();
-// });
+ // });
// keyboard shortcuts
- $('body').keydown(function(event) {
- if(document.activeElement.id == "search") {
+ $('body').keydown(function (event) {
+ if (document.activeElement.id == "search") {
return;
}
if (event.which == 74) {
@@ -641,18 +658,18 @@ function boot() {
// // this is legacy code
function infini_scroll() {
- if(App.loading) {
+ if (App.loading) {
}
else {
var dh = $('#items').height() - $(window).height();
var st = $(window).scrollTop();
- if ( (dh-st) < 100 ){
+ if ((dh - st) < 100) {
App.items.boot();
}
}
window.setTimeout(infini_scroll, 1000);
}
-var ItemSelector = {
+var ItemSelector = {
selected_index: 0,
}