Skip to content

Commit

Permalink
client: Move sidebar collapsing to React
Browse files Browse the repository at this point in the history
  • Loading branch information
jtojnar committed Jan 4, 2021
1 parent 8adf6fe commit f48398d
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 138 deletions.
24 changes: 23 additions & 1 deletion assets/js/Sources.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import { LoadingState } from './requests/LoadingState';

export class SourcesChangeEvent extends Event {
constructor(sources) {
super('change');
this.sources = sources;
}
}

export class SourcesStateChangeEvent extends Event {
constructor(state) {
super('statechange');
this.state = state;
}
}

/**
* Object storing list of sources and their information.
*/
export class Sources extends EventTarget {
/**
* @param {Object[]} sources
* @param {LoadingState} state
*/
constructor({sources = []}) {
constructor({
sources = [],
state = LoadingState.INITIAL
}) {
super();

this.sources = sources;
this.state = state;
}

update(sources) {
Expand All @@ -33,4 +47,12 @@ export class Sources extends EventTarget {
this.dispatchEvent(event);
}
}

setState(state) {
const event = new SourcesStateChangeEvent(state);

if (this.state !== state) {
this.dispatchEvent(event);
}
}
}
14 changes: 5 additions & 9 deletions assets/js/selfoss-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,8 +428,6 @@ var selfoss = {
});
},

sourcesNavLoaded: false,


/**
* anonymize links
Expand Down Expand Up @@ -491,13 +489,11 @@ var selfoss = {
}
});

if (selfoss.sourcesNavLoaded) {
var source = $(item).data('entry-source');
if (Object.keys(sourceUnreadDiff).includes(source)) {
sourceUnreadDiff[source] += -1;
} else {
sourceUnreadDiff[source] = -1;
}
const source = $(item).data('entry-source');
if (Object.keys(sourceUnreadDiff).includes(source)) {
sourceUnreadDiff[source] += -1;
} else {
sourceUnreadDiff[source] = -1;
}
});

Expand Down
47 changes: 0 additions & 47 deletions assets/js/selfoss-events-navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,6 @@ import * as sourceRequests from './requests/sources';
* initialize navigation events
*/
selfoss.events.navigation = function() {
// hide/show filters
$('#nav-filter-title').unbind('click').click(function() {
$('#nav-filter').slideToggle('slow');
$('#nav-filter-title').toggleClass('nav-filter-collapsed nav-filter-expanded');
$('#nav-filter-title').find('svg').toggleClass('fa-caret-down fa-caret-right');
$('#nav-filter-title').attr('aria-expanded', function(i, attr) {
return attr == 'true' ? 'false' : 'true';
});
});

// hide/show tags
$('#nav-tags-title').unbind('click').click(function() {
$('#nav-tags').slideToggle('slow');
$('#nav-tags-title').toggleClass('nav-tags-collapsed nav-tags-expanded');
$('#nav-tags-title').find('svg').toggleClass('fa-caret-down fa-caret-right');
$('#nav-tags-title').attr('aria-expanded', function(i, attr) {
return attr == 'true' ? 'false' : 'true';
});
});

// hide/show sources
$('#nav-sources-title').unbind('click').click(function() {
if (!selfoss.db.online) {
return;
}

var toggle = function() {
$('#nav-sources').slideToggle('slow');
$('#nav-sources-title').toggleClass('nav-sources-collapsed nav-sources-expanded');
$('#nav-sources-title').find('svg').toggleClass('fa-caret-down fa-caret-right');
$('#nav-sources-title').attr('aria-expanded', function(i, attr) {
return attr == 'true' ? 'false' : 'true';
});
};

selfoss.filter.update({ sourcesNav: $('#nav-sources-title').hasClass('nav-sources-collapsed') });
if (selfoss.filter.sourcesNav && !selfoss.sourcesNavLoaded) {
sourceRequests.getStats().then((data) => {
selfoss.sources.update(data);
}).catch(function(error) {
selfoss.ui.showError(selfoss.ui._('error_loading_stats') + ' ' + error.message);
});
} else {
toggle();
}
});

// emulate clicking when using keyboard
$('.entry-title-link').unbind('keypress').keypress(function(e) {
if (e.keyCode === 13) { // ENTER key
Expand Down
22 changes: 10 additions & 12 deletions assets/js/selfoss-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { initIcons } from './icons';
import * as NavSources from './templates/NavSources';
import * as NavFilters from './templates/NavFilters';
import * as NavTags from './templates/NavTags';
import { LoadingState } from './requests/LoadingState';

/**
* ui change functions
Expand Down Expand Up @@ -66,14 +67,11 @@ selfoss.ui = {

<div class="separator"><hr /></div>

<div id="nav-tags-wrapper" class="offlineable">
<h2><button type="button" id="nav-tags-title" class="nav-section-toggle nav-tags-expanded" aria-expanded="true"><i class="fas fa-caret-down fa-lg fa-fw"></i> {selfoss.ui._('tags')}</button></h2>
<ul id="nav-tags" aria-labelledby="nav-tags-title">
</ul>
<h2><button type="button" id="nav-sources-title" class="nav-section-toggle nav-sources-collapsed" aria-expanded="false"><i class="fas fa-caret-right fa-lg fa-fw"></i> {selfoss.ui._('sources')}</button></h2>
<ul id="nav-sources" aria-labelledby="nav-sources-title">
</ul>
<div class="nav-ts-wrapper offlineable">
<div id="nav-tags-wrapper" />
<div id="nav-sources-wrapper" />
</div>

<div class="nav-unavailable offlineable">
<span class="fa-stack fa-2x">
<i class="fas fa-wifi fa-stack-1x"></i>
Expand Down Expand Up @@ -135,7 +133,7 @@ selfoss.ui = {
document.body.appendChild(script);
}

NavTags.anchor(document.querySelector('#nav-tags'), selfoss.tags, selfoss.filter);
NavTags.anchor(document.querySelector('#nav-tags-wrapper'), selfoss.tags, selfoss.filter);

selfoss.tags.addEventListener('statechange', (event) => {
if (event.state === 'loading') {
Expand All @@ -158,17 +156,17 @@ selfoss.ui = {

selfoss.tags.addEventListener('change', setupTags);

selfoss.sources.addEventListener('change', () => {
NavSources.anchor(document.querySelector('#nav-sources'), selfoss.sources, selfoss.filter);
NavSources.anchor(document.querySelector('#nav-sources-wrapper'), selfoss.sources, selfoss.filter);

selfoss.sources.addEventListener('change', () => {
if (selfoss.filter.source) {
if (!selfoss.db.isValidSource(selfoss.filter.source)) {
selfoss.ui.showError(selfoss.ui._('error_unknown_source') + ' '
+ selfoss.filter.source);
}
}

selfoss.sourcesNavLoaded = true;
selfoss.sources.setState(LoadingState.SUCCESS);
if ($('#nav-sources-title').hasClass('nav-sources-collapsed')) {
$('#nav-sources-title').click(); // expand sources nav
}
Expand Down Expand Up @@ -718,7 +716,7 @@ selfoss.ui = {
});
selfoss.tags.update(tags);

if (selfoss.sourcesNavLoaded) {
if (selfoss.sources.sources.length > 0) {
const sources = selfoss.sources.sources.map((source) => {
if (!(source.id in sourceCounts)) {
return source;
Expand Down
57 changes: 31 additions & 26 deletions assets/js/templates/NavFilters.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import ReactDOM from 'react-dom';
import classNames from 'classnames';
import { FilterType } from '../Filter';
import { filterTypeToString } from '../helpers/uri';
import Collapse from '@kunukn/react-collapse';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

function handleClick(e, filterType) {
e.preventDefault();
Expand All @@ -19,6 +21,7 @@ function handleClick(e, filterType) {
}

export function NavFilters({filter}) {
const [expanded, setExpanded] = React.useState(true);
const [currentType, setCurrenttype] = React.useState(filter.type);
const [offlineState, setOfflineState] = React.useState(selfoss.offlineState.value);
const [allItemsCount, setallItemsCount] = React.useState(selfoss.allItemsCount.value);
Expand Down Expand Up @@ -94,32 +97,34 @@ export function NavFilters({filter}) {

return (
<React.Fragment>
<h2><button type="button" id="nav-filter-title" className="nav-section-toggle nav-filter-expanded" aria-expanded="true"><i className="fas fa-caret-down fa-lg fa-fw"></i> {selfoss.ui._('filter')}</button></h2>
<ul id="nav-filter" aria-labelledby="nav-filter-title">
<li>
<a id="nav-filter-newest" href="#" className={classNames({'nav-filter-newest': true, active: currentType === FilterType.NEWEST})} onClick={(event) => handleClick(event, FilterType.NEWEST)}>
{selfoss.ui._('newest')}
<span className={classNames({'offline-count': true, offline: offlineState, online: !offlineState, diff: allItemsCount !== allItemsOfflineCount && allItemsOfflineCount})} title={selfoss.ui._('offline_count')}>{allItemsOfflineCount > 0 ? allItemsOfflineCount : ''}</span>
<span className="count" title={selfoss.ui._('online_count')}>{allItemsCount > 0 ? allItemsCount : ''}</span>
</a>
</li>
<li>
<a id="nav-filter-unread" href="#" className={classNames({'nav-filter-unread': true, active: currentType === FilterType.UNREAD})} onClick={(event) => handleClick(event, FilterType.UNREAD)}>
{selfoss.ui._('unread')}
<span className={classNames({'unread-count': true, offline: offlineState, online: !offlineState, unread: unreadItemsCount > 0})}>
<span className={classNames({'offline-count': true, offline: offlineState, online: !offlineState, diff: unreadItemsCount !== unreadItemsOfflineCount && unreadItemsOfflineCount})} title={selfoss.ui._('offline_count')}>{unreadItemsOfflineCount > 0 ? unreadItemsOfflineCount : ''}</span>
<span className="count" title={selfoss.ui._('online_count')}>{unreadItemsCount > 0 ? unreadItemsCount : ''}</span>
</span>
</a>
</li>
<li>
<a id="nav-filter-starred" href="#" className={classNames({'nav-filter-starred': true, active: currentType === FilterType.STARRED})} onClick={(event) => handleClick(event, FilterType.STARRED)}>
{selfoss.ui._('starred')}
<span className={classNames({'offline-count': true, offline: offlineState, online: !offlineState, diff: starredItemsCount !== starredItemsOfflineCount && starredItemsOfflineCount})} title={selfoss.ui._('offline_count')}>{starredItemsOfflineCount > 0 ? starredItemsOfflineCount : ''}</span>
<span className="count" title={selfoss.ui._('online_count')}>{starredItemsCount > 0 ? starredItemsCount : ''}</span>
</a>
</li>
</ul>
<h2><button type="button" id="nav-filter-title" className={classNames({'nav-section-toggle': true, 'nav-filter-collapsed': !expanded, 'nav-filter-expanded': expanded})} aria-expanded={expanded} onClick={() => setExpanded(!expanded)}><FontAwesomeIcon icon={['fas', expanded ? 'caret-down' : 'caret-right']} size="lg" fixedWidth /> {selfoss.ui._('filter')}</button></h2>
<Collapse isOpen={expanded} className="collapse-css-transition">
<ul id="nav-filter" aria-labelledby="nav-filter-title">
<li>
<a id="nav-filter-newest" href="#" className={classNames({'nav-filter-newest': true, active: currentType === FilterType.NEWEST})} onClick={(event) => handleClick(event, FilterType.NEWEST)}>
{selfoss.ui._('newest')}
<span className={classNames({'offline-count': true, offline: offlineState, online: !offlineState, diff: allItemsCount !== allItemsOfflineCount && allItemsOfflineCount})} title={selfoss.ui._('offline_count')}>{allItemsOfflineCount > 0 ? allItemsOfflineCount : ''}</span>
<span className="count" title={selfoss.ui._('online_count')}>{allItemsCount > 0 ? allItemsCount : ''}</span>
</a>
</li>
<li>
<a id="nav-filter-unread" href="#" className={classNames({'nav-filter-unread': true, active: currentType === FilterType.UNREAD})} onClick={(event) => handleClick(event, FilterType.UNREAD)}>
{selfoss.ui._('unread')}
<span className={classNames({'unread-count': true, offline: offlineState, online: !offlineState, unread: unreadItemsCount > 0})}>
<span className={classNames({'offline-count': true, offline: offlineState, online: !offlineState, diff: unreadItemsCount !== unreadItemsOfflineCount && unreadItemsOfflineCount})} title={selfoss.ui._('offline_count')}>{unreadItemsOfflineCount > 0 ? unreadItemsOfflineCount : ''}</span>
<span className="count" title={selfoss.ui._('online_count')}>{unreadItemsCount > 0 ? unreadItemsCount : ''}</span>
</span>
</a>
</li>
<li>
<a id="nav-filter-starred" href="#" className={classNames({'nav-filter-starred': true, active: currentType === FilterType.STARRED})} onClick={(event) => handleClick(event, FilterType.STARRED)}>
{selfoss.ui._('starred')}
<span className={classNames({'offline-count': true, offline: offlineState, online: !offlineState, diff: starredItemsCount !== starredItemsOfflineCount && starredItemsOfflineCount})} title={selfoss.ui._('offline_count')}>{starredItemsOfflineCount > 0 ? starredItemsOfflineCount : ''}</span>
<span className="count" title={selfoss.ui._('online_count')}>{starredItemsCount > 0 ? starredItemsCount : ''}</span>
</a>
</li>
</ul>
</Collapse>
</React.Fragment>
);
}
Expand Down
57 changes: 50 additions & 7 deletions assets/js/templates/NavSources.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@ import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import { filterTypeToString } from '../helpers/uri';
import Collapse from '@kunukn/react-collapse';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { LoadingState } from '../requests/LoadingState';
import * as sourceRequests from '../requests/sources';

function handleTitleClick(expanded, [sourcesState, setSourcesState], toggle) {
if (!selfoss.db.online) {
return;
}

selfoss.filter.update({ sourcesNav: !expanded });
if (selfoss.filter.sourcesNav && sourcesState === LoadingState.INITIAL) {
sourceRequests.getStats().then((data) => {
setSourcesState(LoadingState.SUCCESS);
selfoss.sources.update(data);
}).catch(function(error) {
setSourcesState(LoadingState.FAILURE);
selfoss.ui.showError(selfoss.ui._('error_loading_stats') + ' ' + error.message);
});
} else {
toggle();
}
}

function handleClick(e, sourceId) {
e.preventDefault();
Expand All @@ -16,36 +39,56 @@ function handleClick(e, sourceId) {
}

export function NavSources({sourcesRepository, filter}) {
const [expanded, setExpanded] = React.useState(false);
const [currentSource, setCurrentSource] = React.useState(filter.source);
const [sourcesState, setSourcesState] = React.useState(sourcesRepository.state);
const [sources, setSources] = React.useState(sourcesRepository.sources);

React.useEffect(() => {
const filterListener = (event) => {
setCurrentSource(event.filter.source);
};
const sourcesStateListener = (event) => {
setSourcesState(event.state);
};
const sourcesListener = (event) => {
setSources(event.sources);
};

// It might happen that filter changes between creating the component and setting up the event handlers.
filterListener({ filter });
sourcesStateListener({ state: sourcesRepository.state });
sourcesListener({ sources: sourcesRepository.sources });

filter.addEventListener('change', filterListener);
sourcesRepository.addEventListener('statechange', sourcesStateListener);
sourcesRepository.addEventListener('change', sourcesListener);

return () => {
filter.removeEventListener('change', filterListener);
sourcesRepository.removeEventListener('statechange', sourcesStateListener);
sourcesRepository.removeEventListener('change', sourcesListener);
};
}, [sourcesRepository, filter]);

return sources.map((source) =>
<li key={source.id}>
<a href="#" className={classNames({active: currentSource === source.id, unread: source.unread > 0})} onClick={(event) => handleClick(event, source.id)}>
<span className="nav-source">{source.title}</span>
<span className="unread">{source.unread > 0 ? source.unread : ''}</span>
</a>
</li>
const reallyExpanded = expanded && sourcesState === LoadingState.SUCCESS;

return (
<React.Fragment>
<h2><button type="button" id="nav-sources-title" className={classNames({'nav-section-toggle': true, 'nav-sources-collapsed': !reallyExpanded, 'nav-sources-expanded': reallyExpanded})} aria-expanded={reallyExpanded} onClick={() => handleTitleClick(reallyExpanded, [sourcesState, setSourcesState], () => setExpanded((expanded) => !expanded))}><FontAwesomeIcon icon={['fas', reallyExpanded ? 'caret-down' : 'caret-right']} size="lg" fixedWidth /> {selfoss.ui._('sources')}</button></h2>
<Collapse isOpen={reallyExpanded} className="collapse-css-transition">
<ul id="nav-sources" aria-labelledby="nav-sources-title">
{sources.map((source) =>
<li key={source.id}>
<a href="#" className={classNames({active: currentSource === source.id, unread: source.unread > 0})} onClick={(event) => handleClick(event, source.id)}>
<span className="nav-source">{source.title}</span>
<span className="unread">{source.unread > 0 ? source.unread : ''}</span>
</a>
</li>
)}
</ul>
</Collapse>
</React.Fragment>
);
}

Expand Down
Loading

0 comments on commit f48398d

Please sign in to comment.