diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 02bc257b12b..c6ca0b0c90c 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -25,6 +25,7 @@ //= require ./discourse/lib/key-value-store //= require ./discourse/lib/computed //= require ./discourse/lib/formatter +//= require ./discourse/lib/text-direction //= require ./discourse/lib/eyeline //= require ./discourse/lib/show-modal //= require ./discourse/mixins/scrolling diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index c70f0b3bc96..10d1a4fa534 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -8,6 +8,7 @@ import { emojiSearch, isSkinTonableEmoji } from 'pretty-text/emoji'; import { emojiUrlFor } from 'discourse/lib/text'; import { getRegister } from 'discourse-common/lib/get-owner'; import { findRawTemplate } from 'discourse/lib/raw-templates'; +import { siteDir } from 'discourse/lib/text-direction'; import { determinePostReplaceSelection, clipboardData } from 'discourse/lib/utilities'; import toMarkdown from 'discourse/lib/to-markdown'; import deprecated from 'discourse-common/lib/deprecated'; @@ -107,6 +108,17 @@ class Toolbar { perform: e => e.applyList(i => !i ? "1. " : `${parseInt(i) + 1}. `, 'list_item') }); + if (Discourse.SiteSettings.support_mixed_text_direction) { + this.addButton({ + id: 'toggle-direction', + group: 'extras', + icon: 'arrows-h', + shortcut: 'Shift+6', + title: 'composer.toggle_direction', + perform: e => e.toggleDirection(), + }); + } + if (site.mobileView) { this.groups.push({group: 'mobileExtras', buttons: []}); } @@ -647,6 +659,14 @@ export default Ember.Component.extend({ return null; }, + _toggleDirection() { + const $textArea = $(".d-editor-input"); + let currentDir = $textArea.attr('dir') ? $textArea.attr('dir') : siteDir(), + newDir = currentDir === 'ltr' ? 'rtl' : 'ltr'; + + $textArea.attr('dir', newDir).focus(); + }, + paste(e) { if (!$(".d-editor-input").is(":focus")) { return; @@ -724,6 +744,7 @@ export default Ember.Component.extend({ addText: text => this._addText(selected, text), replaceText: text => this._addText({pre: '', post: ''}, text), getText: () => this.get('value'), + toggleDirection: () => this._toggleDirection(), }; if (button.sendAction) { diff --git a/app/assets/javascripts/discourse/components/text-field.js.es6 b/app/assets/javascripts/discourse/components/text-field.js.es6 index a9246efa7db..c2bb0eaf51c 100644 --- a/app/assets/javascripts/discourse/components/text-field.js.es6 +++ b/app/assets/javascripts/discourse/components/text-field.js.es6 @@ -1,7 +1,35 @@ import computed from "ember-addons/ember-computed-decorators"; +import { siteDir } from "discourse/lib/text-direction"; +import { isRTL } from "discourse/lib/text-direction"; +import { isLTR } from "discourse/lib/text-direction"; export default Ember.TextField.extend({ - attributeBindings: ['autocorrect', 'autocapitalize', 'autofocus', 'maxLength'], + attributeBindings: ['autocorrect', 'autocapitalize', 'autofocus', 'maxLength', 'dir'], + + @computed + dir() { + if (Discourse.SiteSettings.support_mixed_text_direction) { + let val = this.value; + if (val) { + return isRTL(val) ? 'rtl' : 'ltr'; + } else { + return siteDir(); + } + } + }, + + keyUp() { + if (Discourse.SiteSettings.support_mixed_text_direction) { + let val = this.value; + if (isRTL(val)) { + this.set('dir', 'rtl'); + } else if (isLTR(val)) { + this.set('dir', 'ltr'); + } else { + this.set('dir', siteDir()); + } + } + }, @computed("placeholderKey") placeholder(placeholderKey) { diff --git a/app/assets/javascripts/discourse/helpers/category-link.js.es6 b/app/assets/javascripts/discourse/helpers/category-link.js.es6 index 336e25314ee..de7f73f5793 100644 --- a/app/assets/javascripts/discourse/helpers/category-link.js.es6 +++ b/app/assets/javascripts/discourse/helpers/category-link.js.es6 @@ -1,4 +1,5 @@ import { registerUnbound } from 'discourse-common/lib/helpers'; +import { isRTL } from "discourse/lib/text-direction"; import { iconHTML } from 'discourse-common/lib/icon-library'; var get = Em.get, @@ -38,6 +39,7 @@ export function categoryBadgeHTML(category, opts) { let color = get(category, 'color'); let html = ""; let parentCat = null; + let categoryDir = ""; if (!opts.hideParent) { parentCat = Discourse.Category.findById(get(category, 'parent_category_id')); @@ -66,10 +68,14 @@ export function categoryBadgeHTML(category, opts) { let categoryName = escapeExpression(get(category, 'name')); + if (Discourse.SiteSettings.support_mixed_text_direction) { + categoryDir = isRTL(categoryName) ? 'dir="rtl"' : 'dir="ltr"'; + } + if (restricted) { - html += `${iconHTML('lock')}${categoryName}`; + html += `${iconHTML('lock')}${categoryName}`; } else { - html += `${categoryName}`; + html += `${categoryName}`; } html += ""; diff --git a/app/assets/javascripts/discourse/helpers/dir-span.js.es6 b/app/assets/javascripts/discourse/helpers/dir-span.js.es6 new file mode 100644 index 00000000000..6ce29e8f598 --- /dev/null +++ b/app/assets/javascripts/discourse/helpers/dir-span.js.es6 @@ -0,0 +1,14 @@ +import { registerUnbound } from "discourse-common/lib/helpers"; +import { isRTL } from 'discourse/lib/text-direction'; + +function setDir(text) { + if (Discourse.SiteSettings.support_mixed_text_direction) { + let textDir = isRTL(text) ? 'rtl' : 'ltr'; + return `${text}`; + } + return text; +} + +export default registerUnbound('dir-span', function(str) { + return new Handlebars.SafeString(setDir(str)); +}); diff --git a/app/assets/javascripts/discourse/initializers/post-decorations.js.es6 b/app/assets/javascripts/discourse/initializers/post-decorations.js.es6 index d17b2087acd..0cab9b554de 100644 --- a/app/assets/javascripts/discourse/initializers/post-decorations.js.es6 +++ b/app/assets/javascripts/discourse/initializers/post-decorations.js.es6 @@ -1,13 +1,18 @@ import highlightSyntax from 'discourse/lib/highlight-syntax'; import lightbox from 'discourse/lib/lightbox'; +import { setTextDirections } from "discourse/lib/text-direction"; import { withPluginApi } from 'discourse/lib/plugin-api'; export default { name: "post-decorations", - initialize() { + initialize(container) { withPluginApi('0.1', api => { + const siteSettings = container.lookup('site-settings:main'); api.decorateCooked(highlightSyntax); api.decorateCooked(lightbox); + if (siteSettings.support_mixed_text_direction) { + api.decorateCooked(setTextDirections); + } api.decorateCooked($elem => { const players = $('audio', $elem); diff --git a/app/assets/javascripts/discourse/lib/text-direction.js.es6 b/app/assets/javascripts/discourse/lib/text-direction.js.es6 new file mode 100644 index 00000000000..2a4b9da2c36 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/text-direction.js.es6 @@ -0,0 +1,28 @@ +const ltrChars = 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF'; +const rtlChars = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC'; + +export function isRTL(text) { + const rtlDirCheck = new RegExp('^[^'+ltrChars+']*['+rtlChars+']'); + + return rtlDirCheck.test(text); +} + +export function isLTR(text) { + const ltrDirCheck = new RegExp('^[^'+rtlChars+']*['+ltrChars+']'); + + return ltrDirCheck.test(text); +} + +export function setTextDirections($elem) { + $elem.find('*').each((i, e) => { + let $e = $(e), + textContent = $e.text(); + if (textContent) { + isRTL(textContent) ? $e.attr('dir', 'rtl') : $e.attr('dir', 'ltr'); + } + }); +} + +export function siteDir() { + return $('html').hasClass('rtl') ? 'rtl' : 'ltr'; +} diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index 69a4f9c27f2..4239398f053 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -3,6 +3,7 @@ import { flushMap } from 'discourse/models/store'; import RestModel from 'discourse/models/rest'; import { propertyEqual } from 'discourse/lib/computed'; import { longDate } from 'discourse/lib/formatter'; +import { isRTL } from 'discourse/lib/text-direction'; import computed from 'ember-addons/ember-computed-decorators'; import ActionSummary from 'discourse/models/action-summary'; import { popupAjaxError } from 'discourse/lib/ajax-error'; @@ -58,7 +59,13 @@ const Topic = RestModel.extend({ @computed('fancy_title') fancyTitle(title) { - return censor(emojiUnescape(title || ""), Discourse.Site.currentProp('censored_words')); + let fancyTitle = censor(emojiUnescape(title || ""), Discourse.Site.currentProp('censored_words')); + + if (Discourse.SiteSettings.support_mixed_text_direction) { + let titleDir = isRTL(title) ? 'rtl' : 'ltr'; + return '' + fancyTitle + ''; + } + return fancyTitle; }, // returns createdAt if there's no bumped date diff --git a/app/assets/javascripts/discourse/templates/components/categories-only.hbs b/app/assets/javascripts/discourse/templates/components/categories-only.hbs index f10712bb61e..2fa528e3a92 100644 --- a/app/assets/javascripts/discourse/templates/components/categories-only.hbs +++ b/app/assets/javascripts/discourse/templates/components/categories-only.hbs @@ -16,7 +16,7 @@
{{category-title-link category=c}}
- {{{c.description_excerpt}}} + {{{dir-span c.description_excerpt}}}
diff --git a/app/assets/javascripts/discourse/templates/components/category-title-link.hbs b/app/assets/javascripts/discourse/templates/components/category-title-link.hbs index fcb0bdeb071..3403e70d494 100644 --- a/app/assets/javascripts/discourse/templates/components/category-title-link.hbs +++ b/app/assets/javascripts/discourse/templates/components/category-title-link.hbs @@ -3,7 +3,7 @@ {{d-icon 'lock'}} {{/if}} - {{category.name}} + {{dir-span category.name}} {{#if category.uploaded_logo.url}}
{{cdn-img src=category.uploaded_logo.url class="category-logo"}}
diff --git a/app/assets/javascripts/discourse/templates/list/topic-excerpt.raw.hbs b/app/assets/javascripts/discourse/templates/list/topic-excerpt.raw.hbs index 0a42a6a0c56..1c7b5eead0e 100644 --- a/app/assets/javascripts/discourse/templates/list/topic-excerpt.raw.hbs +++ b/app/assets/javascripts/discourse/templates/list/topic-excerpt.raw.hbs @@ -1,6 +1,6 @@ {{#if topic.hasExcerpt}}
- {{{topic.escapedExcerpt}}} + {{{dir-span topic.escapedExcerpt}}} {{#if topic.excerptTruncated}} {{i18n 'read_more'}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/navigation/category.hbs b/app/assets/javascripts/discourse/templates/navigation/category.hbs index d8dcac7040c..55c7e7ef849 100644 --- a/app/assets/javascripts/discourse/templates/navigation/category.hbs +++ b/app/assets/javascripts/discourse/templates/navigation/category.hbs @@ -5,7 +5,7 @@ {{#if category.uploaded_logo.url}} {{cdn-img src=category.uploaded_logo.url class="category-logo"}} {{#if category.description}} -

{{{category.description}}}

+

{{{dir-span category.description}}}

{{/if}} {{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/category-row.hbs b/app/assets/javascripts/select-kit/templates/components/category-row.hbs index 985fe9cd74a..85cdfd8f156 100644 --- a/app/assets/javascripts/select-kit/templates/components/category-row.hbs +++ b/app/assets/javascripts/select-kit/templates/components/category-row.hbs @@ -15,7 +15,7 @@ {{/if}} {{#if shouldDisplayDescription}} -
{{{description}}}
+
{{{dir-span description}}}
{{/if}} {{else}} {{{label}}} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index dc948cbbdb6..0edc451ef10 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1278,6 +1278,7 @@ en: olist_title: "Numbered List" ulist_title: "Bulleted List" list_item: "List item" + toggle_direction: "Toggle Direction" help: "Markdown Editing Help" collapse: "minimize the composer panel" abandon: "close composer and discard draft" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index d698d82fbab..a328a54a71a 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -966,6 +966,7 @@ en: default_locale: "The default language of this Discourse instance" allow_user_locale: "Allow users to choose their own language interface preference" set_locale_from_accept_language_header: "set interface language for anonymous users from their web browser's language headers. (EXPERIMENTAL, does not work with anonymous cache)" + support_mixed_text_direction: "Support mixed left-to-right and right-to-left text directions." min_post_length: "Minimum allowed post length in characters" min_first_post_length: "Minimum allowed first post (topic body) length in characters" min_private_message_post_length: "Minimum allowed post length in characters for messages" diff --git a/config/site_settings.yml b/config/site_settings.yml index e02399e1515..3fddeee679f 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -79,6 +79,9 @@ basic: set_locale_from_accept_language_header: default: false validator: "AllowUserLocaleEnabledValidator" + support_mixed_text_direction: + client: true + default: false categories_topics: default: 20 min: 5