Mooeypoo has uploaded a new change for review. https://gerrit.wikimedia.org/r/209166
Change subject: [wip^n] Create a basic data model layer for Flow JS ...................................................................... [wip^n] Create a basic data model layer for Flow JS Create a dm for Flow; start with a flow Board and Topic models with basic fetching from the API. Change-Id: Iba245037078f548edd2dc10519e8f2c0c650cb00 --- M .jshintrc M Hooks.php M Resources.php M modules/flow-initialize.js A modules/flow/dm/api/mw.flow.dm.APIResultsProvider.js A modules/flow/dm/api/mw.flow.dm.APIResultsQueue.js A modules/flow/dm/api/mw.flow.dm.APITopicsProvider.js A modules/flow/dm/mixins/mw.flow.dm.List.js A modules/flow/dm/mw.flow.dm.Board.js A modules/flow/dm/mw.flow.dm.Item.js A modules/flow/dm/mw.flow.dm.Topic.js A modules/flow/dm/mw.flow.dm.js A modules/flow/mw.flow.js 13 files changed, 1,164 insertions(+), 2 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Flow refs/changes/66/209166/1 diff --git a/.jshintrc b/.jshintrc index 85b889e..dc3cc52 100644 --- a/.jshintrc +++ b/.jshintrc @@ -27,6 +27,7 @@ "supernew": true, // suppress warnings about "weird" object constructions "trailing": true, // disallow trailing whitespace "undef" : true, // prohibits the use of undefined variables - "unused": "vars" // complain about unused variables but not arguments + "unused": "vars", // complain about unused variables but not arguments + "jquery": true // "white": true // enforce Crockford rules } diff --git a/Hooks.php b/Hooks.php index 695c5e5..c2ac9f3 100644 --- a/Hooks.php +++ b/Hooks.php @@ -999,7 +999,6 @@ } } } - return true; } diff --git a/Resources.php b/Resources.php index 6b8aa95..b49423d 100644 --- a/Resources.php +++ b/Resources.php @@ -349,6 +349,22 @@ 'mediawiki.Uri', ), ) + $mobile, + 'ext.flow.dm' => $flowResourceTemplate + array( + 'scripts' => array( // Component order is important + 'flow/mw.flow.js', + 'flow/dm/mw.flow.dm.js', + 'flow/dm/mw.flow.dm.Item.js', + 'flow/dm/mixins/mw.flow.dm.List.js', + 'flow/dm/api/mw.flow.dm.APIResultsProvider.js', + 'flow/dm/api/mw.flow.dm.APIResultsQueue.js', + 'flow/dm/api/mw.flow.dm.APITopicsProvider.js', + 'flow/dm/mw.flow.dm.Topic.js', + 'flow/dm/mw.flow.dm.Board.js', + ), + 'dependencies' => array( + 'oojs' + ) + ) + $mobile, 'ext.flow' => $flowResourceTemplate + array( 'scripts' => array( // Component order is important // MW UI @@ -385,6 +401,7 @@ 'jquery.throttle-debounce', 'mediawiki.jqueryMsg', 'ext.flow.jquery.conditionalScroll', + 'ext.flow.dm', 'mediawiki.api', 'mediawiki.util', 'mediawiki.api.options', // required by switch-editor feature diff --git a/modules/flow-initialize.js b/modules/flow-initialize.js index 279a1e3..dafca71 100644 --- a/modules/flow-initialize.js +++ b/modules/flow-initialize.js @@ -10,5 +10,8 @@ */ $( document ).ready( function () { mw.flow.initComponent( $( '.flow-component' ) ); + + // Load data model + mw.flow.Initialize( $( '.flow-component' ) ); } ); }( jQuery ) ); diff --git a/modules/flow/dm/api/mw.flow.dm.APIResultsProvider.js b/modules/flow/dm/api/mw.flow.dm.APIResultsProvider.js new file mode 100644 index 0000000..b6729e2 --- /dev/null +++ b/modules/flow/dm/api/mw.flow.dm.APIResultsProvider.js @@ -0,0 +1,234 @@ +/** + * Resource Provider object. + * + * @class + * @mixins OO.EventEmitter + * + * @constructor + * @abstract + * @param {string} apiurl The URL to the api + * @param {Object} [config] Configuration options + * @cfg {number} fetchLimit The default number of results to fetch + * @cfg {string} lang The language of the API + * @cfg {number} offset Initial offset, if relevant, to call results from + * @cfg {Object} ajaxSettings The settings for the ajax call + * @cfg {Object} staticParams The data parameters that are static and should + * always be sent to the API request, as opposed to user parameters. + * @cfg {Object} userParams Initial user parameters to be sent as data to + * the API request. These can change per request, like the search query term + * or sizing parameters for images, etc. + */ +mw.flow.dm.APIResultsProvider = function VeDmResourceProvider( apiurl, config ) { + config = config || {}; + + this.setAPIurl( apiurl ); + this.fetchLimit = config.fetchLimit || 30; + this.lang = config.lang; + this.offset = config.offset || 0; + this.ajaxSettings = config.ajaxSettings || {}; + + this.staticParams = config.staticParams || {}; + this.userParams = config.userParams || {}; + + this.toggleDepleted( false ); + + // Mixin constructors + OO.EventEmitter.call( this ); +}; + +/* Setup */ +OO.mixinClass( mw.flow.dm.APIResultsProvider, OO.EventEmitter ); + +/* Methods */ + + +/** + * Get results from the source + * + * @param {number} howMany Number of results to ask for + * @return {jQuery.Promise} Promise that is resolved into an array + * of available results, or is rejected if no results are available. + */ +mw.flow.dm.APIResultsProvider.prototype.getResults = function ( howMany ) { + var xhr, + aborted = false, + provider = this; + + return this.fetchAPIresults( howMany ) + .then( + function ( results ) { + if ( !results || results.length === 0 ) { + provider.toggleDepleted( true ); + return []; + } + return results; + }, + // Process failed, return an empty promise + function () { + provider.toggleDepleted( true ); + return $.Deferred().resolve( [] ); + } + ) + .promise( { abort: function () { + aborted = true; + if ( xhr ) { + xhr.abort(); + } + } } ); +}; + +/** + * Call the API for search results. + * + * @abstract + * @param {number} [howMany] The number of results to retrieve + * @return {jQuery.Promise} Promise that resolves with an array of objects that contain + * the fetched data. + */ +mw.flow.dm.APIResultsProvider.prototype.fetchAPIresults = function ( howMany ) { + return $.Deferred().resolve().promise( { abort: $.noop } ); +}; + +/** + * Set API url + * + * @param {string} apiurl API url + */ +mw.flow.dm.APIResultsProvider.prototype.setAPIurl = function ( apiurl ) { + this.apiurl = apiurl; +}; + +/** + * Set api url + * + * @returns {string} API url + */ +mw.flow.dm.APIResultsProvider.prototype.getAPIurl = function () { + return this.apiurl; +}; + +/** + * Get the static, non-changing data parameters sent to the API + * + * @returns {Object} Data parameters + */ +mw.flow.dm.APIResultsProvider.prototype.getStaticParams = function () { + return this.staticParams; +}; + +/** + * Get the user-inputted dybamic data parameters sent to the API + * + * @returns {Object} Data parameters + */ +mw.flow.dm.APIResultsProvider.prototype.getUserParams = function () { + return this.userParams; +}; + +/** + * Set the data parameters sent to the API + * + * @param {Object} params User defined data parameters + */ +mw.flow.dm.APIResultsProvider.prototype.setUserParams = function ( params ) { + // Assymetrically compare (params is subset of this.userParams) + if ( !ve.compare( params, this.userParams, true ) ) { + this.userParams = $.extend( {}, this.userParams, params ); + // Reset offset + this.setOffset( 0 ); + // Reset depleted status + this.toggleDepleted( false ); + } +}; + +/** + * Get fetch limit or 'page' size. This is the number + * of results per request. + * + * @returns {number} limit + */ +mw.flow.dm.APIResultsProvider.prototype.getDefaultFetchLimit = function () { + return this.limit; +}; + +/** + * Set limit + * + * @param {number} limit Default number of results to fetch from the API + */ +mw.flow.dm.APIResultsProvider.prototype.setDefaultFetchLimit = function ( limit ) { + this.limit = limit; +}; + +/** + * Get provider API language + * + * @returns {string} Provider API language + */ +mw.flow.dm.APIResultsProvider.prototype.getLang = function () { + return this.lang; +}; + +/** + * Set provider API language + * + * @param {string} lang Provider API language + */ +mw.flow.dm.APIResultsProvider.prototype.setLang = function ( lang ) { + this.lang = lang; +}; + +/** + * Get result offset + * + * @returns {number} Offset Results offset for the upcoming request + */ +mw.flow.dm.APIResultsProvider.prototype.getOffset = function () { + return this.offset; +}; + +/** + * Set result offset + * + * @param {number} Results offset for the upcoming request + */ +mw.flow.dm.APIResultsProvider.prototype.setOffset = function ( offset ) { + this.offset = offset; +}; + +/** + * Check whether the provider is depleted and has no more results + * to hand off. + * + * @returns {boolean} The provider is depleted + */ +mw.flow.dm.APIResultsProvider.prototype.isDepleted = function () { + return this.depleted; +}; + +/** + * Toggle depleted state + * + * @param {boolean} isDepleted The provider is depleted + */ +mw.flow.dm.APIResultsProvider.prototype.toggleDepleted = function ( isDepleted ) { + this.depleted = isDepleted !== undefined ? isDepleted : !this.depleted; +}; + +/** + * Get the default ajax settings + * + * @returns {Object} Ajax settings + */ +mw.flow.dm.APIResultsProvider.prototype.getAjaxSettings = function () { + return this.ajaxSettings; +}; + +/** + * Get the default ajax settings + * + * @param {Object} settings Ajax settings + */ +mw.flow.dm.APIResultsProvider.prototype.setAjaxSettings = function ( settings ) { + this.ajaxSettings = settings; +}; diff --git a/modules/flow/dm/api/mw.flow.dm.APIResultsQueue.js b/modules/flow/dm/api/mw.flow.dm.APIResultsQueue.js new file mode 100644 index 0000000..a21c2b8 --- /dev/null +++ b/modules/flow/dm/api/mw.flow.dm.APIResultsQueue.js @@ -0,0 +1,198 @@ +/** + * Resource Queue object. + * + * @class + * @mixins OO.EventEmitter + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {number} limit The default number of results to fetch + * @cfg {number} threshold The default number of extra results + * that the queue should always strive to have on top of the + * individual requests for items. + */ +mw.flow.dm.APIResultsQueue = function VeDmResourceQueue( config ) { + config = config || {}; + + this.fileRepoPromise = null; + this.providers = []; + this.providerPromises = []; + this.queue = []; + + this.params = {}; + + this.limit = config.limit || 20; + this.setThreshold( config.threshold || 10 ); + + // Mixin constructors + OO.EventEmitter.call( this ); +}; + +/* Setup */ +OO.mixinClass( mw.flow.dm.APIResultsQueue, OO.EventEmitter ); + +/* Methods */ + +/** + * Set up the queue and its resources. + * This should be overrided if there are any setup steps to perform. + * + * @return {jQuery.Promise} Promise that resolves when the resources + * are set up. Note: The promise must have an .abort() functionality. + */ +mw.flow.dm.APIResultsQueue.prototype.setup = function () { + return $.Deferred().resolve().promise( { abort: $.noop } ); +}; + +/** + * Get items from the queue + * + * @param {number} [howMany] How many items to retrieve. Defaults to the + * default limit supplied on initialization. + * @return {jQuery.Promise} Promise that resolves into an array of items. + */ +mw.flow.dm.APIResultsQueue.prototype.get = function ( howMany ) { + var fetchingPromise = null, + me = this; + + howMany = howMany || this.limit; + + // Check if the queue has enough items + if ( this.queue.length < howMany + this.threshold ) { + // Call for more results + fetchingPromise = this.queryProviders( howMany + this.threshold ) + .then( function ( items ) { + // Add to the queue + me.queue = me.queue.concat.apply( me.queue, items ); + } ); + } + + return $.when( fetchingPromise ) + .then( function () { + return me.queue.splice( 0, howMany ); + } ); + +}; + +/** + * Get results from all providers + * + * @param {number} [howMany] How many items to retrieve. Defaults to the + * default limit supplied on initialization. + * @return {jQuery.Promise} Promise that is resolved into an array + * of fetched items. Note: The promise must have an .abort() functionality. + */ +mw.flow.dm.APIResultsQueue.prototype.queryProviders = function ( howMany ) { + var i, len, + queue = this; + + // Make sure there are resources set up + return this.setup() + .then( function () { + // Abort previous requests + for ( i = 0, len = queue.providerPromises.length; i < len; i++ ) { + queue.providerPromises[i].abort(); + } + queue.providerPromises = []; + // Set up the query to all providers + for ( i = 0, len = queue.providers.length; i < len; i++ ) { + if ( !queue.providers[i].isDepleted() ) { + queue.providerPromises.push( + queue.providers[i].getResults( howMany ) + ); + } + } + + return $.when.apply( $, queue.providerPromises ) + .then( Array.prototype.concat.bind( [] ) ); + } ); +}; + +/** + * Set the search query for all the providers. + * + * This also makes sure to abort any previous promises. + * + * @param {Object} params API search parameters + */ +mw.flow.dm.APIResultsQueue.prototype.setParams = function ( params ) { + var i, len; + if ( !ve.compare( params, this.params, true ) ) { + this.params = ve.extendObject( this.params, params ); + // Reset queue + this.queue = []; + // Reset promises + for ( i = 0, len = this.providerPromises.length; i < len; i++ ) { + this.providerPromises[i].abort(); + } + // Change queries + for ( i = 0, len = this.providers.length; i < len; i++ ) { + this.providers[i].setUserParams( this.params ); + } + } +}; + +/** + * Get the data parameters sent to the API + * + * @returns {Object} params API search parameters + */ +mw.flow.dm.APIResultsQueue.prototype.getParams = function () { + return this.params; +}; + +/** + * Set the providers + * + * @param {ve.dm.APIResultsProvider[]} providers An array of providers + */ +mw.flow.dm.APIResultsQueue.prototype.setProviders = function ( providers ) { + this.providers = providers; +}; + +/** + * Add a provbider to the group + * + * @param {ve.dm.APIResultsProvider} provider A provider object + */ +mw.flow.dm.APIResultsQueue.prototype.addProvider = function ( provider ) { + this.providers.push( provider ); +}; + +/** + * Set the providers + * + * @returns {ve.dm.APIResultsProvider[]} providers An array of providers + */ +mw.flow.dm.APIResultsQueue.prototype.getProviders = function () { + return this.providers; +}; + +/** + * Get the queue size + * + * @return {number} Queue size + */ +mw.flow.dm.APIResultsQueue.prototype.getQueueSize = function () { + return this.queue.length; +}; + +/** + * Set queue threshold + * + * @param {number} threshold Queue threshold, below which we will + * request more items + */ +mw.flow.dm.APIResultsQueue.prototype.setThreshold = function ( threshold ) { + this.threshold = threshold; +}; + +/** + * Get queue threshold + * + * @returns {number} threshold Queue threshold, below which we will + * request more items + */ +mw.flow.dm.APIResultsQueue.prototype.getThreshold = function () { + return this.threshold; +}; diff --git a/modules/flow/dm/api/mw.flow.dm.APITopicsProvider.js b/modules/flow/dm/api/mw.flow.dm.APITopicsProvider.js new file mode 100644 index 0000000..743e793 --- /dev/null +++ b/modules/flow/dm/api/mw.flow.dm.APITopicsProvider.js @@ -0,0 +1,85 @@ +/** + * Flow posts resource provider. + * + * @class + * @extends mw.flow.dm.APIResultsProvider + * + * @constructor + * @param {string} page The page associated with the topics + * @param {Object} [config] Configuration options + * @cfg {string} [scriptDirUrl] The url of the API script + */ +mw.flow.dm.APITopicsProvider = function MwFlowDmAPITopicsProvider( page, config ) { + config = config || {}; + + this.page = page; + + // Parent constructor + mw.flow.dm.APITopicsProvider.super.call( + this, + mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php', + $.extend( { + staticParams: { + action: 'flow', + submodule: 'view-topiclist', + page: page + } + }, config ) + ); +}; + +/* Inheritance */ +OO.inheritClass( mw.flow.dm.APITopicsProvider, mw.flow.dm.APIResultsProvider ); + +/* Methods */ + +/** + * Call the API for search results. + * + * @param {number} [howMany] The number of results to retrieve + * @return {jQuery.Promise} Promise that resolves with an array of objects that contain + * the fetched data. + */ +mw.flow.dm.APITopicsProvider.prototype.fetchAPIresults = function ( howMany ) { + var xhr, apiCallConfig, + provider = this; + + howMany = howMany || this.getDefaultFetchLimit(); + + apiCallConfig = $.extend( + {}, + this.getUserParams(), + { + vtloffset: this.getOffset(), + vtllimit: howMany + } ); + + xhr = new mw.Api().get( $.extend( this.getStaticParams(), apiCallConfig ), this.getAjaxSettings() ); + return xhr + .then( function ( data ) { + var i, len, post, + result = {}, + topiclist = OO.getProp( data.flow, 'view-topiclist', 'result', 'topiclist' ); + + if ( data.error ) { + provider.toggleDepleted( true ); + return []; + } + + // If there are no more posts, mark the board to prevent redundant api calls + if ( Object.keys( topiclist.roots ).length < howMany ) { + provider.toggleDepleted( true ); + } else { + provider.setOffset( provider.getOffset() + howMany ); + } + + // Connect the information to the topic + for ( i = 0, len = topiclist.roots.length; i < len; i++ ) { + post = topiclist.posts[ topiclist.roots[i] ]; + result[ topiclist.roots[i] ] = topiclist.revisions[ post ]; + } + + return result; + } ) + .promise( { abort: xhr.abort } ); +}; diff --git a/modules/flow/dm/mixins/mw.flow.dm.List.js b/modules/flow/dm/mixins/mw.flow.dm.List.js new file mode 100644 index 0000000..06af3f2 --- /dev/null +++ b/modules/flow/dm/mixins/mw.flow.dm.List.js @@ -0,0 +1,110 @@ +/** + * Flow List mixin + * Must be mixed into an mw.flow.dm.Item element + * + * @mixin + * @abstract + * @constructor + * @param {Object} config Configuration options + */ +mw.flow.dm.List = function mwFlowDmList( config ) { + // Configuration initialization + config = config || {}; + + this.items = []; +}; + +/* Events */ + +/** + * @event add Items have been added + * @param {flow.dm.Item[]} items Added items + * @param {number} index Index items were added at + */ + +/** + * @event remove Items have been removed + * @param {flow.dm.Item[]} items Removed items + */ + +/* Methods */ + +/** + * Get all items + * + * @return {mw.flow.dm.Item[]} Items in the list + */ +mw.flow.dm.List.prototype.getItems = function () { + return this.items.slice( 0 ); +}; + +/** + * Get number of items + * + * @return {number} Number of items in the list + */ +mw.flow.dm.List.prototype.getItemCount = function () { + return this.items.length; +}; + +/** + * Add items + * + * @param {mw.flow.dm.Item[]} items Items to add + * @param {number} index Index to add items at + * @chainable + */ +mw.flow.dm.List.prototype.addItems = function ( items, index ) { + var i, at, len, item, currentIndex; + + // Support adding existing items at new locations + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[i]; + // Check if item exists then remove it first, effectively "moving" it + currentIndex = this.items.indexOf( item ); + if ( currentIndex >= 0 ) { + this.removeItems( [ item ] ); + // Adjust index to compensate for removal + if ( currentIndex < index ) { + index--; + } + } + } + + if ( index === undefined || index < 0 || index >= this.items.length ) { + at = this.items.length; + this.items.push.apply( this.items, items ); + } else if ( index === 0 ) { + at = 0; + this.items.unshift.apply( this.items, items ); + } else { + at = index; + this.items.splice.apply( this.items, [ index, 0 ].concat( items ) ); + } + this.emit( 'add', items, at ); + + return this; +}; + +/** + * Remove items + * + * @param {mw.flow.dm.Item[]} items Items to remove + * @chainable + */ +mw.flow.dm.List.prototype.removeItems = function ( items ) { + var i, len, item, index, + removed = []; + + // Remove specific items 101 + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[i]; + index = this.items.indexOf( item ); + if ( index !== -1 ) { + removed = removed.concat( this.items.splice( index, 1 ) ); + } + } + this.emit( 'remove', removed ); + + return this; +}; diff --git a/modules/flow/dm/mw.flow.dm.Board.js b/modules/flow/dm/mw.flow.dm.Board.js new file mode 100644 index 0000000..a3c5d69 --- /dev/null +++ b/modules/flow/dm/mw.flow.dm.Board.js @@ -0,0 +1,141 @@ +/** + * Flow Board + * + * @constructor + * + * @extends mw.flow.dm.Item + * @mixins mw.flow.dm.List + * + * @param {Object} data API data to build board with + * @param {string} data.id Board Id + * @param {mw.Title} data.pageTitle Current page title + * @param {string} [data.highlight] An Id of a highlighted post, if exists + * @param {Object} [config] Configuration options + * @cfg {number} [tocPostsLimit] Post count for the table of contents. This is also + * used as the number of posts to fetch when more posts are requested for the table + * of contents. Defaults to 10 + * @cfg {number} [visibleItems] Number of visible posts on the page. This is also + * used as the number of posts to fetch when more posts are requested on scrolling + * the page. Defaults to the tocPostsLimit + */ +mw.flow.dm.Board = function mwFlowDmBoard( data, config ) { + config = config || {}; + + // Parent constructor + mw.flow.dm.Board.super.call( this, config ); + + // Mixing constructor + mw.flow.dm.List.call( this, config ); + + // TODO: Fill this stuff in properly + this.id = data.id; + this.pageTitle = data.pageTitle; + this.highlight = data.highlight || null; + + // Configuration options + this.tocPostsLimit = config.tocPostsLimit || 10; + this.visibleItems = config.visibleItems || this.tocPostsLimit; + + // Set up the API queue + this.queue = new mw.flow.dm.APIResultsQueue( { + limit: this.tocPostsLimit, + threshhold: 5 + } ); + this.queue.addProvider( + new mw.flow.dm.APITopicsProvider( + this.getPageTitle().getPrefixedDb(), + { + fetchLimit: this.tocPostsLimit + } + ) + ); +}; + +/* Initialization */ + +OO.inheritClass( mw.flow.dm.Board, mw.flow.dm.Item ); +OO.mixinClass( mw.flow.dm.Board, mw.flow.dm.List ); + +/** + * Initialize the board and fetch topics + * + * TODO: We should at some point replace this to first go over the + * available DOM and get the initial topics from there, saving an + * API call. + * + * @return {jQuery.Promise} Promise that resolves when the board finished initializing + */ +mw.flow.dm.Board.prototype.initialize = function () { + return this.fetchTopics(); +}; +/** + * Fetch topics from the API queue + * + * @return {jQuery.Promise} Promise that resolves when the topics finished + * populating + */ +mw.flow.dm.Board.prototype.fetchTopics = function ( numVisibleTopics ) { + numVisibleTopics = numVisibleTopics === undefined ? this.tocPostsLimit : numVisibleTopics; + + return this.queue.get() + .then( this.addTopicsFromApi.bind( this, numVisibleTopics ) ); +}; + +/** + * Add topics from API response + * + * @param {number} numVisibleTopics Number of visible topics; those topics will be + * fetched so we have all the information to display. The rest will only receive + * basic information that we need to display in the ToC, and they will be considered + * 'stubs' until their information is fetched as well. + * @param {Object} result API result + * @return {jQuery.Promise} Promise that resolves when all visible topics have finished + * fetching the data they require to be displayed. + */ +mw.flow.dm.Board.prototype.addTopicsFromApi = function ( numVisibleTopics, result ) { + var topicId, topic, + count = 0, + promises = [], + topics = [], + topicList = result[ 0 ]; + + for ( topicId in topicList ) { + topic = new mw.flow.dm.Topic( topicId, topicList[topicId] ); + if ( count < numVisibleTopics ) { + // Get full information for the topic, since + // it is visible + promises.push( topic.fetchInformation() ); + } + count++; + topics.push( topic ); + } + // Add topics + this.addItems( topics ); + + return $.when.apply( this, promises ); +}; + +/** + * Get board UUID + * @return {string} UUID + */ +mw.flow.dm.Board.prototype.getId = function () { + return this.id; +}; + +/** + * Get page title + * @return {mw.Title} Page title + */ +mw.flow.dm.Board.prototype.getPageTitle = function () { + return this.pageTitle; +}; + +/** + * Get the maximum post limit to fetch for the ToC + * + * @return {number} Fetching limit + */ +mw.flow.dm.Board.prototype.getToCPostLimit = function () { + return this.tocPostsLimit; +}; diff --git a/modules/flow/dm/mw.flow.dm.Item.js b/modules/flow/dm/mw.flow.dm.Item.js new file mode 100644 index 0000000..c6f5662 --- /dev/null +++ b/modules/flow/dm/mw.flow.dm.Item.js @@ -0,0 +1,14 @@ +/** + * Flow Item + * + * @abstract + * @mixins OO.EventEmitter + */ +mw.flow.dm.Item = function mwFlowDmItem() { + // Mixin constructor + OO.EventEmitter.call( this ); +}; + +/* Inheritance */ + +OO.mixinClass( mw.flow.dm.Item, OO.EventEmitter ); diff --git a/modules/flow/dm/mw.flow.dm.Topic.js b/modules/flow/dm/mw.flow.dm.Topic.js new file mode 100644 index 0000000..8905300 --- /dev/null +++ b/modules/flow/dm/mw.flow.dm.Topic.js @@ -0,0 +1,308 @@ +/** + * Flow Topic + * + * @constructor + * + * @extends mw.flow.dm.Item + * @mixins mw.flow.dm.List + * + * @param {string} id Topic Id + * @param {Object} data API data to build topic with + * @param {Object} [config] Configuration options + */ +mw.flow.dm.Topic = function mwFlowDmTopic( id, data, config ) { + config = config || {}; + + // Parent constructor + mw.flow.dm.Topic.super.call( this, config ); + + // Mixing constructor + mw.flow.dm.List.call( this, config ); + + this.id = id; + this.content = data.content; + this.timestamp = data.timestamp; + this.articleTitle = data.articleTitle; + this.author = data.author; + this.creator = data.creator; + + this.summary = data.summary && data.summary.revision && data.summary.revision.content; + + this.actions = data.actions; + + // I assume this is per post, and not so relevant for a topic + // as a whole? + // this.maxThreadingDepth = !!data.isMaxThreadingDepth; + + this.moderated = !!data.isModerated; + this.moderationReason = data.moderateReason; + this.moderationState = data.moderateState; + this.moderator = data.moderator; + + // What is this? +// this.isOriginalContent = !!data.isOriginalContent; + + this.watched = !!data.isWatched; + this.lastUpdate = data.last_updated; + this.watchable = data.watchable; + + // TODO: These should be added as Reply objects + this.replies = data.replies; + + + // Configuration + this.highlighted = !!config.highlighted; + this.stub = true; +}; + +/* Initialization */ + +OO.inheritClass( mw.flow.dm.Topic, mw.flow.dm.Item ); +OO.mixinClass( mw.flow.dm.Topic, mw.flow.dm.List ); + +/** + * Fetch information about this topic from the API + * and populate it. + * @return {jQuery.Promise} Promise that resolves when all the + * information is ready and available + */ +mw.flow.dm.Topic.prototype.fetchInformation = function () { + this.unStub(); + return $.Deferred().resolve(); +}; + +/** + * Get topic id + */ +mw.flow.dm.Topic.prototype.getId = function () { + return this.id; +}; + +/** + * Check if a topic is a stub + * @return {Boolean} Topic is a stub + */ +mw.flow.dm.Topic.prototype.isStub = function () { + return this.stub; +}; + +/** + * Unstub a topic when all available information exists on it + */ +mw.flow.dm.Topic.prototype.unStub = function () { + this.stub = false; +}; + +/** + * Get topic content + * + * @return {string} Topic content + */ +mw.flow.dm.Topic.prototype.getContent = function () { + return this.content; +}; + +/** + * Get topic raw content + * + * @return {string} Topic raw content + */ +mw.flow.dm.Topic.prototype.getRawContent = function () { + return this.content.content; +}; + +/** + * Get topic timestamp + * @return {number} Topic timestamp + */ +mw.flow.dm.Topic.prototype.getTimestamp = function () { + return this.timestamp; +}; + +/** + * Get topic article title + * @return {string} Article title + */ +mw.flow.dm.Topic.prototype.getArticleTitle = function () { + return this.articleTitle; +}; + +/** + * Get topic author + * + * @return {string} Topic author + */ +mw.flow.dm.Topic.prototype.getAuthor = function () { + return this.author; +}; + +/** + * Get topic creator + * + * @return {string} Topic creator + */ +mw.flow.dm.Topic.prototype.getCreator = function () { + return this.creator; +}; + +/** + * Check if topic is moderated + * @return {boolean} Topic is moderated + */ +mw.flow.dm.Topic.prototype.isModerated = function () { + return this.moderated; +}; + +/** + * Toggle the moderated state of a topic + * @param {boolean} [moderate] Moderate the topic + * @fires moderated + */ +mw.flow.dm.Topic.prototype.toggleModerated = function ( moderate ) { + this.moderated = moderate || !this.moderated; + if ( !this.moderated ) { + this.setModerationReason( '' ); + } + this.emit( 'moderated', this.moderated ); +}; + +/** + * Get topic moderation reason + * + * @return {string} Moderation reason + */ +mw.flow.dm.Topic.prototype.getModerationReason = function () { + return this.moderationReason; +}; + +/** + * Set topic moderation reason + * + * @return {string} Moderation reason + */ +mw.flow.dm.Topic.prototype.setModerationReason = function ( reason ) { + this.moderationReason = reason; +}; + +/** + * Get topic moderation state + * + * @return {string} Moderation state + */ +mw.flow.dm.Topic.prototype.getModerationState = function () { + return this.moderationState; +}; + +/** + * Set topic moderation state + * + * @return {string} Moderation state + */ +mw.flow.dm.Topic.prototype.setModerationReason = function ( state ) { + this.moderationState = state; +}; + +/** + * Get topic moderator + * + * @return {mw.User} Moderator + */ +mw.flow.dm.Topic.prototype.getModerator = function () { + return this.moderator; +}; + +/** + * Get topic moderator + * + * @param {mw.User} mod Moderator + */ +mw.flow.dm.Topic.prototype.setModerator = function ( mod ) { + this.moderator = mod; +}; + +/** + * Check topic watched status + * + * @return {boolean} Topic is watched + */ +mw.flow.dm.Topic.prototype.isWatched = function () { + return this.watched; +}; + +/** + * Toggle the watched state of a topic + * @param {boolean} [watch] Watch the topic + * @fires watched + */ +mw.flow.dm.Topic.prototype.toggleWatched = function ( watch ) { + this.watched = watch || !this.watched; + + this.emit( 'watched', this.moderated ); +}; + +/** + * Check topic watchable status + * + * @return {boolean} Topic is watchable + */ +mw.flow.dm.Topic.prototype.isWatchable = function () { + return this.watchable; +}; + +/** + * Toggle the watchable state of a topic + * @param {boolean} [watchable] Topic is watchable + * @fires watchable + */ +mw.flow.dm.Topic.prototype.toggleWatchable = function ( watchable ) { + this.watchable = watchable || !this.watchable; + + this.emit( 'watchable', this.watchable ); +}; + +/** + * Get topic last update + * + * @return {string} Topic last update + */ +mw.flow.dm.Topic.prototype.getLastUpdate = function () { + return this.lastUpdate; +}; + +/** + * Set topic last update + * + * @param {string} lastUpdate Topic last update + */ +mw.flow.dm.Topic.prototype.setLastUpdate = function ( lastUpdate ) { + this.lastUpdate = lastUpdate; +}; + +/** + * Get the topic summary + * + * @return {string} Topic summary + */ +mw.flow.dm.Topic.prototype.getSummary = function () { + return this.summary; +}; + +/** + * Get the topic summary + * + * @param {string} Topic summary + * @fires summary + */ +mw.flow.dm.Topic.prototype.setSummary = function ( summary ) { + this.summary = summary; + this.emit( 'summary', this.summary ); +}; + +/** + * Get the topic action + * + * @return {object} Topic actions + */ +mw.flow.dm.Topic.prototype.getActions = function () { + return this.actions; +}; diff --git a/modules/flow/dm/mw.flow.dm.js b/modules/flow/dm/mw.flow.dm.js new file mode 100644 index 0000000..c482a37 --- /dev/null +++ b/modules/flow/dm/mw.flow.dm.js @@ -0,0 +1,2 @@ +mw.flow = mw.flow || {}; +mw.flow.dm = {}; diff --git a/modules/flow/mw.flow.js b/modules/flow/mw.flow.js new file mode 100644 index 0000000..b3e3996 --- /dev/null +++ b/modules/flow/mw.flow.js @@ -0,0 +1,50 @@ +mw.flow = mw.flow || {}; + +mw.flow.totalInstanceCount = 0; +/** + * Initialize Flow components into the data model + * + * @param {jQuery} [$container] Flow board container. + */ +mw.flow.Initialize = function mwFlowInitialize( $container ) { + var board, boardId, +// uri = new mw.Uri( window.location.href ), + uid = String( window.location.hash.match( /[0-9a-z]{16,19}$/i ) || '' ); + + $container = $container || $( '.flow-component' ); + + boardId = $container.data( 'flowId' ); + if ( !boardId ) { + // We probably don't need this for a Flow Board, as there is + // only one and it always exists where it should, even when it's + // empty. + + // Generate an ID for this component + boardId = 'flow-generated-' + mw.flow.totalInstanceCount++; + $container.data( 'flowId', boardId ); + } + + // TODO: Migrate the logic (this should be in the ui widget + // but the logic of whether we need to see the newer posts + // or only the highlighted post should probably be here) + + // if ( uri.query.fromnotif ) { + // _flowHighlightPost( $container, uid, 'newer' ); + // } else { + // _flowHighlightPost( $container, uid ); + // } + board = new mw.flow.dm.Board( { + id: boardId, + pageTitle: mw.Title.newFromText( mw.config.get( 'wgPageName' ) ) || '', + tocPostsLimit: 20, + visibleItems: 10, + // Handle URL parameters + highlight: uid || null + } ); + // Initialize + board.initialize() + .then( function () { + // Debugging + console.log( board.getItems() ); + } ); +}; -- To view, visit https://gerrit.wikimedia.org/r/209166 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Iba245037078f548edd2dc10519e8f2c0c650cb00 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/Flow 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