Category UI improvements

Objectives:
* Ensure items don't get moved to the end when their sort-key is edited
* Add placeholder text and pending styling to input
* Auto-expand input to the end of the line
* Make the minimum input width smaller

Changes:

ve.ui.MWMetaDialog.js
* Added calls to fitInput on initialize
* Fixed sort key update and insert handlers to maintain item position when updating

ve.ui.GroupElement.js
* Added index argument to addItems, allowing items to be inserted at a specific location

ve.ui.PagePanelLayout.js
* Fixed CSS class name

ve.ui.StackPanelLayout.js, ve.ui.MenuWidget.js, ve.ui.SelectWidget.js
* Passed index argument through to group element

ve.ui.PanelLayout.js
* Fixed overflow direction for scrolling option

ve.ui.Inspector.css
* Moved border-box properties to text input widget class
* Set input widget within inspectors to be 100% by default

ve.ui.Layout.css
* Updated CSS class name
* Whitespace fixes

ve.ui.Widget.css
* Made text input widgets's wrapper default to 20em wide and the input inside it be 100%, using border-box to ensure proper sizing
* Adjusted category list item and input styles to make input appear more like a category item
* Whitespace fixes

ve.ui.MWCategoryInputWidget.js
* Made category input widget inherit text input widget, rather than just input widget

ve.ui.MWCategoryWidget.js
* Replaced group functionality by mixing in group element
* Added fitInput, which automatically make the input fill the rest of the line or take up the entire next line depending on how much space is left

VisualEditor.i18n.php
* Adjusted placeholder text for category input

Change-Id: I79a18a7b849804027473084a42c36133fdacad57
This commit is contained in:
Trevor Parscal 2013-05-03 11:30:33 -07:00 committed by Gerrit Code Review
parent 2cbc5045b6
commit e888d7b985
13 changed files with 165 additions and 82 deletions

View file

@ -23,7 +23,7 @@ $messages['en'] = array(
'visualeditor-ca-editsource' => 'Edit source',
'visualeditor-ca-ve-edit' => 'VisualEditor',
'visualeditor-ca-ve-create' => 'VisualEditor',
'visualeditor-category-input-placeholder' => 'Category name',
'visualeditor-category-input-placeholder' => 'Add category',
'visualeditor-category-settings-label' => 'Category settings',
'visualeditor-dialog-meta-title' => 'Page settings',
'visualeditor-dialog-content-title' => 'Content settings',

View file

@ -46,7 +46,8 @@ ve.ui.MWMetaDialog.static.icon = 'settings';
* @method
*/
ve.ui.MWMetaDialog.prototype.onOpen = function () {
var surfaceModel = this.surface.getModel();
var surfaceModel = this.surface.getModel(),
categoryWidget = this.categoryWidget;
// Force all previous transactions to be separate from this history state
surfaceModel.breakpoint();
@ -54,6 +55,11 @@ ve.ui.MWMetaDialog.prototype.onOpen = function () {
// Parent method
ve.ui.PagedDialog.prototype.onOpen.call( this );
// Update input position once visible
setTimeout( function () {
categoryWidget.fitInput();
} );
};
/**
@ -175,15 +181,11 @@ ve.ui.MWMetaDialog.prototype.onNewCategory = function ( item ) {
* @param {Object} item
*/
ve.ui.MWMetaDialog.prototype.onUpdateSortKey = function ( item ) {
// Store the offset and index before removing
var offset = item.metaItem.offset,
index = item.metaItem.index;
var offset = item.metaItem.getOffset(),
index = item.metaItem.getIndex();
// Replace meta item with updated one
item.metaItem.remove();
// It would seem as if insertItem happens before the onRemove event is sent to CategoryWidget,
// Remove the reference there so it doesn't try to get removed again onMetaListInsert
delete this.categoryWidget.categories[item.name];
// Insert updated meta item at same offset and index
this.metaList.insertMeta( this.getCategoryItemForInsertion( item ), offset, index );
};
@ -194,9 +196,12 @@ ve.ui.MWMetaDialog.prototype.onUpdateSortKey = function ( item ) {
* @param {Object} ve.dm.MetaItem
*/
ve.ui.MWMetaDialog.prototype.onMetaListInsert = function ( metaItem ) {
// Responsible for adding UI components.
// Responsible for adding UI components
if ( metaItem.element.type === 'MWcategory' ) {
this.categoryWidget.addItems( [ this.getCategoryItemFromMetaListItem( metaItem ) ] );
this.categoryWidget.addItems(
[ this.getCategoryItemFromMetaListItem( metaItem ) ],
this.metaList.findItem( metaItem.getOffset(), metaItem.getIndex(), 'MWcategory' )
);
}
};

View file

@ -38,24 +38,37 @@ ve.ui.GroupElement.prototype.getItems = function () {
*
* @method
* @param {ve.ui.Element[]} items Item
* @param {number} [index] Index to insert items after
* @chainable
*/
ve.ui.GroupElement.prototype.addItems = function ( items ) {
var i, len, item;
ve.ui.GroupElement.prototype.addItems = function ( items, index ) {
var i, len, item,
$items = $( [] );
for ( i = 0, len = items.length; i < len; i++ ) {
item = items[i];
// Check if item exists then remove it first, effectively "moving" it
if ( this.items.indexOf( item ) !== -1 ) {
this.removeItems( [item] );
this.removeItems( [ item ] );
}
// Add the item
this.items.push( item );
this.$.append( item.$ );
this.$items = this.$items.add( item.$ );
$items = $items.add( item.$ );
}
if ( index === undefined || index < 0 || index >= this.items.length ) {
this.$group.append( $items );
this.items.push.apply( this.items, items );
} else if ( index === 0 ) {
this.$group.prepend( $items );
this.items.unshift.apply( this.items, items );
} else {
this.$items.eq( index ).before( $items );
this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
}
this.$items = this.$items.add( $items );
return this;
};

View file

@ -31,7 +31,7 @@ ve.ui.PagePanelLayout = function VeUiPagePanelLayout( config ) {
// Initialization
this.$label.addClass( 've-ui-icon-' + config.icon + '-big' );
this.$.append( this.$label ).addClass( 've-ui-editorPanelLayout' );
this.$.append( this.$label ).addClass( 've-ui-pagedPanelLayout' );
};
/* Inheritance */

View file

@ -48,9 +48,10 @@ ve.mixinClass( ve.ui.StackPanelLayout, ve.ui.GroupElement );
*
* @method
* @param {ve.ui.PanelLayout[]} items Items to add
* @param {number} [index] Index to insert items after
* @chainable
*/
ve.ui.StackPanelLayout.prototype.addItems = function ( items ) {
ve.ui.StackPanelLayout.prototype.addItems = function ( items, index ) {
var i, len;
for ( i = 0, len = items.length; i < len; i++ ) {
@ -60,7 +61,7 @@ ve.ui.StackPanelLayout.prototype.addItems = function ( items ) {
items[i].$.hide();
}
}
ve.ui.GroupElement.prototype.addItems.call( this, items );
ve.ui.GroupElement.prototype.addItems.call( this, items, index );
return this;
};

View file

@ -25,7 +25,7 @@ ve.ui.PanelLayout = function VeUiPanelLayout( config ) {
// Initialization
this.$.addClass( 've-ui-panelLayout' );
if ( config.scroll ) {
this.$.css( 'overflow-x', 'auto' );
this.$.css( 'overflow-y', 'auto' );
}
};

View file

@ -43,10 +43,7 @@
white-space: nowrap;
}
.ve-ui-window-body .ve-ui-textInputWidget input {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
.ve-ui-window-body .ve-ui-textInputWidget {
width: 100%;
}

View file

@ -23,15 +23,16 @@
/* ve.ui.EditorPanelLayout */
.ve-ui-editorPanelLayout {
.ve-ui-pagedPanelLayout {
padding: 1.5em;
width: 100%;
-webkit-box-sizing:border-box;
-moz-box-sizing:border-box;
box-sizing:border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
overflow: hidden;
}
.ve-ui-editorPanelLayout > .ve-ui-labeledElement-label {
.ve-ui-pagedPanelLayout > .ve-ui-labeledElement-label {
font-size: 1.5em;
padding-left: 1.75em;
margin-bottom: 1em;

View file

@ -194,6 +194,14 @@
/* ve.ui.TextInputWidget */
.ve-ui-textInputWidget {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
width: 20em;
position: relative;
}
.ve-ui-textInputWidget input,
.ve-ui-textInputWidget input:focus[readonly],
.ve-ui-widget-disabled.ve-ui-textInputWidget input:focus {
@ -205,6 +213,10 @@
box-shadow: 0 0 0 white, inset 0 0.1em 0.2em #ddd;
padding: 0.5em;
border-radius: 0.25em;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
/* Animation */
-webkit-transition: border-color 200ms, box-shadow 200ms, background-color 200ms;
@ -354,7 +366,8 @@
.ve-ui-mwCategoryListItemWidget {
position: relative;
float: left;
margin: .25em;
margin-right: 0.5em;
margin-top: 0.5em;
}
.ve-ui-mwCategoryListItemButton {
@ -425,17 +438,17 @@
.ve-ui-mwCategoryInputWidget {
float: left;
width: 15em;
margin-top: 0.5em;
}
.ve-ui-mwCategoryInputWidget input {
display: inline-block;
font-size: 1.1em;
font-family: sans-serif;
background: none;
/* HACK: to match the categoryListItem height */
border: 1px solid #fff;
outline: none;
padding: 0.5em;
border-radius: 2em;
font-size: 1.05em;
margin-top: 0;
padding-left: 0.75em;
padding-right: 0.75em;
background-color: #fff;
}
.ve-ui-mwCategoryPopupMenu {
@ -445,7 +458,7 @@
}
.ve-ui-mwCategoryPopupMenu .ve-ui-iconButtonWidget {
display:block;
display: block;
float: left;
}
@ -491,4 +504,4 @@
/* @noflip */
.ve-ui-ltr {
direction: ltr;
}
}

View file

@ -11,7 +11,7 @@
* Creates an ve.ui.MWCategoryInputWidget object.
*
* @class
* @extends ve.ui.InputWidget
* @extends ve.ui.TextInputWidget
* @mixins ve.ui.PendingInputWidget
* @mixins ve.ui.LookupInputWidget
*
@ -25,7 +25,7 @@ ve.ui.MWCategoryInputWidget = function VeUiMWCategoryInputWidget( categoryWidget
}, config );
// Parent constructor
ve.ui.InputWidget.call( this, config );
ve.ui.TextInputWidget.call( this, config );
// Mixin constructors
ve.ui.PendingInputWidget.call( this );
@ -43,15 +43,11 @@ ve.ui.MWCategoryInputWidget = function VeUiMWCategoryInputWidget( categoryWidget
/* Inheritance */
ve.inheritClass( ve.ui.MWCategoryInputWidget, ve.ui.InputWidget );
ve.inheritClass( ve.ui.MWCategoryInputWidget, ve.ui.TextInputWidget );
ve.mixinClass( ve.ui.MWCategoryInputWidget, ve.ui.PendingInputWidget );
ve.mixinClass( ve.ui.MWCategoryInputWidget, ve.ui.LookupInputWidget );
/* Static Properties */
ve.ui.MWCategoryInputWidget.static.inputType = 'text';
/* Methods */
/**

View file

@ -11,6 +11,7 @@
* @class
* @abstract
* @extends ve.ui.Widget
* @mixin ve.ui.GroupElement
*
* @constructor
* @param {Object} [config] Config options
@ -22,21 +23,23 @@ ve.ui.MWCategoryWidget = function VeUiMWCategoryWidget( config ) {
// Parent constructor
ve.ui.Widget.call( this, config );
// Mixin constructors
ve.ui.GroupElement.call( this, this.$$( '<div>' ), config );
// Properties
this.categories = {};
this.$categories = this.$$( '<div>' );
this.popupState = false;
this.savedPopupState = false;
this.popup = new ve.ui.MWCategoryPopupWidget( {
'$$': this.$$, 'align': 'right', '$overlay': config.$overlay
} );
this.categoryInput = new ve.ui.MWCategoryInputWidget( this, {
this.input = new ve.ui.MWCategoryInputWidget( this, {
'$$': this.$$, '$overlay': config.$overlay, '$container': this.$
} );
// Events
this.categoryInput.$input.on( 'keydown', ve.bind( this.onLookupInputKeyDown, this ) );
this.categoryInput.lookupMenu.connect( this, { 'select': 'onLookupMenuItemSelect' } );
this.input.$input.on( 'keydown', ve.bind( this.onLookupInputKeyDown, this ) );
this.input.lookupMenu.connect( this, { 'select': 'onLookupMenuItemSelect' } );
this.popup.connect( this, {
'removeCategory': 'onRemoveCategory',
'updateSortkey': 'onUpdateSortkey',
@ -46,8 +49,8 @@ ve.ui.MWCategoryWidget = function VeUiMWCategoryWidget( config ) {
// Initialization
this.$.addClass( 've-ui-mwCategoryListWidget' )
.append(
this.$categories,
this.categoryInput.$,
this.$group,
this.input.$,
this.$$( '<div>' ).css( 'clear', 'both' )
);
};
@ -77,12 +80,12 @@ ve.inheritClass( ve.ui.MWCategoryWidget, ve.ui.Widget );
* @param {jQuery.Event} e Input key down event
*/
ve.ui.MWCategoryWidget.prototype.onLookupInputKeyDown = function ( e ) {
if ( this.categoryInput.getValue() !== '' && e.which === 13 ) {
if ( this.input.getValue() !== '' && e.which === 13 ) {
this.emit(
'newCategory',
this.categoryInput.getCategoryItemFromValue( this.categoryInput.getValue() )
this.input.getCategoryItemFromValue( this.input.getValue() )
);
this.categoryInput.setValue( '' );
this.input.setValue( '' );
}
};
@ -94,8 +97,8 @@ ve.ui.MWCategoryWidget.prototype.onLookupInputKeyDown = function ( e ) {
*/
ve.ui.MWCategoryWidget.prototype.onLookupMenuItemSelect = function ( item ) {
if ( item && item.getData() !== '' ) {
this.emit( 'newCategory', this.categoryInput.getCategoryItemFromValue( item.getData() ) );
this.categoryInput.setValue( '' );
this.emit( 'newCategory', this.input.getCategoryItemFromValue( item.getData() ) );
this.input.setValue( '' );
}
};
@ -161,37 +164,53 @@ ve.ui.MWCategoryWidget.prototype.getCategories = function () {
* Adds category items.
*
* @method
* @param {Object[]} items [description]
* @param {Object[]} items Items to add
* @param {number} [index] Index to insert items after
* @chainable
*/
ve.ui.MWCategoryWidget.prototype.addItems = function ( items ) {
var i, len, item, categoryGroupItem,
existingCategoryGroupItem = null;
ve.ui.MWCategoryWidget.prototype.addItems = function ( items, index ) {
var i, len, item, categoryItem,
categoryItems = [],
existingCategoryItem = null;
for ( i = 0, len = items.length; i < len; i++ ) {
item = items[i];
// Filter out categories derived from aliens.
// TODO: Remove the block below once aliens no longer add items to metalist.
if( 'html/0/about' in item.metaItem.element.attributes ) {
return this;
// HACK: Filter out categories derived from aliens
// TODO: Remove this bit once aliens no longer add items to metalist
if ( 'html/0/about' in item.metaItem.element.attributes ) {
continue;
}
categoryGroupItem = new ve.ui.MWCategoryItemWidget( { '$$': this.$$, 'item': item } );
// Bind category item events.
categoryGroupItem.connect( this, {
// Create a widget using the item data
categoryItem = new ve.ui.MWCategoryItemWidget( { '$$': this.$$, 'item': item } );
categoryItem.connect( this, {
'savePopupState': 'onSavePopupState',
'togglePopupMenu': 'onTogglePoupupMenu'
} );
// Auto-remove existing items by value
if ( item.value in this.categories ) {
existingCategoryGroupItem = this.categories[item.value];
this.categories[item.value].metaItem.remove();
// Save reference to item
existingCategoryItem = this.categories[item.value];
// Removal in model will trigger #removeItems in widget
existingCategoryItem.metaItem.remove();
// Adjust index to compensate for removal
index = Math.max( index - 1, 0 );
}
this.categories[item.value] = categoryGroupItem;
if ( existingCategoryGroupItem ) {
categoryGroupItem.sortKey = existingCategoryGroupItem.sortKey;
// Index item by value
this.categories[item.value] = categoryItem;
// Copy sortKey from old item when "moving"
if ( existingCategoryItem ) {
categoryItem.sortKey = existingCategoryItem.sortKey;
}
this.$categories.append( categoryGroupItem.$ );
categoryItems.push( categoryItem );
}
ve.ui.GroupElement.prototype.addItems.call( this, categoryItems, index );
this.fitInput();
return this;
};
@ -202,10 +221,46 @@ ve.ui.MWCategoryWidget.prototype.addItems = function ( items ) {
* @param {string[]} names Names of categories to remove
*/
ve.ui.MWCategoryWidget.prototype.removeItems = function ( names ) {
var i, len;
var i, len, categoryItem,
items = [];
for ( i = 0, len = names.length; i < len; i++ ) {
this.categories[names[i]].$.remove();
categoryItem = this.categories[names[i]];
categoryItem.disconnect( this );
items.push( categoryItem );
delete this.categories[names[i]];
}
ve.ui.GroupElement.prototype.removeItems.call( this, items );
this.fitInput();
};
/**
* Auto-fit the input.
*
* @method
*/
ve.ui.MWCategoryWidget.prototype.fitInput = function () {
var gap, min, margin, $lastItem,
$input = this.input.$;
if ( !$input.is( ':visible') ) {
return;
}
$input.css( { 'width': 'inherit' } );
min = $input.outerWidth();
$input.css( { 'width': '100%' } );
$lastItem = this.$.find( '.ve-ui-mwCategoryListItemWidget:last' );
if ( $lastItem.length ) {
margin = $input.offset().left - this.$.offset().left;
// Try to fit to the right of the last item
gap = ( $input.offset().left + $input.outerWidth() ) -
( $lastItem.offset().left + $lastItem.outerWidth() );
if ( gap >= min ) {
$input.css( { 'width': Math.round( gap - ( ( margin ) * 2 ) ) + 'px' } );
}
}
};

View file

@ -132,12 +132,13 @@ ve.ui.MenuWidget.prototype.selectItem = function ( item, silent ) {
*
* @method
* @param {ve.ui.MenuItemWidget[]} items Items to add
* @param {number} [index] Index to insert items after
* @chainable
*/
ve.ui.MenuWidget.prototype.addItems = function ( items ) {
ve.ui.MenuWidget.prototype.addItems = function ( items, index ) {
var i, len, item;
ve.ui.SelectWidget.prototype.addItems.call( this, items );
ve.ui.SelectWidget.prototype.addItems.call( this, items, index );
for ( i = 0, len = items.length; i < len; i++ ) {
item = items[i];

View file

@ -328,9 +328,10 @@ ve.ui.SelectWidget.prototype.getClosestSelectableItem = function ( index ) {
*
* @method
* @param {ve.ui.OptionWidget[]} items Items to add
* @param {number} [index] Index to insert items after
* @chainable
*/
ve.ui.SelectWidget.prototype.addItems = function ( items ) {
ve.ui.SelectWidget.prototype.addItems = function ( items, index ) {
var i, len, item, hash;
for ( i = 0, len = items.length; i < len; i++ ) {
@ -344,7 +345,7 @@ ve.ui.SelectWidget.prototype.addItems = function ( items ) {
this.hashes[hash] = item;
}
}
ve.ui.GroupElement.prototype.addItems.call( this, items );
ve.ui.GroupElement.prototype.addItems.call( this, items, index );
return this;
};