Fix #186 - Add RTL support to the compose form textarea and statuses output
This commit is contained in:
		
							parent
							
								
									809455aaae
								
							
						
					
					
						commit
						d180aaa2a7
					
				
					 6 changed files with 57 additions and 4 deletions
				
			
		|  | @ -1,5 +1,6 @@ | ||||||
| import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; | import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; | ||||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||||
|  | import { isRtl } from '../rtl'; | ||||||
| 
 | 
 | ||||||
| const textAtCursorMatchesToken = (str, caretPosition) => { | const textAtCursorMatchesToken = (str, caretPosition) => { | ||||||
|   let word; |   let word; | ||||||
|  | @ -176,6 +177,11 @@ const AutosuggestTextarea = React.createClass({ | ||||||
|     const { value, suggestions, fileDropDate, disabled, placeholder, onKeyUp } = this.props; |     const { value, suggestions, fileDropDate, disabled, placeholder, onKeyUp } = this.props; | ||||||
|     const { isFileDragging, suggestionsHidden, selectedSuggestion } = this.state; |     const { isFileDragging, suggestionsHidden, selectedSuggestion } = this.state; | ||||||
|     const className = isFileDragging ? 'autosuggest-textarea__textarea file-drop' : 'autosuggest-textarea__textarea'; |     const className = isFileDragging ? 'autosuggest-textarea__textarea file-drop' : 'autosuggest-textarea__textarea'; | ||||||
|  |     const style     = { direction: 'ltr' }; | ||||||
|  | 
 | ||||||
|  |     if (isRtl(value)) { | ||||||
|  |       style.direction = 'rtl'; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <div className='autosuggest-textarea'> |       <div className='autosuggest-textarea'> | ||||||
|  | @ -192,6 +198,7 @@ const AutosuggestTextarea = React.createClass({ | ||||||
|           onBlur={this.onBlur} |           onBlur={this.onBlur} | ||||||
|           onDragEnter={this.onDragEnter} |           onDragEnter={this.onDragEnter} | ||||||
|           onDragExit={this.onDragExit} |           onDragExit={this.onDragExit} | ||||||
|  |           style={style} | ||||||
|         /> |         /> | ||||||
| 
 | 
 | ||||||
|         <div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'> |         <div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'> | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||||
| import PureRenderMixin from 'react-addons-pure-render-mixin'; | import PureRenderMixin from 'react-addons-pure-render-mixin'; | ||||||
| import escapeTextContentForBrowser from 'escape-html'; | import escapeTextContentForBrowser from 'escape-html'; | ||||||
| import emojify from '../emoji'; | import emojify from '../emoji'; | ||||||
|  | import { isRtl } from '../rtl'; | ||||||
| import { FormattedMessage } from 'react-intl'; | import { FormattedMessage } from 'react-intl'; | ||||||
| import Permalink from './permalink'; | import Permalink from './permalink'; | ||||||
| 
 | 
 | ||||||
|  | @ -92,6 +93,11 @@ const StatusContent = React.createClass({ | ||||||
| 
 | 
 | ||||||
|     const content = { __html: emojify(status.get('content')) }; |     const content = { __html: emojify(status.get('content')) }; | ||||||
|     const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) }; |     const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) }; | ||||||
|  |     const directionStyle = { direction: 'ltr' }; | ||||||
|  | 
 | ||||||
|  |     if (isRtl(status.get('content'))) { | ||||||
|  |       directionStyle.direction = 'rtl'; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     if (status.get('spoiler_text').length > 0) { |     if (status.get('spoiler_text').length > 0) { | ||||||
|       let mentionsPlaceholder = ''; |       let mentionsPlaceholder = ''; | ||||||
|  | @ -116,14 +122,14 @@ const StatusContent = React.createClass({ | ||||||
| 
 | 
 | ||||||
|           {mentionsPlaceholder} |           {mentionsPlaceholder} | ||||||
| 
 | 
 | ||||||
|           <div style={{ display: hidden ? 'none' : 'block' }} dangerouslySetInnerHTML={content} /> |           <div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} /> | ||||||
|         </div> |         </div> | ||||||
|       ); |       ); | ||||||
|     } else { |     } else { | ||||||
|       return ( |       return ( | ||||||
|         <div |         <div | ||||||
|           className='status__content' |           className='status__content' | ||||||
|           style={{ cursor: 'pointer' }} |           style={{ cursor: 'pointer', ...directionStyle }} | ||||||
|           onMouseDown={this.handleMouseDown} |           onMouseDown={this.handleMouseDown} | ||||||
|           onMouseUp={this.handleMouseUp} |           onMouseUp={this.handleMouseUp} | ||||||
|           dangerouslySetInnerHTML={content} |           dangerouslySetInnerHTML={content} | ||||||
|  |  | ||||||
							
								
								
									
										27
									
								
								app/assets/javascripts/components/rtl.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/assets/javascripts/components/rtl.jsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | ||||||
|  | // U+0590  to U+05FF  - Hebrew | ||||||
|  | // U+0600  to U+06FF  - Arabic | ||||||
|  | // U+0700  to U+074F  - Syriac | ||||||
|  | // U+0750  to U+077F  - Arabic Supplement | ||||||
|  | // U+0780  to U+07BF  - Thaana | ||||||
|  | // U+07C0  to U+07FF  - N'Ko | ||||||
|  | // U+0800  to U+083F  - Samaritan | ||||||
|  | // U+08A0  to U+08FF  - Arabic Extended-A | ||||||
|  | // U+FB1D  to U+FB4F  - Hebrew presentation forms | ||||||
|  | // U+FB50  to U+FDFF  - Arabic presentation forms A | ||||||
|  | // U+FE70  to U+FEFF  - Arabic presentation forms B | ||||||
|  | 
 | ||||||
|  | const rtlChars = /[\u0590-\u083F]|[\u08A0-\u08FF]|[\uFB1D-\uFDFF]|[\uFE70-\uFEFF]/mg; | ||||||
|  | 
 | ||||||
|  | export function isRtl(text) { | ||||||
|  |   if (text.length === 0) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const matches = text.match(rtlChars); | ||||||
|  | 
 | ||||||
|  |   if (!matches) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return matches.length / text.trim().length > 0.3; | ||||||
|  | }; | ||||||
|  | @ -37,4 +37,17 @@ module StreamEntriesHelper | ||||||
|   def proper_status(status) |   def proper_status(status) | ||||||
|     status.reblog? ? status.reblog : status |     status.reblog? ? status.reblog : status | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   def rtl?(text) | ||||||
|  |     return false if text.empty? | ||||||
|  | 
 | ||||||
|  |     matches = /[\p{Hebrew}|\p{Arabic}|\p{Syriac}|\p{Thaana}|\p{Nko}]+/m.match(text) | ||||||
|  | 
 | ||||||
|  |     return false unless matches | ||||||
|  | 
 | ||||||
|  |     rtl_size = matches.to_a.reduce(0) { |acc, elem| acc + elem.size }.to_f | ||||||
|  |     ltr_size = text.strip.size.to_f | ||||||
|  | 
 | ||||||
|  |     rtl_size / ltr_size > 0.3 | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ | ||||||
|   .status__content.e-content.p-name.emojify< |   .status__content.e-content.p-name.emojify< | ||||||
|     - unless status.spoiler_text.blank? |     - unless status.spoiler_text.blank? | ||||||
|       %p= status.spoiler_text |       %p= status.spoiler_text | ||||||
|     = Formatter.instance.format(status) |     %div{ style: "direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status) | ||||||
| 
 | 
 | ||||||
|   - unless status.media_attachments.empty? |   - unless status.media_attachments.empty? | ||||||
|     - if status.media_attachments.first.video? |     - if status.media_attachments.first.video? | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ | ||||||
|   .status__content.e-content.p-name.emojify< |   .status__content.e-content.p-name.emojify< | ||||||
|     - unless status.spoiler_text.blank? |     - unless status.spoiler_text.blank? | ||||||
|       %p= status.spoiler_text |       %p= status.spoiler_text | ||||||
|     = Formatter.instance.format(status) |     %div{ style: "direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status) | ||||||
| 
 | 
 | ||||||
|   - unless status.media_attachments.empty? |   - unless status.media_attachments.empty? | ||||||
|     .status__attachments |     .status__attachments | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue