diff --git a/classes/feeds.php b/classes/feeds.php index 62fd6a5b3901667d775d4e80a31667469bba2c3a..503108e41065b79719042cdb8de1159285d25688 100755 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -133,7 +133,7 @@ class Feeds extends Handler_Protected { $reply['vfeed_group_enabled'] = $vfeed_group_enabled; $plugin_menu_items = ""; - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM, + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2, function ($result) use (&$plugin_menu_items) { $plugin_menu_items .= $result; }, diff --git a/classes/plugin.php b/classes/plugin.php index be837692589564ee7f481770108d0a6960558a62..3bced3b04640df514669b07534096bcac7b3c31f 100644 --- a/classes/plugin.php +++ b/classes/plugin.php @@ -647,6 +647,7 @@ abstract class Plugin { } /** Allows adding custom elements to headlines Select... dropdown + * @deprecated removed, see Plugin::hook_headline_toolbar_select_menu_item2() * @param int $feed_id * @param int $is_cat * @return string @@ -658,6 +659,18 @@ abstract class Plugin { return ""; } + /** Allows adding custom elements to headlines Select... select dropdown (<option> format) + * @param int $feed_id + * @param int $is_cat + * @return string + * @see PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2 + */ + function hook_headline_toolbar_select_menu_item2($feed_id, $is_cat) { + user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; + } + /** Invoked when user tries to subscribe to feed, may override information (i.e. feed URL) used afterwards * @param string $url * @param string $auth_login diff --git a/classes/pluginhost.php b/classes/pluginhost.php index a3a389def29047d53ad48ccc161f323e70bbc965..952d4df77cb9a0f4416e04c6a36a6f58e90d1c9f 100755 --- a/classes/pluginhost.php +++ b/classes/pluginhost.php @@ -189,9 +189,14 @@ class PluginHost { /** @see Plugin::hook_headlines_custom_sort_override() */ const HOOK_HEADLINES_CUSTOM_SORT_OVERRIDE = "hook_headlines_custom_sort_override"; - /** @see Plugin::hook_headline_toolbar_select_menu_item() */ + /** @see Plugin::hook_headline_toolbar_select_menu_item() + * @deprecated removed, see PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2 + */ const HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM = "hook_headline_toolbar_select_menu_item"; + /** @see Plugin::hook_headline_toolbar_select_menu_item() */ + const HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2 = "hook_headline_toolbar_select_menu_item2"; + /** @see Plugin::hook_pre_subscribe() */ const HOOK_PRE_SUBSCRIBE = "hook_pre_subscribe"; @@ -270,9 +275,10 @@ class PluginHost { * @param mixed $args */ function run_hooks(string $hook, ...$args): void { - $method = strtolower($hook); - foreach ($this->get_hooks($hook) as $plugin) { + $method = strtolower((string)$hook); + + foreach ($this->get_hooks((string)$hook) as $plugin) { //Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE); try { @@ -291,9 +297,9 @@ class PluginHost { * @param mixed $check */ function run_hooks_until(string $hook, $check, ...$args): bool { - $method = strtolower($hook); + $method = strtolower((string)$hook); - foreach ($this->get_hooks($hook) as $plugin) { + foreach ($this->get_hooks((string)$hook) as $plugin) { try { $result = $plugin->$method(...$args); @@ -315,9 +321,9 @@ class PluginHost { * @param mixed $args */ function run_hooks_callback(string $hook, Closure $callback, ...$args): void { - $method = strtolower($hook); + $method = strtolower((string)$hook); - foreach ($this->get_hooks($hook) as $plugin) { + foreach ($this->get_hooks((string)$hook) as $plugin) { //Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE); try { @@ -336,9 +342,9 @@ class PluginHost { * @param mixed $args */ function chain_hooks_callback(string $hook, Closure $callback, &...$args): void { - $method = strtolower($hook); + $method = strtolower((string)$hook); - foreach ($this->get_hooks($hook) as $plugin) { + foreach ($this->get_hooks((string)$hook) as $plugin) { //Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE); try { @@ -358,7 +364,7 @@ class PluginHost { function add_hook(string $type, Plugin $sender, int $priority = 50): void { $priority = (int) $priority; - if (!method_exists($sender, strtolower($type))) { + if (!method_exists($sender, strtolower((string)$type))) { user_error( sprintf("Plugin %s tried to register a hook without implementation: %s", get_class($sender), $type), @@ -422,7 +428,7 @@ class PluginHost { asort($plugins); - $this->load(join(",", $plugins), $kind, $owner_uid, $skip_init); + $this->load(join(",", $plugins), (int)$kind, $owner_uid, $skip_init); } /** diff --git a/index.php b/index.php index 538346549516c841d55cdbf2de7dc17f71158944..2dbc078cd056e26fa3e8ff7df7d7f1da7f5cb34b 100644 --- a/index.php +++ b/index.php @@ -215,20 +215,13 @@ ?> </select> - <div class="catchup-button" dojoType="fox.form.ComboButton" onclick="Feeds.catchupCurrent()"> - <span><?= __('Mark as read') ?></span> - <div dojoType="dijit.DropDownMenu"> - <div dojoType="dijit.MenuItem" onclick="Feeds.catchupCurrent('1day')"> - <?= __('Older than one day') ?> - </div> - <div dojoType="dijit.MenuItem" onclick="Feeds.catchupCurrent('1week')"> - <?= __('Older than one week') ?> - </div> - <div dojoType="dijit.MenuItem" onclick="Feeds.catchupCurrent('2week')"> - <?= __('Older than two weeks') ?> - </div> - </div> - </div> + <select class="catchup-button" id="main-catchup-dropdown" dojoType="fox.form.Select" + data-prevent-value-change="true"> + <option value=""><?= __('Mark as read') ?></option> + <option value="1day"><?= __('Older than one day') ?></option> + <option value="1week"><?= __('Older than one week') ?></option> + <option value="2week"><?= __('Older than two weeks') ?></option> + </select> </form> diff --git a/js/Feeds.js b/js/Feeds.js index 5ef554af08f292a2135cfb53728cd957a93d49f1..714eb77d25c9c380225fb012bb650fa822c6771c 100644 --- a/js/Feeds.js +++ b/js/Feeds.js @@ -282,6 +282,10 @@ const Feeds = { CommonDialogs.safeModeWarning(); } + dojo.connect(dijit.byId("main-catchup-dropdown"), 'onItemClick', + (item) => Feeds.catchupCurrent(item.option.value) + ); + // bw_limit disables timeout() so we request initial counters separately if (App.getInitParam("bw_limit")) { this.requestCounters(); diff --git a/js/Headlines.js b/js/Headlines.js index 7557676ee81bfeb0487d739e9cf458e8724a05f5..b64ba6bca147f281609a9447fe59b6b420626826 100755 --- a/js/Headlines.js +++ b/js/Headlines.js @@ -626,6 +626,12 @@ const Headlines = { const search_query = Feeds._search_query ? Feeds._search_query.query : ""; const target = dijit.byId('toolbar-headlines'); + // TODO: is this needed? destroyDescendants() below might take care of it (?) + if (this._headlinesSelectClickHandle) + dojo.disconnect(this._headlinesSelectClickHandle); + + target.destroyDescendants(); + if (tb && typeof tb == 'object') { target.attr('innerHTML', ` @@ -646,27 +652,37 @@ const Headlines = { </span> <span class='right'> <span id='selected_prompt'></span> - <div class='select-articles-dropdown' dojoType='fox.form.DropDownButton' title='"${__('Select articles')}'> - <span>${__("Select...")}</span> - <div dojoType='dijit.Menu' style='display: none;'> - <div dojoType='dijit.MenuItem' onclick='Headlines.select("all")'>${__('All')}</div> - <div dojoType='dijit.MenuItem' onclick='Headlines.select("unread")'>${__('Unread')}</div> - <div dojoType='dijit.MenuItem' onclick='Headlines.select("invert")'>${__('Invert')}</div> - <div dojoType='dijit.MenuItem' onclick='Headlines.select("none")'>${__('None')}</div> - <div dojoType='dijit.MenuSeparator'></div> - <div dojoType='dijit.MenuItem' onclick='Headlines.selectionToggleUnread()'>${__('Toggle unread')}</div> - <div dojoType='dijit.MenuItem' onclick='Headlines.selectionToggleMarked()'>${__('Toggle starred')}</div> - <div dojoType='dijit.MenuItem' onclick='Headlines.selectionTogglePublished()'>${__('Toggle published')}</div> - <div dojoType='dijit.MenuSeparator'></div> - <div dojoType='dijit.MenuItem' onclick='Headlines.catchupSelection()'>${__('Mark as read')}</div> - <div dojoType='dijit.MenuItem' onclick='Article.selectionSetScore()'>${__('Set score')}</div> - ${tb.plugin_menu_items} + + <select class='select-articles-dropdown' + id='headlines-select-articles-dropdown' + data-prevent-value-change="true" + data-dropdown-skip-first="true" + dojoType="fox.form.Select" + title="${__('Show articles')}"> + <option value='' selected="selected">${__("Select...")}</option> + <option value='headlines_select_all'>${__('All')}</option> + <option value='headlines_select_unread'>${__('Unread')}</option> + <option value='headlines_select_invert'>${__('Invert')}</option> + <option value='headlines_select_none'>${__('None')}</option> + <option></option> + <option value='headlines_selectionToggleUnread'>${__('Toggle unread')}</option> + <option value='headlines_selectionToggleMarked'>${__('Toggle starred')}</option> + <option value='headlines_selectionTogglePublished'>${__('Toggle published')}</option> + <option></option> + <option value='headlines_catchupSelection'>${__('Mark as read')}</option> + <option value='article_selectionSetScore'>${__('Set score')}</option> + ${tb.plugin_menu_items != '' ? + ` + <option></option> + ${tb.plugin_menu_items} + ` : ''} ${headlines.id === 0 && !headlines.is_cat ? ` - <div dojoType='dijit.MenuSeparator'></div> - <div dojoType='dijit.MenuItem' class='text-error' onclick='Headlines.deleteSelection()'>${__('Delete permanently')}</div> + <option></option> + <option class='text-error' value='headlines_deleteSelection'>${__('Delete permanently')}</option> ` : ''} - </div> + </select> + ${tb.plugin_buttons} </span> `); @@ -675,6 +691,48 @@ const Headlines = { } dojo.parser.parse(target.domNode); + + this._headlinesSelectClickHandle = dojo.connect(dijit.byId("headlines-select-articles-dropdown"), 'onItemClick', + (item) => { + const action = item.option.value; + + switch (action) { + case 'headlines_select_all': + Headlines.select('all'); + break; + case 'headlines_select_unread': + Headlines.select('unread'); + break; + case 'headlines_select_invert': + Headlines.select('invert'); + break; + case 'headlines_select_none': + Headlines.select('none'); + break; + case 'headlines_selectionToggleUnread': + Headlines.selectionToggleUnread(); + break; + case 'headlines_selectionToggleMarked': + Headlines.selectionToggleMarked(); + break; + case 'headlines_selectionTogglePublished': + Headlines.selectionTogglePublished(); + break; + case 'headlines_catchupSelection': + Headlines.catchupSelection(); + break; + case 'article_selectionSetScore': + Article.selectionSetScore(); + break; + case 'headlines_deleteSelection': + Headlines.deleteSelection(); + break; + default: + if (!PluginHost.run_until(PluginHost.HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2, true, action)) + console.warn('unknown headlines action', action); + } + } + ); }, onLoaded: function (reply, offset, append) { console.log("Headlines.onLoaded: offset=", offset, "append=", append); diff --git a/js/form/Select.js b/js/form/Select.js index 530880e2da732eeb1722ccf4b182427d43af1db3..0c73cd52cc2c059bc51ea508c723ef21d8fc1b1b 100755 --- a/js/form/Select.js +++ b/js/form/Select.js @@ -1,8 +1,66 @@ -/* global dijit, define */ -define(["dojo/_base/declare", "dijit/form/Select"], function (declare) { - return declare("fox.form.Select", dijit.form.Select, { +/* eslint-disable prefer-rest-params */ +/* global define */ +// FIXME: there probably is a better, more dojo-like notation for custom data- properties +define(["dojo/_base/declare", + "dijit/form/Select", + "dojo/_base/lang", // lang.hitch + "dijit/MenuItem", + "dijit/MenuSeparator", + "dojo/aspect", + ], function (declare, select, lang, MenuItem, MenuSeparator, aspect) { + return declare("fox.form.Select", select, { focus: function() { return; // Stop dijit.form.Select from keeping focus after closing the menu }, + startup: function() { + this.inherited(arguments); + + if (this.attr('data-dropdown-skip-first') == 'true') { + aspect.before(this, "_loadChildren", () => { + this.options = this.options.splice(1); + }); + } + }, + // hook invoked when dropdown MenuItem is clicked + onItemClick: function(/*item, menu*/) { + // + }, + _setValueAttr: function(/*anything*/ newValue, /*Boolean?*/ priorityChange){ + if (this.attr('data-prevent-value-change') == 'true' && newValue != '') + return; + + this.inherited(arguments); + }, + // the only difference from dijit/form/Select is _onItemClicked() handler + _getMenuItemForOption: function(/*_FormSelectWidget.__SelectOption*/ option){ + // summary: + // For the given option, return the menu item that should be + // used to display it. This can be overridden as needed + if (!option.value && !option.label){ + // We are a separator (no label set for it) + return new MenuSeparator({ownerDocument: this.ownerDocument}); + } else { + // Just a regular menu option + const click = lang.hitch(this, "_setValueAttr", option); + const item = new MenuItem({ + option: option, + label: (this.labelType === 'text' ? (option.label || '').toString() + .replace(/&/g, '&').replace(/</g, '<') : + option.label) || this.emptyLabel, + onClick: () => { + this.onItemClick(item, this.dropDown); + + click(); + }, + ownerDocument: this.ownerDocument, + dir: this.dir, + textDir: this.textDir, + disabled: option.disabled || false + }); + item.focusNode.setAttribute("role", "option"); + + return item; + } + }, }); });