feat(ui): add RTL support for application (#14701)

* Minor: add RTL support for description

* fix: unit test

* add direction provider

* sync local file

* Update he-he.json

* add useGridLayoutDirection custom hook to handle the RTL in grid layout

* sync local

* handle RTL in in feed editor

* handle RTL for block editor

* remove the RTL toggle button from the editor
This commit is contained in:
Sachin Chaurasiya 2024-01-16 10:48:08 +05:30 committed by GitHub
parent d1a1003a9d
commit 3291b07001
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1997 additions and 114 deletions

View File

@ -20,6 +20,7 @@ import 'react-toastify/dist/ReactToastify.min.css';
import ApplicationConfigProvider from './components/ApplicationConfigProvider/ApplicationConfigProvider'; import ApplicationConfigProvider from './components/ApplicationConfigProvider/ApplicationConfigProvider';
import AppRouter from './components/AppRouter/AppRouter'; import AppRouter from './components/AppRouter/AppRouter';
import { AuthProvider } from './components/Auth/AuthProviders/AuthProvider'; import { AuthProvider } from './components/Auth/AuthProviders/AuthProvider';
import DirectionProvider from './components/DirectionProvider/DirectionProvider';
import DomainProvider from './components/Domain/DomainProvider/DomainProvider'; import DomainProvider from './components/Domain/DomainProvider/DomainProvider';
import { EntityExportModalProvider } from './components/Entity/EntityExportModalProvider/EntityExportModalProvider.component'; import { EntityExportModalProvider } from './components/Entity/EntityExportModalProvider/EntityExportModalProvider.component';
import ErrorBoundary from './components/ErrorBoundary/ErrorBoundary'; import ErrorBoundary from './components/ErrorBoundary/ErrorBoundary';
@ -43,27 +44,29 @@ const App: FC<AppProps> = ({ routeElements }) => {
<Router history={history}> <Router history={history}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<ErrorBoundary> <ErrorBoundary>
<ApplicationConfigProvider routeElements={routeElements}> <DirectionProvider>
<AuthProvider childComponentType={AppRouter}> <ApplicationConfigProvider routeElements={routeElements}>
<TourProvider> <AuthProvider childComponentType={AppRouter}>
<HelmetProvider> <TourProvider>
<WebAnalyticsProvider> <HelmetProvider>
<PermissionProvider> <WebAnalyticsProvider>
<WebSocketProvider> <PermissionProvider>
<GlobalSearchProvider> <WebSocketProvider>
<DomainProvider> <GlobalSearchProvider>
<EntityExportModalProvider> <DomainProvider>
<AppRouter /> <EntityExportModalProvider>
</EntityExportModalProvider> <AppRouter />
</DomainProvider> </EntityExportModalProvider>
</GlobalSearchProvider> </DomainProvider>
</WebSocketProvider> </GlobalSearchProvider>
</PermissionProvider> </WebSocketProvider>
</WebAnalyticsProvider> </PermissionProvider>
</HelmetProvider> </WebAnalyticsProvider>
</TourProvider> </HelmetProvider>
</AuthProvider> </TourProvider>
</ApplicationConfigProvider> </AuthProvider>
</ApplicationConfigProvider>
</DirectionProvider>
</ErrorBoundary> </ErrorBoundary>
</I18nextProvider> </I18nextProvider>
</Router> </Router>

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#555555" viewBox="0 0 18 18"> <polygon points="15 12 13 10 15 8 15 12"></polygon> <line x1="9" x2="5" y1="4" y2="4"></line> <path d="M5,3A3,3,0,0,0,5,9H6V3H5Z"></path> <rect height="11" width="1" x="5" y="4"></rect> <rect height="11" width="1" x="7" y="4"></rect> </svg>

Before

Width:  |  Height:  |  Size: 316 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#555555" viewBox="0 0 18 18"> <polygon points="3 11 5 9 3 7 3 11"></polygon> <line x1="15" x2="11" y1="4" y2="4"></line> <path d="M11,3a3,3,0,0,0,0,6h1V3H11Z"></path> <rect height="11" width="1" x="11" y="4"></rect> <rect height="11" width="1" x="13" y="4"></rect> </svg>

Before

Width:  |  Height:  |  Size: 317 B

View File

@ -18,6 +18,7 @@ import React, {
useImperativeHandle, useImperativeHandle,
useRef, useRef,
} from 'react'; } from 'react';
import { useTranslation } from 'react-i18next';
import { EDITOR_OPTIONS } from '../../constants/BlockEditor.constants'; import { EDITOR_OPTIONS } from '../../constants/BlockEditor.constants';
import { formatContent } from '../../utils/BlockEditorUtils'; import { formatContent } from '../../utils/BlockEditorUtils';
import './block-editor.less'; import './block-editor.less';
@ -37,6 +38,7 @@ export interface BlockEditorProps {
const BlockEditor = forwardRef<BlockEditorRef, BlockEditorProps>( const BlockEditor = forwardRef<BlockEditorRef, BlockEditorProps>(
({ content = '', editable = true, onChange }, ref) => { ({ content = '', editable = true, onChange }, ref) => {
const { i18n } = useTranslation();
const editorSlots = useRef<EditorSlotsRef>(null); const editorSlots = useRef<EditorSlotsRef>(null);
const editor = useCustomEditor({ const editor = useCustomEditor({
@ -89,6 +91,20 @@ const BlockEditor = forwardRef<BlockEditorRef, BlockEditorProps>(
setTimeout(() => editor.setEditable(editable)); setTimeout(() => editor.setEditable(editable));
}, [editable, editor]); }, [editable, editor]);
useEffect(() => {
const editorWrapper = document.getElementById('block-editor-wrapper');
if (!editorWrapper) {
return;
}
editorWrapper.setAttribute('dir', i18n.dir());
// text align right if rtl
if (i18n.dir() === 'rtl') {
editorWrapper.style.textAlign = 'right';
} else {
editorWrapper.style.textAlign = 'left';
}
}, [i18n]);
return ( return (
<div className="block-editor-wrapper" id="block-editor-wrapper"> <div className="block-editor-wrapper" id="block-editor-wrapper">
<EditorContent <EditorContent

View File

@ -15,6 +15,7 @@ import { NodeSelection, Plugin } from '@tiptap/pm/state';
// @ts-ignore // @ts-ignore
import { EditorView, __serializeForClipboard } from '@tiptap/pm/view'; import { EditorView, __serializeForClipboard } from '@tiptap/pm/view';
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import i18n from '../../../../utils/i18next/LocalUtil';
import { BlockAndDragHandleOptions } from './BlockAndDragDrop'; import { BlockAndDragHandleOptions } from './BlockAndDragDrop';
import { absoluteRect, nodeDOMAtCoords, nodePosAtDOM } from './helpers'; import { absoluteRect, nodeDOMAtCoords, nodePosAtDOM } from './helpers';
@ -128,7 +129,11 @@ export const BlockAndDragHandle = (options: BlockAndDragHandleOptions) => {
return; return;
} }
dragHandleElement.style.left = `${rect.left - rect.width}px`; if (i18n.dir() === 'rtl') {
dragHandleElement.style.right = `${rect.right - rect.width}px`;
} else {
dragHandleElement.style.left = `${rect.left - rect.width}px`;
}
dragHandleElement.style.top = `${rect.top}px`; dragHandleElement.style.top = `${rect.top}px`;
showDragHandle(); showDragHandle();
}; };
@ -177,9 +182,15 @@ export const BlockAndDragHandle = (options: BlockAndDragHandleOptions) => {
return; return;
} }
blockHandleElement.style.left = `${ if (i18n.dir() === 'rtl') {
rect.left - rect.width - options.blockHandleWidth blockHandleElement.style.right = `${
}px`; rect.right - rect.width - options.blockHandleWidth
}px`;
} else {
blockHandleElement.style.left = `${
rect.left - rect.width - options.blockHandleWidth
}px`;
}
blockHandleElement.style.top = `${rect.top}px`; blockHandleElement.style.top = `${rect.top}px`;
showBlockHandle(); showBlockHandle();
}; };

View File

@ -19,6 +19,7 @@ export const absoluteRect = (node: Element) => {
top: data.top, top: data.top,
left: data.left, left: data.left,
width: data.width, width: data.width,
right: data.right,
}; };
}; };

View File

@ -451,3 +451,9 @@
padding: 0px; padding: 0px;
} }
} }
.block-editor-wrapper[dir='rtl'] {
.tiptap.ProseMirror-focused .is-node-empty.has-focus::before {
float: right;
}
}

View File

@ -28,6 +28,7 @@ import { AssetsType } from '../../../enums/entity.enum';
import { Document } from '../../../generated/entity/docStore/document'; import { Document } from '../../../generated/entity/docStore/document';
import { EntityReference } from '../../../generated/entity/type'; import { EntityReference } from '../../../generated/entity/type';
import { PageType } from '../../../generated/system/ui/page'; import { PageType } from '../../../generated/system/ui/page';
import { useGridLayoutDirection } from '../../../hooks/useGridLayoutDirection';
import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface';
import '../../../pages/MyDataPage/my-data.less'; import '../../../pages/MyDataPage/my-data.less';
import { getUserById } from '../../../rest/userAPI'; import { getUserById } from '../../../rest/userAPI';
@ -249,6 +250,9 @@ function CustomizeMyData({
fetchMyData(); fetchMyData();
}, []); }, []);
// call the hook to set the direction of the grid layout
useGridLayoutDirection();
return ( return (
<ActivityFeedProvider> <ActivityFeedProvider>
<PageLayoutV1 <PageLayoutV1

View File

@ -0,0 +1,23 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigProvider } from 'antd';
import React, { FC, ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
const DirectionProvider: FC<{ children: ReactNode }> = ({ children }) => {
const { i18n } = useTranslation();
return <ConfigProvider direction={i18n.dir()}>{children}</ConfigProvider>;
};
export default DirectionProvider;

View File

@ -12,7 +12,7 @@
*/ */
import classNames from 'classnames'; import classNames from 'classnames';
import { debounce } from 'lodash'; import { debounce, isNil } from 'lodash';
import Emoji from 'quill-emoji'; import Emoji from 'quill-emoji';
import 'quill-emoji/dist/quill-emoji.css'; import 'quill-emoji/dist/quill-emoji.css';
import 'quill-mention'; import 'quill-mention';
@ -42,11 +42,7 @@ import {
userMentionItemWithAvatar, userMentionItemWithAvatar,
} from '../../utils/FeedUtils'; } from '../../utils/FeedUtils';
import { LinkBlot } from '../../utils/QuillLink/QuillLink'; import { LinkBlot } from '../../utils/QuillLink/QuillLink';
import { import { insertMention, insertRef } from '../../utils/QuillUtils';
directionHandler,
insertMention,
insertRef,
} from '../../utils/QuillUtils';
import { getEntityIcon } from '../../utils/TableUtils'; import { getEntityIcon } from '../../utils/TableUtils';
import { useApplicationConfigContext } from '../ApplicationConfigProvider/ApplicationConfigProvider'; import { useApplicationConfigContext } from '../ApplicationConfigProvider/ApplicationConfigProvider';
import { editorRef } from '../common/RichTextEditor/RichTextEditor.interface'; import { editorRef } from '../common/RichTextEditor/RichTextEditor.interface';
@ -74,7 +70,7 @@ export const FeedEditor = forwardRef<editorRef, FeedEditorProp>(
}: FeedEditorProp, }: FeedEditorProp,
ref ref
) => { ) => {
const { t } = useTranslation(); const { t, i18n } = useTranslation();
const editorRef = useRef<ReactQuill>(null); const editorRef = useRef<ReactQuill>(null);
const [value, setValue] = useState<string>(defaultValue ?? ''); const [value, setValue] = useState<string>(defaultValue ?? '');
const [isMentionListOpen, toggleMentionList] = useState(false); const [isMentionListOpen, toggleMentionList] = useState(false);
@ -180,7 +176,6 @@ export const FeedEditor = forwardRef<editorRef, FeedEditorProp>(
handlers: { handlers: {
insertMention: insertMention, insertMention: insertMention,
insertRef: insertRef, insertRef: insertRef,
direction: directionHandler,
}, },
}, },
'emoji-toolbar': true, 'emoji-toolbar': true,
@ -281,6 +276,29 @@ export const FeedEditor = forwardRef<editorRef, FeedEditorProp>(
} }
}, [focused, editorRef]); }, [focused, editorRef]);
useEffect(() => {
// get the editor container
const container = document.getElementById('om-quill-editor');
if (container && editorRef.current) {
// get the editor instance
const editorInstance = editorRef.current.getEditor();
const direction = i18n.dir();
// get the current direction of the editor
const { align } = editorInstance.getFormat();
if (direction === 'rtl' && isNil(align)) {
container.setAttribute('data-dir', direction);
editorInstance.format('align', 'right', 'user');
} else if (align === 'right') {
editorInstance.format('align', false, 'user');
container.setAttribute('data-dir', 'ltr');
}
editorInstance.format('direction', direction, 'user');
}
}, [i18n, editorRef]);
return ( return (
<div <div
className={className} className={className}

View File

@ -11,8 +11,6 @@
* limitations under the License. * limitations under the License.
*/ */
import LTRIcon from '../../../assets/svg/ic-ltr.svg';
import RTLIcon from '../../../assets/svg/ic-rtl.svg';
import MarkdownIcon from '../../../assets/svg/markdown.svg'; import MarkdownIcon from '../../../assets/svg/markdown.svg';
import i18n from '../../../utils/i18next/LocalUtil'; import i18n from '../../../utils/i18next/LocalUtil';
@ -43,55 +41,6 @@ const markdownButton = (): HTMLButtonElement => {
return button; return button;
}; };
const getRTLButtonIcon = (mode: 'rtl' | 'ltr') => `
<img
alt="rtl-icon"
class="svg-icon"
height="24px"
width="24px"
src="${mode === 'rtl' ? RTLIcon : LTRIcon}" />`;
const toggleEditorDirection = (button: HTMLButtonElement) => {
const editorElement = document.querySelector(
'.toastui-editor.md-mode.active'
);
if (editorElement) {
const editorElementDir = editorElement.getAttribute('dir');
const newDir = editorElementDir === 'rtl' ? 'ltr' : 'rtl';
const textAlign = newDir === 'rtl' ? 'right' : 'left';
editorElement.setAttribute('dir', newDir);
editorElement.setAttribute('style', `text-align: ${textAlign};`);
button.innerHTML = getRTLButtonIcon(newDir === 'rtl' ? 'ltr' : 'rtl');
}
};
const rtlButton = (): HTMLButtonElement => {
const button = document.createElement('button');
button.onclick = () => toggleEditorDirection(button);
button.className = 'toastui-editor-toolbar-icons rtl-icon';
button.id = 'rtl-button';
button.style.cssText = 'background-image: none; margin: 0; margin-top: 4px;';
button.type = 'button';
button.innerHTML = getRTLButtonIcon('rtl');
return button;
};
const rtlButtonUpdateHandler = (toolbarState: {
active: boolean;
disabled?: boolean;
}) => {
const rtlButtonElement = document.getElementById('rtl-button');
if (rtlButtonElement) {
(rtlButtonElement as HTMLButtonElement).disabled =
toolbarState.disabled || false;
}
};
export const EDITOR_TOOLBAR_ITEMS = [ export const EDITOR_TOOLBAR_ITEMS = [
'heading', 'heading',
'bold', 'bold',
@ -104,12 +53,6 @@ export const EDITOR_TOOLBAR_ITEMS = [
'quote', 'quote',
'code', 'code',
'codeblock', 'codeblock',
{
name: i18n.t('label.rtl-ltr-direction'),
el: rtlButton(),
tooltip: i18n.t('label.rtl-ltr-direction'),
onUpdated: rtlButtonUpdateHandler,
},
{ {
name: i18n.t('label.markdown-guide'), name: i18n.t('label.markdown-guide'),
el: markdownButton(), el: markdownButton(),

View File

@ -23,6 +23,7 @@ import React, {
useImperativeHandle, useImperativeHandle,
useState, useState,
} from 'react'; } from 'react';
import i18n from '../../../utils/i18next/LocalUtil';
import { EDITOR_TOOLBAR_ITEMS } from './EditorToolBar'; import { EDITOR_TOOLBAR_ITEMS } from './EditorToolBar';
import './rich-text-editor.less'; import './rich-text-editor.less';
import { editorRef, RichTextEditorProp } from './RichTextEditor.interface'; import { editorRef, RichTextEditorProp } from './RichTextEditor.interface';
@ -72,10 +73,34 @@ const RichTextEditor = forwardRef<editorRef, RichTextEditorProp>(
setEditorValue(initialValue); setEditorValue(initialValue);
}, [initialValue]); }, [initialValue]);
// handle the direction of the editor
useEffect(() => {
const dir = i18n.dir();
const editorElement = document.querySelector('.toastui-editor.md-mode');
const previewElement = document.querySelector(
'.toastui-editor-md-preview'
);
const textAlign = dir === 'rtl' ? 'right' : 'left';
if (editorElement) {
editorElement.setAttribute('dir', dir);
editorElement.setAttribute('style', `text-align: ${textAlign};`);
}
if (previewElement) {
previewElement.setAttribute('dir', dir);
previewElement.setAttribute('style', `text-align: ${textAlign};`);
}
}, []);
return ( return (
<div className={classNames(className)} style={style}> <div className={classNames(className)} style={style}>
{readonly ? ( {readonly ? (
<div className="border p-xs rounded-4" data-testid="viewer"> <div
className={classNames('border p-xs rounded-4', {
'text-right': i18n.dir() === 'rtl',
})}
data-testid="viewer"
dir={i18n.dir()}>
<Viewer <Viewer
extendedAutolinks={extendedAutolinks} extendedAutolinks={extendedAutolinks}
initialValue={editorValue} initialValue={editorValue}

View File

@ -32,7 +32,7 @@ const RichTextEditorPreviewer = ({
showReadMoreBtn = true, showReadMoreBtn = true,
maxLength = DESCRIPTION_MAX_PREVIEW_CHARACTERS, maxLength = DESCRIPTION_MAX_PREVIEW_CHARACTERS,
}: PreviewerProp) => { }: PreviewerProp) => {
const { t } = useTranslation(); const { t, i18n } = useTranslation();
const [content, setContent] = useState<string>(''); const [content, setContent] = useState<string>('');
// initially read more will be false // initially read more will be false
@ -97,8 +97,11 @@ const RichTextEditorPreviewer = ({
return ( return (
<div <div
className={classNames('rich-text-editor-container', className)} className={classNames('rich-text-editor-container', className, {
data-testid="viewer-container"> 'text-right': i18n.dir() === 'rtl',
})}
data-testid="viewer-container"
dir={i18n.dir()}>
<div <div
className={classNames('markdown-parser', textVariant)} className={classNames('markdown-parser', textVariant)}
data-testid="markdown-parser"> data-testid="markdown-parser">

View File

@ -45,7 +45,6 @@ export const TOOLBAR_ITEMS = [
[{ list: 'ordered' }, { list: 'bullet' }], [{ list: 'ordered' }, { list: 'bullet' }],
['link'], ['link'],
['insertMention', 'insertRef', 'emoji'], ['insertMention', 'insertRef', 'emoji'],
[{ direction: 'rtl' }],
]; ];
export enum TaskOperation { export enum TaskOperation {

View File

@ -0,0 +1,61 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { useTranslation } from 'react-i18next';
import { useGridLayoutDirection } from './useGridLayoutDirection';
jest.mock('react-i18next', () => ({
useTranslation: jest.fn().mockImplementation(() => ({
i18n: {
dir: jest.fn(),
},
})),
}));
describe('useGridLayoutDirection hook', () => {
let container: HTMLDivElement;
let child: HTMLDivElement;
beforeEach(() => {
container = document.createElement('div');
container.className = 'react-grid-layout';
document.body.appendChild(container);
child = document.createElement('div');
child.className = 'react-grid-item';
container.appendChild(child);
});
afterEach(() => {
document.body.removeChild(container);
});
it('should not change direction if isLoading is true', () => {
renderHook(() => useGridLayoutDirection(true));
expect(container.getAttribute('dir')).toBeNull();
expect(child.getAttribute('dir')).toBeNull();
});
it('should set direction to ltr for container and i18n direction for children if isLoading is false', () => {
(useTranslation as jest.Mock).mockImplementationOnce(() => ({
i18n: {
dir: jest.fn().mockReturnValue('rtl'),
},
}));
renderHook(() => useGridLayoutDirection(false));
expect(container.getAttribute('dir')).toBe('ltr');
expect(child.getAttribute('dir')).toBe('rtl');
});
});

View File

@ -0,0 +1,33 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
export const useGridLayoutDirection = (isLoading = false) => {
const { i18n } = useTranslation();
useEffect(() => {
if (!isLoading) {
const gridLayoutContainer = document.querySelector('.react-grid-layout');
const children = document.querySelectorAll('.react-grid-item');
if (gridLayoutContainer && children) {
// parent container should be ltr to avoid RTL issues
gridLayoutContainer.setAttribute('dir', 'ltr');
// children should change direction based on i18n direction
children.forEach((child) => child.setAttribute('dir', i18n.dir()));
}
}
}, [i18n, isLoading]);
};

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@ import { AssetsType, EntityType } from '../../enums/entity.enum';
import { Thread } from '../../generated/entity/feed/thread'; import { Thread } from '../../generated/entity/feed/thread';
import { PageType } from '../../generated/system/ui/page'; import { PageType } from '../../generated/system/ui/page';
import { EntityReference } from '../../generated/type/entityReference'; import { EntityReference } from '../../generated/type/entityReference';
import { useGridLayoutDirection } from '../../hooks/useGridLayoutDirection';
import { getDocumentByFQN } from '../../rest/DocStoreAPI'; import { getDocumentByFQN } from '../../rest/DocStoreAPI';
import { getActiveAnnouncement } from '../../rest/feedsAPI'; import { getActiveAnnouncement } from '../../rest/feedsAPI';
import { getUserById } from '../../rest/userAPI'; import { getUserById } from '../../rest/userAPI';
@ -190,6 +191,9 @@ const MyDataPage = () => {
fetchAnnouncements(); fetchAnnouncements();
}, []); }, []);
// call the hook to set the direction of the grid layout
useGridLayoutDirection(isLoading);
if (showWelcomeScreen) { if (showWelcomeScreen) {
return ( return (
<div className="bg-white full-height"> <div className="bg-white full-height">

View File

@ -89,6 +89,7 @@ jest.mock('utils/i18next/LocalUtil', () => ({
t: (key) => key, t: (key) => key,
}), }),
t: (key) => key, t: (key) => key,
dir: jest.fn().mockReturnValue('ltr'),
})); }));
/** /**
* mock react-i18next * mock react-i18next
@ -97,7 +98,7 @@ jest.mock('react-i18next', () => ({
...jest.requireActual('react-i18next'), ...jest.requireActual('react-i18next'),
useTranslation: jest.fn().mockReturnValue({ useTranslation: jest.fn().mockReturnValue({
t: (key) => key, t: (key) => key,
i18n: { language: 'en-US' }, i18n: { language: 'en-US', dir: jest.fn().mockReturnValue('ltr') },
}), }),
})); }));

View File

@ -21,19 +21,3 @@ export function insertRef() {
const ref = this.quill.getModule('mention'); const ref = this.quill.getModule('mention');
ref.openMenu('#'); ref.openMenu('#');
} }
export function directionHandler(value) {
const { align } = this.quill.getFormat();
// get the editor container
const container = document.getElementById('om-quill-editor');
if (value === 'rtl' && align == null) {
container.setAttribute('data-dir', value);
this.quill.format('align', 'right', 'user');
} else if (!value && align === 'right') {
this.quill.format('align', false, 'user');
container.setAttribute('data-dir', 'ltr');
}
this.quill.format('direction', value, 'user');
}

View File

@ -17,6 +17,7 @@ import deDe from '../../locale/languages/de-de.json';
import enUS from '../../locale/languages/en-us.json'; import enUS from '../../locale/languages/en-us.json';
import esES from '../../locale/languages/es-es.json'; import esES from '../../locale/languages/es-es.json';
import frFR from '../../locale/languages/fr-fr.json'; import frFR from '../../locale/languages/fr-fr.json';
import heHE from '../../locale/languages/he-he.json';
import jaJP from '../../locale/languages/ja-jp.json'; import jaJP from '../../locale/languages/ja-jp.json';
import nlNL from '../../locale/languages/nl-nl.json'; import nlNL from '../../locale/languages/nl-nl.json';
import ptBR from '../../locale/languages/pt-br.json'; import ptBR from '../../locale/languages/pt-br.json';
@ -32,6 +33,7 @@ export enum SupportedLocales {
Español = 'es-ES', Español = 'es-ES',
Русский = 'ru-RU', Русский = 'ru-RU',
Deutsh = 'de-DE', Deutsh = 'de-DE',
Hebrew = 'he-HE',
Nederlands = 'nl-NL', Nederlands = 'nl-NL',
} }
@ -53,6 +55,7 @@ export const getInitOptions = (): InitOptions => {
'es-ES': { translation: esES }, 'es-ES': { translation: esES },
'ru-RU': { translation: ruRU }, 'ru-RU': { translation: ruRU },
'de-DE': { translation: deDe }, 'de-DE': { translation: deDe },
'he-HE': { translation: heHE },
'nl-NL': { translation: nlNL }, 'nl-NL': { translation: nlNL },
}, },
fallbackLng: ['en-US'], fallbackLng: ['en-US'],