diff --git a/tests/unit/accordion/common.js b/tests/unit/accordion/common.js
index 453506dbdfe..55778b9598b 100644
--- a/tests/unit/accordion/common.js
+++ b/tests/unit/accordion/common.js
@@ -21,6 +21,7 @@ common.testWidget( "accordion", {
"activeHeader": "ui-icon-triangle-1-s",
"header": "ui-icon-triangle-1-e"
},
+ textDir: null,
// Callbacks
activate: null,
diff --git a/tests/unit/autocomplete/common.js b/tests/unit/autocomplete/common.js
index a4d57f95db9..469ff69bdf4 100644
--- a/tests/unit/autocomplete/common.js
+++ b/tests/unit/autocomplete/common.js
@@ -21,6 +21,7 @@ common.testWidget( "autocomplete", {
collision: "none"
},
source: null,
+ textDir: null,
// Callbacks
change: null,
diff --git a/tests/unit/button/common-deprecated.js b/tests/unit/button/common-deprecated.js
index 0f03bdba271..3f48b83aefa 100644
--- a/tests/unit/button/common-deprecated.js
+++ b/tests/unit/button/common-deprecated.js
@@ -18,6 +18,7 @@ common.testWidget( "button", {
label: null,
showLabel: true,
text: true,
+ textDir: null,
// Callbacks
create: null
diff --git a/tests/unit/button/common.js b/tests/unit/button/common.js
index 91ce1cff981..ef3699d5a65 100644
--- a/tests/unit/button/common.js
+++ b/tests/unit/button/common.js
@@ -13,6 +13,7 @@ common.testWidget( "button", {
iconPosition: "beginning",
label: null,
showLabel: true,
+ textDir: null,
// Callbacks
create: null
diff --git a/tests/unit/checkboxradio/common.js b/tests/unit/checkboxradio/common.js
index 48c348a3e23..2a936fe802b 100644
--- a/tests/unit/checkboxradio/common.js
+++ b/tests/unit/checkboxradio/common.js
@@ -13,6 +13,7 @@ common.testWidget( "checkboxradio", {
disabled: null,
icon: true,
label: null,
+ textDir: null,
// Callbacks
create: null
diff --git a/tests/unit/controlgroup/common.js b/tests/unit/controlgroup/common.js
index f04a018a8af..db964703614 100644
--- a/tests/unit/controlgroup/common.js
+++ b/tests/unit/controlgroup/common.js
@@ -20,6 +20,7 @@ common.testWidget( "controlgroup", {
"controlgroupLabel": ".ui-controlgroup-label"
},
onlyVisible: true,
+ textDir: null,
// Callbacks
create: null
diff --git a/tests/unit/menu/common.js b/tests/unit/menu/common.js
index 96815c6b27f..bd389a63b14 100644
--- a/tests/unit/menu/common.js
+++ b/tests/unit/menu/common.js
@@ -17,6 +17,7 @@ common.testWidget( "menu", {
at: "right top"
},
role: "menu",
+ textDir: null,
// Callbacks
blur: null,
diff --git a/tests/unit/selectmenu/common.js b/tests/unit/selectmenu/common.js
index 97a47f2a688..50bf9263d66 100644
--- a/tests/unit/selectmenu/common.js
+++ b/tests/unit/selectmenu/common.js
@@ -19,6 +19,7 @@ common.testWidget( "selectmenu", {
at: "left bottom",
collision: "none"
},
+ textDir: null,
width: false,
// Callbacks
diff --git a/tests/unit/tabs/common.js b/tests/unit/tabs/common.js
index dd2b7381512..c75abda216c 100644
--- a/tests/unit/tabs/common.js
+++ b/tests/unit/tabs/common.js
@@ -18,6 +18,7 @@ common.testWidget( "tabs", {
heightStyle: "content",
hide: null,
show: null,
+ textDir: null,
// Callbacks
activate: null,
diff --git a/tests/unit/tooltip/common-deprecated.js b/tests/unit/tooltip/common-deprecated.js
index 2a543733579..8213250b429 100644
--- a/tests/unit/tooltip/common-deprecated.js
+++ b/tests/unit/tooltip/common-deprecated.js
@@ -18,6 +18,7 @@ common.testWidget( "tooltip", {
collision: "flipfit flip"
},
show: true,
+ textDir: null,
tooltipClass: null,
track: false,
diff --git a/tests/unit/tooltip/common.js b/tests/unit/tooltip/common.js
index 73797fe497d..5e749cfdd07 100644
--- a/tests/unit/tooltip/common.js
+++ b/tests/unit/tooltip/common.js
@@ -18,6 +18,7 @@ common.testWidget( "tooltip", {
collision: "flipfit flip"
},
show: true,
+ textDir: null,
track: false,
// Callbacks
diff --git a/ui/widget.js b/ui/widget.js
index c101e59d4c1..3587a428673 100644
--- a/ui/widget.js
+++ b/ui/widget.js
@@ -697,6 +697,32 @@ $.Widget.prototype = {
return !( $.isFunction( callback ) &&
callback.apply( this.element[ 0 ], [ event ].concat( data ) ) === false ||
event.isDefaultPrevented() );
+ },
+
+ _getTextDir: function( text ) {
+ if ( this.options.textDir === "auto" ) {
+
+ // Look for first strong (either English or Arabic/Hebrew) character.
+ // Resolve text direction accordingly ("rtl" for Arabic/Hebrew, "ltr" otherwise).
+ var matcher = /[A-Za-z\u05d0-\u065f\u066a-\u06ef\u06fa-\u07ff\ufb1d-\ufdff\ufe70-\ufefc]/.exec( text );
+ return ( matcher && ( matcher[ 0 ] > "z" ) ) ? "rtl" : "ltr";
+ }
+ return this.options.textDir;
+ },
+
+ _applyTextDir: function( param ) {
+ if ( typeof param === "string" ) {
+ param = param.replace( /[\u202A\u202B\u202C]/g, "" );
+
+ // Unicode directional characters: 202A and 202B used to enforce text direction.
+ // 202C - POP formatter closing directional segment.
+ return ( this._getTextDir( param ) === "rtl" ? "\u202B" : "\u202A" ) + param + "\u202C";
+ } else if ( param.jquery ) {
+ var isField = param.is( "input" ) || param.is( "textarea" );
+ param.css( "direction", this._getTextDir( isField ? param.val() : param.text() ) );
+ } else if ( param.nodeType === 1 ) {
+ param.style.direction = this._getTextDir( param.textContent );
+ }
}
};
diff --git a/ui/widgets/accordion.js b/ui/widgets/accordion.js
index 530d7354340..bfea9edfd10 100644
--- a/ui/widgets/accordion.js
+++ b/ui/widgets/accordion.js
@@ -54,6 +54,7 @@ return $.widget( "ui.accordion", {
activeHeader: "ui-icon-triangle-1-s",
header: "ui-icon-triangle-1-e"
},
+ textDir: null,
// Callbacks
activate: null,
@@ -305,6 +306,13 @@ return $.widget( "ui.accordion", {
this._addClass( this.active.next(), "ui-accordion-content-active" );
this.active.next().show();
+ if ( this.options.textDir ) {
+ var that = this;
+ this.headers.each( function( i, header ) {
+ header.textContent = that._applyTextDir( header.textContent );
+ } );
+ }
+
this.headers
.attr( "role", "tab" )
.each( function() {
diff --git a/ui/widgets/autocomplete.js b/ui/widgets/autocomplete.js
index 60d32654428..dd6c3ad1288 100644
--- a/ui/widgets/autocomplete.js
+++ b/ui/widgets/autocomplete.js
@@ -50,6 +50,7 @@ $.widget( "ui.autocomplete", {
collision: "none"
},
source: null,
+ textDir: null,
// Callbacks
change: null,
@@ -89,6 +90,26 @@ $.widget( "ui.autocomplete", {
this._addClass( "ui-autocomplete-input" );
this.element.attr( "autocomplete", "off" );
+ if ( this.options.textDir ) {
+ var textDir = this._getTextDir( this._value() );
+ this.element.css( "direction", textDir );
+ if ( this.options.textDir === "auto" ) {
+ this.element.css( "text-align", textDir === "rtl" ? "right" : "left" );
+ this._on( this.element, {
+ keyup: function( e ) {
+ var keyCode = $.ui.keyCode;
+ if ( e.keyCode < keyCode.PAGE_UP || e.keyCode >= keyCode.DELETE ) {
+ var textDir = this._getTextDir( this._value() );
+ this.element.css( "direction", textDir )
+ .css( "text-align", textDir === "rtl" ? "right" : "left" );
+ }
+ }
+ } );
+ } else {
+ this.element.css( "text-align",
+ this.element.css( "direction" ) === "rtl" ? "right" : "left" );
+ }
+ }
this._on( this.element, {
keydown: function( event ) {
@@ -288,6 +309,9 @@ $.widget( "ui.autocomplete", {
if ( false !== this._trigger( "select", event, { item: item } ) ) {
this._value( item.value );
+ if ( this.options.textDir === "auto" ) {
+ this.element.css( "direction", this._getTextDir( item.value ) );
+ }
}
// reset the term after the select event
@@ -521,6 +545,15 @@ $.widget( "ui.autocomplete", {
_suggest: function( items ) {
var ul = this.menu.element.empty();
this._renderMenu( ul, items );
+
+ if ( this.options.textDir ) {
+ ul.css( "text-align", ul.css( "direction" ) === "rtl" ? "right" : "left" );
+ var that = this;
+ ul.children().each( function( i, li ) {
+ li.style.direction = that._getTextDir( li.textContent );
+ } );
+ }
+
this.isNewMenu = true;
this.menu.refresh();
diff --git a/ui/widgets/button.js b/ui/widgets/button.js
index 42cfec06d2a..0e10737cea4 100644
--- a/ui/widgets/button.js
+++ b/ui/widgets/button.js
@@ -49,7 +49,8 @@ $.widget( "ui.button", {
icon: null,
iconPosition: "beginning",
label: null,
- showLabel: true
+ showLabel: true,
+ textDir: null
},
_getCreateOptions: function() {
@@ -96,6 +97,14 @@ $.widget( "ui.button", {
this.element.html( this.options.label );
}
}
+
+ if ( this.options.textDir ) {
+ this._applyTextDir( this.element );
+ if ( this.hasTitle ) {
+ this.element.attr( "title", this._applyTextDir( this.element.attr( "title" ) ) );
+ }
+ }
+
this._addClass( "ui-button", "ui-widget" );
this._setOption( "disabled", this.options.disabled );
this._enhance();
@@ -218,6 +227,9 @@ $.widget( "ui.button", {
options.showLabel = true;
}
this._super( options );
+ if ( ( this.options.textDir && options.label ) || options.textDir ) {
+ this._applyTextDir( this.element );
+ }
},
_setOption: function( key, value ) {
diff --git a/ui/widgets/checkboxradio.js b/ui/widgets/checkboxradio.js
index 228c9fab944..ecea576cef2 100644
--- a/ui/widgets/checkboxradio.js
+++ b/ui/widgets/checkboxradio.js
@@ -44,7 +44,8 @@ $.widget( "ui.checkboxradio", [ $.ui.formResetMixin, {
classes: {
"ui-checkboxradio-label": "ui-corner-all",
"ui-checkboxradio-icon": "ui-corner-all"
- }
+ },
+ textDir: null
},
_getCreateOptions: function() {
@@ -105,6 +106,13 @@ $.widget( "ui.checkboxradio", [ $.ui.formResetMixin, {
this._addClass( this.label, "ui-checkboxradio-radio-label" );
}
+ if ( this.options.textDir ) {
+ var markup = $.parseHTML( this.options.label );
+ if ( markup && markup.length === 1 && markup[ 0 ].nodeType === 3 ) {
+ this.options.label = this._applyTextDir( this.options.label );
+ }
+ }
+
if ( this.options.label && this.options.label !== this.originalLabel ) {
this._updateLabel();
} else if ( this.originalLabel ) {
@@ -208,6 +216,13 @@ $.widget( "ui.checkboxradio", [ $.ui.formResetMixin, {
this._super( key, value );
+ if ( this.options.textDir && ( key === "label" || key === "textDir" ) ) {
+ var markup = $.parseHTML( this.options.label );
+ if ( markup && markup.length === 1 && markup[ 0 ].nodeType === 3 ) {
+ this.options.label = this._applyTextDir( this.options.label );
+ }
+ }
+
if ( key === "disabled" ) {
this._toggleClass( this.label, null, "ui-state-disabled", value );
this.element[ 0 ].disabled = value;
diff --git a/ui/widgets/controlgroup.js b/ui/widgets/controlgroup.js
index c79f3fcaf22..eccc37e2d78 100644
--- a/ui/widgets/controlgroup.js
+++ b/ui/widgets/controlgroup.js
@@ -45,7 +45,8 @@ return $.widget( "ui.controlgroup", {
"checkboxradio": "input[type='checkbox'], input[type='radio']",
"selectmenu": "select",
"spinner": ".ui-spinner-input"
- }
+ },
+ textDir: null
},
_create: function() {
@@ -92,8 +93,11 @@ return $.widget( "ui.controlgroup", {
if ( element.children( ".ui-controlgroup-label-contents" ).length ) {
return;
}
- element.contents()
- .wrapAll( "" );
+ var dir = that.options.textDir ?
+ "dir='" + that._getTextDir( element.text() ) + "' " : "";
+
+ element.contents().wrapAll(
+ "" );
} );
that._addClass( labels, null, "ui-widget ui-widget-content ui-state-default" );
childWidgets = childWidgets.concat( labels.get() );
@@ -138,6 +142,9 @@ return $.widget( "ui.controlgroup", {
instanceOptions.classes =
that._resolveClassesValues( instanceOptions.classes, instance );
}
+ if ( that.options.textDir ) {
+ instanceOptions.textDir = that.options.textDir;
+ }
element[ widget ]( instanceOptions );
// Store an instance of the controlgroup to be able to reference
diff --git a/ui/widgets/menu.js b/ui/widgets/menu.js
index 302d202ae1e..db7af676ad2 100644
--- a/ui/widgets/menu.js
+++ b/ui/widgets/menu.js
@@ -51,6 +51,7 @@ return $.widget( "ui.menu", {
at: "right top"
},
role: "menu",
+ textDir: null,
// Callbacks
blur: null,
@@ -370,6 +371,16 @@ return $.widget( "ui.menu", {
if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) {
this.blur();
}
+ if ( this.options.textDir ) {
+ menus.each( function() {
+ var item = $( this );
+ item.css( "text-align", item.css( "direction" ) === "rtl" ? "right" : "left" );
+ } );
+
+ items.each( function() {
+ that._applyTextDir( this );
+ } );
+ }
},
_itemRole: function() {
diff --git a/ui/widgets/selectmenu.js b/ui/widgets/selectmenu.js
index 52139f73eea..235e6c9576b 100644
--- a/ui/widgets/selectmenu.js
+++ b/ui/widgets/selectmenu.js
@@ -60,6 +60,7 @@ return $.widget( "ui.selectmenu", [ $.ui.formResetMixin, {
collision: "none"
},
width: false,
+ textDir: null,
// Callbacks
change: null,
@@ -318,6 +319,10 @@ return $.widget( "ui.selectmenu", [ $.ui.formResetMixin, {
var that = this,
currentOptgroup = "";
+ if ( this.options.textDir ) {
+ ul.css( "text-align", ul.css( "direction" ) === "rtl" ? "right" : "left" );
+ }
+
$.each( items, function( index, item ) {
var li;
@@ -359,6 +364,9 @@ return $.widget( "ui.selectmenu", [ $.ui.formResetMixin, {
_setText: function( element, value ) {
if ( value ) {
+ if ( this.options.textDir ) {
+ value = this._applyTextDir( value );
+ }
element.text( value );
} else {
element.html( " " );
diff --git a/ui/widgets/tabs.js b/ui/widgets/tabs.js
index 14f94ae836c..ec2707132b6 100644
--- a/ui/widgets/tabs.js
+++ b/ui/widgets/tabs.js
@@ -52,6 +52,7 @@ $.widget( "ui.tabs", {
heightStyle: "content",
hide: null,
show: null,
+ textDir: null,
// Callbacks
activate: null,
@@ -371,6 +372,13 @@ $.widget( "ui.tabs", {
"aria-hidden": "true"
} );
+ if ( this.options.textDir ) {
+ var that = this;
+ this.tabs.each( function( i, tab ) {
+ that._applyTextDir( $( tab ).find( ".ui-tabs-anchor" ) );
+ } );
+ }
+
// Make sure one tab is in the tab order
if ( !this.active.length ) {
this.tabs.eq( 0 ).attr( "tabIndex", 0 );
diff --git a/ui/widgets/tooltip.js b/ui/widgets/tooltip.js
index 9a3a590639f..989ad9a65fd 100644
--- a/ui/widgets/tooltip.js
+++ b/ui/widgets/tooltip.js
@@ -61,6 +61,7 @@ $.widget( "ui.tooltip", {
},
show: true,
track: false,
+ textDir: null,
// Callbacks
close: null,
@@ -265,6 +266,9 @@ $.widget( "ui.tooltip", {
tooltipData = this._find( target );
if ( tooltipData ) {
tooltipData.tooltip.find( ".ui-tooltip-content" ).html( content );
+ if ( this.options.textDir ) {
+ this._applyTextDir( tooltipData.tooltip.find( ".ui-tooltip-content" ) );
+ }
return;
}
@@ -288,6 +292,10 @@ $.widget( "ui.tooltip", {
this._addDescribedBy( target, tooltip.attr( "id" ) );
tooltip.find( ".ui-tooltip-content" ).html( content );
+ if ( this.options.textDir ) {
+ this._applyTextDir( tooltip.find( ".ui-tooltip-content" ) );
+ }
+
// Support: Voiceover on OS X, JAWS on IE <= 9
// JAWS announces deletions even when aria-relevant="additions"
// Voiceover will sometimes re-read the entire log region's contents from the beginning