Revamp classic edit toolbar not to hardcode paths in HTML

Also, try out a way to have per-module LESS variables defined in PHP.
This might come in handy in the future… Maybe for skin theme support?

(I recommend reviewing the file changes in the order below. :D)

includes/resourceloader/ResourceLoaderFileModule.php
  * Pass the context (ResourceLoaderContext) deeper down via
    readStyleFiles() and readStyleFile(). We need it to compile the
    .less files for the right language.
  * Extract LESS compiler creation to getLessCompiler().
  * Allow passing a LESS compiler instance to compileLessFile(), rather
    than getting one after the method is called.

  All of the changes are backwards-compatible.

includes/resourceloader/ResourceLoaderEditToolbarModule.php
  * New module to support getting the language data and passing it to
    LESS variables.

  It might be a good idea to factor out a reusable class for a LESS
  module with additional variables, but that would require more
  attention to design than I gave it.

resources/src/mediawiki.action/mediawiki.action.edit.toolbar/mediawiki.action.edit.toolbar.less
  * Glue code to use the language data defined by the module above and
    put it in final CSS.

includes/EditPage.php
  * Do not hardcode image URLs in output HTML, as they are provided in
    CSS now. This gets rid of some usage of globals.

  In fact, we should be able to finally move the inline JavaScript
  calls out of getEditToolbar(), but I'm already introducing too many
  changes for one patch. That can be done later.

languages/Language.php
  * Add getImageFiles() to complement existing getImageFile() method.
    Misleadingly named, it returns paths for images for the toolbar
    only (and no other ones at all).

skins/common/ → resources/src/mediawiki.action/mediawiki.action.edit.toolbar/
  * Moved all of the button images to new location.

  Also, boring cleanup that was harder before because we treated the
  paths as public API:
  * Placed default ones in en/ subdirectory.
  * Renamed cyrl/ to ru/.
  * Renamed ksh/button_S_italic.png → ksh/button_italic.png.

languages/messages/
  * Adjusting paths and filenames for the changes above.

resources/src/mediawiki.action/mediawiki.action.edit.css
resources/src/mediawiki.action/mediawiki.action.edit.js
  * Added styles and updated the script to make it possible to have
    non-<img> elements as toolbar buttons.
  * Consolidated styles that were already required, but defined
    somewhere else:
    * `cursor: pointer;` (from shared.css)
    * `vertical-align: middle;` (from commonElements.css)

Bug: 69277
Change-Id: I39d8ed4258c7da0fe4fe4c665cdb26c86420769c
This commit is contained in:
Bartosz Dziewoński 2014-08-20 15:13:43 +02:00
parent d1b6cd35d4
commit 285c52039b
45 changed files with 253 additions and 50 deletions

View file

@ -873,6 +873,7 @@ $wgAutoloadLocalClasses = array(
'includes/resourceloader/DerivativeResourceLoaderContext.php',
'ResourceLoader' => 'includes/resourceloader/ResourceLoader.php',
'ResourceLoaderContext' => 'includes/resourceloader/ResourceLoaderContext.php',
'ResourceLoaderEditToolbarModule' => 'includes/resourceloader/ResourceLoaderEditToolbarModule.php',
'ResourceLoaderFileModule' => 'includes/resourceloader/ResourceLoaderFileModule.php',
'ResourceLoaderFilePageModule' => 'includes/resourceloader/ResourceLoaderFilePageModule.php',
'ResourceLoaderFilePath' => 'includes/resourceloader/ResourceLoaderFilePath.php',

View file

@ -3553,22 +3553,22 @@ HTML
* @return string
*/
static function getEditToolbar() {
global $wgStylePath, $wgContLang, $wgLang, $wgOut;
global $wgContLang, $wgOut;
global $wgEnableUploads, $wgForeignFileRepos;
$imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
/**
* $toolarray is an array of arrays each of which includes the
* filename of the button image (without path), the opening
* tag, the closing tag, optionally a sample text that is
* opening tag, the closing tag, optionally a sample text that is
* inserted between the two when no selection is highlighted
* and. The tip text is shown when the user moves the mouse
* over the button.
*
* Images are defined in ResourceLoaderEditToolbarModule.
*/
$toolarray = array(
array(
'image' => $wgLang->getImageFile( 'button-bold' ),
'id' => 'mw-editbutton-bold',
'open' => '\'\'\'',
'close' => '\'\'\'',
@ -3576,7 +3576,6 @@ HTML
'tip' => wfMessage( 'bold_tip' )->text(),
),
array(
'image' => $wgLang->getImageFile( 'button-italic' ),
'id' => 'mw-editbutton-italic',
'open' => '\'\'',
'close' => '\'\'',
@ -3584,7 +3583,6 @@ HTML
'tip' => wfMessage( 'italic_tip' )->text(),
),
array(
'image' => $wgLang->getImageFile( 'button-link' ),
'id' => 'mw-editbutton-link',
'open' => '[[',
'close' => ']]',
@ -3592,7 +3590,6 @@ HTML
'tip' => wfMessage( 'link_tip' )->text(),
),
array(
'image' => $wgLang->getImageFile( 'button-extlink' ),
'id' => 'mw-editbutton-extlink',
'open' => '[',
'close' => ']',
@ -3600,7 +3597,6 @@ HTML
'tip' => wfMessage( 'extlink_tip' )->text(),
),
array(
'image' => $wgLang->getImageFile( 'button-headline' ),
'id' => 'mw-editbutton-headline',
'open' => "\n== ",
'close' => " ==\n",
@ -3608,7 +3604,6 @@ HTML
'tip' => wfMessage( 'headline_tip' )->text(),
),
$imagesAvailable ? array(
'image' => $wgLang->getImageFile( 'button-image' ),
'id' => 'mw-editbutton-image',
'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
'close' => ']]',
@ -3616,7 +3611,6 @@ HTML
'tip' => wfMessage( 'image_tip' )->text(),
) : false,
$imagesAvailable ? array(
'image' => $wgLang->getImageFile( 'button-media' ),
'id' => 'mw-editbutton-media',
'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
'close' => ']]',
@ -3624,7 +3618,6 @@ HTML
'tip' => wfMessage( 'media_tip' )->text(),
) : false,
array(
'image' => $wgLang->getImageFile( 'button-nowiki' ),
'id' => 'mw-editbutton-nowiki',
'open' => "<nowiki>",
'close' => "</nowiki>",
@ -3632,7 +3625,6 @@ HTML
'tip' => wfMessage( 'nowiki_tip' )->text(),
),
array(
'image' => $wgLang->getImageFile( 'button-sig' ),
'id' => 'mw-editbutton-signature',
'open' => '--~~~~',
'close' => '',
@ -3640,7 +3632,6 @@ HTML
'tip' => wfMessage( 'sig_tip' )->text(),
),
array(
'image' => $wgLang->getImageFile( 'button-hr' ),
'id' => 'mw-editbutton-hr',
'open' => "\n----\n",
'close' => '',
@ -3656,7 +3647,8 @@ HTML
}
$params = array(
$wgStylePath . '/common/images/' . $tool['image'],
// Images are defined in ResourceLoaderEditToolbarModule
false,
// Note that we use the tip both for the ALT tag and the TITLE tag of the image.
// Older browsers show a "speedtip" type message only for ALT.
// Ideally these should be different, realistically they

View file

@ -0,0 +1,102 @@
<?php
/**
* Resource loader module for the edit toolbar.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
/**
* ResourceLoader module for the edit toolbar.
*
* @since 1.24
*/
class ResourceLoaderEditToolbarModule extends ResourceLoaderFileModule {
/**
* Serialize a string (escape and quote) for use as a CSS string value.
* http://www.w3.org/TR/2013/WD-cssom-20131205/#serialize-a-string
*
* @param string $value
* @return string
*/
private static function cssSerializeString( $value ) {
if ( strstr( $value, "\0" ) ) {
throw new Exception( "Invalid character in CSS string" );
}
$value = strtr( $value, array( '\\' => '\\\\', '"' => '\\"' ) );
$value = preg_replace_callback( '/[\x01-\x1f\x7f-\x9f]/', function ( $match ) {
return '\\' . base_convert( ord( $match[0] ), 10, 16 ) . ' ';
}, $value );
return '"' . $value . '"';
}
/**
* Get language-specific LESS variables for this module.
*
* @return array
*/
private function getLessVars( ResourceLoaderContext $context ) {
$language = Language::factory( $context->getLanguage() );
// This is very conveniently formatted and we can pass it right through
$vars = $language->getImageFiles();
// lessc tries to be helpful and parse our variables as LESS source code
foreach ( $vars as $key => &$value ) {
$value = self::cssSerializeString( $value );
}
return $vars;
}
/**
* @param ResourceLoaderContext $context
* @return int UNIX timestamp
*/
public function getModifiedTime( ResourceLoaderContext $context ) {
return max(
parent::getModifiedTime( $context ),
$this->getHashMtime( $context )
);
}
/**
* @param ResourceLoaderContext $context
* @return string Hash
*/
public function getModifiedHash( ResourceLoaderContext $context ) {
return md5(
parent::getModifiedHash( $context ) .
serialize( $this->getLessVars( $context ) )
);
}
/**
* Get a LESS compiler instance for this module.
*
* Set our variables in it.
*
* @throws MWException
* @param ResourceLoaderContext $context
* @return lessc
*/
protected function getLessCompiler( ResourceLoaderContext $context ) {
$compiler = parent::getLessCompiler();
$compiler->setVariables( $this->getLessVars( $context ) );
return $compiler;
}
}

View file

@ -385,7 +385,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
public function getStyles( ResourceLoaderContext $context ) {
$styles = $this->readStyleFiles(
$this->getStyleFiles( $context ),
$this->getFlip( $context )
$this->getFlip( $context ),
$context
);
// Collect referenced files
$this->localFileRefs = array_unique( $this->localFileRefs );
@ -816,14 +817,14 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
*
* @param array $styles List of media type/list of file paths pairs, to read, remap and
* concetenate
*
* @param bool $flip
* @param ResourceLoaderContext $context (optional)
*
* @throws MWException
* @return array List of concatenated and remapped CSS data from $styles,
* keyed by media type
*/
public function readStyleFiles( array $styles, $flip ) {
public function readStyleFiles( array $styles, $flip, $context = null ) {
if ( empty( $styles ) ) {
return array();
}
@ -831,7 +832,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
$uniqueFiles = array_unique( $files, SORT_REGULAR );
$styleFiles = array();
foreach ( $uniqueFiles as $file ) {
$styleFiles[] = $this->readStyleFile( $file, $flip );
$styleFiles[] = $this->readStyleFile( $file, $flip, $context );
}
$styles[$media] = implode( "\n", $styleFiles );
}
@ -845,11 +846,12 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
*
* @param string $path File path of style file to read
* @param bool $flip
* @param ResourceLoaderContext $context (optional)
*
* @return string CSS data in script file
* @throws MWException If the file doesn't exist
*/
protected function readStyleFile( $path, $flip ) {
protected function readStyleFile( $path, $flip, $context = null ) {
$localPath = $this->getLocalPath( $path );
$remotePath = $this->getRemotePath( $path );
if ( !file_exists( $localPath ) ) {
@ -859,7 +861,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
}
if ( $this->getStyleSheetLang( $localPath ) === 'less' ) {
$style = $this->compileLessFile( $localPath );
$compiler = $this->getLessCompiler( $context );
$style = $this->compileLessFile( $localPath, $compiler );
$this->hasGeneratedStyles = true;
} else {
$style = file_get_contents( $localPath );
@ -908,12 +911,29 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
* @since 1.22
* @throws Exception If lessc encounters a parse error
* @param string $fileName File path of LESS source
* @param lessc $compiler Compiler to use, if not default
* @return string CSS source
*/
protected function compileLessFile( $fileName ) {
$compiler = ResourceLoader::getLessCompiler( $this->getConfig() );
protected function compileLessFile( $fileName, $compiler = null ) {
if ( !$compiler ) {
$compiler = $this->getLessCompiler();
}
$result = $compiler->compileFile( $fileName );
$this->localFileRefs += array_keys( $compiler->allParsedFiles() );
return $result;
}
/**
* Get a LESS compiler instance for this module in given context.
*
* Just calls ResourceLoader::getLessCompiler() by default to get a global compiler.
*
* @param ResourceLoaderContext $context
* @throws MWException
* @since 1.24
* @return lessc
*/
protected function getLessCompiler( ResourceLoaderContext $context = null ) {
return ResourceLoader::getLessCompiler( $this->getConfig() );
}
}

View file

@ -784,6 +784,14 @@ class Language {
return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
}
/**
* @return array
* @since 1.24
*/
function getImageFiles() {
return self::$dataCache->getItem( $this->mCode, 'imageFiles' );
}
/**
* @return array
*/

View file

@ -512,16 +512,16 @@ $linkPrefixCharset = 'a-zA-Z\\x{80}-\\x{10ffff}';
* basis if needed.
*/
$imageFiles = array(
'button-bold' => 'button_bold.png',
'button-italic' => 'button_italic.png',
'button-link' => 'button_link.png',
'button-extlink' => 'button_extlink.png',
'button-headline' => 'button_headline.png',
'button-image' => 'button_image.png',
'button-media' => 'button_media.png',
'button-nowiki' => 'button_nowiki.png',
'button-sig' => 'button_sig.png',
'button-hr' => 'button_hr.png',
'button-bold' => 'en/button_bold.png',
'button-italic' => 'en/button_italic.png',
'button-link' => 'en/button_link.png',
'button-extlink' => 'en/button_extlink.png',
'button-headline' => 'en/button_headline.png',
'button-image' => 'en/button_image.png',
'button-media' => 'en/button_media.png',
'button-nowiki' => 'en/button_nowiki.png',
'button-sig' => 'en/button_sig.png',
'button-hr' => 'en/button_hr.png',
);
/**

View file

@ -209,6 +209,6 @@ $magicWords = array(
);
$imageFiles = array(
'button-italic' => 'ksh/button_S_italic.png',
'button-italic' => 'ksh/button_italic.png',
);

View file

@ -433,9 +433,9 @@ $fallback8bitEncoding = 'windows-1251';
$linkPrefixExtension = false;
$imageFiles = array(
'button-bold' => 'cyrl/button_bold.png',
'button-italic' => 'cyrl/button_italic.png',
'button-link' => 'cyrl/button_link.png',
'button-bold' => 'ru/button_bold.png',
'button-italic' => 'ru/button_italic.png',
'button-link' => 'ru/button_link.png',
);
$linkTrail = '/^([a-zабвгдеёжзийклмнопрстуфхцчшщъыьэюя]+)(.*)$/sDu';

View file

@ -984,8 +984,10 @@ return array(
'mediawiki.action.edit' => array(
'scripts' => 'resources/src/mediawiki.action/mediawiki.action.edit.js',
'styles' => 'resources/src/mediawiki.action/mediawiki.action.edit.css',
'dependencies' => array(
'mediawiki.action.edit.styles',
'mediawiki.action.edit.toolbar',
'jquery.textSelection',
'jquery.byteLimit',
),
@ -995,6 +997,10 @@ return array(
'styles' => 'resources/src/mediawiki.action/mediawiki.action.edit.styles.css',
'position' => 'top',
),
'mediawiki.action.edit.toolbar' => array(
'class' => 'ResourceLoaderEditToolbarModule',
'styles' => 'resources/src/mediawiki.action/mediawiki.action.edit.toolbar/mediawiki.action.edit.toolbar.less',
),
'mediawiki.action.edit.collapsibleFooter' => array(
'scripts' => 'resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js',
'styles' => 'resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.css',

View file

@ -0,0 +1,18 @@
/*!
* Styles for elements of the editing form, loaded only when JavaScript is enabled.
*/
.mw-toolbar-editbutton {
width: 23px;
height: 22px;
cursor: pointer;
vertical-align: middle;
/* Cross-browser inline-block */
/* Firefox 2 */
display: -moz-inline-block;
/* Modern browsers */
display: inline-block;
/* IE7 */
zoom: 1;
*display: inline;
}

View file

@ -15,6 +15,8 @@
* @private
*/
function insertButton( b, speedTip, tagOpen, tagClose, sampleText, imageId ) {
var $button;
// Backwards compatibility
if ( typeof b !== 'object' ) {
b = {
@ -26,15 +28,24 @@
imageId: imageId
};
}
var $image = $( '<img>' ).attr( {
width: 23,
height: 22,
if ( b.imageFile ) {
$button = $( '<img>' ).attr( {
src: b.imageFile,
alt: b.speedTip,
title: b.speedTip,
id: b.imageId || undefined,
'class': 'mw-toolbar-editbutton'
} ).click( function ( e ) {
} );
} else {
$button = $( '<div>' ).attr( {
title: b.speedTip,
id: b.imageId || undefined,
'class': 'mw-toolbar-editbutton'
} );
}
$button.click( function ( e ) {
if ( b.onClick !== undefined ) {
b.onClick( e );
} else {
@ -44,7 +55,7 @@
return false;
} );
$toolbar.append( $image );
$toolbar.append( $button );
}
isReady = false;
@ -74,7 +85,7 @@
* @param {Object} button Object with the following properties.
* You are required to provide *either* the `onClick` parameter, or the three parameters
* `tagOpen`, `tagClose` and `sampleText`, but not both (they're mutually exclusive).
* @param {string} button.imageFile Image to use for the button.
* @param {string} [button.imageFile] Image to use for the button.
* @param {string} button.speedTip Tooltip displayed when user mouses over the button.
* @param {Function} [button.onClick] Function to be executed when the button is clicked.
* @param {string} [button.tagOpen]
@ -82,7 +93,8 @@
* @param {string} [button.sampleText] Alternative to `onClick`. `tagOpen`, `tagClose` and
* `sampleText` together provide the markup that should be inserted into page text at
* current cursor position.
* @param {string} [button.imageId] `id` attribute of the button HTML element.
* @param {string} [button.imageId] `id` attribute of the button HTML element. Can be
* used to define the image with CSS if it's not provided as `imageFile`.
*/
addButton: function () {
if ( isReady ) {

View file

Before

Width:  |  Height:  |  Size: 533 B

After

Width:  |  Height:  |  Size: 533 B

View file

Before

Width:  |  Height:  |  Size: 484 B

After

Width:  |  Height:  |  Size: 484 B

View file

Before

Width:  |  Height:  |  Size: 532 B

After

Width:  |  Height:  |  Size: 532 B

View file

Before

Width:  |  Height:  |  Size: 557 B

After

Width:  |  Height:  |  Size: 557 B

View file

Before

Width:  |  Height:  |  Size: 874 B

After

Width:  |  Height:  |  Size: 874 B

View file

Before

Width:  |  Height:  |  Size: 255 B

After

Width:  |  Height:  |  Size: 255 B

View file

Before

Width:  |  Height:  |  Size: 260 B

After

Width:  |  Height:  |  Size: 260 B

View file

Before

Width:  |  Height:  |  Size: 250 B

After

Width:  |  Height:  |  Size: 250 B

View file

Before

Width:  |  Height:  |  Size: 435 B

After

Width:  |  Height:  |  Size: 435 B

View file

Before

Width:  |  Height:  |  Size: 440 B

After

Width:  |  Height:  |  Size: 440 B

View file

Before

Width:  |  Height:  |  Size: 200 B

After

Width:  |  Height:  |  Size: 200 B

View file

Before

Width:  |  Height:  |  Size: 483 B

After

Width:  |  Height:  |  Size: 483 B

View file

Before

Width:  |  Height:  |  Size: 250 B

After

Width:  |  Height:  |  Size: 250 B

View file

Before

Width:  |  Height:  |  Size: 280 B

After

Width:  |  Height:  |  Size: 280 B

View file

Before

Width:  |  Height:  |  Size: 728 B

After

Width:  |  Height:  |  Size: 728 B

View file

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 322 B

View file

Before

Width:  |  Height:  |  Size: 920 B

After

Width:  |  Height:  |  Size: 920 B

View file

Before

Width:  |  Height:  |  Size: 459 B

After

Width:  |  Height:  |  Size: 459 B

View file

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 392 B

View file

Before

Width:  |  Height:  |  Size: 512 B

After

Width:  |  Height:  |  Size: 512 B

View file

Before

Width:  |  Height:  |  Size: 485 B

After

Width:  |  Height:  |  Size: 485 B

View file

Before

Width:  |  Height:  |  Size: 874 B

After

Width:  |  Height:  |  Size: 874 B

View file

@ -1,5 +1,5 @@
button_S_italic.png
button_italic.png
-------------------
Source : http://commons.wikimedia.org/wiki/Image:Button_S_italic.png
License: Public domain

View file

Before

Width:  |  Height:  |  Size: 368 B

After

Width:  |  Height:  |  Size: 368 B

View file

Before

Width:  |  Height:  |  Size: 254 B

After

Width:  |  Height:  |  Size: 254 B

View file

Before

Width:  |  Height:  |  Size: 423 B

After

Width:  |  Height:  |  Size: 423 B

View file

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 278 B

View file

@ -0,0 +1,42 @@
@import "mediawiki.mixins";
#mw-editbutton-bold {
.background-image("images/@{button-bold}");
}
#mw-editbutton-italic {
.background-image("images/@{button-italic}");
}
#mw-editbutton-link {
.background-image("images/@{button-link}");
}
#mw-editbutton-extlink {
.background-image("images/@{button-extlink}");
}
#mw-editbutton-headline {
.background-image("images/@{button-headline}");
}
#mw-editbutton-image {
.background-image("images/@{button-image}");
}
#mw-editbutton-media {
.background-image("images/@{button-media}");
}
#mw-editbutton-nowiki {
.background-image("images/@{button-nowiki}");
}
// Who decided to make only this single one different than the name of the data item?
#mw-editbutton-signature {
.background-image("images/@{button-sig}");
}
#mw-editbutton-hr {
.background-image("images/@{button-hr}");
}

View file

@ -135,10 +135,6 @@ span.texhtml {
clear: both;
}
#toolbar img {
cursor: pointer;
}
/**
* File description page
*/

View file

@ -6,7 +6,7 @@
* @see https://github.com/sebastianbergmann/phpunit/blob/master/src/Extensions/PhptTestCase.php
* @author Sam Smith <samsmith@wikimedia.org>
*/
class LessFileCompilationTest extends MediaWikiTestCase {
class LessFileCompilationTest extends ResourceLoaderTestCase {
/**
* @var string $file
@ -38,7 +38,13 @@ class LessFileCompilationTest extends MediaWikiTestCase {
"$thisString must refer to a readable file"
);
$compiler = ResourceLoader::getLessCompiler( RequestContext::getMain()->getConfig() );
$rlContext = static::getResourceLoaderContext();
// Bleh
$method = new ReflectionMethod( $this->module, 'getLessCompiler' );
$method->setAccessible( true );
$compiler = $method->invoke( $this->module, $rlContext );
$this->assertNotNull( $compiler->compileFile( $this->file ) );
}