%PDF- %PDF-
| Direktori : /www/varak.net/wiki.varak.net/extensions/MobileFrontend/src/mobile.startup/ |
| Current File : //www/varak.net/wiki.varak.net/extensions/MobileFrontend/src/mobile.startup/Page.js |
var
HTML = mw.html,
mfExtend = require( './mfExtend' ),
time = require( './time' ),
util = require( './util' ),
Section = require( './Section' ),
Thumbnail = require( './Thumbnail' ),
View = require( './View' ),
HEADING_SELECTOR = mw.config.get( 'wgMFMobileFormatterHeadings' ).join( ',' ),
BLACKLISTED_THUMBNAIL_CLASS_SELECTORS = [ 'noviewer', 'metadata' ];
/**
* Mobile page view object
*
* @class Page
* @uses Section
* @extends View
*
* @param {Object} options Configuration options
*/
function Page( options ) {
var thumb;
// If thumbnail is not passed it should be made false (truthy) so that it
// renders a placeholder when absent.
if ( options.thumbnail === undefined ) {
options.thumbnail = false;
}
this.options = options;
options.languageUrl = mw.util.getUrl( 'Special:MobileLanguages/' + options.title );
View.call( this, options );
// Fallback if no displayTitle provided
options.displayTitle = this.getDisplayTitle();
// allow usage in templates.
// FIXME: Should View map all options to properties?
this.title = options.title;
this.displayTitle = options.displayTitle;
this.thumbnail = options.thumbnail;
this.url = options.url || mw.util.getUrl( options.title );
this.id = options.id;
this.isMissing = options.isMissing !== undefined ? options.isMissing : options.id === 0;
thumb = this.thumbnail;
if ( thumb && thumb.width ) {
this.thumbnail.isLandscape = thumb.width > thumb.height;
}
this.wikidataDescription = options.wikidataDescription;
}
mfExtend( Page, View, {
/**
* @memberof Page
* @instance
* @mixes View#defaults
* @property {Object} defaults Default options hash.
* @property {number} defaults.id Page ID. The default value of 0 represents a
* new or missing page. Be sure to override it to avoid side effects.
* @property {string} defaults.title Title of the page. It includes prefix where needed and
* is human readable, e.g. Talk:The man who lived.
* @property {string} defaults.displayTitle HTML title of the page for display. Falls back
* to defaults.title (escaped) if no value is provided. Must be safe HTML!
* @property {number} defaults.namespaceNumber the number of the
* namespace the page belongs to
* @property {Object} defaults.protection List of permissions as returned by API,
* e.g. [{ edit: ['*'] }]
* @property {Array} defaults.sections Array of {Section} objects.
* @property {boolean} defaults.isMainPage Whether the page is the Main Page.
* @property {boolean} defaults.isMissing Whether the page exists in the wiki.
* @property {Object} defaults.thumbnail thumbnail definition corresponding to page image
* @property {boolean} defaults.thumbnail.isLandscape whether the image is in
* landscape format
* @property {number} defaults.thumbnail.width of image in pixels.
* @property {number} defaults.thumbnail.height of image in pixels.
* @property {string} defaults.thumbnail.source url for image
*/
defaults: {
id: 0,
title: '',
displayTitle: '',
namespaceNumber: 0,
protection: {
edit: [ '*' ]
},
sections: [],
isMissing: false,
isMainPage: false,
url: undefined,
thumbnail: {
isLandscape: undefined,
source: undefined,
width: undefined,
height: undefined
}
},
/**
* @inheritdoc
* @memberof Page
* @instance
*/
isBorderBox: false,
/**
* Retrieve the title that should be displayed to the user
* @memberof Page
* @instance
* @return {string} HTML
*/
getDisplayTitle: function () {
return this.options.displayTitle || HTML.escape( this.options.title );
},
/**
* Determine if current page is in a specified namespace
* @memberof Page
* @instance
* @param {string} namespace Name of namespace
* @return {boolean}
*/
inNamespace: function ( namespace ) {
return this.options.namespaceNumber === mw.config.get( 'wgNamespaceIds' )[namespace];
},
/**
* Find the heading in the page.
* This has the benefit of excluding any additional h2s and h3s that may
* have been added programatically.
* @method
* @param {number} sectionIndex as defined by the PHP parser.
* It should correspond to the section id
* used in the edit link for the section.
* Note, confusingly, this is different from section "ID" which is
* used in methods
* @return {jQuery.Object}
*/
findSectionHeadingByIndex: function ( sectionIndex ) {
if ( sectionIndex < 1 ) {
// negative indexes will search from the end, which is behaviour we do not want.
// return an empty set when this happens.
return this.$();
} else {
return this.$( HEADING_SELECTOR )
// Headings must strictly be a child element of a section element
// or the parser-output.
// Not an ancestor!
.filter( '.mw-parser-output > *, [class^="mf-section-"] > *' ).eq( sectionIndex - 1 );
}
},
/**
* Finds all child elements that match the selector in a given section or subsection.
* Returns any direct child elements that match the selector,
* (i.e. searches only one level deep)
* as well as any elements that match the selector within those children.
* If the Page has no headings (e.g. a stub),
* then the search will target all nodes within the page.
*
* This code should work on desktop (PHP parser HTML)
* as well as mobile formatted HTML (PHP parser + MobileFormatter)
* @method
* @param {number} sectionIndex as defined by the PHP parser. It should correspond to
* the section id used in the edit link for the section.
* Note, confusingly, this is different from section "ID" which is
* used in methods
* @param {string} selector to match
* @return {jQuery.Object}
*/
findChildInSectionLead: function ( sectionIndex, selector ) {
var $heading, $nextHeading, $container, $lead,
headingSelector = HEADING_SELECTOR;
function withNestedChildren( $matchingNodes ) {
return $matchingNodes.find( selector ).addBack();
}
if ( sectionIndex === 0 ) {
// lead is easy
$lead = this.getLeadSectionElement();
if ( $lead && $lead.length ) {
return withNestedChildren( $lead.children( selector ) );
} else {
$heading = this.findSectionHeadingByIndex( 1 );
return $heading.length ? withNestedChildren( $heading.prevAll( selector ) ) :
// this page is a stub so search entire page
this.$( selector );
}
}
// find heading associated with the section by looking at its
// index position in the article
// section ids relate to the element position in the page and the first heading
// lead has been dealt with above, so first heading corresponds to section 1,
// the first heading in the article.
$heading = this.findSectionHeadingByIndex( sectionIndex );
// If section-heading is present on the heading,
// then we know the page has been MobileFormatted
// and that this is a wrapped section
if ( $heading.hasClass( 'section-heading' ) ) {
// get content of section
$container = $heading.next();
// inside section find the first heading
$nextHeading = $container.find( headingSelector ).eq( 0 );
return $nextHeading.length ?
// find all amboxes before the next heading
withNestedChildren( $nextHeading.prevAll( selector ) ) :
// There is no subheadings inside
// Grab all issues in section
withNestedChildren( $container.children( selector ) );
} else {
// the heading relates to a subsection (or unwrapped desktop section),
// so grab elements between this and the next one
$nextHeading = $heading.eq( 0 ).nextAll( headingSelector ).eq( 0 );
return $heading.nextUntil( $nextHeading, selector );
}
},
/**
* Get the lead section of the page view.
* @memberof Page
* @instance
* @return {jQuery.Object|null}
*/
getLeadSectionElement: function () {
/*
* The page is formatted as follows:
* <div id="bodyContent">
* <!-- content of the page.. -->
* <div id="mw-content-text">
* <div class="mf-section-0">lead section</div>
* <h2></h2>
* <div class="mf-section-1">second section</div>
* </div>
* </div>
*/
if ( this.$( '.mf-section-0' ).length ) {
return this.$( '.mf-section-0' );
}
// no lead section found
return null;
},
/**
* Determines if content model is wikitext
* @memberof Page
* @instance
* @return {boolean}
*/
isWikiText: function () {
return mw.config.get( 'wgPageContentModel' ) === 'wikitext';
},
/**
* Checks whether the current page is the main page
* @memberof Page
* @instance
* @return {boolean}
*/
isMainPage: function () {
return this.options.isMainPage;
},
/**
* Checks whether the current page is watched
* @memberof Page
* @instance
* @return {boolean}
*/
isWatched: function () {
return this.options.isWatched;
},
/**
* Return the latest revision id for this page
* @memberof Page
* @instance
* @return {number}
*/
getRevisionId: function () {
return this.options.revId;
},
/**
* Return prefixed page title
* @memberof Page
* @instance
* @return {string}
*/
getTitle: function () {
return this.options.title;
},
/**
* Return page id
* @memberof Page
* @instance
* @return {number}
*/
getId: function () {
return this.options.id;
},
/**
* return namespace id
* @memberof Page
* @instance
* @return {number} namespace Number
*/
getNamespaceId: function () {
var nsId,
args = this.options.title.split( ':' );
if ( args[1] ) {
nsId = mw.config.get( 'wgNamespaceIds' )[ args[0].toLowerCase().replace( ' ', '_' ) ] || 0;
} else {
nsId = 0;
}
return nsId;
},
/**
* Determines if current page is a talk page
* @memberof Page
* @instance
* @return {boolean} Whether the page is a talk page or not
*/
isTalkPage: function () {
var ns = this.getNamespaceId();
// all talk pages are odd Numbers (except the case of special pages)
return ns > 0 && ns % 2 === 1;
},
/**
* @inheritdoc
* @memberof Page
* @instance
*/
preRender: function () {
this.sections = [];
this._sectionLookup = {};
this.title = this.options.title;
this.options.sections.forEach( function ( sectionData ) {
var section = new Section( sectionData );
this.sections.push( section );
this._sectionLookup[section.id] = section;
}.bind( this ) );
},
/**
* Return all the thumbnails in the article.
* Images which have a class or link container (.image|.thumbimage)
* that matches one of the items of the constant BLACKLISTED_THUMBNAIL_CLASS_SELECTORS
* will be excluded.
* A thumbnail nested inside one of these classes will still be returned.
* e.g. `<div class="noviewer"><a class="image"><img></a></div>` is not a valid thumbnail
* `<a class="image noviewer"><img></a>` is not a valid thumbnail
* `<a class="image"><img class="noviewer"></a>` is not a valid thumbnail
* @memberof Page
* @instance
* @return {Thumbnail[]}
*/
getThumbnails: function () {
var $thumbs,
$el = this.$el,
blacklistSelector = '.' + BLACKLISTED_THUMBNAIL_CLASS_SELECTORS.join( ',.' ),
thumbs = [];
if ( !this._thumbs ) {
$thumbs = $el.find( 'a.image, a.thumbimage' )
.not( blacklistSelector );
$thumbs.each( function () {
var $a = $el.find( this ),
$lazyImage = $a.find( '.lazy-image-placeholder' ),
// Parents need to be checked as well.
valid = $a.parents( blacklistSelector ).length === 0 &&
$a.find( blacklistSelector ).length === 0,
legacyMatch = $a.attr( 'href' ).match( /title=([^/&]+)/ ),
match = $a.attr( 'href' ).match( /[^/]+$/ );
// filter out invalid lazy loaded images if so far image is valid
if ( $lazyImage.length && valid ) {
// if the regex matches it means the image has one of the classes
// thus we must invert the result
valid = !new RegExp( '\\b(' + BLACKLISTED_THUMBNAIL_CLASS_SELECTORS.join( '|' ) + ')\\b' )
.test( $lazyImage.data( 'class' ) );
}
if ( valid && ( legacyMatch || match ) ) {
thumbs.push(
new Thumbnail( {
el: $a,
filename: decodeURIComponent(
legacyMatch ? legacyMatch[1] : match[0]
)
} )
);
}
} );
this._thumbs = thumbs;
}
return this._thumbs;
},
/**
* FIXME: Change function signature to take the anchor of the heading
* @memberof Page
* @instance
* @param {string} id of the section as defined by MobileFormatter.
* Note, that currently, this is different from
* the PHP parser in that it relates to top-level sections.
* For example, mf-section-1 would relate to section 1. See FIXME.
* @return {Section}
*/
getSection: function ( id ) {
return this._sectionLookup[ id ];
},
/**
* Obtain the list of high level (and grouped) sections.
* Note that this list will not include subsections.
* @memberof Page
* @instance
* @return {Array} of Section instances
*/
getSections: function () {
return this.sections;
},
/**
* Returns a jQuery object representing all redlinks on the page.
* @memberof Page
* @instance
* @return {jQuery.Object}
*/
getRedLinks: function () {
return this.$( '.new' );
}
} );
/**
* Create a Page object from an API response.
*
* @memberof Page
* @param {Object} resp as representing a page in the API
* @return {Page}
*/
Page.newFromJSON = function ( resp ) {
var revision, displayTitle,
thumb = resp.thumbnail,
pageprops = resp.pageprops || {
displaytitle: HTML.escape( resp.title )
},
terms = resp.terms;
if ( pageprops || terms ) {
// The label is either the display title or the label pageprop
// (the latter used by Wikidata)
// Long term we want to consolidate these.
// Note that pageprops.displaytitle is HTML, while
// terms.label[0] is plain text.
displayTitle = terms && terms.label ?
HTML.escape( terms.label[0] ) : pageprops.displaytitle;
}
// Add Wikidata descriptions if available (T101719)
resp.wikidataDescription = resp.description || undefined;
if ( thumb ) {
resp.thumbnail.isLandscape = thumb.width > thumb.height;
}
// page may or may not exist.
if ( resp.revisions && resp.revisions[0] ) {
revision = resp.revisions[0];
resp.lastModified = time.getLastModifiedMessage(
new Date( revision.timestamp ).getTime() / 1000,
revision.user
);
}
return new Page(
util.extend( resp, {
id: resp.pageid,
isMissing: !!resp.missing,
url: mw.util.getUrl( resp.title ),
displayTitle: displayTitle // this is HTML!
} )
);
};
/**
* Selector for matching headings
*
* @memberof Page
*/
Page.HEADING_SELECTOR = HEADING_SELECTOR;
module.exports = Page;