From 40feb61dbe9580573e74429d4a65bf40f9ead389 Mon Sep 17 00:00:00 2001 From: Mark Holmquist Date: Wed, 26 Mar 2014 23:05:47 -0700 Subject: [PATCH] Add truncatable text field, use for some fields Source + author are one such field, title is another. Loads of the mingle card will be split out. Change-Id: Ib2937cc55304118f82e5b2e87b822b2b2811ef2b Mingle: https://wikimedia.mingle.thoughtworks.com/projects/multimedia/cards/243 --- MultimediaViewer.php | 19 +- MultimediaViewerHooks.php | 1 + resources/mmv/ui/mmv.ui.metadataPanel.js | 104 ++++++----- .../mmv/ui/mmv.ui.truncatableTextField.js | 172 ++++++++++++++++++ .../mmv/ui/mmv.ui.truncatableTextField.less | 7 + tests/qunit/mmv/mmv.lightboxinterface.test.js | 14 +- .../qunit/mmv/ui/mmv.ui.metadataPanel.test.js | 5 +- .../ui/mmv.ui.truncatableTextField.test.js | 166 +++++++++++++++++ 8 files changed, 428 insertions(+), 60 deletions(-) create mode 100644 resources/mmv/ui/mmv.ui.truncatableTextField.js create mode 100644 resources/mmv/ui/mmv.ui.truncatableTextField.less create mode 100644 tests/qunit/mmv/ui/mmv.ui.truncatableTextField.test.js diff --git a/MultimediaViewer.php b/MultimediaViewer.php index 48a96c110..dfa4e3f35 100644 --- a/MultimediaViewer.php +++ b/MultimediaViewer.php @@ -383,6 +383,22 @@ $wgResourceModules += array( ), ), + 'mmv.ui.truncatableTextField' => $wgMediaViewerResourceTemplate + array( + 'scripts' => array( + 'mmv/ui/mmv.ui.truncatableTextField.js', + ), + + 'styles' => array( + 'mmv/ui/mmv.ui.truncatableTextField.less', + ), + + 'dependencies' => array( + 'mmv.HtmlUtils', + 'mmv.ui', + 'oojs', + ), + ), + 'mmv.ui.metadataPanel' => $wgMediaViewerResourceTemplate + array( 'scripts' => array( 'mmv/ui/mmv.ui.metadataPanel.js', @@ -393,6 +409,7 @@ $wgResourceModules += array( ), 'dependencies' => array( + 'mmv.HtmlUtils', 'mmv.ui', 'mmv.ui.stripeButtons', 'mmv.ui.categories', @@ -400,7 +417,7 @@ $wgResourceModules += array( 'mmv.ui.fileUsage', 'mmv.ui.permission', 'mmv.ui.reuse.dialog', - 'mmv.HtmlUtils', + 'mmv.ui.truncatableTextField', 'moment', 'oojs', ), diff --git a/MultimediaViewerHooks.php b/MultimediaViewerHooks.php index 82daf9dc7..8381ada61 100644 --- a/MultimediaViewerHooks.php +++ b/MultimediaViewerHooks.php @@ -216,6 +216,7 @@ class MultimediaViewerHooks { 'tests/qunit/mmv/ui/mmv.ui.reuse.share.test.js', 'tests/qunit/mmv/ui/mmv.ui.reuse.tab.test.js', 'tests/qunit/mmv/ui/mmv.ui.reuse.utils.test.js', + 'tests/qunit/mmv/ui/mmv.ui.truncatableTextField.test.js', 'tests/qunit/mmv/mmv.testhelpers.js', ), 'dependencies' => array( diff --git a/resources/mmv/ui/mmv.ui.metadataPanel.js b/resources/mmv/ui/mmv.ui.metadataPanel.js index 2c1714d7e..a0835c52c 100644 --- a/resources/mmv/ui/mmv.ui.metadataPanel.js +++ b/resources/mmv/ui/mmv.ui.metadataPanel.js @@ -166,30 +166,23 @@ .appendTo( this.$titleAndCredit ); this.$title = $( '' ) - .addClass( 'mw-mmv-title' ) - .appendTo( this.$titlePara ); + .addClass( 'mw-mmv-title' ); + + this.title = new mw.mmv.ui.TruncatableTextField( this.$titlePara, this.$title ); }; /** * Initializes the credit elements. */ MPP.initializeCredit = function () { - this.$source = $( '' ) - .addClass( 'mw-mmv-source' ); - - this.$author = $( '' ) - .addClass( 'mw-mmv-author' ); - this.$credit = $( '

' ) - .addClass( 'mw-mmv-credit empty' ) - .html( - mw.message( - 'multimediaviewer-credit', - this.$author.get( 0 ).outerHTML, - this.$source.get( 0 ).outerHTML - ).plain() - ) - .appendTo( this.$titleAndCredit ); + .addClass( 'mw-mmv-credit empty' ); + + this.creditField = new mw.mmv.ui.TruncatableTextField( + this.$titleAndCredit, + this.$credit, + { max: 200, small: 160 } + ); }; /** @@ -457,7 +450,7 @@ * @param {string} title */ MPP.setFileTitle = function ( title ) { - this.$title.text( title ); + this.title.set( title ); }; /** @@ -486,13 +479,54 @@ this.$datetimeLi.removeClass( 'empty' ); }; + /** + * Bignasty function for setting source and author. Both #setAuthor and + * #setSource use this with some shortcuts. + * @param {string} source With unsafe HTML + * @param {string} author With unsafe HTML + */ + MPP.setCredit = function ( source, author ) { + if ( source ) { + this.source = $( '' ) + .addClass( 'mw-mlb-source' ) + .append( $.parseHTML( source ) ) + .html(); + } else { + this.source = null; + } + + if ( author ) { + this.author = $( '' ) + .addClass( 'mw-mlb-author' ) + .append( $.parseHTML( author ) ) + .html(); + } else { + this.author = null; + } + + if ( author && source ) { + this.creditField.set( + mw.message( + 'multimediaviewer-credit', + this.author, + this.source + ).plain() + ); + } else if ( author ) { + this.creditField.set( author ); + } else if ( source ) { + this.creditField.set( source ); + } + + this.$credit.toggleClass( 'empty', !( author || source ) ); + }; + /** * Sets the source in the panel * @param {string} source Warning - unsafe HTML sometimes goes here */ MPP.setSource = function ( source ) { - this.$source.html( this.htmlUtils.htmlToTextWithLinks( source ) ); - this.$credit.removeClass( 'empty' ); + this.setCredit( source, this.author ); }; /** @@ -500,32 +534,7 @@ * @param {string} author Warning - unsafe HTML sometimes goes here */ MPP.setAuthor = function ( author ) { - this.$author.html( this.htmlUtils.htmlToTextWithLinks( author ) ); - this.$credit.removeClass( 'empty' ); - }; - - /** - * Consolidate the source and author fields into a credit field - * @param {boolean} source Do we have the source field? - * @param {boolean} author Do we have the author field? - */ - MPP.consolidateCredit = function ( source, author ) { - if ( source && author ) { - this.$credit.html( - mw.message( - 'multimediaviewer-credit', - this.$author.get( 0 ).outerHTML, - this.$source.get( 0 ).outerHTML - ).plain() - ); - } else { - // Clobber the contents and only have one of the fields - if ( source ) { - this.$credit.empty().append( this.$source ); - } else if ( author ) { - this.$credit.empty().append( this.$author ); - } - } + this.setCredit( this.source, author ); }; /** @@ -661,10 +670,7 @@ this.setAuthor( imageData.author ); } - this.consolidateCredit( !!imageData.source, !!imageData.author ); - this.buttons.set( imageData, repoData ); - this.description.set( imageData.description, image.caption ); this.categories.set( repoData.getArticlePath(), imageData.categories ); diff --git a/resources/mmv/ui/mmv.ui.truncatableTextField.js b/resources/mmv/ui/mmv.ui.truncatableTextField.js new file mode 100644 index 000000000..2ae830dbf --- /dev/null +++ b/resources/mmv/ui/mmv.ui.truncatableTextField.js @@ -0,0 +1,172 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer 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. + * + * MultimediaViewer 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 MultimediaViewer. If not, see . + */ + +( function ( mw, $, oo ) { + var TTFP; + + /** + * Represents any text field that needs to be truncated to be readable. + * @class mw.mmv.ui.TruncatableTextField + * @extends mw.mmv.ui.Element + * @constructor + * @param {jQuery} $container The container for the element. + * @param {jQuery} $element The element where we should put the text. + * @param {Object} [sizes] Overrides for the max and small properties. + * @param {number} [sizes.max=100] Override the max property. + * @param {number} [sizes.small=80] Override the small property. + */ + function TruncatableTextField( $container, $element, sizes ) { + mw.mmv.ui.Element.call( this, $container ); + + /** @property {jQuery} $element The DOM element that holds text for this element. */ + this.$element = $element; + + /** @property {mw.mmv.HtmlUtils} htmlUtils Our HTML utility instance. */ + this.htmlUtils = new mw.mmv.HtmlUtils(); + + this.$container.append( this.$element ); + + if ( sizes ) { + if ( sizes.max ) { + this.max = sizes.max; + } + + if ( sizes.small ) { + this.small = sizes.small; + } + } + } + + oo.inheritClass( TruncatableTextField, mw.mmv.ui.Element ); + + TTFP = TruncatableTextField.prototype; + + /** + * Maximum length of the field - we'll cut out the rest of the text. + * @property {number} max + */ + TTFP.max = 100; + + /** + * Maximum ideal length of the field - we'll make the font smaller after this. + * @property {number} small + */ + TTFP.small = 80; + + /** + * Sets the string for the element. + * @param {string} value Warning - unsafe HTML is allowed here. + * @override + */ + TTFP.set = function ( value ) { + this.whitelistHtmlAndSet( value ); + this.shrink(); + }; + + /** + * Whitelists HTML in the DOM element. Just a shortcut because + * this class has only one element member. Then sets the text. + * @param {string} value Has unsafe HTML. + */ + TTFP.whitelistHtmlAndSet = function ( value ) { + var $newEle = $.parseHTML( this.htmlUtils.htmlToTextWithLinks( value ) ); + this.$element.empty().append( $newEle ); + }; + + /** + * Makes the text smaller via a few different methods. + */ + TTFP.shrink = function () { + this.changeStyle(); + this.$element = this.truncate( this.$element.get( 0 ), this.max, true ); + }; + + /** + * Changes the element style if a certain length is reached. + */ + TTFP.changeStyle = function () { + this.$element.toggleClass( 'mw-mmv-truncate-toolong', this.$element.text().length > this.small ); + }; + + /** + * Truncate the text in the DOM element according to a few different rules. + * @param {HTMLElement} element + * @param {number} maxlen Maximum text length for the element. + * @param {number} [appendEllipsis=true] Whether to stick an ellipsis at the end. + * @returns {jQuery} + */ + TTFP.truncate = function ( element, maxlen, appendEllipsis ) { + var $result, curEle, + curlen = ( element.textContent || { length: 0 } ).length; + + if ( appendEllipsis === undefined ) { + appendEllipsis = true; + } + + if ( curlen <= maxlen ) { + // Easy case + return $( element ); + } + + // Make room for the ellipsis + maxlen -= appendEllipsis ? 1 : 0; + + // We're going to build up rather than remove until ready + curlen = 0; + + // Create an empty element to dump things into + $result = $( element ).clone().empty(); + + // Fetch the first child. + curEle = element.firstChild; + + while ( curEle !== null && curlen < maxlen ) { + if ( curEle.nodeType === curEle.TEXT_NODE ) { + if ( curEle.textContent.length < ( maxlen - curlen ) ) { + $result.append( curEle.cloneNode( true ) ); + } else { + $result.append( this.truncateText( curEle.textContent, maxlen - curlen ) ); + break; + } + } else { + $result.append( this.truncate( curEle.cloneNode( true ), maxlen - curlen, false ) ); + } + + curlen = $result.text().length; + curEle = curEle.nextSibling; + } + + if ( appendEllipsis ) { + $result.append( '…' ); + } + + $( element ).replaceWith( $result ); + return $result; + }; + + /** + * Truncate text to a maximum width. + * @param {string} text + * @param {number} maxlen + */ + TTFP.truncateText = function ( text, maxlen ) { + // Just return the substr for now. + return text.substr( 0, maxlen ); + }; + + mw.mmv.ui.TruncatableTextField = TruncatableTextField; +}( mediaWiki, jQuery, OO ) ); diff --git a/resources/mmv/ui/mmv.ui.truncatableTextField.less b/resources/mmv/ui/mmv.ui.truncatableTextField.less new file mode 100644 index 000000000..a0f9cc9b9 --- /dev/null +++ b/resources/mmv/ui/mmv.ui.truncatableTextField.less @@ -0,0 +1,7 @@ +.mw-mmv-truncate-toolong { + font-size: 0.8em; + + &.mw-mmv-title { + font-size: 1em; + } +} diff --git a/tests/qunit/mmv/mmv.lightboxinterface.test.js b/tests/qunit/mmv/mmv.lightboxinterface.test.js index 2a8bd5019..ecc28ef49 100644 --- a/tests/qunit/mmv/mmv.lightboxinterface.test.js +++ b/tests/qunit/mmv/mmv.lightboxinterface.test.js @@ -18,13 +18,13 @@ stubScrollTo(); function checkIfUIAreasAttachedToDocument( inDocument ) { - var msg = inDocument === 1 ? ' ' : ' not '; - assert.strictEqual( $( '.mw-mmv-wrapper' ).length, inDocument, 'Wrapper area' + msg + 'attached.' ); - assert.strictEqual( $( '.mw-mmv-main' ).length, inDocument, 'Main area' + msg + 'attached.' ); - assert.strictEqual( $( '.mw-mmv-title' ).length, inDocument, 'Title area' + msg + 'attached.' ); - assert.strictEqual( $( '.mw-mmv-author' ).length, inDocument, 'Author area' + msg + 'attached.' ); - assert.strictEqual( $( '.mw-mmv-image-desc' ).length, inDocument, 'Description area' + msg + 'attached.' ); - assert.strictEqual( $( '.mw-mmv-image-links' ).length, inDocument, 'Links area' + msg + 'attached.' ); + var msg = ( inDocument === 1 ? ' ' : ' not ' ) + 'attached.'; + assert.strictEqual( $( '.mw-mmv-wrapper' ).length, inDocument, 'Wrapper area' + msg ); + assert.strictEqual( $( '.mw-mmv-main' ).length, inDocument, 'Main area' + msg ); + assert.strictEqual( $( '.mw-mmv-title' ).length, inDocument, 'Title area' + msg ); + assert.strictEqual( $( '.mw-mmv-credit' ).length, inDocument, 'Author/source area' + msg ); + assert.strictEqual( $( '.mw-mmv-image-desc' ).length, inDocument, 'Description area' + msg ); + assert.strictEqual( $( '.mw-mmv-image-links' ).length, inDocument, 'Links area' + msg ); } // UI areas not attached to the document yet. diff --git a/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js b/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js index d52fd73dd..897be2633 100644 --- a/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js +++ b/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js @@ -118,7 +118,7 @@ ); } ); - QUnit.test( 'Setting image information works as expected', 15, function ( assert ) { + QUnit.test( 'Setting image information works as expected', 14, function ( assert ) { var gender, $qf = $( '#qunit-fixture' ), panel = new mw.mmv.ui.MetadataPanel( $qf, $( '

' ).appendTo( $qf ) ), @@ -168,9 +168,8 @@ assert.strictEqual( panel.$title.text(), title, 'Title is correctly set' ); assert.ok( !panel.$credit.hasClass( 'empty' ), 'Credit is not empty' ); assert.ok( !panel.$datetimeLi.hasClass( 'empty' ), 'Date/Time is not empty' ); - assert.strictEqual( panel.$source.html(), 'LostBar', 'Source is correctly set' ); + assert.strictEqual( panel.creditField.$element.html(), imageData.author + ' - LostBar', 'Source and author are correctly set' ); assert.ok( panel.$datetime.text().indexOf( 'August 26 2013' ) > 0, 'Correct date is displayed' ); - assert.strictEqual( panel.$author.text(), imageData.author, 'Author is correctly set' ); assert.strictEqual( panel.$license.text(), 'CC BY 2.0', 'License is correctly set' ); assert.ok( panel.$username.text().indexOf( imageData.lastUploader ) > 0, 'Correct username is displayed' ); diff --git a/tests/qunit/mmv/ui/mmv.ui.truncatableTextField.test.js b/tests/qunit/mmv/ui/mmv.ui.truncatableTextField.test.js new file mode 100644 index 000000000..0beca5d20 --- /dev/null +++ b/tests/qunit/mmv/ui/mmv.ui.truncatableTextField.test.js @@ -0,0 +1,166 @@ +/* + * This file is part of the MediaWiki extension MediaViewer. + * + * MediaViewer 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. + * + * MediaViewer 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 MediaViewer. If not, see . + */ + +( function( mw, $ ) { + QUnit.module( 'mmv.ui.TruncatableTextField', QUnit.newMwEnvironment() ); + + QUnit.test( 'Normal constructor', 2, function ( assert ) { + var $container = $( '#qunit-fixture' ), + $element = $( '
' ).appendTo( $container ).text( 'This is a unique string.' ), + ttf = new mw.mmv.ui.TruncatableTextField( $container, $element ); + + assert.strictEqual( ttf.$element.text(), 'This is a unique string.', 'The constructor set the element to the right thing.' ); + assert.strictEqual( ttf.$element.closest( '#qunit-fixture' ).length, 1, 'The constructor put the element into the container.' ); + } ); + + QUnit.test( 'Set method', 1, function ( assert ) { + var $container = $( '#qunit-fixture' ).empty(), + $element = $( '
' ).appendTo( $container ), + text = ( new Array( 500 ) ).join( 'a' ), + ttf = new mw.mmv.ui.TruncatableTextField( $container, $element ); + + ttf.htmlUtils.htmlToTextWithLinks = function ( value ) { return value; }; + ttf.shrink = function () {}; + ttf.set( text ); + + assert.strictEqual( $container.text(), text, 'Text is set accurately.' ); + } ); + + QUnit.test( 'Truncate method', 1, function ( assert ) { + var $container = $( '#qunit-fixture' ).empty(), + $element = $( '
' ).appendTo( $container ), + textOne = ( new Array( 50 ) ).join( 'a' ), + textTwo = ( new Array( 100 ) ).join( 'b' ), + ttf = new mw.mmv.ui.TruncatableTextField( $container, $element ); + + $element.append( + $( '' ).text( textOne ), + $( '' ).text( textTwo ) + ); + + // We only want to test the element exclusion here + ttf.truncateText = function () { return ''; }; + ttf.truncate( $element.get( 0 ), ttf.max, false ); + + assert.strictEqual( $container.text(), textOne, 'The too-long element is excluded.' ); + } ); + + QUnit.test( 'Truncate method', 2, function ( assert ) { + var $container = $( '#qunit-fixture' ).empty(), + $element = $( '
' ).appendTo( $container ), + textOne = ( new Array( 5 ) ).join( 'a' ), + textTwo = ( new Array( 5 ) ).join( 'b' ), + textThree = ( new Array( 5 ) ).join( 'c' ), + textFour = ( new Array( 5 ) ).join( 'd' ), + textFive = ( new Array( 100 ) ).join( 'e' ), + textFiveTruncated = ( new Array( 85 ) ).join( 'e' ), + textSix = ( new Array( 100 ) ).join( 'f' ), + ttf = new mw.mmv.ui.TruncatableTextField( $container, $element ); + + $element.append( + $( '' ) + .append( + textOne, + $( '' ).text( textTwo ) + .append( + $( '' ).text( textThree ), + $( '' ).text( textFour ) + ), + $( '' ).text( textFive ), + textSix + ) + ); + + ttf.truncate( $element.get( 0 ), ttf.max, false ); + + assert.strictEqual( $container.text().length, ttf.max, 'Correctly truncated to max length' ); + + assert.strictEqual( + $container.text(), + textOne + textTwo + textThree + textFour + textFiveTruncated, + 'Markup truncated correctly.' ); + } ); + + QUnit.test( 'Truncate method for text', 2, function ( assert ) { + var $container = $( '#qunit-fixture' ).empty(), + $element = $( '
' ).appendTo( $container ), + text = ( new Array( 500 ) ).join( 'a' ), + ttf = new mw.mmv.ui.TruncatableTextField( $container, $element ), + newText = ttf.truncateText( text, ttf.max ); + + assert.strictEqual( newText.length, 100, 'Text is the right length.' ); + assert.strictEqual( newText, ( new Array( 101 ) ).join( 'a' ), 'Text has the right content.' ); + } ); + + QUnit.test( 'Shrink method', 2, function ( assert ) { + var $container = $( '#qunit-fixture' ).empty(), + $element = $( '
' ).appendTo( $container ), + ttf = new mw.mmv.ui.TruncatableTextField( $container, $element ); + + ttf.truncate = function ( ele, max, ell ) { + assert.strictEqual( max, ttf.max, 'Max length is passed in right' ); + assert.strictEqual( ell, true, 'Ellipses are enabled on the first call' ); + }; + + ttf.shrink(); + } ); + + QUnit.test( 'Different max length - text truncation', 2, function ( assert ) { + var $container = $( '#qunit-fixture' ).empty(), + $element = $( '
' ).appendTo( $container ), + text = ( new Array( 500 ) ).join( 'a' ), + ttf = new mw.mmv.ui.TruncatableTextField( $container, $element, { max: 200 } ), + newText = ttf.truncateText( text, ttf.max ); + + assert.strictEqual( newText.length, 200, 'Text is the right length.' ); + assert.strictEqual( newText, ( new Array( 201 ) ).join( 'a' ), 'Text has the right content.' ); + } ); + + QUnit.test( 'Different max length - DOM truncation', 1, function ( assert ) { + var $container = $( '#qunit-fixture' ).empty(), + $element = $( '
' ).appendTo( $container ), + textOne = ( new Array( 150 ) ).join( 'a' ), + textTwo = ( new Array( 100 ) ).join( 'b' ), + ttf = new mw.mmv.ui.TruncatableTextField( $container, $element, { max: 200 } ); + + $element.append( + $( '' ).text( textOne ), + $( '' ).text( textTwo ) + ); + + // We only want to test the element exclusion here + ttf.truncateText = function () { return ''; }; + ttf.truncate( $element.get( 0 ), ttf.max, false ); + + assert.strictEqual( $container.text(), textOne, 'The too-long element is removed.' ); + } ); + + QUnit.test( 'Changing style for slightly too-long elements', 3, function ( assert ) { + var $container = $( '#qunit-fixture' ).empty(), + $element = $( '
' ).appendTo( $container ).text( ( new Array( 500 ) ).join( 'a' ) ), + ttf = new mw.mmv.ui.TruncatableTextField( $container, $element ); + + ttf.changeStyle(); + assert.ok( ttf.$element.hasClass( 'mw-mmv-truncate-toolong' ), 'Class set on too-long text.' ); + ttf.$element.text( 'a' ); + ttf.changeStyle(); + assert.ok( !ttf.$element.hasClass( 'mw-mmv-truncate-toolong' ), 'Class unset on short text.' ); + ttf.$element.text( ( new Array( 300 ) ).join( 'a' ) ); + ttf.changeStyle(); + assert.ok( ttf.$element.hasClass( 'mw-mmv-truncate-toolong' ), 'Class re-set on too-long text.' ); + } ); +}( mediaWiki, jQuery ) );