/**
* @module preview/model
*/
import { requiresSummary } from '../ui/renderer';
const selectors = [];
/**
* Page Preview types as defined in Schema:Popups
* https://meta.wikimedia.org/wiki/Schema:Popups
*
* @constant {Object}
*/
const previewTypes = {
/** Empty preview used in error situations */
TYPE_GENERIC: 'generic',
/** Standard page preview with or without thumbnail */
TYPE_PAGE: 'page',
/** Disambiguation page preview */
TYPE_DISAMBIGUATION: 'disambiguation'
};
export { previewTypes };
/**
* Preview Model
*
* @typedef {Object} PreviewModel
* @property {string} url The canonical URL of the page being previewed
* @property {string} type One of the previewTypes.TYPE_… constants.
*
* @global
*/
/**
* @typedef {Object} PagePreviewModel
* @extends PreviewModel
* @property {string} title
* @property {Array|undefined} extract `undefined` if the extract isn't
* viable, e.g. if it's empty after having ellipsis and parentheticals
* removed; this can be used to present default or error states
* @property {string} languageCode
* @property {string} languageDirection Either "ltr" or "rtl", or an empty string if undefined.
* @property {{source: string, width: number, height: number}|undefined} thumbnail
* @property {number} pageId Currently not used by any known popup type.
*
* @global
*/
/**
* @typedef {Object} ReferencePreviewModel
* @extends PreviewModel
* @property {string} extract An HTML snippet, not necessarily with a single top-level node
* @property {string} referenceType A type identifier, e.g. "web"
* @property {string} sourceElementId ID of the parent element that triggered the preview
*
* @global
*/
/**
* Creates a page preview model.
*
* @param {string} title
* @param {string} url The canonical URL of the page being previewed
* @param {string} languageCode
* @param {string} languageDirection Either "ltr" or "rtl"
* @param {Array|undefined|null} extract
* @param {string} type
* @param {{source: string, width: number, height: number}|undefined} [thumbnail]
* @param {number} [pageId]
* @return {PagePreviewModel}
*/
export function createModel(
title,
url,
languageCode,
languageDirection,
extract,
type,
thumbnail,
pageId
) {
const processedExtract = processExtract( extract ),
previewType = getPagePreviewType( type, processedExtract );
return {
title,
url,
languageCode,
languageDirection,
extract: processedExtract,
type: previewType,
thumbnail,
pageId
};
}
/**
* Creates an empty page preview model.
*
* @param {string} title
* @param {string} url
* @return {PagePreviewModel}
*/
export function createNullModel( title, url ) {
return createModel( title, url, '', '', [], '' );
}
/**
* @param {HTMLElement} element
* @param {string} selector
* @return {boolean}
*/
const elementMatchesSelector = ( element, selector ) => {
return element.matches( selector );
};
/**
* Recursively checks the element and its parents.
*
* @param {HTMLElement} element
* @return {HTMLElement|null}
*/
export function findNearestEligibleTarget( element ) {
if ( selectors.length ) {
const selector = selectors.join( ', ' );
return element.closest( selector );
}
return null;
}
/**
* @typedef {Object} PreviewType
* @property {string} name identifier for preview type
* @property {string} selector a CSS selector
* @type {PreviewType[]}
*/
const registeredPreviewTypes = [];
/**
* Determines the applicable popup type based on title and link element.
*
* @param {HTMLAnchorElement} el
* @return {string|null} One of the previewTypes.TYPE_… constants
*/
export function getPreviewType( el ) {
const candidates = registeredPreviewTypes.filter(
( type ) => elementMatchesSelector( el, type.selector )
);
// If the filter returned some possibilities, use the last registered one.
if ( candidates.length > 0 ) {
return candidates[ candidates.length - 1 ].name;
}
return null;
}
/**
* Processes the extract returned by the TextExtracts MediaWiki API query
* module.
*
* If the extract is `undefined`, `null`, or empty, then `undefined` is
* returned.
*
* @param {Array|undefined|null} extract
* @return {Array|undefined} Array when extract is an not empty array, undefined
* otherwise
*/
function processExtract( extract ) {
if ( extract === undefined || extract === null || extract.length === 0 ) {
return undefined;
}
return extract;
}
/**
* Determines the page preview type based on whether or not:
* a. Is the preview empty.
* b. The preview type matches one of previewTypes.
* c. Assume standard page preview if both above are false
*
* @param {string} type
* @param {Array|undefined} [processedExtract]
* @return {string} One of the previewTypes.TYPE_… constants.
*/
function getPagePreviewType( type, processedExtract ) {
// If the preview type requires a summary to display and no extract was
// found show the generic (error) preview.
if ( processedExtract === undefined && requiresSummary( type ) ) {
return previewTypes.TYPE_GENERIC;
}
switch ( type ) {
case previewTypes.TYPE_GENERIC:
case previewTypes.TYPE_DISAMBIGUATION:
case previewTypes.TYPE_PAGE:
return type;
default:
/**
* Assume type="page" if extract exists & not one of previewTypes.
* Note:
* - Restbase response includes "type" prop but other gateways don't.
* - event-logging Schema:Popups requires type="page" but restbase
* provides type="standard". Model must conform to event-logging schema.
*/
return previewTypes.TYPE_PAGE;
}
}
const dwellDelay = {};
/**
* Determines the delay before showing the preview when dwelling a link.
*
* @param {string} type
* @return {number}
*/
export function getDwellDelay( type ) {
return dwellDelay[ type ] || 0;
}
/***
* Set the delay before showing the preview when dwelling a link.
*
* @param {string} type
* @param {number} delay
*/
export function setDwellTime( type, delay ) {
dwellDelay[ type ] = delay;
}
/**
* Allows extensions to register their own page previews.
*
* @stable
* @param {string} type
* @param {string} selector A valid CSS selector to associate preview with
* @param {number} [delay] optional delay between hovering and displaying preview.
* If not defined, delay will be zero.
*/
export function registerModel( type, selector, delay ) {
selectors.push( selector );
registeredPreviewTypes.push( {
name: type,
selector
} );
if ( delay ) {
setDwellTime( type, delay );
}
}
export const test = {
/** For testing only */
reset: () => {
while ( registeredPreviewTypes.length ) {
registeredPreviewTypes.pop();
}
}
};