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