Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1432100
mw.widgets.TitleWidget.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
18 KB
Referenced Files
None
Subscribers
None
mw.widgets.TitleWidget.js
View Options
/*!
* MediaWiki Widgets - TitleWidget class.
*
* @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
(
function
()
{
const
hasOwn
=
Object
.
prototype
.
hasOwnProperty
;
/**
* @classdesc Mixin for title widgets.
*
* @class
* @abstract
*
* @constructor
* @description Create an instance of `mw.widgets.TitleWidget`.
* @param {Object} [config] Configuration options
* @param {number} [config.limit=10] Number of results to show
* @param {number} [config.namespace] Namespace to prepend to queries
* @param {number} [config.maxLength=255] Maximum query length
* @param {boolean} [config.relative=true] If a namespace is set, display titles relative to it
* @param {boolean} [config.suggestions=true] Display search suggestions
* @param {boolean} [config.showRedirectTargets=true] Show the targets of redirects
* @param {boolean} [config.showImages=false] Show page images
* @param {boolean} [config.showDescriptions=false] Show page descriptions
* @param {boolean} [config.showDisambigsLast=false] Show disambiguation pages as the last results
* @param {boolean} [config.showMissing] Show the user's input as a missing page when a page with this
* exact name doesn't exist. Disabled by default when the namespace option is used, otherwise
* enabled by default.
* @param {boolean} [config.showInterwikis=false] Show pages with a valid interwiki prefix
* @param {boolean} [config.searchFragments=false] Search for hash fragments on a specific page when typed
* @param {boolean} [config.addQueryInput=true] Add exact user's input query to results
* @param {boolean} [config.excludeCurrentPage=false] Exclude the current page from suggestions
* @param {boolean} [config.excludeDynamicNamespaces=false] Exclude pages whose namespace is negative
* @param {boolean} [config.validateTitle=true] Whether the input must be a valid title
* @param {boolean} [config.required=false] Whether the input must not be empty
* @param {boolean} [config.highlightSearchQuery=true] Highlight the partial query the user used for this title
* @param {Object} [config.cache] Result cache which implements a 'set' method, taking keyed values as an argument
* @param {mw.Api} [config.api] API object to use, creates a default mw.Api instance if not specified
*/
mw
.
widgets
.
TitleWidget
=
function
MwWidgetsTitleWidget
(
config
)
{
// Config initialization
config
=
Object
.
assign
(
{
maxLength
:
255
,
limit
:
10
},
config
);
// Properties
this
.
limit
=
config
.
limit
;
this
.
maxLength
=
config
.
maxLength
;
this
.
namespace
=
config
.
namespace
!==
undefined
?
config
.
namespace
:
null
;
this
.
relative
=
config
.
relative
!==
undefined
?
config
.
relative
:
true
;
this
.
suggestions
=
config
.
suggestions
!==
undefined
?
config
.
suggestions
:
true
;
this
.
showRedirectTargets
=
config
.
showRedirectTargets
!==
false
;
this
.
showImages
=
!!
config
.
showImages
;
this
.
showDescriptions
=
!!
config
.
showDescriptions
;
this
.
showDisambigsLast
=
!!
config
.
showDisambigsLast
;
this
.
showMissing
=
config
.
showMissing
!==
undefined
?
!!
config
.
showMissing
:
this
.
namespace
===
null
;
this
.
showInterwikis
=
!!
config
.
showInterwikis
;
this
.
searchFragments
=
!!
config
.
searchFragments
;
this
.
addQueryInput
=
config
.
addQueryInput
!==
false
;
this
.
excludeCurrentPage
=
!!
config
.
excludeCurrentPage
;
this
.
excludeDynamicNamespaces
=
!!
config
.
excludeDynamicNamespaces
;
this
.
validateTitle
=
config
.
validateTitle
!==
undefined
?
config
.
validateTitle
:
true
;
this
.
highlightSearchQuery
=
config
.
highlightSearchQuery
===
undefined
?
true
:
!!
config
.
highlightSearchQuery
;
this
.
cache
=
config
.
cache
;
this
.
api
=
config
.
api
||
new
mw
.
Api
();
this
.
compare
=
new
Intl
.
Collator
(
mw
.
language
.
bcp47
(
mw
.
config
.
get
(
'wgContentLanguage'
)
),
{
sensitivity
:
'base'
}
).
compare
;
this
.
sectionsCache
=
{};
// Initialization
this
.
$element
.
addClass
(
'mw-widget-titleWidget'
);
};
/* Setup */
OO
.
initClass
(
mw
.
widgets
.
TitleWidget
);
/* Static properties */
mw
.
widgets
.
TitleWidget
.
static
.
interwikiPrefixesPromiseCache
=
{};
/* Methods */
/**
* Get the current value of the search query.
*
* @abstract
* @method
* @return {string} Search query
*/
mw
.
widgets
.
TitleWidget
.
prototype
.
getQueryValue
=
null
;
/**
* Get the namespace to prepend to titles in suggestions, if any.
*
* @return {number|null} Namespace number
*/
mw
.
widgets
.
TitleWidget
.
prototype
.
getNamespace
=
function
()
{
return
this
.
namespace
;
};
/**
* Set the namespace to prepend to titles in suggestions, if any.
*
* @param {number|null} namespace Namespace number
*/
mw
.
widgets
.
TitleWidget
.
prototype
.
setNamespace
=
function
(
namespace
)
{
this
.
namespace
=
namespace
;
};
mw
.
widgets
.
TitleWidget
.
prototype
.
getInterwikiPrefixesPromise
=
function
()
{
if
(
!
this
.
showInterwikis
)
{
return
$
.
Deferred
().
resolve
(
[]
).
promise
();
}
const
api
=
this
.
getApi
();
const
cache
=
this
.
constructor
.
static
.
interwikiPrefixesPromiseCache
;
const
key
=
api
.
defaults
.
ajax
.
url
;
if
(
!
Object
.
prototype
.
hasOwnProperty
.
call
(
cache
,
key
)
)
{
cache
[
key
]
=
api
.
get
(
{
action
:
'query'
,
meta
:
'siteinfo'
,
siprop
:
'interwikimap'
,
// Cache client-side for a day since this info is mostly static
maxage
:
60
*
60
*
24
,
smaxage
:
60
*
60
*
24
,
// Workaround T97096 by setting uselang=content
uselang
:
'content'
}
).
then
(
(
data
)
=>
data
.
query
.
interwikimap
.
map
(
(
interwiki
)
=>
interwiki
.
prefix
)
);
}
return
cache
[
key
];
};
/**
* Suggest link fragments from the sections API.
*
* @param {string} title Title, extracted form the user input
* @param {string} fragmentQuery Partial link fragment, from the user input
* @return {jQuery.Promise} Suggestions promise
*/
mw
.
widgets
.
TitleWidget
.
prototype
.
getSectionSuggestions
=
function
(
title
,
fragmentQuery
)
{
const
normalizedTitle
=
mw
.
Title
.
newFromText
(
title
||
mw
.
config
.
get
(
'wgRelevantPageName'
)
);
if
(
!
normalizedTitle
)
{
return
$
.
Deferred
().
resolve
(
[]
).
promise
();
}
const
normalizedTitleText
=
normalizedTitle
.
getPrefixedText
();
this
.
sectionsCache
[
normalizedTitleText
]
=
this
.
sectionsCache
[
normalizedTitleText
]
||
this
.
getApi
().
get
(
{
action
:
'parse'
,
page
:
normalizedTitleText
,
prop
:
'sections'
}
);
function
normalizeFragment
(
fragment
)
{
return
fragment
.
toLowerCase
().
replace
(
/_/g
,
' '
);
}
return
this
.
sectionsCache
[
normalizedTitleText
].
then
(
(
response
)
=>
{
const
sections
=
OO
.
getProp
(
response
,
'parse'
,
'sections'
)
||
[];
const
normalizedFragmentQuery
=
normalizeFragment
(
fragmentQuery
);
const
results
=
sections
.
filter
(
(
section
)
=>
normalizeFragment
(
section
.
line
).
indexOf
(
normalizedFragmentQuery
)
!==
-
1
).
map
(
(
section
)
=>
{
const
fragment
=
section
.
linkAnchor
.
replace
(
/_/g
,
' '
);
// TODO: Make promise abortable
return
{
title
:
title
+
'#'
+
fragment
,
// `title`` could be empty for a relative fragment, so store the normalized
// title if that is needed later.
normalizedTitle
:
normalizedTitle
+
'#'
+
fragment
,
ns
:
normalizedTitle
.
getNamespaceId
(),
// Sort prefix matches to the top
index
:
normalizeFragment
(
section
.
line
).
indexOf
(
normalizedFragmentQuery
)
===
0
?
0
:
1
};
}
);
// Sorting also happens later, but we need to do it now before we truncate
results
.
sort
(
(
a
,
b
)
=>
a
.
index
-
b
.
index
);
// Fake query result
return
{
query
:
{
pages
:
results
.
slice
(
0
,
this
.
limit
)
}
};
}
).
promise
(
{
abort
:
function
()
{}
}
);
};
/**
* Get a promise which resolves with an API response for suggested
* links for the current query.
*
* @return {jQuery.Promise} Suggestions promise
*/
mw
.
widgets
.
TitleWidget
.
prototype
.
getSuggestionsPromise
=
function
()
{
const
api
=
this
.
getApi
(),
query
=
this
.
getQueryValue
(),
promiseAbortObject
=
{
abort
:
function
()
{
// Do nothing. This is just so OOUI doesn't break due to abort being undefined.
}
};
if
(
this
.
searchFragments
)
{
const
hashIndex
=
query
.
indexOf
(
'#'
);
if
(
hashIndex
!==
-
1
)
{
return
this
.
getSectionSuggestions
(
query
.
slice
(
0
,
hashIndex
),
query
.
slice
(
hashIndex
+
1
)
);
}
}
if
(
!
mw
.
Title
.
newFromText
(
query
)
)
{
// Don't send invalid titles to the API.
// Just pretend it returned nothing so we can show the 'invalid title' section
return
$
.
Deferred
().
resolve
(
{}
).
promise
(
promiseAbortObject
);
}
return
this
.
getInterwikiPrefixesPromise
().
then
(
(
interwikiPrefixes
)
=>
{
// Optimization: check we have any prefixes.
if
(
interwikiPrefixes
.
length
)
{
const
interwiki
=
query
.
slice
(
0
,
Math
.
max
(
0
,
query
.
indexOf
(
':'
)
)
);
if
(
interwiki
!==
''
&&
interwikiPrefixes
.
indexOf
(
interwiki
)
!==
-
1
)
{
// Interwiki prefix is valid: return the original query as a valid title
// NB: This doesn't check if the title actually exists on the other wiki
return
$
.
Deferred
().
resolve
(
{
query
:
{
pages
:
[
{
title
:
query
}
]
}
}
).
promise
(
promiseAbortObject
);
}
}
// Not a interwiki: do a prefix-search API lookup of the query.
const
prefixSearchRequest
=
api
.
get
(
this
.
getApiParams
(
query
)
);
promiseAbortObject
.
abort
=
prefixSearchRequest
.
abort
.
bind
(
prefixSearchRequest
);
// TODO ew
return
prefixSearchRequest
.
then
(
(
prefixSearchResponse
)
=>
{
if
(
!
this
.
showMissing
)
{
return
prefixSearchResponse
;
}
const
title
=
this
.
namespace
&&
this
.
getMWTitle
(
query
);
// Add the query title as the first result, after looking up its details.
const
queryTitleRequest
=
api
.
get
(
{
action
:
'query'
,
titles
:
title
?
title
.
getPrefixedDb
()
:
query
}
);
promiseAbortObject
.
abort
=
queryTitleRequest
.
abort
.
bind
(
queryTitleRequest
);
return
queryTitleRequest
.
then
(
(
queryTitleResponse
)
=>
{
// By default, return the prefix-search result.
const
result
=
prefixSearchResponse
;
if
(
prefixSearchResponse
.
query
===
undefined
)
{
// There are no prefix-search results, so make the only result the query title.
// The API response structures are identical because both API calls are action=query.
result
.
query
=
queryTitleResponse
.
query
;
}
else
if
(
queryTitleResponse
.
query
.
pages
&&
queryTitleResponse
.
query
.
pages
[
-
1
]
!==
undefined
&&
!
this
.
responseContainsNonExistingTitle
(
prefixSearchResponse
,
queryTitleResponse
.
query
.
pages
[
-
1
].
title
)
)
{
// There are prefix-search results, but the query title isn't in them,
// so add it as a new result. It's under the new key 'queryTitle', because
// all other results will be under their page ID or a negative integer ID,
// and the keys aren't actually used for anything.
result
.
query
.
pages
.
queryTitle
=
queryTitleResponse
.
query
.
pages
[
-
1
];
// Give it the lowest possible sort-index (the API only returns index > 0)
// to make this result be sorted at the top.
result
.
query
.
pages
.
queryTitle
.
index
=
0
;
}
return
result
;
}
);
}
);
}
).
promise
(
promiseAbortObject
);
};
/**
* Check for the existence of a given title in an API result set.
*
* As the title is known not to exist, this doesn't check in apiResponse.query.pages,
* but only in the redirect targets in apiResponse.query.redirects.
*
* @private
* @param {Object} apiResponse The API result set to search in.
* @param {string} title The page title to search for.
* @return {boolean}
*/
mw
.
widgets
.
TitleWidget
.
prototype
.
responseContainsNonExistingTitle
=
function
(
apiResponse
,
title
)
{
// Make sure there are redirects in the data.
if
(
apiResponse
.
query
.
redirects
===
undefined
)
{
return
false
;
}
// Check the targets against the given title.
for
(
const
redirect
in
apiResponse
.
query
.
redirects
)
{
if
(
apiResponse
.
query
.
redirects
[
redirect
].
to
===
title
)
{
return
true
;
}
}
// The title wasn't found.
return
false
;
};
/**
* Get API params for a given query.
*
* @param {string} query User query
* @return {Object} API params
*/
mw
.
widgets
.
TitleWidget
.
prototype
.
getApiParams
=
function
(
query
)
{
const
params
=
{
action
:
'query'
,
prop
:
[
'info'
,
'pageprops'
],
generator
:
'prefixsearch'
,
gpssearch
:
query
,
gpsnamespace
:
this
.
namespace
!==
null
?
this
.
namespace
:
undefined
,
gpslimit
:
this
.
limit
,
ppprop
:
'disambiguation'
};
if
(
this
.
showRedirectTargets
)
{
params
.
redirects
=
true
;
}
if
(
this
.
showImages
)
{
params
.
prop
.
push
(
'pageimages'
);
params
.
pithumbsize
=
80
;
params
.
pilimit
=
this
.
limit
;
}
if
(
this
.
showDescriptions
)
{
params
.
prop
.
push
(
'description'
);
}
return
params
;
};
/**
* Get the API object for title requests.
*
* @return {mw.Api} MediaWiki API
*/
mw
.
widgets
.
TitleWidget
.
prototype
.
getApi
=
function
()
{
return
this
.
api
;
};
/**
* Get option widgets from the server response.
*
* @param {Object} data Query result
* @return {OO.ui.OptionWidget[]} Menu items
*/
mw
.
widgets
.
TitleWidget
.
prototype
.
getOptionsFromData
=
function
(
data
)
{
const
currentPageName
=
new
mw
.
Title
(
mw
.
config
.
get
(
'wgRelevantPageName'
)
).
getPrefixedText
(),
items
=
[],
titles
=
[],
disambigs
=
[],
titleObj
=
mw
.
Title
.
newFromText
(
this
.
getQueryValue
()
),
redirectsTo
=
{},
redirectIndices
=
{},
pageData
=
{};
if
(
data
.
redirects
)
{
for
(
let
r
=
0
,
rLen
=
data
.
redirects
.
length
;
r
<
rLen
;
r
++
)
{
const
redirect
=
data
.
redirects
[
r
];
redirectsTo
[
redirect
.
to
]
=
redirectsTo
[
redirect
.
to
]
||
[];
redirectsTo
[
redirect
.
to
].
push
(
redirect
.
from
);
// Save the lowest index for this redirect target.
redirectIndices
[
redirect
.
to
]
=
Math
.
min
(
redirectIndices
[
redirect
.
to
]
||
redirect
.
index
,
redirect
.
index
);
}
}
for
(
const
index
in
data
.
pages
)
{
const
suggestionPage
=
data
.
pages
[
index
];
// When excludeCurrentPage is set, don't list the current page unless the user has type the full title
if
(
this
.
excludeCurrentPage
&&
suggestionPage
.
title
===
currentPageName
&&
suggestionPage
.
title
!==
titleObj
.
getPrefixedText
()
)
{
continue
;
}
// When excludeDynamicNamespaces is set, ignore all pages with negative namespace
if
(
this
.
excludeDynamicNamespaces
&&
suggestionPage
.
ns
<
0
)
{
continue
;
}
pageData
[
suggestionPage
.
title
]
=
{
known
:
suggestionPage
.
known
!==
undefined
,
missing
:
suggestionPage
.
missing
!==
undefined
,
redirect
:
suggestionPage
.
redirect
!==
undefined
,
disambiguation
:
OO
.
getProp
(
suggestionPage
,
'pageprops'
,
'disambiguation'
)
!==
undefined
,
imageUrl
:
OO
.
getProp
(
suggestionPage
,
'thumbnail'
,
'source'
),
description
:
suggestionPage
.
description
,
// Sort index
index
:
suggestionPage
.
index
!==
undefined
?
suggestionPage
.
index
:
redirectIndices
[
suggestionPage
.
title
],
originalData
:
suggestionPage
};
// Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true
// and we encounter a cross-namespace redirect.
if
(
this
.
namespace
===
null
||
this
.
namespace
===
suggestionPage
.
ns
)
{
titles
.
push
(
suggestionPage
.
title
);
}
const
redirects
=
hasOwn
.
call
(
redirectsTo
,
suggestionPage
.
title
)
?
redirectsTo
[
suggestionPage
.
title
]
:
[];
for
(
let
i
=
0
,
iLen
=
redirects
.
length
;
i
<
iLen
;
i
++
)
{
pageData
[
redirects
[
i
]
]
=
{
missing
:
false
,
known
:
true
,
redirect
:
true
,
disambiguation
:
false
,
description
:
mw
.
msg
(
'mw-widgets-titleinput-description-redirect'
,
suggestionPage
.
title
),
// Sort index, just below its target
index
:
pageData
[
suggestionPage
.
title
].
index
+
0.5
,
originalData
:
suggestionPage
};
titles
.
push
(
redirects
[
i
]
);
}
}
titles
.
sort
(
(
a
,
b
)
=>
pageData
[
a
].
index
-
pageData
[
b
].
index
);
// If not found, run value through mw.Title to avoid treating a match as a
// mismatch where normalisation would make them matching (T50476)
const
pageExistsExact
=
(
hasOwn
.
call
(
pageData
,
this
.
getQueryValue
()
)
&&
(
!
pageData
[
this
.
getQueryValue
()
].
missing
||
pageData
[
this
.
getQueryValue
()
].
known
)
);
const
pageExists
=
pageExistsExact
||
(
titleObj
&&
hasOwn
.
call
(
pageData
,
titleObj
.
getPrefixedText
()
)
&&
(
!
pageData
[
titleObj
.
getPrefixedText
()
].
missing
||
pageData
[
titleObj
.
getPrefixedText
()
].
known
)
);
// Offer the exact text as a suggestion if the page exists
if
(
this
.
addQueryInput
&&
pageExists
&&
!
pageExistsExact
)
{
titles
.
unshift
(
this
.
getQueryValue
()
);
// Ensure correct page metadata gets used
pageData
[
this
.
getQueryValue
()
]
=
pageData
[
titleObj
.
getPrefixedText
()
];
}
if
(
this
.
cache
)
{
this
.
cache
.
set
(
pageData
);
}
for
(
let
t
=
0
,
tLen
=
titles
.
length
;
t
<
tLen
;
t
++
)
{
const
page
=
hasOwn
.
call
(
pageData
,
titles
[
t
]
)
?
pageData
[
titles
[
t
]
]
:
{};
const
option
=
this
.
createOptionWidget
(
this
.
getOptionWidgetData
(
titles
[
t
],
page
)
);
if
(
this
.
showDisambigsLast
&&
page
.
disambiguation
)
{
disambigs
.
push
(
option
);
}
else
{
items
.
push
(
option
);
}
}
return
items
.
concat
(
disambigs
);
};
/**
* Create a menu option widget with specified data.
*
* @param {Object} data Data for option widget
* @return {OO.ui.MenuOptionWidget} Data for option widget
*/
mw
.
widgets
.
TitleWidget
.
prototype
.
createOptionWidget
=
function
(
data
)
{
return
new
mw
.
widgets
.
TitleOptionWidget
(
data
);
};
/**
* Get menu option widget data from the title and page data.
*
* @param {string} title Title object
* @param {Object} data Page data
* @return {Object} Data for option widget
*/
mw
.
widgets
.
TitleWidget
.
prototype
.
getOptionWidgetData
=
function
(
title
,
data
)
{
let
description
=
data
.
description
;
if
(
!
description
&&
(
data
.
missing
&&
!
data
.
known
)
)
{
description
=
mw
.
msg
(
'mw-widgets-titleinput-description-new-page'
);
}
const
mwTitle
=
new
mw
.
Title
(
OO
.
getProp
(
data
,
'originalData'
,
'normalizedTitle'
)
||
title
);
return
{
data
:
this
.
namespace
!==
null
&&
this
.
relative
?
mwTitle
.
getRelativeText
(
this
.
namespace
)
:
title
,
url
:
mwTitle
.
getUrl
(),
showImages
:
this
.
showImages
,
imageUrl
:
this
.
showImages
?
data
.
imageUrl
:
null
,
description
:
this
.
showDescriptions
?
description
:
null
,
missing
:
data
.
missing
&&
!
data
.
known
,
redirect
:
data
.
redirect
,
disambiguation
:
data
.
disambiguation
,
query
:
this
.
highlightSearchQuery
?
this
.
getQueryValue
()
:
null
,
compare
:
this
.
compare
};
};
/**
* Get title object corresponding to given value, or #getQueryValue if not given.
*
* @param {string} [value] Value to get a title for
* @return {mw.Title|null} Title object, or null if value is invalid
*/
mw
.
widgets
.
TitleWidget
.
prototype
.
getMWTitle
=
function
(
value
)
{
const
title
=
value
!==
undefined
?
value
:
this
.
getQueryValue
(),
// mw.Title doesn't handle null well
titleObj
=
mw
.
Title
.
newFromText
(
title
,
this
.
namespace
!==
null
?
this
.
namespace
:
undefined
);
return
titleObj
;
};
/**
* Check if the query is valid.
*
* @return {boolean} The query is valid
*/
mw
.
widgets
.
TitleWidget
.
prototype
.
isQueryValid
=
function
()
{
if
(
!
this
.
validateTitle
)
{
return
true
;
}
if
(
!
this
.
required
&&
this
.
getQueryValue
()
===
''
)
{
return
true
;
}
return
!!
this
.
getMWTitle
();
};
}()
);
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, May 16, 21:26 (1 d, 10 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
e8/ca/03a1239339c0ea0e48afb0567ae4
Default Alt Text
mw.widgets.TitleWidget.js (18 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment