Mooeypoo has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/350763 )
Change subject: [wip] Add a 'saved queries' quick filters feature ...................................................................... [wip] Add a 'saved queries' quick filters feature Change-Id: I5cede87633147736d3b4ee5b8ea178ae21bd441f --- M includes/Preferences.php M languages/i18n/en.json M languages/i18n/qqq.json M resources/Resources.php A resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js A resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js M resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js M resources/src/mediawiki.rcfilters/mw.rcfilters.init.js A resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinkMenuOptionWidget.less A resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinksWidget.less A resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveQuickLinkWidget.less M resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js M resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js A resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinkMenuOptionWidget.js A resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinksWidget.js A resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveQuickLinkWidget.js 16 files changed, 827 insertions(+), 7 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core refs/changes/63/350763/1 diff --git a/includes/Preferences.php b/includes/Preferences.php index b428e87..4017619 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -915,6 +915,9 @@ 'label-message' => 'tog-hideminor', 'section' => 'rc/advancedrc', ]; + $defaultPreferences['rcfilters-saved-queries'] = [ + 'type' => 'api', + ]; if ( $config->get( 'RCWatchCategoryMembership' ) ) { $defaultPreferences['hidecategorization'] = [ diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 8129205..92586a9 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -1365,6 +1365,11 @@ "recentchanges-legend-plusminus": "(<em>±123</em>)", "recentchanges-submit": "Show", "rcfilters-activefilters": "Active filters", + "rcfilters-quickfilters": "Quick links", + "rcfilters-savedqueries-defaultlabel": "Saved filters", + "rcfilters-savedqueries-rename": "Rename", + "rcfilters-savedqueries-setdefault": "Set as default", + "rcfilters-savedqueries-remove": "Remove", "rcfilters-restore-default-filters": "Restore default filters", "rcfilters-clear-all-filters": "Clear all filters", "rcfilters-search-placeholder": "Filter recent changes (browse or start typing)", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 88f6ead..2286e19 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -1553,6 +1553,11 @@ "recentchanges-legend-plusminus": "{{optional}}\nA plus/minus sign with a number for the legend.", "recentchanges-submit": "Label for submit button in [[Special:RecentChanges]]\n{{Identical|Show}}", "rcfilters-activefilters": "Title for the filters selection showing the active filters.", + "rcfilters-quickfilters": "Label for the button that opens the quick filters menu in [[Special:RecentChanges]]", + "rcfilters-savedqueries-defaultlabel": "Default name for saving a new set of quick filters [[Special:RecentChanges]]", + "rcfilters-savedqueries-rename": "Label for the menu option that edits a quick filter in [[Special:RecentChanges]]", + "rcfilters-savedqueries-setdefault": "Label for the menu option that sets a quick filter as default in [[Special:RecentChanges]]", + "rcfilters-savedqueries-remove": "Label for the menu option that removes a quick filter as default in [[Special:RecentChanges]]", "rcfilters-restore-default-filters": "Label for the button that resets filters to defaults", "rcfilters-clear-all-filters": "Title for the button that clears all filters", "rcfilters-search-placeholder": "Placeholder for the filter search input.", diff --git a/resources/Resources.php b/resources/Resources.php index 1721de8..4e03398 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1746,6 +1746,8 @@ 'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js', 'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js', 'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js', + 'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js', + 'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js', 'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js', 'resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js', ], @@ -1767,6 +1769,9 @@ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FloatingMenuSelectWidget.js', 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js', 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js', + 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinksWidget.js', + 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinkMenuOptionWidget.js', + 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveQuickLinkWidget.js', 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js', 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js', 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.HighlightColorPickerWidget.js', @@ -1789,6 +1794,9 @@ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less', 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.HighlightColorPickerWidget.less', 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemHighlightButton.less', + 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinksWidget.less', + 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinkMenuOptionWidget.less', + 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveQuickLinkWidget.less', ], 'skinStyles' => [ 'monobook' => [ @@ -1798,6 +1806,11 @@ ], 'messages' => [ 'rcfilters-activefilters', + 'rcfilters-quickfilters', + 'rcfilters-savedqueries-defaultlabel', + 'rcfilters-savedqueries-rename', + 'rcfilters-savedqueries-setdefault', + 'rcfilters-savedqueries-remove', 'rcfilters-restore-default-filters', 'rcfilters-clear-all-filters', 'rcfilters-search-placeholder', diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js new file mode 100644 index 0000000..d5df0dc --- /dev/null +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js @@ -0,0 +1,149 @@ +( function ( mw, $ ) { + /** + * View mdel for saved queries + * + * @mixins OO.EventEmitter + * + * @constructor + * @param {Object} [config] Configuration options + */ + mw.rcfilters.dm.SavedQueriesModel = function MwRcfiltersDmSavedQueriesModel( config ) { + config = config || {}; + + // Mixin constructor + OO.EventEmitter.call( this ); + OO.EmitterList.call( this ); + + this.default = null; + + // Events + this.aggregate( { update: 'queryItemUpdate' } ); + this.connect( this, { queryItemUpdate: [ 'emit', 'itemUpdate' ] } ); + }; + + /* Initialization */ + + OO.initClass( mw.rcfilters.dm.SavedQueriesModel ); + OO.mixinClass( mw.rcfilters.dm.SavedQueriesModel, OO.EventEmitter ); + OO.mixinClass( mw.rcfilters.dm.SavedQueriesModel, OO.EmitterList ); + + /* Events */ + + /** + * @event initialize + * + * Model is initialized + */ + + /** + * @event default + * @param {string} QueryID Query identifier + * + * Default value has changed + */ + + /* Methods */ + + /** + * Initialize the saved queries model by reading it from the user's settings. + * The structure of the saved queries is: + * { + * query_id_1: { + * data:{ + * params: (Object) Definition of the parameters + * highlights: (Object) Definition of the highlights + * }, + * label: (optional) Name of this query + * } + * } + * + * @fires initialize + */ + mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function () { + var savedQueries = JSON.parse( mw.user.options.get( 'rcfilters-saved-queries' ) ) || {}, + items = []; + + this.clearItems(); + $.each( savedQueries, function ( id, data ) { + items.push( + new mw.rcfilters.dm.SavedQueryItemModel( + id, + data.label, + data.data + ) + ); + } ); + + this.addItems( items ); + + this.emit( 'initialize' ); + }; + + /** + * Get query by its identifier + * + * @param {string} queryID Query identifier + * @return {mw.rcfilters.dm.SavedQueryItemModel|undefined} Item matching + * the search. Undefined if not found. + */ + mw.rcfilters.dm.SavedQueriesModel.prototype.getItemByID = function ( queryID ) { + return this.getItems().filter( function ( item ) { + return item.getID() === queryID; + } )[ 0 ]; + }; + + /** + * Get the object representing the state of the entire model and items + * + * @return {Object} Object representing the state of the model and items + */ + mw.rcfilters.dm.SavedQueriesModel.prototype.getState = function () { + var obj = {}; + + // Translate the items to the saved object + this.getItems().forEach( function ( item ) { + obj[ item.getID() ] = item.getState(); + } ); + + if ( this.getDefault() ) { + obj.default = this.getDefault(); + } + + return obj; + }; + + /** + * Set a default query. Null to unset default. + * + * @param {string} itemID Query identifier + * @fires default + */ + mw.rcfilters.dm.SavedQueriesModel.prototype.setDefault = function ( itemID ) { + if ( this.default !== itemID ) { + this.default = itemID; + this.emit( 'default', this.default ); + } + }; + + /** + * Get the default query ID + * + * @return {string} Default query identifier + */ + mw.rcfilters.dm.SavedQueriesModel.prototype.getDefault = function () { + return this.default; + }; + + /** + * Save the state in the user settings + */ + mw.rcfilters.dm.SavedQueriesModel.prototype.save = function () { + var stringified = JSON.stringify( this.getState() ); + + // Save the preference in general + new mw.Api().saveOption( 'rcfilters-saved-queries', stringified ); + // Save the preference for this session + mw.user.options.set( 'rcfilters-saved-queries', stringified ); + }; + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js new file mode 100644 index 0000000..35067aa --- /dev/null +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js @@ -0,0 +1,67 @@ +( function ( mw, $ ) { + /** + * View model for a single saved query + * + * @mixins OO.EventEmitter + * + * @constructor + * @param {string} id Unique identifier + * @param {string} label Saved query label + * @param {Object} data Saved query data + * @param {Object} [config] Configuration options + */ + mw.rcfilters.dm.SavedQueryItemModel = function MwRcfiltersDmSavedQueriesModel( id, label, data, config ) { + config = config || {}; + + // Mixin constructor + OO.EventEmitter.call( this ); + + this.id = id; + this.label = label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ); + this.data = data; + }; + + /* Initialization */ + + OO.initClass( mw.rcfilters.dm.SavedQueryItemModel ); + OO.mixinClass( mw.rcfilters.dm.SavedQueryItemModel, OO.EventEmitter ); + + /* Events */ + + /** + * @update + * + * Model has been updated + */ + + /* Methods */ + + /** + * Get an object representing the state of this item + */ + mw.rcfilters.dm.SavedQueryItemModel.prototype.getState = function () { + return { + data: this.getData(), + label: this.getLabel() + }; + }; + + mw.rcfilters.dm.SavedQueryItemModel.prototype.getID = function () { + return this.id; + }; + + mw.rcfilters.dm.SavedQueryItemModel.prototype.getLabel = function () { + return this.label; + }; + + mw.rcfilters.dm.SavedQueryItemModel.prototype.updateLabel = function ( newLabel ) { + if ( this.label !== newLabel ) { + this.label = newLabel; + this.emit( 'update' ); + } + }; + + mw.rcfilters.dm.SavedQueryItemModel.prototype.getData = function () { + return this.data; + }; +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js index 669420c..bcb59bb 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js @@ -4,10 +4,12 @@ * * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel Changes list view model + * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model */ - mw.rcfilters.Controller = function MwRcfiltersController( filtersModel, changesListModel ) { + mw.rcfilters.Controller = function MwRcfiltersController( filtersModel, changesListModel, savedQueriesModel ) { this.filtersModel = filtersModel; this.changesListModel = changesListModel; + this.savedQueriesModel = savedQueriesModel; this.requestCounter = 0; }; @@ -23,6 +25,7 @@ var $changesList = $( '.mw-changeslist' ).first().contents(); // Initialize the model this.filtersModel.initializeFilters( filterStructure ); + this.savedQueriesModel.initialize(); this.updateStateBasedOnUrl(); // Update the changes list with the existing data @@ -31,7 +34,6 @@ $changesList.length ? $changesList : 'NO_RESULTS', $( 'fieldset.rcoptions' ).first() ); - }; /** @@ -341,4 +343,49 @@ } ); }; + + mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label ) { + var randomID = ( new Date() ).getTime(); + + label = label || mw.msg( 'rcfilters-savedqueries-defaultname' ); + + this.savedQueriesModel.addItems( [ + new mw.rcfilters.dm.SavedQueryItemModel( + randomID, + label, + // Data + { + params: this.filtersModel.getParametersFromFilters(), + highlights: this.filtersModel.getHighlightParameters() + } + ) + ] ); + + this.savedQueriesModel.save(); + }; + + mw.rcfilters.Controller.prototype.removeSavedQuery = function ( queryID ) { + var query = this.savedQueriesModel.getItemByID( queryID ); + + this.savedQueriesModel.removeItems( [ query ] ); + this.savedQueriesModel.save(); + }; + + mw.rcfilters.Controller.prototype.renameSavedQuery = function ( queryID, newLabel ) { + var queryItem = this.savedQueriesModel.getItemByID( queryID ); + + if ( queryItem ) { + queryItem.updateLabel( newLabel ); + } + this.savedQueriesModel.save(); + }; + + mw.rcfilters.Controller.prototype.loadSavedQuery = function ( queryID ) { + var queryItem = this.savedQueriesModel.getItemByID( queryID ), + data = queryItem.getData(); + + this.updateChangesList( $.extend( {}, data.params, data.highlights ) ); + this.updateStateBasedOnUrl(); + }; + }( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js index 4a586e4..dd8fae0 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js @@ -11,11 +11,12 @@ init: function () { var filtersModel = new mw.rcfilters.dm.FiltersViewModel(), changesListModel = new mw.rcfilters.dm.ChangesListViewModel(), - controller = new mw.rcfilters.Controller( filtersModel, changesListModel ), + savedQueriesModel = new mw.rcfilters.dm.SavedQueriesModel(), + controller = new mw.rcfilters.Controller( filtersModel, changesListModel, savedQueriesModel ), $overlay = $( '<div>' ) .addClass( 'mw-rcfilters-ui-overlay' ), filtersWidget = new mw.rcfilters.ui.FilterWrapperWidget( - controller, filtersModel, { $overlay: $overlay } ); + controller, filtersModel, savedQueriesModel, { $overlay: $overlay } ); // TODO: The changesListWrapperWidget should be able to initialize // after the model is ready. diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinkMenuOptionWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinkMenuOptionWidget.less new file mode 100644 index 0000000..0d9add6 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinkMenuOptionWidget.less @@ -0,0 +1,39 @@ +.mw-rcfilters-ui-quickLinkMenuOptionWidget { + padding: 0.5em; + &:not( :last-child ) { + border-bottom: 1px solid #ccc; + } + + .mw-rcfilters-ui-cell { + vertical-align: middle; + } + + &-icon span { + display: inline-block; + } + + &-input { + display: inline-block; + width: 15em; + } + + &-label { + max-width: 15em; + display: inline-block; + vertical-align: middle; + text-overflow: ellipsis; + overflow: hidden; + cursor: pointer !important; + margin-left: 0.5px; + } + + &-icon, + &-button { + width: 2em; + } + + &-content { + width: 100%; + } + +} diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinksWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinksWidget.less new file mode 100644 index 0000000..c5436d6 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.QuickLinksWidget.less @@ -0,0 +1,7 @@ +.mw-rcfilters-ui-quickLinksWidget { + float: right; + + &-menu { + width: 100%; + } +} diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveQuickLinkWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveQuickLinkWidget.less new file mode 100644 index 0000000..9b754fa --- /dev/null +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveQuickLinkWidget.less @@ -0,0 +1,22 @@ +.mw-rcfilters-ui-saveQuickLinkWidget { + &-popup { + &-layout { + padding-bottom: 1.5em; + } + + & > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-head { + & > .oo-ui-iconWidget { + margin: 0.75em 0.5em; + float: left; + } + + & > .oo-ui-labelElement-label { + font-size: 1.2em; + padding: 0.3em; + margin-left: 0; + font-weight: bold; + } + } + } + +} diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js index 4192aad..a6e1c66 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js @@ -8,10 +8,11 @@ * @constructor * @param {mw.rcfilters.Controller} controller Controller * @param {mw.rcfilters.dm.FiltersViewModel} model View model + * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model * @param {Object} config Configuration object * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups */ - mw.rcfilters.ui.FilterTagMultiselectWidget = function MwRcfiltersUiFilterTagMultiselectWidget( controller, model, config ) { + mw.rcfilters.ui.FilterTagMultiselectWidget = function MwRcfiltersUiFilterTagMultiselectWidget( controller, model, savedQueriesModel, config ) { var title = new OO.ui.LabelWidget( { label: mw.msg( 'rcfilters-activefilters' ), classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-title' ] @@ -23,6 +24,7 @@ this.controller = controller; this.model = model; + this.queriesModel = savedQueriesModel; this.$overlay = config.$overlay || this.$element; // Parent @@ -60,6 +62,11 @@ classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-resetButton' ] } ); + this.saveQueryButton = new mw.rcfilters.ui.SaveQuickLinkWidget( + this.controller, + this.savedQueriesModel + ); + this.emptyFilterMessage = new OO.ui.LabelWidget( { label: mw.msg( 'rcfilters-empty-filter' ), classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-emptyFilters' ] @@ -71,6 +78,7 @@ // Stop propagation for mousedown, so that the widget doesn't // trigger the focus on the input and scrolls up when we click the reset button this.resetButton.$element.on( 'mousedown', function ( e ) { e.stopPropagation(); } ); + this.saveQueryButton.$element.on( 'mousedown', function ( e ) { e.stopPropagation(); } ); this.model.connect( this, { initialize: 'onModelInitialize', itemUpdate: 'onModelItemUpdate', @@ -96,6 +104,10 @@ .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' ), $( '<div>' ) .addClass( 'mw-rcfilters-ui-cell' ) + .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-save' ) + .append( this.saveQueryButton.$element ), + $( '<div>' ) + .addClass( 'mw-rcfilters-ui-cell' ) .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-reset' ) .append( this.resetButton.$element ) ) diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js index b7ebf34..d9b7fab 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js @@ -8,11 +8,12 @@ * @constructor * @param {mw.rcfilters.Controller} controller Controller * @param {mw.rcfilters.dm.FiltersViewModel} model View model + * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model * @param {Object} [config] Configuration object * @cfg {Object} [filters] A definition of the filter groups in this list * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups */ - mw.rcfilters.ui.FilterWrapperWidget = function MwRcfiltersUiFilterWrapperWidget( controller, model, config ) { + mw.rcfilters.ui.FilterWrapperWidget = function MwRcfiltersUiFilterWrapperWidget( controller, model, savedQueriesModel, config ) { config = config || {}; // Parent @@ -22,18 +23,29 @@ this.controller = controller; this.model = model; + this.queriesModel = savedQueriesModel; this.$overlay = config.$overlay || this.$element; this.filterTagWidget = new mw.rcfilters.ui.FilterTagMultiselectWidget( this.controller, this.model, + this.savedQueriesModel, + { $overlay: this.$overlay } + ); + + this.quickLinksWidget = new mw.rcfilters.ui.QuickLinksWidget( + this.controller, + this.queriesModel, { $overlay: this.$overlay } ); // Initialize this.$element .addClass( 'mw-rcfilters-ui-filterWrapperWidget' ) - .append( this.filterTagWidget.$element ); + .append( + this.quickLinksWidget.$element, + this.filterTagWidget.$element + ); }; /* Initialization */ diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinkMenuOptionWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinkMenuOptionWidget.js new file mode 100644 index 0000000..0281032 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinkMenuOptionWidget.js @@ -0,0 +1,216 @@ +( function ( mw ) { + /** + * Quick links menu option widget + * + * @extends OO.ui.Widget + * @mixins OO.ui.mixin.LabelElement + * @mixins OO.ui.mixin.IconElement + * + * @constructor + * @param {mw.rcfilters.dm.SavedQueryItemModel} model View model + * @param {Object} [config] Configuration object + * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups + */ + mw.rcfilters.ui.QuickLinkMenuOptionWidget = function MwRcfiltersUiQuickLinksWidget( model, config ) { + config = config || {}; + + this.model = model; + + // Parent + mw.rcfilters.ui.QuickLinkMenuOptionWidget.parent.call( this, $.extend( { + data: this.model.getID() + }, config ) ); + + // Mixin constructors + OO.ui.mixin.LabelElement.call( this, $.extend( { + label: this.model.getLabel() + }, config ) ); + OO.ui.mixin.IconElement.call( this, $.extend( { + icon: 'bookmark' + }, config ) ); + + this.edit = false; + this.$overlay = config.$overlay || this.$element; + + this.popupButton = new OO.ui.ButtonWidget( { + classes: [ 'mw-rcfilters-ui-quickLinkMenuOptionWidget-button' ], + icon: 'ellipsis', + framed: false + } ); + this.menu = new OO.ui.FloatingMenuSelectWidget( { + classes: [ 'mw-rcfilters-ui-quickLinkMenuOptionWidget-menu' ], + widget: this.popupButton, + width: 200, + horizontalPosition: 'end', + $container: this.popupButton.$element, + $autoCloseIgnore: this.$overlay, + items: [ + new OO.ui.MenuOptionWidget( { + data: 'edit', + icon: 'edit', + label: mw.msg( 'rcfilters-savedqueries-rename' ) + } ), + new OO.ui.MenuOptionWidget( { + data: 'delete', + icon: 'close', + label: mw.msg( 'rcfilters-savedqueries-remove' ) + } ), + new OO.ui.MenuOptionWidget( { + data: 'default', + icon: 'heart', + label: mw.msg( 'rcfilters-savedqueries-setdefault' ) + } ), + ] + } ); + + this.editInput = new OO.ui.TextInputWidget( { + classes: [ 'mw-rcfilters-ui-quickLinkMenuOptionWidget-input' ] + } ); + this.saveButton = new OO.ui.ButtonWidget( { + icon: 'check', + flags: [ 'primary', 'progressive' ] + } ); + this.toggleEdit( false ); + + // Events + this.model.connect( this, { update: 'onModelUpdate' } ); + this.popupButton.connect( this, { click: 'onPopupButtonClick' } ); + this.menu.connect( this, { + choose: 'onMenuChoose' + } ); + this.saveButton.connect( this, { click: 'onSaveButtonClick' } ); + this.editInput.$input.on( { + blur: this.onInputBlur.bind( this ), + keypress: this.onInputKeypress.bind( this ), + keyup: this.onInputKeyup.bind( this ) + } ); + this.$element.on( { click: this.emit.bind( this, 'click' ) } ); + this.$label.on( { click: this.emit.bind( this, 'click' ) } ); + + // Initialize + this.$overlay.append( this.menu.$element ); + this.$element + .addClass( 'mw-rcfilters-ui-quickLinkMenuOptionWidget' ) + .addClass( 'mw-rcfilters-ui-quickLinkMenuOptionWidget-query-' + this.model.getID() ) + .append( + $( '<div>' ) + .addClass( 'mw-rcfilters-ui-table' ) + .append( + $( '<div>' ) + .addClass( 'mw-rcfilters-ui-row' ) + .append( + $( '<div>' ) + .addClass( 'mw-rcfilters-ui-cell' ) + .addClass( 'mw-rcfilters-ui-quickLinkMenuOptionWidget-icon' ) + .append( this.$icon ), + $( '<div>' ) + .addClass( 'mw-rcfilters-ui-cell' ) + .addClass( 'mw-rcfilters-ui-quickLinkMenuOptionWidget-content' ) + .append( + this.$label + .addClass( 'mw-rcfilters-ui-quickLinkMenuOptionWidget-label' ), + this.editInput.$element, + this.saveButton.$element + ), + this.popupButton.$element + .addClass( 'mw-rcfilters-ui-cell' ) + ) + ) + ); + }; + + /* Initialization */ + OO.inheritClass( mw.rcfilters.ui.QuickLinkMenuOptionWidget, OO.ui.Widget ); + OO.mixinClass( mw.rcfilters.ui.QuickLinkMenuOptionWidget, OO.ui.mixin.LabelElement ); + OO.mixinClass( mw.rcfilters.ui.QuickLinkMenuOptionWidget, OO.ui.mixin.IconElement ); + + mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.onModelUpdate = function () { + this.setLabel( this.model.getLabel() ); + }; + + mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.onPopupButtonClick = function () { + this.menu.toggle(); + }; + + mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.onMenuChoose = function ( item ) { + var action = item.getData(); + + if ( action === 'edit' ) { + this.toggleEdit( true ); + } else if ( action === 'delete' ) { + this.emit( 'delete' ); + } else if ( action === 'default' ) { + this.emit( 'default', !this.default ); + } + this.menu.toggle( false ); + }; + /** + * Respond to save button click + */ + mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.onSaveButtonClick = function () { + this.emit( 'edit', this.editInput.getValue() ); + this.toggleEdit( false ); + }; + + /** + * Respond to input keypress event + * + * @param {jQuery.Event} e Event data + */ + mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.onInputKeypress = function ( e ) { + if ( e.which === OO.ui.Keys.ENTER ) { + this.emit( 'edit', this.editInput.getValue() ); + this.toggleEdit( false ); + } + }; + + /** + * Respond to input keyup event, this is the way to intercept 'escape' key + * @param {jQuery.Event} e Event data + */ + mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.onInputKeyup = function ( e ) { + if ( e.which === OO.ui.Keys.ESCAPE ) { + // Return the input to the original label + this.editInput.setValue( this.getLabel() ); + this.toggleEdit( false ); + return false; + } + }; + + /** + * Respond to blur event on the input + */ + mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.onInputBlur = function () { + this.emit( 'edit', this.editInput.getValue() ); + this.toggleEdit( false ); + }; + + /** + * Toggle edit mode on this widget + * + * @param {boolean} isEdit Widget is in edit mode + */ + mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.toggleEdit = function ( isEdit ) { + isEdit = isEdit === undefined ? !this.editing : isEdit; + + if ( this.editing !== isEdit ) { + this.$element.toggleClass( 'mw-rcfilters-ui-quickLinkMenuOptionWidget-edit', isEdit ); + this.editInput.setValue( this.getLabel() ); + + this.editInput.toggle( isEdit ); + this.$label.toggleClass( 'oo-ui-element-hidden', isEdit ); + this.popupButton.toggle( !isEdit ); + this.saveButton.toggle( isEdit ); + + if ( isEdit ) { + this.editInput.$input.focus(); + } + this.editing = isEdit; + } + }; + + mw.rcfilters.ui.QuickLinkMenuOptionWidget.prototype.getID = function () { + return this.model.getID(); + }; + +}( mediaWiki ) ); diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinksWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinksWidget.js new file mode 100644 index 0000000..334b35d --- /dev/null +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.QuickLinksWidget.js @@ -0,0 +1,121 @@ +( function ( mw ) { + /** + * Quick links widget + * + * @extends OO.ui.Widget + * @mixins OO.ui.mixin.PendingElement + * + * @constructor + * @param {mw.rcfilters.Controller} controller Controller + * @param {mw.rcfilters.dm.SavedQueriesModel} model View model + * @param {Object} [config] Configuration object + * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups + */ + mw.rcfilters.ui.QuickLinksWidget = function MwRcfiltersUiQuickLinksWidget( controller, model, config ) { + config = config || {}; + + // Parent + mw.rcfilters.ui.QuickLinksWidget.parent.call( this, config ); + + this.controller = controller; + this.model = model; + this.$overlay = config.$overlay || this.$element; + + this.menu = new OO.ui.ButtonGroupWidget( { + classes: [ 'mw-rcfilters-ui-quickLinksWidget-menu' ] + } ); + this.button = new OO.ui.PopupButtonWidget( { + classes: [ 'mw-rcfilters-ui-quickLinksWidget-button' ], + label: mw.msg( 'rcfilters-quickfilters' ), + icon: 'bookmark', + $overlay: this.$overlay, + popup: { + anchor: false, + $autoCloseIgnore: this.$overlay, + $content: this.menu.$element + } + } ); + + this.menu.aggregate( { + click: 'menuItemClick', + delete: 'menuItemDelete', + default: 'menuItemDefault', + edit: 'menuItemEdit' + } ); + + // Events + this.button.connect( this, { click: 'onButtonClick' } ); + this.model.connect( this, { + // initialize: 'onModelInitialize', + add: 'onModelAddItem', + remove: 'onModelRemoveItem' + } ); + this.menu.connect( this, { + menuItemClick: 'onMenuItemClick', + menuItemRemove: 'onMenuItemRemove', + menuItemDefault: 'onMenuItemDefault', + menuItemEdit: 'onMenuItemEdit' + } ); + + this.button.toggle( !this.menu.isEmpty() ); + // Initialize + this.$element + .addClass( 'mw-rcfilters-ui-quickLinksWidget' ) + .append( this.button.$element ); + + }; + + /* Initialization */ + OO.inheritClass( mw.rcfilters.ui.QuickLinksWidget, OO.ui.Widget ); + + mw.rcfilters.ui.QuickLinksWidget.prototype.onButtonClick = function () { + this.menu.toggle( true ); + }; + + mw.rcfilters.ui.QuickLinksWidget.prototype.onMenuItemClick = function ( item ) { + this.controller.loadSavedQuery( item.getID() ); + }; + mw.rcfilters.ui.QuickLinksWidget.prototype.onMenuItemRemove = function ( item ) { + this.controller.removeSavedQuery( item.getID() ); + this.menu.removeItems( [ item ] ); + }; + + mw.rcfilters.ui.QuickLinksWidget.prototype.onMenuItemDefault = function ( item ) { + debugger; + }; + + mw.rcfilters.ui.QuickLinksWidget.prototype.onMenuItemEdit = function ( item, newLabel ) { + this.controller.renameSavedQuery( item.getID(), newLabel ); + }; + + + + // mw.rcfilters.ui.QuickLinksWidget.prototype.onModelInitialize = function () { + // var menuOptions = []; + + // this.model.getItems().forEach( function ( item ) { + // menuOptions.push( + // new mw.rcfilters.ui.QuickLinkMenuOptionWidget( item ) + // ); + // } ); + + // this.menu.addItems( menuOptions ); + // this.button.toggle( !this.menu.isEmpty() ); + // }; + + + mw.rcfilters.ui.QuickLinksWidget.prototype.onModelAddItem = function ( item ) { + if ( this.menu.getItemFromData( item.getID() ) ) { + return; + } + + this.menu.addItems( [ + new mw.rcfilters.ui.QuickLinkMenuOptionWidget( item, { $overlay: this.$overlay } ) + ] ); + this.button.toggle( !this.menu.isEmpty() ); + }; + mw.rcfilters.ui.QuickLinksWidget.prototype.onModelRemoveItem = function ( item ) { + this.menu.removeItems( [ this.model.getItemByID( item.getID() ) ] ); + this.button.toggle( !this.menu.isEmpty() ); + }; +}( mediaWiki ) ); diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveQuickLinkWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveQuickLinkWidget.js new file mode 100644 index 0000000..68af930 --- /dev/null +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveQuickLinkWidget.js @@ -0,0 +1,101 @@ +( function ( mw ) { + /** + * Save quick link widget + * + * @extends OO.ui.Widget + * + * @constructor + * @param {mw.rcfilters.Controller} controller Controller + * @param {mw.rcfilters.dm.SavedQueriesModel} model View model + * @param {Object} [config] Configuration object + */ + mw.rcfilters.ui.SaveQuickLinkWidget = function MwRcfiltersUiSaveQuickLinkWidget( controller, model, config ) { + var layout, + $popupContent = $( '<div>' ); + + config = config || {}; + + this.controller = controller; + this.model = model; + + // Parent + mw.rcfilters.ui.SaveQuickLinkWidget.parent.call( this, $.extend( { + framed: false, + icon: 'bookmark', + $overlay: this.$overlay, + popup: { + classes: [ 'mw-rcfilters-ui-saveQuickLinkWidget-popup' ], + padded: true, + head: true, + label: 'Add as quick link', + $content: $popupContent + } + }, config ) ); + // // HACK: Add an icon to the popup head label + this.popup.$head.prepend( ( new OO.ui.IconWidget( { icon: 'bookmark' } ) ).$element ); + + this.input = new OO.ui.TextInputWidget( { + validate: 'non-empty' + } ); + layout = new OO.ui.FieldLayout( this.input, { + label: 'Name', + align: 'top' + } ); + + this.applyButton = new OO.ui.ButtonWidget( { + label: 'Create quick link', + classes: [ 'mw-rcfilters-ui-saveQuickLinkWidget-popup-buttons-apply' ], + flags: [ 'primary', 'progressive' ] + } ); + this.cancelButton = new OO.ui.ButtonWidget( { + label: 'Cancel', + classes: [ 'mw-rcfilters-ui-saveQuickLinkWidget-popup-buttons-cancel' ] + } ); + + $popupContent + .append( + $( '<div>' ) + .addClass( 'mw-rcfilters-ui-saveQuickLinkWidget-popup-layout' ) + .append( layout.$element ), + $( '<div>' ) + .addClass( 'mw-rcfilters-ui-saveQuickLinkWidget-popup-buttons' ) + .append( + this.cancelButton.$element, + this.applyButton.$element + ) + ); + + // Events + this.popup.connect( this, { ready: 'onPopupReady' } ); + this.cancelButton.connect( this, { click: 'onCancelButtonClick' } ); + this.applyButton.connect( this, { click: 'onApplyButtonClick' } ); + + // Initialize + this.$element + .addClass( 'mw-rcfilters-ui-saveQuickLinkWidget' ); + }; + + /* Initialization */ + OO.inheritClass( mw.rcfilters.ui.SaveQuickLinkWidget, OO.ui.PopupButtonWidget ); + + mw.rcfilters.ui.SaveQuickLinkWidget.prototype.onPopupReady = function () { + this.input.focus(); + }; + + mw.rcfilters.ui.SaveQuickLinkWidget.prototype.onCancelButtonClick = function () { + this.popup.toggle( false ); + }; + mw.rcfilters.ui.SaveQuickLinkWidget.prototype.onApplyButtonClick = function () { + var widget = this, + label = this.input.getValue(); + + this.input.getValidity() + .then( + function () { + widget.controller.saveCurrentQuery( label ); + widget.input.setValue( '' ); + widget.popup.toggle( false ); + } + ); + }; +}( mediaWiki ) ); -- To view, visit https://gerrit.wikimedia.org/r/350763 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I5cede87633147736d3b4ee5b8ea178ae21bd441f Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/core Gerrit-Branch: master Gerrit-Owner: Mooeypoo <mor...@gmail.com> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits