mw.loader.implement( "ext.gadget.readinglist@", {
"main": "resources/ext.gadget.readinglist/gadget.js",
"files": {
"resources/ext.gadget.readinglist/gadget.js": function ( require, module, exports ) {
/**
* When re-generating this file use:
* http://localhost:8888/w/load.php?modules=ext.gadget.readinglist&debug=true
*/
// <nowiki>
const { showOverlay, createMembersOverlay, editListOverlay } = require( './overlays.js');
const { getCollectionsWithMembership, fromBase64, saveReadingList,
deleteCollection, getReadingListUrl,
addToList, toBase64 } = require( './api.js' );
const READING_LIST_URL = getReadingListUrl( mw.user.getName() );
(function () {
if ( mw.user.isAnon() ) {
return;
}
const mwuibutton = ( text, className ) => {
const editBtn = document.createElement('button');
editBtn.classList.add( 'mw-ui-button', className );
editBtn.textContent = text;
return editBtn;
};
function addImportFromTable() {
Array.from( document.querySelectorAll('.wikitable') )
.filter((node) => node.querySelectorAll('a[title]').length > 4)
.forEach((table) => {
const titles = {
[window.location.host]: Array.from(table.querySelectorAll('a[title]')).map((link) => link.getAttribute('title'))
};
const link = document.createElement('a');
link.textContent = 'Import into list';
const row = document.createElement('tr');
const col = document.createElement('td');
col.setAttribute('colspan', table.querySelectorAll('thead').length);
table.appendChild(row);
const base64 = toBase64(`Imported from tablez on ${mw.config.get('wgTitle')}`, '', titles);
link.setAttribute('href', `${READING_LIST_URL}?limport=${base64}`);
col.appendChild(link);
table.appendChild(col);
} );
}
function addToUserMenu() {
mw.util.addPortletLink( 'p-personal', READING_LIST_URL, 'My reading lists', 'pt-readinglists', null, 'l', 'pt-watchlist' );
}
function addBookmarkNextToWatchAction() {
let link = mw.util.addPortletLink('p-views', '#', 'Bookmark', 'pt-bookmark');
if ( !link ) {
link = mw.util.addPortletLink('page-actions', '#', 'Bookmark', 'pt-bookmark' );
}
if (!link ) {
return;
}
// Hack: no native support for vector icons.
setTimeout( () => {
link.classList.remove( 'vector-tab-noicon' );
}, 300 );
link.querySelector( 'a' ).classList.add(
'mw-ui-icon',
'mw-ui-button',
'mw-ui-quiet',
'mw-ui-icon-element',
'mw-ui-icon-bookmark',
'mw-ui-icon-small'
)
link.addEventListener( 'click', () => {
const title = mw.config.get( 'wgPageName' );
showOverlay(
getCollectionsWithMembership( mw.user.getName(), title ).then(( collections ) => {
return createMembersOverlay(
title,
collections
);
} )
);
} );
}
const isSpecialReadingList = mw.config.get( 'wgTitle').indexOf('ReadingListz/') > -1 ||
(mw.config.get( 'wgTitle').indexOf('ReadingList/') > -1 && !location.host.match(/(mediawiki.org|meta.wikimedia.org)/) );
function editList() {
const pathSplit = window.location.pathname.split('/');
const id = parseInt(pathSplit[4],10);
const h1 = document.querySelector('.readinglist-collection-summary h1');
const desc = document.querySelector('.readinglist-collection-description');
const title = h1 ? h1.textContent : '';
const description = desc ? desc.textContent : '';
showOverlay(
Promise.resolve(
editListOverlay( id, title, description, null, () => {
window.location.pathname = `${READING_LIST_URL}/${id}?updated=${new Date()}`;
} )
)
);
}
function createList() {
showOverlay(
Promise.resolve(
editListOverlay( null, '', '', null, () => {
window.location.pathname = `${READING_LIST_URL}/?updated=${new Date()}`;
} )
)
);
}
function deleteList() {
const pathSplit = window.location.pathname.split('/');
const id = parseInt(pathSplit[4],10);
const ok = confirm('Are you sure you want to delete this list?');
if ( ok ) {
deleteCollection(id).then(() => {
mw.notify('List has been deleted.');
window.location.pathname = READING_LIST_URL;
})
}
}
// Makes Special:ReadingListz loook like Special:ReadingList
function registerTemporaryReadingListPage() {
if ( isSpecialReadingList ) {
const action = mwuibutton( '' );
const container = document.createElement('div')
container.setAttribute('id','reading-list-container')
$('#mw-content-text').html('').append(container)
$('#firstHeading').text('Reading lists');
if ( document.querySelectorAll( '.mw-portlet-associated-pages a' ).length === 0 ) {
const link = mw.util.addPortletLink('p-associated-pages', READING_LIST_URL, 'Your lists');
if ( link ) {
link.classList.add( 'vector-tab-noicon' );
}
}
mw.loader.using( 'special.readinglist.scripts' );
const callback = (mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type === 'childList') {
action.removeEventListener('click', createList );
const pathSplit = window.location.pathname.split('/');
// create at username
if ( pathSplit.length === 4 ) {
action.removeEventListener( 'click', deleteList );
action.textContent = 'Create list';
action.addEventListener( 'click', createList );
const editBtn = document.querySelector( '.readinglist-collection-summary .rl-edit-btn' );
if ( editBtn ) {
editBtn.parentNode.removeChild(editBtn);
}
} else {
action.textContent = 'Delete list';
action.removeEventListener( 'click', createList );
action.addEventListener( 'click', deleteList );
const summaryArea = document.querySelector( '.readinglist-collection-summary' );
const existingEdit = summaryArea.querySelector('.rl-edit-btn');
if ( summaryArea && !existingEdit ) {
const editBtn = mwuibutton( 'Edit list', 'rl-edit-btn' );
editBtn.addEventListener( 'click', editList );
summaryArea.appendChild(editBtn)
}
}
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
const config = { attributes: true, childList: true, subtree: true };
observer.observe(document.querySelector('#reading-list-container'), config);
mw.util.$content[0].appendChild( action );
}
}
function importFunctionality() {
const importValue = mw.util.getParamValue('limport') || mw.util.getParamValue('lexport');
if ( importValue ) {
const btn = mwuibutton( 'Import this list!' );
mw.util.$content[0].appendChild( btn );
btn.addEventListener( 'click', () => {
const list = fromBase64( importValue );
saveReadingList( null, list.name, list.description ).then(( id ) => {
Promise.all(
Object.keys(list.list).map((project) =>
addToList( id, list.list[project], project)
)
).then(() => {
mw.notify('List successfully imported!');
window.location.pathname = READING_LIST_URL;
});
});
} );
}
}
// Can be removed when these are added to menus
addToUserMenu();
addBookmarkNextToWatchAction();
// Experimental feature for testing import feature: remove whenever necessary.
addImportFromTable();
// @todo: Can removed when $wgReadingListsWebAuthenticatedPreviews is true everywhere.
registerTemporaryReadingListPage();
importFunctionality();
}());
},
"resources/ext.gadget.readinglist/icons.json": {
"cdxIconBookmark": "\u003Cpath d=\"M5 1a2 2 0 00-2 2v16l7-5 7 5V3a2 2 0 00-2-2z\"/\u003E",
"cdxIconBookmarkOutline": "\u003Cpath d=\"M5 1a2 2 0 00-2 2v16l7-5 7 5V3a2 2 0 00-2-2zm10 14.25-5-3.5-5 3.5V3h10z\"/\u003E"
},
"resources/ext.gadget.readinglist/api.js": function ( require, module, exports ) {
const api = new mw.Api();
const DEFAULT_READING_LIST_NAME = 'Saved';
const DEFAULT_READING_LIST_DESCRIPTION = 'Default list for your saved articles.';
//
// Existing API functions
//
/**
*
* @param {number} id of list
* @return {JQuery.Promise<any>}
*/
function deleteCollection( id ) {
return api.postWithToken( 'csrf', {
action: 'readinglists',
list: id,
command: 'delete'
} );
}
/**
* @param {string} name
* @param {string} description
* @param {Object} list
* @return {string}
*/
function toBase64( name, description, list ) {
const str = JSON.stringify( { name, description, list } );
try {
return window.btoa( dataStr );
} catch ( e ) {
return window.btoa(
encodeURIComponent( str ).replace(
/%([0-9A-F]{2})/g,
function toSolidBytes( match, p1 ) {
return String.fromCharCode( '0x' + p1 );
}
)
);
}
}
/**
* Adapted from https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
*
* @param {string} data
* @return {Object}
*/
function fromBase64( data ) {
let plainData;
try {
plainData = window.atob( data );
} catch ( e ) {
plainData = decodeURIComponent( window.atob( str ).split( '' ).map( ( c ) => {
return '%' + ( '00' + c.charCodeAt( 0 ).toString( 16 ) ).slice( -2 );
} ).join( '' ) );
}
return JSON.parse( plainData );
}
/**
* Sets up the reading list feature for new users who have never used it before.
*
* @return {jQuery.Promise<any>}
* @param {string|null} id if an existing collection
* @param {string} name of list
* @param {string} description of list
* @return {JQuery.Promise<any>}
*/
function saveReadingList( id, name, description ) {
if ( id ) {
return api.postWithToken( 'csrf', {
action: 'readinglists',
list: id,
name,
description,
command: 'update'
} );
} else {
return api.postWithToken( 'csrf', {
action: 'readinglists',
name,
description,
command: 'create'
} ).then( ( result ) => result && result.create && result.create.id );
}
}
/**
* @param {string} ownerName person who owns the list
* @param {number} [id] of the list
* @param {string} [title] of the list
* @return {string}
*/
const getReadingListUrl = ( ownerName, id, title ) => {
const READING_LIST_HOST = location.host.indexOf( 'localhost' ) > -1 ?
'' : 'https://meta.wikimedia.org';
let titlePath = 'ReadingLists';
if ( ownerName ) {
titlePath += `/${ownerName}`;
}
if ( id ) {
titlePath += `/${id}`;
}
const titleWithName = title ? `${titlePath}/${encodeURIComponent( title )}` : titlePath;
try {
return READING_LIST_HOST + (
new mw.Title( titleWithName, -1 )
).getUrl();
} catch ( e ) {
// Uncaught Error: Unable to parse title
// e.g. Special:ReadingLists/1/<script>
return READING_LIST_HOST + (
new mw.Title( titlePath, -1 )
).getUrl();
}
};
/**
* Converts API response to WVUI compatible response.
*
* @param {ApiQueryResponseReadingListItem} collection from API response
* @param {string} ownerName of collection
* @return {Card} modified collection
*/
const readingListToCard = ( collection, ownerName ) => {
const description = collection.default ?
DEFAULT_READING_LIST_DESCRIPTION : collection.description;
const name = collection.default ? DEFAULT_READING_LIST_NAME : collection.name;
const url = getReadingListUrl( ownerName, collection.id, name );
return Object.assign( {}, collection, { ownerName, name, description, url } );
};
/**
*
* @param {string} ownerName (username)
* @param {number[]} marked a list of collection IDs which have a certain title
* @return {Promise<Card[]>}
*/
function getCollections( ownerName, marked ) {
return new Promise( ( resolve, reject ) => {
api.get( {
action: 'query',
format: 'json',
rldir: 'descending',
rlsort: 'updated',
meta: 'readinglists',
formatversion: 2
} ).then( function ( /** @type {ApiQueryResponseReadingLists} */ data ) {
resolve(
( data.query.readinglists || [] ).map( ( collection ) =>
readingListToCard( collection, ownerName, marked ) )
);
}, function ( /** @type {string} */ err ) {
// setup a reading list and try again.
if ( err === 'readinglists-db-error-not-set-up' ) {
setupCollections().then( () => getCollections( ownerName, marked ) )
.then( ( /** @type {Card[]} */ collections ) => resolve( collections ) );
} else {
reject( err );
}
} );
} );
}
/**
* Sets up the reading list feature for new users who have never used it before.
*
* @return {jQuery.Promise<any>}
*/
function setupCollections() {
return api.postWithToken( 'csrf', {
action: 'readinglists',
command: 'setup'
} );
}
//
// New API functions
//
/**
*
* @return {string}
*/
function getCurrentProjectName() {
// Use wgServer to avoid issues with ".m." domain
const server = mw.config.get( 'wgServer' );
if ( server.indexOf( 'wikipedia.beta.wmflabs' ) > -1 ) {
return server.replace( 'wikipedia.beta.wmflabs', 'wikipedia' ).replace( 'https://', '' );
}
return server.indexOf( '//' ) === 0 ?
window.location.protocol + server : server;
}
/**
* Return the collections belonging to ownerName collections but with isMember key.
*
* @param {string} ownerName
* @param {string} title to check for existence in those collections
* @return {Function}
*/
function getCollectionsWithMembership( ownerName, title ) {
// make sure it's an array
return getCollections( ownerName ).then((cards) => {
return api.get( {
action: 'query',
format: 'json',
meta: 'readinglists',
rldir: 'descending',
rlsort: 'updated',
rlproject: getCurrentProjectName(),
rltitle: title,
formatversion: 2
} ).then( function ( /** @type {ApiQueryResponseReadingLists} */data ) {
const marked = data.query.readinglists.map( ( collection ) => collection.id );
return cards.map( ( card ) => {
return Object.assign( card, {
marked: marked.indexOf( card.id ) > -1
} );
} );
} );
});
}
/**
* @param {number} id
* @param {string|array} titleOrTitles
* @param {string} [projectName]
* @return {JQuery.Promise<any>}
*/
function addToList( id, titleOrTitles, projectName ) {
let project = projectName || getCurrentProjectName();
const title = typeof titleOrTitles === 'string' ? titleOrTitles : undefined;
const batch = typeof titleOrTitles !== 'string' ? JSON.stringify(
titleOrTitles.map(
( title ) => ( {
title,
project
} )
)
) : undefined;
project = batch ? undefined : project;
return api.postWithToken( 'csrf', {
action: 'readinglists',
list: id,
project,
title,
batch,
command: 'createentry'
} ).then( ( result ) => ( { id } ) );
}
/**
* @param {number} id of list
* @param {string} title of page
* @return {JQuery.Promise<ApiQueryResponseReadingListEntryItem>}
*/
function findItemInList( id, title ) {
return api.get( { action: 'query', format: 'json',
list: 'readinglistentries',
rlelists: id
} ).then( function ( data ) {
const items = data.query.readinglistentries.filter(
( /** @type {ApiQueryResponseReadingListEntryItem} */ item ) => item.title === title
);
if ( items.length === 0 ) {
throw new Error( 'findItemInList doesn\'t know how to deal with pagination yet.' );
} else {
return items[ 0 ];
}
} );
}
// Note the remove from list function currently doesn't work.
// See https://phabricator.wikimedia.org/T198990
/**
* @param {number} id
* @param {string} title
* @return {JQuery.Promise<any>}
*/
function removeFromList( id, title ) {
return findItemInList( id, title ).then( function ( entry ) {
return api.postWithToken( 'csrf', {
action: 'readinglists',
entry: entry.id,
command: 'deleteentry'
} );
} );
}
module.exports = {
getReadingListUrl,
fromBase64,
toBase64,
saveReadingList,
// New:
deleteCollection,
removeFromList,
addToList,
getCollectionsWithMembership
};
},
"resources/ext.gadget.readinglist/CollectionDialog.vue": function ( require, module, exports ) {
const { CdxButton, CdxCard, CdxIcon } = require( '@wikimedia/codex' );
const { getReadingListUrl } = require( './api.js' );
const { cdxIconBookmark, cdxIconBookmarkOutline } = require( './icons.json' );
const CdxDialog = require( './Dialog.vue' );
// @vue/component
module.exports = {
name: 'CollectionDialog',
components: {
CdxDialog,
CdxButton,
CdxIcon,
CdxCard
},
props: {
collections: {
type: Array
}
},
computed: {
collectionsUrl: () => getReadingListUrl( mw.user.getName() ),
collectionsWithThumb() {
return this.collections;
},
markedIcon: () => cdxIconBookmark,
unmarkedIcon: () => cdxIconBookmarkOutline
},
data: function () {
const selected = {};
this.collections.forEach(( collection ) => {
selected[collection.id] = collection.marked;
});
return {
selected
};
},
methods: {
createList: function () {
this.$emit( 'create' );
},
hide: function () {
this.$emit( 'hide' );
},
getName: function ( id ) {
const collections = this.collections.filter( ( c ) => c.id === id );
if ( !collections.length ) {
throw new Error( 'Unable to locate collection with id ' + id );
}
return collections[ 0 ].name;
},
select: function ( id ) {
this.$emit(
'select',
id,
this.selected[ id ],
this.getName( id ),
() => {
this.selected[ id ] = !this.selected[ id ];
}
);
}
},
props: {
collections: []
}
};;
module.exports.template = "<cdx-dialog class=\"dialog-collection\" @cancel=\"hide\" :simple=\"false\" cancel-msg=\"Cancel\" title=\"Add to existing list\"> \
<ul> \
<li v-for=\"(collection, i) in collectionsWithThumb\" :key=\"i\" @click=\"select(collection.id)\"> \
<cdx-card :key=\"collection.id\" :data-selected=\"selected[collection.id]\"> \
<template #title=\"\"> \
{{ collection.name }} \
<\/template> \
<template #description=\"\"> \
{{ collection.description }} \
<\/template> \
<template #supporting-text=\"\"> \
<cdx-icon v-if=\"selected[collection.id]\" :icon=\"markedIcon\"><\/cdx-icon> \
<cdx-icon v-else=\"\" :icon=\"unmarkedIcon\"><\/cdx-icon> \
<\/template> \
<\/cdx-card> \
<\/li> \
<\/ul> \
<footer> \
<cdx-button @click=\"createList\">Add to new list<\/cdx-button> \
<a :href=\"collectionsUrl\">Manage lists<\/a> \
<\/footer> \
<\/cdx-dialog>";
},
"resources/ext.gadget.readinglist/CollectionEditorDialog.vue": function ( require, module, exports ) {
const { CdxTextInput, CdxCard, CdxButton } = require( '@wikimedia/codex' );
const CdxDialog = require( './Dialog.vue' );
// @vue/component
module.exports = {
name: 'CollectionDialog',
components: {
CdxDialog,
CdxTextInput,
CdxButton,
CdxCard
},
data() {
return {
exists: !!this.initialTitle,
title: this.initialTitle,
description: this.initialDescription
};
},
computed: {
label() {
return this.exists ? 'Edit list' : 'Create list';
},
getDialogTitle() {
return !this.initialDescription ? 'Create reading list' : undefined;
},
isSaveDisabled() {
return !this.title;
},
suggestion() {
return {
title: this.title,
description: this.description
};
}
},
props: {
initialTitle: {
type: String,
default: ''
},
initialDescription: {
type: String,
default: ''
}
},
methods: {
cancel() {
this.$emit( 'hide' );
},
save() {
this.$emit( 'save', this.title, this.description );
}
}
};;
module.exports.template = "<cdx-dialog @cancel=\"cancel\" :simple=\"true\" :title=\"getDialogTitle\"> \
<div class=\"dialog-collection-editor-panel\"> \
<cdx-card class=\"dialog-collection-editor-panel-preview\"> \
<template #title=\"\"> \
{{ title }} \
<\/template> \
<template #description=\"\"> \
{{ description }} \
<\/template> \
<\/cdx-card> \
<label>Name<\/label> \
<cdx-text-input v-model=\"title\" placeholder=\"Name this list\" class=\"dialog-collection-editor-panel-input\"><\/cdx-text-input> \
<label>Description<\/label> \
<cdx-text-input v-model=\"description\" placeholder=\"Describe this list\" class=\"dialog-collection-input dialog-collection-editor-panel-input-description\"><\/cdx-text-input> \
<\/div> \
<template #footer=\"\"> \
<cdx-button :disabled=\"isSaveDisabled\" @click=\"save\">{{ label }}<\/cdx-button> \
<\/template> \
<\/cdx-dialog>";
},
"resources/ext.gadget.readinglist/Dialog.vue": function ( require, module, exports ) {
const wvuiIconClose = 'M4.34 2.93l12.73 12.73-1.41 1.41L2.93 4.35z M17.07 4.34L4.34 17.07l-1.41-1.41L15.66 2.93z';
const { CdxButton, CdxIcon } = require( '@wikimedia/codex' );
module.exports = {
name: 'CdxDialog',
components: {
CdxButton,
CdxIcon
},
computed: {
rootClass() {
return {
'wvui-dialog': true,
'wvui-dialog-simple': this.simple,
'wvui-dialog-complex': !this.simple
};
}
},
methods: {
onContinue() {
this.$emit( 'continue' )
},
onCancel() {
this.$emit( 'cancel' );
}
},
props: {
continueDisabled: {
type: Boolean,
default: false
},
closeIcon: {
type: String,
default: wvuiIconClose
},
continueMsg: {
type: String,
default: ''
},
cancelMsg: {
type: String,
default: ''
},
title: {
type: String,
default: 'Title of dialog'
},
simple: {
type: Boolean,
default: true
}
}
};;
module.exports.template = "<div :class=\"rootClass\"> \
<div class=\"wvui-dialog-shield\" @click=\"onCancel\"><\/div> \
<div class=\"wvui-dialog-container\" @click.stop=\"\"> \
<header class=\"wvui-dialog-container-heading\"> \
<h2>{{ title }}<\/h2> \
<cdx-icon v-if=\"cancelMsg && !simple\" class=\"wvui-dialog-container-heading-cancel\" :icon=\"closeIcon\" @click=\"onCancel\">{{ cancelMsg }}<\/cdx-icon> \
<\/header> \
<div class=\"wvui-dialog-container-content\"> \
<slot><\/slot> \
<\/div> \
<nav class=\"wvui-dialog-container-footer\"> \
<slot name=\"footer\"><\/slot> \
<\/nav> \
<\/div> \
<\/div>";
},
"resources/ext.gadget.readinglist/overlays.js": function ( require, module, exports ) {
const CollectionDialog = require( './CollectionDialog.vue' );
const CollectionEditorDialog = require( './CollectionEditorDialog.vue' );
const Vue = require( 'vue' ).default || require( 'vue' );
const { removeFromList, addToList, saveReadingList } = require( './api.js' );
function registerOverlayArea() {
if ( document.querySelectorAll('.readinglist-overlay-area').length ) {
return;
}
const node = document.createElement('div');
node.classList.add('readinglist-overlay-area');
document.body.appendChild(node);
}
function hideOverlay( promise ) {
registerOverlayArea();
const container = document.querySelector( '.readinglist-overlay-area' );
container.innerHTML = '';
}
function showOverlay( promise ) {
registerOverlayArea();
return promise.then((app) => {
app.mount( '.readinglist-overlay-area' );
});
}
function editListOverlay( existingID, name, description, title, onSaveFn ) {
const onSave = onSaveFn || ( () => {} );
return Vue.createMwApp( CollectionEditorDialog, {
initialTitle: name,
initialDescription: description,
onSave: ( name, description ) => {
hideOverlay();
saveReadingList( existingID, name, description ).then((id) => {
mw.notify( existingID ? 'List edited!' : 'List created!');
if ( title ) {
addToList( id, title ).then(() => {
mw.notify('Added title to list!');
onSave();
});
} else {
onSave();
}
}, () => {
mw.notify('Error creating list');
})
},
onHide: () => {
hideOverlay();
}
} );
}
/**
*
* @return {Object}
*/
function createMembersOverlay( title, collections ) {
return Vue.createMwApp( CollectionDialog, {
collections,
onSelect: ( id, isSelected, name, callback ) => {
if ( isSelected ) {
removeFromList( id, title ).then( () => {
mw.notify( isSelected ? `Removed page from ${name}` : `Added page to ${name}.` );
callback();
} );
} else {
addToList( id, title ).then( () => {
mw.notify( isSelected ? `Removed page from ${name}` : `Added page to ${name}.` );
callback();
} );
}
},
onCreate: () => {
showOverlay( Promise.resolve( editListOverlay( null, '', '', title ) ) );
},
onHide: () => {
hideOverlay();
}
} );
}
module.exports = {
showOverlay,
hideOverlay,
editListOverlay,
createMembersOverlay
};
// </nowiki>
}
}
}, {
"css": [
".mw-ui-icon-vector-gadget-pt-readinglists:before {\n background-image: linear-gradient(transparent, transparent), url(\"data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2220%22 height=%2220%22 viewBox=%220 0 20 20%22%3E%3Ctitle%3E bullet list %3C/title%3E%3Cg fill=%22%23000%22%3E%3Cpath d=%22M7 15h12v2H7zm0-6h12v2H7zm0-6h12v2H7z%22/%3E%3Ccircle cx=%223%22 cy=%224%22 r=%222%22/%3E%3Ccircle cx=%223%22 cy=%2210%22 r=%222%22/%3E%3Ccircle cx=%223%22 cy=%2216%22 r=%222%22/%3E%3C/g%3E%3C/svg%3E\");\n}\n.mw-ui-icon-bookmark:before {\n background-image: linear-gradient(transparent, transparent), url(\"data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2220%22 height=%2220%22 viewBox=%220 0 20 20%22%3E%3Ctitle%3E bookmark outlined %3C/title%3E%3Cg fill=%22%23000%22%3E%3Cpath d=%22M5 1a2 2 0 0 0-2 2v16l7-5 7 5V3a2 2 0 0 0-2-2zm10 14.25-5-3.5-5 3.5V3h10z%22/%3E%3C/g%3E%3C/svg%3E\");\n}\n.readinglist-overlay-area {\n z-index: 9999;\n}\n\n.dialog-collection h2 {\n font-size: 1.2em;\n border: 0;\n font-weight: bold;\n padding: 0;\n margin: 0.5em 0 0;\n}\n.dialog-collection footer {\n text-align: center;\n position: absolute;\n bottom: 10px;\n left: 0;\n right: 0;\n}\n.dialog-collection footer a {\n color: #333;\n}\n.dialog-collection ul {\n height: calc(100% - 100px);\n overflow: scroll;\n}\n.dialog-collection li {\n display: flex;\n align-items: center;\n}\n.dialog-collection li .cdx-card__text,\n.dialog-collection li .cdx-card {\n flex-grow: 1;\n}\n.dialog-collection li .cdx-card__text__supporting-text {\n justify-content: end;\n display: flex;\n}\n\n.dialog-collection-editor-panel {\n min-width: 300px;\n}\n.dialog-collection-editor-panel-preview {\n border-top: 1px solid rgba(0, 0, 0, 0.2);\n border-bottom: 1px solid rgba(0, 0, 0, 0.2);\n}\n.dialog-collection-editor-panel label {\n margin-top: 2em;\n font-weight: bold;\n margin-bottom: 0.5em;\n display: block;\n}\n.dialog-collection-editor-panel-input {\n margin-bottom: 20px;\n display: block;\n}\n.dialog-collection-editor-panel-input-description {\n margin-bottom: 20px;\n}\n\n.wvui-dialog {\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n}\n.wvui-dialog-shield,\n.wvui-dialog {\n position: fixed;\n top: 0;\n bottom: 0;\n left: 0;\n right: 0;\n}\n.wvui-dialog-shield {\n opacity: 0.5;\n background: #ccc;\n}\n.wvui-dialog-container \u003E * {\n margin: 0;\n}\n.wvui-dialog-container-button + .wvui-dialog-container-button {\n margin-top: 0.5em;\n}\n.wvui-dialog-container-heading {\n display: flex;\n}\n.wvui-dialog-container-heading h2 {\n flex-grow: 1;\n margin: 0;\n}\n.wvui-dialog-container {\n position: absolute;\n background: white;\n margin: auto;\n}\n.wvui-dialog-container-heading-continue {\n display: none;\n}\n.wvui-dialog-simple {\n align-items: center;\n}\n.wvui-dialog-simple .wvui-dialog-container-heading {\n margin: 0 0 1.25em;\n}\n.wvui-dialog-simple .wvui-dialog-container {\n padding: 1.5em;\n position: absolute;\n background: white;\n margin: auto;\n max-width: 400px;\n}\n.wvui-dialog-complex .wvui-dialog-container {\n height: 100%;\n max-height: 500px;\n}\n.wvui-dialog-complex .wvui-dialog-container-content {\n overflow: scroll;\n height: 100%;\n}\n@media all and (max-width: 400px) {\n .wvui-dialog-complex {\n align-items: flex-start;\n }\n .wvui-dialog-complex .wvui-dialog-container {\n width: 100%;\n padding-bottom: 40px;\n box-sizing: border-box;\n }\n .wvui-dialog-complex .wvui-dialog-container-heading-cancel {\n order: 1;\n padding: 16px;\n }\n .wvui-dialog-complex .wvui-dialog-container-heading h2 {\n order: 2;\n margin-left: 16px;\n padding: 16px 0;\n }\n .wvui-dialog-complex .wvui-dialog-container-heading-continue {\n display: block;\n order: 3;\n }\n .wvui-dialog-complex .wvui-dialog-container-content {\n padding: 16px;\n box-sizing: border-box;\n }\n .wvui-dialog-complex .wvui-dialog-container-footer {\n display: none;\n }\n}\n@media all and (min-width: 400px) {\n .wvui-dialog-complex .wvui-dialog-container {\n width: 500px;\n max-width: 500px;\n }\n .wvui-dialog-complex .wvui-dialog-container-heading {\n margin: 1.25em 1.25em 1em;\n }\n .wvui-dialog-complex .wvui-dialog-container-content {\n margin: 0 1.25em;\n }\n .wvui-dialog-complex .wvui-dialog-container-footer {\n border-top: 1px solid gray;\n padding: 1.25em;\n text-align: right;\n }\n}\n.wvui-dialog-container-footer {\n margin-top: 1em;\n}"
]
} );