Add support for mixed text directions
This commit is contained in:
parent
4c19088084
commit
caa38aaaad
|
@ -25,6 +25,7 @@
|
||||||
//= require ./discourse/lib/key-value-store
|
//= require ./discourse/lib/key-value-store
|
||||||
//= require ./discourse/lib/computed
|
//= require ./discourse/lib/computed
|
||||||
//= require ./discourse/lib/formatter
|
//= require ./discourse/lib/formatter
|
||||||
|
//= require ./discourse/lib/text-direction
|
||||||
//= require ./discourse/lib/eyeline
|
//= require ./discourse/lib/eyeline
|
||||||
//= require ./discourse/lib/show-modal
|
//= require ./discourse/lib/show-modal
|
||||||
//= require ./discourse/mixins/scrolling
|
//= require ./discourse/mixins/scrolling
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { emojiSearch, isSkinTonableEmoji } from 'pretty-text/emoji';
|
||||||
import { emojiUrlFor } from 'discourse/lib/text';
|
import { emojiUrlFor } from 'discourse/lib/text';
|
||||||
import { getRegister } from 'discourse-common/lib/get-owner';
|
import { getRegister } from 'discourse-common/lib/get-owner';
|
||||||
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
import { findRawTemplate } from 'discourse/lib/raw-templates';
|
||||||
|
import { siteDir } from 'discourse/lib/text-direction';
|
||||||
import { determinePostReplaceSelection, clipboardData } from 'discourse/lib/utilities';
|
import { determinePostReplaceSelection, clipboardData } from 'discourse/lib/utilities';
|
||||||
import toMarkdown from 'discourse/lib/to-markdown';
|
import toMarkdown from 'discourse/lib/to-markdown';
|
||||||
import deprecated from 'discourse-common/lib/deprecated';
|
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')
|
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) {
|
if (site.mobileView) {
|
||||||
this.groups.push({group: 'mobileExtras', buttons: []});
|
this.groups.push({group: 'mobileExtras', buttons: []});
|
||||||
}
|
}
|
||||||
|
@ -647,6 +659,14 @@ export default Ember.Component.extend({
|
||||||
return null;
|
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) {
|
paste(e) {
|
||||||
if (!$(".d-editor-input").is(":focus")) {
|
if (!$(".d-editor-input").is(":focus")) {
|
||||||
return;
|
return;
|
||||||
|
@ -724,6 +744,7 @@ export default Ember.Component.extend({
|
||||||
addText: text => this._addText(selected, text),
|
addText: text => this._addText(selected, text),
|
||||||
replaceText: text => this._addText({pre: '', post: ''}, text),
|
replaceText: text => this._addText({pre: '', post: ''}, text),
|
||||||
getText: () => this.get('value'),
|
getText: () => this.get('value'),
|
||||||
|
toggleDirection: () => this._toggleDirection(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (button.sendAction) {
|
if (button.sendAction) {
|
||||||
|
|
|
@ -1,7 +1,35 @@
|
||||||
import computed from "ember-addons/ember-computed-decorators";
|
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({
|
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")
|
@computed("placeholderKey")
|
||||||
placeholder(placeholderKey) {
|
placeholder(placeholderKey) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { registerUnbound } from 'discourse-common/lib/helpers';
|
import { registerUnbound } from 'discourse-common/lib/helpers';
|
||||||
|
import { isRTL } from "discourse/lib/text-direction";
|
||||||
import { iconHTML } from 'discourse-common/lib/icon-library';
|
import { iconHTML } from 'discourse-common/lib/icon-library';
|
||||||
|
|
||||||
var get = Em.get,
|
var get = Em.get,
|
||||||
|
@ -38,6 +39,7 @@ export function categoryBadgeHTML(category, opts) {
|
||||||
let color = get(category, 'color');
|
let color = get(category, 'color');
|
||||||
let html = "";
|
let html = "";
|
||||||
let parentCat = null;
|
let parentCat = null;
|
||||||
|
let categoryDir = "";
|
||||||
|
|
||||||
if (!opts.hideParent) {
|
if (!opts.hideParent) {
|
||||||
parentCat = Discourse.Category.findById(get(category, 'parent_category_id'));
|
parentCat = Discourse.Category.findById(get(category, 'parent_category_id'));
|
||||||
|
@ -66,10 +68,14 @@ export function categoryBadgeHTML(category, opts) {
|
||||||
|
|
||||||
let categoryName = escapeExpression(get(category, 'name'));
|
let categoryName = escapeExpression(get(category, 'name'));
|
||||||
|
|
||||||
|
if (Discourse.SiteSettings.support_mixed_text_direction) {
|
||||||
|
categoryDir = isRTL(categoryName) ? 'dir="rtl"' : 'dir="ltr"';
|
||||||
|
}
|
||||||
|
|
||||||
if (restricted) {
|
if (restricted) {
|
||||||
html += `${iconHTML('lock')}<span>${categoryName}</span>`;
|
html += `${iconHTML('lock')}<span ${categoryDir}>${categoryName}</span>`;
|
||||||
} else {
|
} else {
|
||||||
html += `<span>${categoryName}</span>`;
|
html += `<span ${categoryDir}>${categoryName}</span>`;
|
||||||
}
|
}
|
||||||
html += "</span>";
|
html += "</span>";
|
||||||
|
|
||||||
|
|
|
@ -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 `<span dir="${textDir}">${text}</span>`;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default registerUnbound('dir-span', function(str) {
|
||||||
|
return new Handlebars.SafeString(setDir(str));
|
||||||
|
});
|
|
@ -1,13 +1,18 @@
|
||||||
import highlightSyntax from 'discourse/lib/highlight-syntax';
|
import highlightSyntax from 'discourse/lib/highlight-syntax';
|
||||||
import lightbox from 'discourse/lib/lightbox';
|
import lightbox from 'discourse/lib/lightbox';
|
||||||
|
import { setTextDirections } from "discourse/lib/text-direction";
|
||||||
import { withPluginApi } from 'discourse/lib/plugin-api';
|
import { withPluginApi } from 'discourse/lib/plugin-api';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "post-decorations",
|
name: "post-decorations",
|
||||||
initialize() {
|
initialize(container) {
|
||||||
withPluginApi('0.1', api => {
|
withPluginApi('0.1', api => {
|
||||||
|
const siteSettings = container.lookup('site-settings:main');
|
||||||
api.decorateCooked(highlightSyntax);
|
api.decorateCooked(highlightSyntax);
|
||||||
api.decorateCooked(lightbox);
|
api.decorateCooked(lightbox);
|
||||||
|
if (siteSettings.support_mixed_text_direction) {
|
||||||
|
api.decorateCooked(setTextDirections);
|
||||||
|
}
|
||||||
|
|
||||||
api.decorateCooked($elem => {
|
api.decorateCooked($elem => {
|
||||||
const players = $('audio', $elem);
|
const players = $('audio', $elem);
|
||||||
|
|
|
@ -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';
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import { flushMap } from 'discourse/models/store';
|
||||||
import RestModel from 'discourse/models/rest';
|
import RestModel from 'discourse/models/rest';
|
||||||
import { propertyEqual } from 'discourse/lib/computed';
|
import { propertyEqual } from 'discourse/lib/computed';
|
||||||
import { longDate } from 'discourse/lib/formatter';
|
import { longDate } from 'discourse/lib/formatter';
|
||||||
|
import { isRTL } from 'discourse/lib/text-direction';
|
||||||
import computed from 'ember-addons/ember-computed-decorators';
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
import ActionSummary from 'discourse/models/action-summary';
|
import ActionSummary from 'discourse/models/action-summary';
|
||||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||||
|
@ -58,7 +59,13 @@ const Topic = RestModel.extend({
|
||||||
|
|
||||||
@computed('fancy_title')
|
@computed('fancy_title')
|
||||||
fancyTitle(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 '<span dir="' + titleDir + '">' + fancyTitle + '</span>';
|
||||||
|
}
|
||||||
|
return fancyTitle;
|
||||||
},
|
},
|
||||||
|
|
||||||
// returns createdAt if there's no bumped date
|
// returns createdAt if there's no bumped date
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<div>
|
<div>
|
||||||
{{category-title-link category=c}}
|
{{category-title-link category=c}}
|
||||||
<div class="category-description">
|
<div class="category-description">
|
||||||
{{{c.description_excerpt}}}
|
{{{dir-span c.description_excerpt}}}
|
||||||
</div>
|
</div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{{d-icon 'lock'}}
|
{{d-icon 'lock'}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<span class="category-name">{{category.name}}</span>
|
<span class="category-name">{{dir-span category.name}}</span>
|
||||||
|
|
||||||
{{#if category.uploaded_logo.url}}
|
{{#if category.uploaded_logo.url}}
|
||||||
<div>{{cdn-img src=category.uploaded_logo.url class="category-logo"}}</div>
|
<div>{{cdn-img src=category.uploaded_logo.url class="category-logo"}}</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{{#if topic.hasExcerpt}}
|
{{#if topic.hasExcerpt}}
|
||||||
<div class="topic-excerpt">
|
<div class="topic-excerpt">
|
||||||
{{{topic.escapedExcerpt}}}
|
{{{dir-span topic.escapedExcerpt}}}
|
||||||
{{#if topic.excerptTruncated}}
|
{{#if topic.excerptTruncated}}
|
||||||
<a href="{{topic.url}}">{{i18n 'read_more'}}</a>
|
<a href="{{topic.url}}">{{i18n 'read_more'}}</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
{{#if category.uploaded_logo.url}}
|
{{#if category.uploaded_logo.url}}
|
||||||
{{cdn-img src=category.uploaded_logo.url class="category-logo"}}
|
{{cdn-img src=category.uploaded_logo.url class="category-logo"}}
|
||||||
{{#if category.description}}
|
{{#if category.description}}
|
||||||
<p>{{{category.description}}}</p>
|
<p>{{{dir-span category.description}}}</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if shouldDisplayDescription}}
|
{{#if shouldDisplayDescription}}
|
||||||
<div class="category-desc">{{{description}}}</div>
|
<div class="category-desc">{{{dir-span description}}}</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{{label}}}
|
{{{label}}}
|
||||||
|
|
|
@ -1278,6 +1278,7 @@ en:
|
||||||
olist_title: "Numbered List"
|
olist_title: "Numbered List"
|
||||||
ulist_title: "Bulleted List"
|
ulist_title: "Bulleted List"
|
||||||
list_item: "List item"
|
list_item: "List item"
|
||||||
|
toggle_direction: "Toggle Direction"
|
||||||
help: "Markdown Editing Help"
|
help: "Markdown Editing Help"
|
||||||
collapse: "minimize the composer panel"
|
collapse: "minimize the composer panel"
|
||||||
abandon: "close composer and discard draft"
|
abandon: "close composer and discard draft"
|
||||||
|
|
|
@ -966,6 +966,7 @@ en:
|
||||||
default_locale: "The default language of this Discourse instance"
|
default_locale: "The default language of this Discourse instance"
|
||||||
allow_user_locale: "Allow users to choose their own language interface preference"
|
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)"
|
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_post_length: "Minimum allowed post length in characters"
|
||||||
min_first_post_length: "Minimum allowed first post (topic body) 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"
|
min_private_message_post_length: "Minimum allowed post length in characters for messages"
|
||||||
|
|
|
@ -79,6 +79,9 @@ basic:
|
||||||
set_locale_from_accept_language_header:
|
set_locale_from_accept_language_header:
|
||||||
default: false
|
default: false
|
||||||
validator: "AllowUserLocaleEnabledValidator"
|
validator: "AllowUserLocaleEnabledValidator"
|
||||||
|
support_mixed_text_direction:
|
||||||
|
client: true
|
||||||
|
default: false
|
||||||
categories_topics:
|
categories_topics:
|
||||||
default: 20
|
default: 20
|
||||||
min: 5
|
min: 5
|
||||||
|
|
Loading…
Reference in New Issue