'use strict' /* global __, ngettext */ define(['dojo/_base/declare'], function (declare) { function decodeHTMLEntities (text) { var textArea = document.createElement('textarea') textArea.innerHTML = text return textArea.value } var img_preview_elem = document.createElement('div') img_preview_elem.style.display = 'none' document.addEventListener('DOMContentLoaded', function () { document.body.appendChild(img_preview_elem) }) Headlines = { vgroup_last_feed: undefined, _headlines_scroll_timeout: 0, _observer_counters_timeout: 0, headlines: [], fullArticles: [], current_first_id: 0, card_count: 0, row_observer: new MutationObserver(mutations => { const modified = [] mutations.each(m => { if ( m.type == 'attributes' && ['class', 'data-score'].indexOf(m.attributeName) != -1 ) { const row = m.target const id = row.getAttribute('data-article-id') if (Headlines.headlines[id]) { const hl = Headlines.headlines[id] if (hl) { const hl_old = Object.extend({}, hl) hl.unread = row.hasClassName('Unread') hl.marked = row.hasClassName('marked') hl.published = row.hasClassName('published') // not sent by backend hl.selected = row.hasClassName('Selected') hl.active = row.hasClassName('active') hl.score = row.getAttribute('data-score') modified.push({ id: hl.id, new: hl, old: hl_old, row: row }) } } } }) Headlines.updateSelectedPrompt() Headlines.updateFloatingTitle(true) if ('requestIdleCallback' in window) window.requestIdleCallback(() => { Headlines.syncModified(modified) }) else Headlines.syncModified(modified) }), syncModified: function (modified) { const ops = { tmark: [], tpub: [], read: [], unread: [], select: [], deselect: [], activate: [], deactivate: [], rescore: {} } modified.each(function (m) { if (m.old.marked != m.new.marked) ops.tmark.push(m.id) if (m.old.published != m.new.published) ops.tpub.push(m.id) if (m.old.unread != m.new.unread) m.new.unread ? ops.unread.push(m.id) : ops.read.push(m.id) if (m.old.selected != m.new.selected) m.new.selected ? ops.select.push(m.row) : ops.deselect.push(m.row) if (m.old.active != m.new.active) m.new.active ? ops.activate.push(m.row) : ops.deactivate.push(m.row) if (m.old.score != m.new.score) { const score = m.new.score ops.rescore[score] = ops.rescore[score] || [] ops.rescore[score].push(m.id) } }) ops.select.each(row => { const cb = dijit.getEnclosingWidget(row.select('.rchk')[0]) if (cb) cb.attr('checked', true) }) ops.deselect.each(row => { const cb = dijit.getEnclosingWidget(row.select('.rchk')[0]) if (cb && !row.hasClassName('active')) cb.attr('checked', false) }) ops.activate.each(row => { const cb = dijit.getEnclosingWidget(row.select('.rchk')[0]) if (cb) cb.attr('checked', true) }) ops.deactivate.each(row => { const cb = dijit.getEnclosingWidget(row.select('.rchk')[0]) if (cb && !row.hasClassName('Selected')) cb.attr('checked', false) }) const promises = [] if (ops.tmark.length != 0) promises.push( xhrPost('backend.php', { op: 'rpc', method: 'markSelected', ids: ops.tmark.toString(), cmode: 2 }) ) if (ops.tpub.length != 0) promises.push( xhrPost('backend.php', { op: 'rpc', method: 'publishSelected', ids: ops.tpub.toString(), cmode: 2 }) ) if (ops.read.length != 0) promises.push( xhrPost('backend.php', { op: 'rpc', method: 'catchupSelected', ids: ops.read.toString(), cmode: 0 }) ) if (ops.unread.length != 0) promises.push( xhrPost('backend.php', { op: 'rpc', method: 'catchupSelected', ids: ops.unread.toString(), cmode: 1 }) ) const scores = Object.keys(ops.rescore) if (scores.length != 0) { scores.each(score => { promises.push( xhrPost('backend.php', { op: 'article', method: 'setScore', id: ops.rescore[score].toString(), score: score }) ) }) } if (promises.length > 0) Promise.all([promises]).then(() => { window.clearTimeout(this._observer_counters_timeout) this._observer_counters_timeout = setTimeout(() => { Feeds.requestCounters(true) }, 1000) }) }, viewFull: function (event, id) { const row = $('RROW-' + id) if (row.hasClassName('marked')) $('slide-out').addClassName('marked') else $('slide-out').removeClassName('marked') if (row.hasClassName('published')) $('slide-out').addClassName('published') else $('slide-out').removeClassName('published') var content = document.getElementById('content-' + id).innerHTML var slide = '


' + this.fullArticles[id].replace('[[CONTENT]]', content) + '


' document.getElementById('slide-out').innerHTML = ' ' const tmp = document.createElement('div') tmp.innerHTML = slide dojo.parser.parse(tmp) document.getElementById('slide-out').appendChild(tmp) document.getElementById('slide-out-shadow').style.visibility = 'visible' document.getElementById('slide-out').style.visibility = 'visible' }, hideFull: function () { document.getElementById('slide-out').style.visibility = 'hidden' document.getElementById('slide-out-shadow').style.visibility = 'hidden' Article.setActiveHeadline(0) }, click: function (event, id, in_body) { in_body = in_body || false if (App.isCombinedMode()) { //console.log("in_body: " + in_body); //console.log("event.ctrlKey: " + event.ctrlKey); //console.log("event.shiftKey: " + event.shiftKey); //console.log("id: " + id); //console.log("Article.getActive(): " + Article.getActive()); //console.log("App.getInitParam('cdm_expanded'): " + App.getInitParam("cdm_expanded")); //console.log("App.getViewMode(): " + App.getViewMode()); if ( id != Article.getActiveHeadline() && (event.ctrlKey || event.shiftKey || App.getViewMode() == 'full') ) { Article.setActiveHeadline(id) Article.openInNewWindow(id) } else if (id != Article.getActiveHeadline()) { Article.setActiveHeadline(id) this.viewFull(event, id) } else { Article.openInNewWindow(id) } return in_body } else if (event.ctrlKey) { Article.openInNewWindow(id) Headlines.toggleUnread(id, 0) } else { Article.view(id) } return false }, initScrollHandler: function () { $('headlines-frame').onscroll = event => { clearTimeout(this._headlines_scroll_timeout) this._headlines_scroll_timeout = window.setTimeout(function () { //console.log('done scrolling', event); Headlines.scrollHandler() }, 50) } }, loadMore: function () { const view_mode = document.forms['toolbar-main'].view_mode.value const unread_in_buffer = $$( '#headlines-frame > div[id*=RROW][class*=Unread]' ).length const num_all = $$('#headlines-frame > div[id*=RROW]').length const num_unread = Feeds.getUnread(Feeds.getActive(), Feeds.activeIsCat()) // TODO implement marked & published let offset = num_all switch (view_mode) { case 'marked': case 'published': console.warn('loadMore: ', view_mode, 'not implemented') break case 'unread': offset = unread_in_buffer break case 'adaptive': if (!(Feeds.getActive() == -1 && !Feeds.activeIsCat())) offset = num_unread > 0 ? unread_in_buffer : num_all break } console.log('loadMore, offset=', offset) Feeds.open({ feed: Feeds.getActive(), is_cat: Feeds.activeIsCat(), offset: offset, append: true }) }, scrollHandler: function () { try { Headlines.unpackVisible() if (App.isCombinedMode()) { Headlines.updateFloatingTitle() // set topmost child in the buffer as active, but not if we're at the beginning (to prevent auto marking // first article as read all the time) if ( $('headlines-frame').scrollTop != 0 && App.getInitParam('cdm_expanded') && App.getInitParam('cdm_auto_catchup') == 1 ) { const rows = $$('#headlines-frame > div[id*=RROW]') for (let i = 0; i < rows.length; i++) { const row = rows[i] if ( $('headlines-frame').scrollTop <= row.offsetTop && row.offsetTop - $('headlines-frame').scrollTop < 100 && row.getAttribute('data-article-id') != Article.getActive() ) { Article.setActive(row.getAttribute('data-article-id')) break } } } } if (!Feeds.infscroll_disabled) { const hsp = $('headlines-spacer') const container = $('headlines-frame') if ( hsp && hsp.offsetTop - 250 <= container.scrollTop + container.offsetHeight ) { hsp.innerHTML = " " + __('Loading, please wait...') + '' Headlines.loadMore() return } } if (App.getInitParam('cdm_auto_catchup') == 1) { let rows = $$('#headlines-frame > div[id*=RROW][class*=Unread]') for (let i = 0; i < rows.length; i++) { const row = rows[i] if ( $('headlines-frame').scrollTop > row.offsetTop + row.offsetHeight / 2 ) { row.removeClassName('Unread') } else { break } } } } catch (e) { console.warn('scrollHandler', e) } }, updateFloatingTitle: function (status_only) { if (!App.isCombinedMode() /* || !App.getInitParam("cdm_expanded")*/) return const safety_offset = 120 /* px, needed for firefox */ const hf = $('headlines-frame') const elems = $$('#headlines-frame > div[id*=RROW]') const ft = $('floatingTitle') for (let i = 0; i < elems.length; i++) { const row = elems[i] if ( row && row.offsetTop + row.offsetHeight > hf.scrollTop + safety_offset ) { const header = row.select('.header')[0] const id = row.getAttribute('data-article-id') if (status_only || id != ft.getAttribute('data-article-id')) { if (id != ft.getAttribute('data-article-id')) { ft.setAttribute('data-article-id', id) ft.innerHTML = header.innerHTML ft.select('.dijitCheckBox')[0].outerHTML = 'expand_more' this.initFloatingMenu() } if (row.hasClassName('Unread')) ft.addClassName('Unread') else ft.removeClassName('Unread') if (row.hasClassName('marked')) ft.addClassName('marked') else ft.removeClassName('marked') if (row.hasClassName('published')) ft.addClassName('published') else ft.removeClassName('published') PluginHost.run(PluginHost.HOOK_FLOATING_TITLE, row) } if ( hf.scrollTop - row.offsetTop <= header.offsetHeight + safety_offset ) ft.fade({ duration: 0.2 }) else ft.appear({ duration: 0.2 }) return } } }, unpackVisible: function () { if (!App.isCombinedMode() || !App.getInitParam('cdm_expanded')) return const rows = $$('#headlines-frame div[id*=RROW][data-content]') const threshold = $('headlines-frame').scrollTop + $('headlines-frame').offsetHeight + 600 for (let i = 0; i < rows.length; i++) { const row = rows[i] if (row.offsetTop <= threshold) { Article.unpack(row) } else { break } } }, objectById: function (id) { return this.headlines[id] }, renderAgain: function () { // TODO: wrap headline elements into a knockoutjs model to prevent all this stuff $$('#headlines-frame > div[id*=RROW]').each(row => { const id = row.getAttribute('data-article-id') const hl = this.headlines[id] if (hl) { const new_row = this.render({}, hl) row.parentNode.replaceChild(new_row, row) if (hl.active) { new_row.addClassName('active') if (App.isCombinedMode()) Article.cdmScrollToId(id) else Article.view(id) } if (hl.selected) this.select('all', id) Article.unpack(new_row) } }) }, render: function (headlines, hl) { let row = null let row_class = '' if (hl.marked) row_class += ' marked' if (hl.published) row_class += ' published' if (hl.unread) row_class += ' Unread' if (headlines.vfeed_group_enabled) row_class += ' vgrlf' if ( headlines.vfeed_group_enabled && hl.feed_title && this.vgroup_last_feed != hl.feed_id ) { let vgrhdr = `
${hl.feed_icon}
${hl.feed_title} done_all
` const tmp = document.createElement('div') tmp.innerHTML = vgrhdr $('headlines-frame').appendChild(tmp.firstChild) this.vgroup_last_feed = hl.feed_id } if (App.isCombinedMode()) { row_class += App.getInitParam('cdm_expanded') ? ' expanded' : ' expandable' img_preview_elem.innerHTML = decodeHTMLEntities(hl.content) + decodeHTMLEntities(hl.enclosures) var img_preview_src = 'images/blank_icon.gif' var imgs = img_preview_elem.querySelectorAll('img') for (var i = 0; i < imgs.length; i++) { if (imgs[i].src) { img_preview_src = imgs[i].src break } } const comments = Article.formatComments(hl) const originally_from = Article.formatOriginallyFrom(hl) var author_display = hl.feed_title if (hl.feed_title == '') author_display = hl.author this.fullArticles[hl.id] = `
star rss_feed
— ${hl.author} ${hl.labels}
${Article.getScorePic( hl.score )} ${ hl.updated }
${hl.note}
[[CONTENT]]
${hl.enclosures}
` if (App.getViewMode() == 'magazine') { row = `
star rss_feed
— ${author_display} ${hl.labels}
${Article.getScorePic(hl.score)} ${ hl.updated }
${hl.content_preview.replace('— ', '')}
` } else if (App.getViewMode() == 'cards') { row = `
star rss_feed
— ${author_display} ${hl.labels}
${Article.getScorePic(hl.score)} ${ hl.updated }
${hl.content_preview.replace('— ', '')}
` } else { row = `
star rss_feed
— ${hl.author} ${hl.labels}
${Article.getScorePic( hl.score )} ${ hl.updated }
${hl.note}
[[CONTENT]]
${hl.enclosures}
` } } else { row = `
star rss_feed
${ hl.title } ${hl.content_preview} ${hl.author} ${hl.labels}
${ hl.feed_title }
${hl.updated}
${Article.getScorePic( hl.score )} ${ hl.feed_icon }
` } const tmp = document.createElement('div') tmp.innerHTML = row dojo.parser.parse(tmp) this.row_observer.observe(tmp.firstChild, { attributes: true }) PluginHost.run(PluginHost.HOOK_HEADLINE_RENDERED, tmp.firstChild) return tmp.firstChild }, onLoaded: function (transport, offset, append) { const reply = App.handleRpcJson(transport) console.log('Headlines.onLoaded: offset=', offset, 'append=', append) let is_cat = false let feed_id = false if (reply) { is_cat = reply['headlines']['is_cat'] feed_id = reply['headlines']['id'] Feeds.last_search_query = reply['headlines']['search_query'] if ( feed_id != -7 && (feed_id != Feeds.getActive() || is_cat != Feeds.activeIsCat()) ) return const headlines_count = reply['headlines-info']['count'] Feeds.infscroll_disabled = parseInt(headlines_count) < 30 console.log( 'received', headlines_count, 'headlines, infscroll disabled=', Feeds.infscroll_disabled ) //this.vgroup_last_feed = reply['headlines-info']['vgroup_last_feed']; this.current_first_id = reply['headlines']['first_id'] if (!append) { // TODO: the below needs to be applied again when switching expanded/expandable on the fly // via hotkeys, not just on feed load $('headlines-frame').removeClassName('cdm') $('headlines-frame').removeClassName('normal') $('headlines-frame').addClassName( App.isCombinedMode() ? 'cdm' : 'normal' ) if (App.getViewMode() == 'cards') $('headlines-frame').addClassName('grid-cards-three') else $('headlines-frame').removeClassName('grid-cards-three') $('headlines-frame').setAttribute( 'is-vfeed', reply['headlines']['is_vfeed'] ? 1 : 0 ) // for floating title because it's placed outside of headlines-frame $('main').removeClassName('expandable') $('main').removeClassName('expanded') if (App.isCombinedMode()) $('main').addClassName( App.getInitParam('cdm_expanded') ? ' expanded' : ' expandable' ) Article.setActive(0) try { $('headlines-frame').scrollTop = 0 Element.hide('floatingTitle') $('floatingTitle').setAttribute('data-article-id', 0) $('floatingTitle').innerHTML = '' } catch (e) { console.warn(e) } this.headlines = [] this.vgroup_last_feed = undefined dojo.html.set($('toolbar-headlines'), reply['headlines']['toolbar'], { parseContent: true }) if (typeof reply['headlines']['content'] == 'string') { $('headlines-frame').innerHTML = reply['headlines']['content'] } else { $('headlines-frame').innerHTML = '' for (let i = 0; i < reply['headlines']['content'].length; i++) { const hl = reply['headlines']['content'][i] $('headlines-frame').appendChild( this.render(reply['headlines'], hl) ) //if (App.getViewMode() == "cards") { //Headlines.card_count++; //if (Headlines.card_count == 1) { //const tmp = document.createElement("div"); //tmp.innerHTML = "
"; //$("headlines-frame").appendChild(tmp); //} else if (Headlines.card_count == 3) { //Headlines.card_count = 0; //const tmp = document.createElement("div"); //tmp.innerHTML = "
"; //$("headlines-frame").appendChild(tmp); //} //} this.headlines[parseInt(hl.id)] = hl } } let hsp = $('headlines-spacer') if (!hsp) { hsp = document.createElement('div') hsp.id = 'headlines-spacer' } dijit.byId('headlines-frame').domNode.appendChild(hsp) this.initHeadlinesMenu() if (Feeds.infscroll_disabled) hsp.innerHTML = "" + __('Click to open next unread feed.') + '' if (Feeds._search_query) { $('feed_title').innerHTML += "" + " (" + __('Cancel search') + ')' + '' } } else if ( headlines_count > 0 && feed_id == Feeds.getActive() && is_cat == Feeds.activeIsCat() ) { const c = dijit.byId('headlines-frame') let hsp = $('headlines-spacer') if (hsp) c.domNode.removeChild(hsp) let headlines_appended = 0 if (typeof reply['headlines']['content'] == 'string') { $('headlines-frame').innerHTML = reply['headlines']['content'] } else { for (let i = 0; i < reply['headlines']['content'].length; i++) { const hl = reply['headlines']['content'][i] if (!this.headlines[parseInt(hl.id)]) { $('headlines-frame').appendChild( this.render(reply['headlines'], hl) ) this.headlines[parseInt(hl.id)] = hl ++headlines_appended } } } Feeds.infscroll_disabled = headlines_appended != 30 console.log( 'appended', headlines_appended, 'headlines, infscroll_disabled=', Feeds.infscroll_disabled ) if (!hsp) { hsp = document.createElement('div') hsp.id = 'headlines-spacer' } c.domNode.appendChild(hsp) this.initHeadlinesMenu() if (Feeds.infscroll_disabled) { hsp.innerHTML = "" + __('Click to open next unread feed.') + '' } } else { console.log('no new headlines received') const first_id_changed = reply['headlines']['first_id_changed'] console.log('first id changed:' + first_id_changed) let hsp = $('headlines-spacer') if (hsp) { if (first_id_changed) { hsp.innerHTML = "" + __('New articles found, reload feed to continue.') + '' } else { hsp.innerHTML = "" + __('Click to open next unread feed.') + '' } } } } else { console.error('Invalid object received: ' + transport.responseText) dijit .byId('headlines-frame') .attr( 'content', "
" + __( 'Could not update headlines (invalid object received - see error console for details)' ) + '
' ) } Feeds.infscroll_in_progress = 0 // this is used to auto-catchup articles if needed after infscroll request has finished, // unpack visible articles, fill buffer more, etc this.scrollHandler() Notify.close() }, reverse: function () { const toolbar = document.forms['toolbar-main'] const order_by = dijit.getEnclosingWidget(toolbar.order_by) let value = order_by.attr('value') if (value != 'date_reverse') value = 'date_reverse' else value = 'default' order_by.attr('value', value) Feeds.reloadCurrent() }, selectionToggleUnread: function (params) { params = params || {} const cmode = params.cmode != undefined ? params.cmode : 2 const no_error = params.no_error || false const ids = params.ids || Headlines.getSelected() if (ids.length == 0) { if (!no_error) alert(__('No articles selected.')) return } ids.each(id => { const row = $('RROW-' + id) if (row) { switch (cmode) { case 0: row.removeClassName('Unread') break case 1: row.addClassName('Unread') break case 2: row.toggleClassName('Unread') } } }) }, selectionToggleMarked: function (ids) { ids = ids || Headlines.getSelected() if (ids.length == 0) { alert(__('No articles selected.')) return } ids.each(id => { this.toggleMark(id) }) }, selectionTogglePublished: function (ids) { ids = ids || Headlines.getSelected() if (ids.length == 0) { alert(__('No articles selected.')) return } ids.each(id => { this.togglePub(id) }) }, toggleMark: function (id) { const row = $('RROW-' + id) const so = $('slide-out') if (row) row.toggleClassName('marked') if (so) so.toggleClassName('marked') }, togglePub: function (id) { const row = $('RROW-' + id) const so = $('slide-out') if (row) row.toggleClassName('published') if (so) so.toggleClassName('published') }, move: function (mode, noscroll, noexpand) { const rows = Headlines.getLoaded() let prev_id = false let next_id = false if (!$('RROW-' + Article.getActive())) { Article.setActive(0) } if (!Article.getActive()) { next_id = rows[0] prev_id = rows[rows.length - 1] } else { for (let i = 0; i < rows.length; i++) { if (rows[i] == Article.getActive()) { // Account for adjacent identical article ids. if (i > 0) prev_id = rows[i - 1] for (let j = i + 1; j < rows.length; j++) { if (rows[j] != Article.getActive()) { next_id = rows[j] break } } break } } } console.log('cur: ' + Article.getActive() + ' next: ' + next_id) if (mode == 'next') { if (next_id || Article.getActive()) { if (App.isCombinedMode()) { const article = $('RROW-' + Article.getActive()) const ctr = $('headlines-frame') if ( !noscroll && article && article.offsetTop + article.offsetHeight > ctr.scrollTop + ctr.offsetHeight ) { Article.scroll(ctr.offsetHeight / 4) } else if (next_id) { Article.setActive(next_id) Article.cdmScrollToId(next_id, true) } } else if (next_id) { Headlines.correctHeadlinesOffset(next_id) Article.view(next_id, noexpand) } } } if (mode == 'prev') { if (prev_id || Article.getActive()) { if (App.isCombinedMode()) { const article = $('RROW-' + Article.getActive()) const prev_article = $('RROW-' + prev_id) const ctr = $('headlines-frame') if (!noscroll && article && article.offsetTop < ctr.scrollTop) { Article.scroll(-ctr.offsetHeight / 3) } else if ( !noscroll && prev_article && prev_article.offsetTop < ctr.scrollTop ) { Article.scroll(-ctr.offsetHeight / 4) } else if (prev_id) { Article.setActive(prev_id) Article.cdmScrollToId(prev_id, noscroll) } } else if (prev_id) { Headlines.correctHeadlinesOffset(prev_id) Article.view(prev_id, noexpand) } } } }, updateSelectedPrompt: function () { const count = Headlines.getSelected().length const elem = $('selected_prompt') if (elem) { elem.innerHTML = ngettext( '%d article selected', '%d articles selected', count ).replace('%d', count) if (count > 0) { changeStyle('.toolbar-hide', 'visibility', 'visible') document.getElementById('more-button').style.visibility = 'hidden' Element.show(elem) } else { document.getElementById('more-button').style.visibility = 'visible' Element.hide(elem) } } }, toggleUnread: function (id, cmode) { const row = $('RROW-' + id) if (row) { //const origClassName = row.className; if (cmode == undefined) cmode = 2 switch (cmode) { case 0: row.removeClassName('Unread') break case 1: row.addClassName('Unread') break case 2: row.toggleClassName('Unread') break } } }, selectionRemoveLabel: function (id, ids) { if (!ids) ids = Headlines.getSelected() if (ids.length == 0) { alert(__('No articles selected.')) return } const query = { op: 'article', method: 'removeFromLabel', ids: ids.toString(), lid: id } xhrPost('backend.php', query, transport => { App.handleRpcJson(transport) this.onLabelsUpdated(transport) }) }, selectionAssignLabel: function (id, ids) { if (!ids) ids = Headlines.getSelected() if (ids.length == 0) { alert(__('No articles selected.')) return } const query = { op: 'article', method: 'assignToLabel', ids: ids.toString(), lid: id } xhrPost('backend.php', query, transport => { App.handleRpcJson(transport) this.onLabelsUpdated(transport) }) }, deleteSelection: function () { const rows = Headlines.getSelected() if (rows.length == 0) { alert(__('No articles selected.')) return } const fn = Feeds.getName(Feeds.getActive(), Feeds.activeIsCat()) let str if (Feeds.getActive() != 0) { str = ngettext( 'Delete %d selected article in %s?', 'Delete %d selected articles in %s?', rows.length ) } else { str = ngettext( 'Delete %d selected article?', 'Delete %d selected articles?', rows.length ) } str = str.replace('%d', rows.length) str = str.replace('%s', fn) if (App.getInitParam('confirm_feed_catchup') == 1 && !confirm(str)) { return } const query = { op: 'rpc', method: 'delete', ids: rows.toString() } xhrPost('backend.php', query, transport => { App.handleRpcJson(transport) Feeds.reloadCurrent() }) }, getSelected: function () { const rv = [] $$('#headlines-frame > div[id*=RROW][class*=Selected]').each(function ( child ) { rv.push(child.getAttribute('data-article-id')) }) // consider active article a honorary member of selected articles if (Article.getActive()) rv.push(Article.getActive()) return rv.uniq() }, getLoaded: function () { const rv = [] const children = $$('#headlines-frame > div[id*=RROW-]') children.each(function (child) { if (Element.visible(child)) { rv.push(child.getAttribute('data-article-id')) } }) return rv }, onRowChecked: function (elem) { const row = elem.domNode.up('div[id*=RROW]') // do not allow unchecking active article checkbox if (row.hasClassName('active')) { elem.attr('checked', 1) return } if (elem.attr('checked')) { row.addClassName('Selected') } else { row.removeClassName('Selected') } }, getRange: function (start, stop) { if (start == stop) return [start] const rows = $$('#headlines-frame > div[id*=RROW]') const results = [] let collecting = false for (let i = 0; i < rows.length; i++) { const row = rows[i] const id = row.getAttribute('data-article-id') if (id == start || id == stop) { if (!collecting) { collecting = true } else { results.push(id) break } } if (collecting) results.push(id) } return results }, select: function (mode, articleId) { // mode = all,none,unread,invert,marked,published let query = '#headlines-frame > div[id*=RROW]' if (articleId) query += '[data-article-id=' + articleId + ']' switch (mode) { case 'none': case 'all': case 'invert': break case 'marked': query += '[class*=marked]' break case 'published': query += '[class*=published]' break case 'unread': query += '[class*=Unread]' break default: console.warn('select: unknown mode', mode) } const rows = $$(query) for (let i = 0; i < rows.length; i++) { const row = rows[i] switch (mode) { case 'none': row.removeClassName('Selected') break case 'invert': row.toggleClassName('Selected') break default: row.addClassName('Selected') } } }, archiveSelection: function () { const rows = Headlines.getSelected() if (rows.length == 0) { alert(__('No articles selected.')) return } const fn = Feeds.getName(Feeds.getActive(), Feeds.activeIsCat()) let str let op if (Feeds.getActive() != 0) { str = ngettext( 'Archive %d selected article in %s?', 'Archive %d selected articles in %s?', rows.length ) op = 'archive' } else { str = ngettext( 'Move %d archived article back?', 'Move %d archived articles back?', rows.length ) str += ' ' + __( 'Please note that unstarred articles might get purged on next feed update.' ) op = 'unarchive' } str = str.replace('%d', rows.length) str = str.replace('%s', fn) if (App.getInitParam('confirm_feed_catchup') == 1 && !confirm(str)) { return } const query = { op: 'rpc', method: op, ids: rows.toString() } xhrPost('backend.php', query, transport => { App.handleRpcJson(transport) Feeds.reloadCurrent() }) }, catchupSelection: function () { const rows = Headlines.getSelected() if (rows.length == 0) { alert(__('No articles selected.')) return } const fn = Feeds.getName(Feeds.getActive(), Feeds.activeIsCat()) let str = ngettext( 'Mark %d selected article in %s as read?', 'Mark %d selected articles in %s as read?', rows.length ) str = str.replace('%d', rows.length) str = str.replace('%s', fn) if (App.getInitParam('confirm_feed_catchup') == 1 && !confirm(str)) { return } Headlines.selectionToggleUnread({ ids: rows, cmode: 0 }) }, catchupRelativeTo: function (below, id) { if (!id) id = Article.getActive() if (!id) { alert(__('No article is selected.')) return } const visible_ids = this.getLoaded() const ids_to_mark = [] if (!below) { for (let i = 0; i < visible_ids.length; i++) { if (visible_ids[i] != id) { const e = $('RROW-' + visible_ids[i]) if (e && e.hasClassName('Unread')) { ids_to_mark.push(visible_ids[i]) } } else { break } } } else { for (let i = visible_ids.length - 1; i >= 0; i--) { if (visible_ids[i] != id) { const e = $('RROW-' + visible_ids[i]) if (e && e.hasClassName('Unread')) { ids_to_mark.push(visible_ids[i]) } } else { break } } } if (ids_to_mark.length == 0) { alert(__('No articles found to mark')) } else { const msg = ngettext( 'Mark %d article as read?', 'Mark %d articles as read?', ids_to_mark.length ).replace('%d', ids_to_mark.length) if (App.getInitParam('confirm_feed_catchup') != 1 || confirm(msg)) { for (var i = 0; i < ids_to_mark.length; i++) { var e = $('RROW-' + ids_to_mark[i]) e.removeClassName('Unread') } } } }, onLabelsUpdated: function (transport) { const data = JSON.parse(transport.responseText) if (data) { data['info-for-headlines'].each(function (elem) { $$('.HLLCTR-' + elem.id).each(function (ctr) { ctr.innerHTML = elem.labels }) }) } }, onActionChanged: function (elem) { eval(elem.value) elem.attr('value', 'false') }, correctHeadlinesOffset: function (id) { const container = $('headlines-frame') const row = $('RROW-' + id) if (!container || !row) return const viewport = container.offsetHeight const rel_offset_top = row.offsetTop - container.scrollTop const rel_offset_bottom = row.offsetTop + row.offsetHeight - container.scrollTop //console.log("Rtop: " + rel_offset_top + " Rbtm: " + rel_offset_bottom); //console.log("Vport: " + viewport); if (rel_offset_top <= 0 || rel_offset_top > viewport) { container.scrollTop = row.offsetTop } else if (rel_offset_bottom > viewport) { container.scrollTop = row.offsetTop + row.offsetHeight - viewport } }, initFloatingMenu: function () { if (!dijit.byId('floatingMenu')) { const menu = new dijit.Menu({ id: 'floatingMenu', selector: '.hlMenuAttach', targetNodeIds: ['floatingTitle'] }) this.headlinesMenuCommon(menu) menu.startup() } }, headlinesMenuCommon: function (menu) { menu.addChild( new dijit.MenuItem({ label: __('Open original article'), onClick: function (event) { Article.openInNewWindow( this.getParent().currentTarget.getAttribute('data-article-id') ) } }) ) menu.addChild( new dijit.MenuItem({ label: __('Display article URL'), onClick: function (event) { Article.displayUrl( this.getParent().currentTarget.getAttribute('data-article-id') ) } }) ) menu.addChild(new dijit.MenuSeparator()) menu.addChild( new dijit.MenuItem({ label: __('Toggle unread'), onClick: function () { let ids = Headlines.getSelected() // cast to string const id = this.getParent().currentTarget.getAttribute('data-article-id') + '' ids = ids.length != 0 && ids.indexOf(id) != -1 ? ids : [id] Headlines.selectionToggleUnread({ ids: ids, no_error: 1 }) } }) ) menu.addChild( new dijit.MenuItem({ label: __('Toggle starred'), onClick: function () { let ids = Headlines.getSelected() // cast to string const id = this.getParent().currentTarget.getAttribute('data-article-id') + '' ids = ids.length != 0 && ids.indexOf(id) != -1 ? ids : [id] Headlines.selectionToggleMarked(ids) } }) ) menu.addChild( new dijit.MenuItem({ label: __('Toggle published'), onClick: function () { let ids = Headlines.getSelected() // cast to string const id = this.getParent().currentTarget.getAttribute('data-article-id') + '' ids = ids.length != 0 && ids.indexOf(id) != -1 ? ids : [id] Headlines.selectionTogglePublished(ids) } }) ) menu.addChild(new dijit.MenuSeparator()) menu.addChild( new dijit.MenuItem({ label: __('Mark above as read'), onClick: function () { Headlines.catchupRelativeTo( 0, this.getParent().currentTarget.getAttribute('data-article-id') ) } }) ) menu.addChild( new dijit.MenuItem({ label: __('Mark below as read'), onClick: function () { Headlines.catchupRelativeTo( 1, this.getParent().currentTarget.getAttribute('data-article-id') ) } }) ) const labels = App.getInitParam('labels') if (labels && labels.length) { menu.addChild(new dijit.MenuSeparator()) const labelAddMenu = new dijit.Menu({ ownerMenu: menu }) const labelDelMenu = new dijit.Menu({ ownerMenu: menu }) labels.each(function (label) { const bare_id = label.id const name = label.caption labelAddMenu.addChild( new dijit.MenuItem({ label: name, labelId: bare_id, onClick: function () { let ids = Headlines.getSelected() // cast to string const id = this.getParent().ownerMenu.currentTarget.getAttribute( 'data-article-id' ) + '' ids = ids.length != 0 && ids.indexOf(id) != -1 ? ids : [id] Headlines.selectionAssignLabel(this.labelId, ids) } }) ) labelDelMenu.addChild( new dijit.MenuItem({ label: name, labelId: bare_id, onClick: function () { let ids = Headlines.getSelected() // cast to string const id = this.getParent().ownerMenu.currentTarget.getAttribute( 'data-article-id' ) + '' ids = ids.length != 0 && ids.indexOf(id) != -1 ? ids : [id] Headlines.selectionRemoveLabel(this.labelId, ids) } }) ) }) menu.addChild( new dijit.PopupMenuItem({ label: __('Assign label'), popup: labelAddMenu }) ) menu.addChild( new dijit.PopupMenuItem({ label: __('Remove label'), popup: labelDelMenu }) ) } }, initHeadlinesMenu: function () { if (!dijit.byId('headlinesMenu')) { const menu = new dijit.Menu({ id: 'headlinesMenu', targetNodeIds: ['headlines-frame'], selector: '.hlMenuAttach' }) this.headlinesMenuCommon(menu) menu.startup() } /* vgroup feed title menu */ if (!dijit.byId('headlinesFeedTitleMenu')) { const menu = new dijit.Menu({ id: 'headlinesFeedTitleMenu', targetNodeIds: ['headlines-frame'], selector: 'div.cdmFeedTitle' }) menu.addChild( new dijit.MenuItem({ label: __('Select articles in group'), onClick: function (event) { Headlines.select( 'all', '#headlines-frame > div[id*=RROW]' + "[data-orig-feed-id='" + this.getParent().currentTarget.getAttribute('data-feed-id') + "']" ) } }) ) menu.addChild( new dijit.MenuItem({ label: __('Mark group as read'), onClick: function () { Headlines.select('none') Headlines.select( 'all', '#headlines-frame > div[id*=RROW]' + "[data-orig-feed-id='" + this.getParent().currentTarget.getAttribute('data-feed-id') + "']" ) Headlines.catchupSelection() } }) ) menu.addChild( new dijit.MenuItem({ label: __('Mark feed as read'), onClick: function () { Feeds.catchupFeedInGroup( this.getParent().currentTarget.getAttribute('data-feed-id') ) } }) ) menu.addChild( new dijit.MenuItem({ label: __('Edit feed'), onClick: function () { CommonDialogs.editFeed( this.getParent().currentTarget.getAttribute('data-feed-id') ) } }) ) menu.startup() } } } return Headlines })