Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit d6c7f55

Browse files
authoredAug 12, 2019
Merge pull request #520 from ckeditor/t/ckeditor5/1151
Feature: Brought support for right–to–left (RTL) languages to various UI components. See ckeditor/ckeditor5#1151.
2 parents e89ad60 + 31881a8 commit d6c7f55

File tree

13 files changed

+291
-104
lines changed

13 files changed

+291
-104
lines changed
 

‎docs/features/blocktoolbar.md

+8
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ To adjust the position of the block toolbar button to match the styles of your w
3131
}
3232
```
3333

34+
If you plan to run the editor in a right–to–left (RTL) language, keep in mind the button will be attached to the **right** boundary of the editable area. In that case, make sure the CSS position adjustment works properly by adding the following styles:
35+
36+
```css
37+
.ck[dir="rtl"] .ck-block-toolbar-button {
38+
transform: translateX( 10px );
39+
}
40+
```
41+
3442
## Installation
3543

3644
<info-box hint>

‎src/dropdown/dropdownview.js

+28-9
Original file line numberDiff line numberDiff line change
@@ -253,18 +253,11 @@ export default class DropdownView extends View {
253253
// If "auto", find the best position of the panel to fit into the viewport.
254254
// Otherwise, simply assign the static position.
255255
if ( this.panelPosition === 'auto' ) {
256-
const defaultPanelPositions = DropdownView.defaultPanelPositions;
257-
258-
this.panelView.position = getOptimalPosition( {
256+
this.panelView.position = DropdownView._getOptimalPosition( {
259257
element: this.panelView.element,
260258
target: this.buttonView.element,
261259
fitInViewport: true,
262-
positions: [
263-
defaultPanelPositions.southEast,
264-
defaultPanelPositions.southWest,
265-
defaultPanelPositions.northEast,
266-
defaultPanelPositions.northWest
267-
]
260+
positions: this._panelPositions
268261
} ).name;
269262
} else {
270263
this.panelView.position = this.panelPosition;
@@ -312,6 +305,24 @@ export default class DropdownView extends View {
312305
focus() {
313306
this.buttonView.focus();
314307
}
308+
309+
/**
310+
* Returns {@link #panelView panel} positions to be used by the
311+
* {@link module:utils/dom/position~getOptimalPosition `getOptimalPosition()`}
312+
* utility considering the direction of the language the UI of the editor is displayed in.
313+
*
314+
* @type {module:utils/dom/position~Options#positions}
315+
* @private
316+
*/
317+
get _panelPositions() {
318+
const { southEast, southWest, northEast, northWest } = DropdownView.defaultPanelPositions;
319+
320+
if ( this.locale.uiLanguageDirection === 'ltr' ) {
321+
return [ southEast, southWest, northEast, northWest ];
322+
} else {
323+
return [ southWest, southEast, northWest, northEast ];
324+
}
325+
}
315326
}
316327

317328
/**
@@ -392,3 +403,11 @@ DropdownView.defaultPanelPositions = {
392403
};
393404
}
394405
};
406+
407+
/**
408+
* A function used to calculate the optimal position for the dropdown panel.
409+
*
410+
* @protected
411+
* @member {Function} module:ui/dropdown/dropdownview~DropdownView._getOptimalPosition
412+
*/
413+
DropdownView._getOptimalPosition = getOptimalPosition;

‎src/editableui/editableuiview.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ export default class EditableUIView extends View {
3434
'ck-content',
3535
'ck-editor__editable',
3636
'ck-rounded-corners'
37-
]
37+
],
38+
lang: locale.contentLanguage,
39+
dir: locale.contentLanguageDirection
3840
}
3941
} );
4042

‎src/editorui/boxed/boxededitoruiview.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ export default class BoxedEditorUIView extends EditorUIView {
6666
'ck-rounded-corners'
6767
],
6868
role: 'application',
69-
dir: 'ltr',
70-
lang: locale.language,
69+
dir: locale.uiLanguageDirection,
70+
lang: locale.uiLanguage,
7171
'aria-labelledby': `ck-editor__aria-label_${ ariaLabelUid }`
7272
},
7373

‎src/editorui/editoruiview.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export default class EditorUIView extends View {
6969
* @private
7070
*/
7171
_renderBodyCollection() {
72+
const locale = this.locale;
7273
const bodyElement = this._bodyCollectionContainer = new Template( {
7374
tag: 'div',
7475
attributes: {
@@ -77,7 +78,8 @@ export default class EditorUIView extends View {
7778
'ck-reset_all',
7879
'ck-body',
7980
'ck-rounded-corners'
80-
]
81+
],
82+
dir: locale.uiLanguageDirection,
8183
},
8284
children: this.body
8385
} ).render();

‎src/toolbar/block/blocktoolbar.js

+18-2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ import iconPilcrow from '@ckeditor/ckeditor5-core/theme/icons/pilcrow.svg';
5353
* | block of content that the button is
5454
* | attached to.
5555
*
56+
* **Note**: If you plan to run the editor in a right–to–left (RTL) language, keep in mind the button
57+
* will be attached to the **right** boundary of the editable area. In that case, make sure the
58+
* CSS position adjustment works properly by adding the following styles:
59+
*
60+
* .ck[dir="rtl"] .ck-block-toolbar-button {
61+
* transform: translateX( 10px );
62+
* }
63+
*
5664
* @extends module:core/plugin~Plugin
5765
*/
5866
export default class BlockToolbar extends Plugin {
@@ -361,9 +369,17 @@ export default class BlockToolbar extends Plugin {
361369
target: targetElement,
362370
positions: [
363371
( contentRect, buttonRect ) => {
372+
let left;
373+
374+
if ( this.editor.locale.uiLanguageDirection === 'ltr' ) {
375+
left = editableRect.left - buttonRect.width;
376+
} else {
377+
left = editableRect.right;
378+
}
379+
364380
return {
365-
top: contentRect.top + contentPaddingTop + ( ( contentLineHeight - buttonRect.height ) / 2 ),
366-
left: editableRect.left - buttonRect.width
381+
top: contentRect.top + contentPaddingTop + ( contentLineHeight - buttonRect.height ) / 2,
382+
left
367383
};
368384
}
369385
]

‎tests/dropdown/dropdownview.js

+88-82
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
1010
import ButtonView from '../../src/button/buttonview';
1111
import DropdownPanelView from '../../src/dropdown/dropdownpanelview';
1212
import global from '@ckeditor/ckeditor5-utils/src/dom/global';
13-
import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
1413
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
1514

1615
describe( 'DropdownView', () => {
@@ -19,7 +18,10 @@ describe( 'DropdownView', () => {
1918
testUtils.createSinonSandbox();
2019

2120
beforeEach( () => {
22-
locale = { t() {} };
21+
locale = {
22+
uiLanguageDirection: 'ltr',
23+
t() {}
24+
};
2325

2426
buttonView = new ButtonView( locale );
2527
panelView = new DropdownPanelView( locale );
@@ -116,7 +118,7 @@ describe( 'DropdownView', () => {
116118
} );
117119

118120
describe( 'view.panelView#position to view#panelPosition', () => {
119-
it( 'does not update until the dropdown is opened', () => {
121+
it( 'does not update until the dropdown is open', () => {
120122
view.isOpen = false;
121123
view.panelPosition = 'nw';
122124

@@ -128,86 +130,37 @@ describe( 'DropdownView', () => {
128130
} );
129131

130132
describe( 'in "auto" mode', () => {
131-
beforeEach( () => {
132-
// Bloat the panel a little to give the positioning algorithm something to
133-
// work with. If the panel was empty, any smart positioning is pointless.
134-
// Placing an empty element in the viewport isn't that hard, right?
135-
panelView.element.style.width = '200px';
136-
panelView.element.style.height = '200px';
137-
} );
138-
139-
it( 'defaults to "south-east" when there is a plenty of space around', () => {
140-
const windowRect = new Rect( global.window );
141-
142-
// "Put" the dropdown in the middle of the viewport.
143-
stubElementClientRect( view.buttonView.element, {
144-
top: windowRect.height / 2,
145-
left: windowRect.width / 2,
146-
width: 10,
147-
height: 10
148-
} );
149-
150-
view.isOpen = true;
151-
152-
expect( panelView.position ).to.equal( 'se' );
153-
} );
154-
155-
it( 'when the dropdown in the north-west corner of the viewport', () => {
156-
stubElementClientRect( view.buttonView.element, {
157-
top: 0,
158-
left: 0,
159-
width: 100,
160-
height: 10
161-
} );
133+
it( 'uses _getOptimalPosition() and a dedicated set of positions (LTR)', () => {
134+
const spy = testUtils.sinon.spy( DropdownView, '_getOptimalPosition' );
135+
const { southEast, southWest, northEast, northWest } = DropdownView.defaultPanelPositions;
162136

163137
view.isOpen = true;
164138

165-
expect( panelView.position ).to.equal( 'se' );
139+
sinon.assert.calledWithExactly( spy, sinon.match( {
140+
element: panelView.element,
141+
target: buttonView.element,
142+
positions: [
143+
southEast, southWest, northEast, northWest
144+
],
145+
fitInViewport: true
146+
} ) );
166147
} );
167148

168-
it( 'when the dropdown in the north-east corner of the viewport', () => {
169-
const windowRect = new Rect( global.window );
170-
171-
stubElementClientRect( view.buttonView.element, {
172-
top: 0,
173-
left: windowRect.right - 100,
174-
width: 100,
175-
height: 10
176-
} );
149+
it( 'uses _getOptimalPosition() and a dedicated set of positions (RTL)', () => {
150+
const spy = testUtils.sinon.spy( DropdownView, '_getOptimalPosition' );
151+
const { southEast, southWest, northEast, northWest } = DropdownView.defaultPanelPositions;
177152

153+
view.locale.uiLanguageDirection = 'rtl';
178154
view.isOpen = true;
179155

180-
expect( panelView.position ).to.equal( 'sw' );
181-
} );
182-
183-
it( 'when the dropdown in the south-west corner of the viewport', () => {
184-
const windowRect = new Rect( global.window );
185-
186-
stubElementClientRect( view.buttonView.element, {
187-
top: windowRect.bottom - 10,
188-
left: 0,
189-
width: 100,
190-
height: 10
191-
} );
192-
193-
view.isOpen = true;
194-
195-
expect( panelView.position ).to.equal( 'ne' );
196-
} );
197-
198-
it( 'when the dropdown in the south-east corner of the viewport', () => {
199-
const windowRect = new Rect( global.window );
200-
201-
stubElementClientRect( view.buttonView.element, {
202-
top: windowRect.bottom - 10,
203-
left: windowRect.right - 100,
204-
width: 100,
205-
height: 10
206-
} );
207-
208-
view.isOpen = true;
209-
210-
expect( panelView.position ).to.equal( 'nw' );
156+
sinon.assert.calledWithExactly( spy, sinon.match( {
157+
element: panelView.element,
158+
target: buttonView.element,
159+
positions: [
160+
southWest, southEast, northWest, northEast
161+
],
162+
fitInViewport: true
163+
} ) );
211164
} );
212165
} );
213166
} );
@@ -372,13 +325,66 @@ describe( 'DropdownView', () => {
372325
sinon.assert.calledOnce( spy );
373326
} );
374327
} );
375-
} );
376328

377-
function stubElementClientRect( element, data ) {
378-
const clientRect = Object.assign( {}, data );
329+
describe( 'DropdownView.defaultPanelPositions', () => {
330+
let positions, buttonRect, panelRect;
331+
332+
beforeEach( () => {
333+
positions = DropdownView.defaultPanelPositions;
334+
335+
buttonRect = {
336+
top: 100,
337+
bottom: 200,
338+
left: 100,
339+
right: 200,
340+
width: 100,
341+
height: 100
342+
};
343+
344+
panelRect = {
345+
top: 0,
346+
bottom: 0,
347+
left: 0,
348+
right: 0,
349+
width: 50,
350+
height: 50
351+
};
352+
} );
353+
354+
it( 'should have a proper length', () => {
355+
expect( Object.keys( positions ) ).to.have.length( 4 );
356+
} );
357+
358+
it( 'should define the "southEast" position', () => {
359+
expect( positions.southEast( buttonRect, panelRect ) ).to.deep.equal( {
360+
top: 200,
361+
left: 100,
362+
name: 'se'
363+
} );
364+
} );
365+
366+
it( 'should define the "southWest" position', () => {
367+
expect( positions.southWest( buttonRect, panelRect ) ).to.deep.equal( {
368+
top: 200,
369+
left: 150,
370+
name: 'sw'
371+
} );
372+
} );
379373

380-
clientRect.right = clientRect.left + clientRect.width;
381-
clientRect.bottom = clientRect.top + clientRect.height;
374+
it( 'should define the "northEast" position', () => {
375+
expect( positions.northEast( buttonRect, panelRect ) ).to.deep.equal( {
376+
top: 50,
377+
left: 100,
378+
name: 'ne'
379+
} );
380+
} );
382381

383-
testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( clientRect );
384-
}
382+
it( 'should define the "northWest" position', () => {
383+
expect( positions.northWest( buttonRect, panelRect ) ).to.deep.equal( {
384+
top: 150,
385+
left: 150,
386+
name: 'nw'
387+
} );
388+
} );
389+
} );
390+
} );

‎tests/editableui/editableuiview.js

+58-5
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe( 'EditableUIView', () => {
1818
testUtils.createSinonSandbox();
1919

2020
beforeEach( () => {
21-
locale = new Locale( 'en' );
21+
locale = new Locale();
2222
editableElement = document.createElement( 'div' );
2323

2424
editingView = new EditingView();
@@ -31,14 +31,21 @@ describe( 'EditableUIView', () => {
3131
view.render();
3232
} );
3333

34+
afterEach( () => {
35+
view.destroy();
36+
editableElement.remove();
37+
} );
38+
3439
describe( 'constructor()', () => {
3540
it( 'sets initial values of attributes', () => {
36-
view = new EditableUIView( locale, editingView );
41+
const view = new EditableUIView( locale, editingView );
3742

3843
expect( view.isFocused ).to.be.false;
3944
expect( view.name ).to.be.null;
4045
expect( view._externalElement ).to.be.undefined;
4146
expect( view._editingView ).to.equal( editingView );
47+
48+
view.destroy();
4249
} );
4350

4451
it( 'renders element from template when no editableElement', () => {
@@ -47,22 +54,58 @@ describe( 'EditableUIView', () => {
4754
expect( view.element.classList.contains( 'ck-content' ) ).to.be.true;
4855
expect( view.element.classList.contains( 'ck-editor__editable' ) ).to.be.true;
4956
expect( view.element.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
57+
expect( view.element.getAttribute( 'lang' ) ).to.equal( 'en' );
58+
expect( view.element.getAttribute( 'dir' ) ).to.equal( 'ltr' );
5059
expect( view._externalElement ).to.be.undefined;
5160
expect( view.isRendered ).to.be.true;
5261
} );
5362

5463
it( 'accepts editableElement as an argument', () => {
55-
view = new EditableUIView( locale, editingView, editableElement );
64+
const view = new EditableUIView( locale, editingView, editableElement );
5665
view.name = editingViewRoot.rootName;
5766

5867
view.render();
68+
5969
expect( view.element ).to.equal( editableElement );
6070
expect( view.element ).to.equal( view._editableElement );
6171
expect( view.element.classList.contains( 'ck' ) ).to.be.true;
6272
expect( view.element.classList.contains( 'ck-editor__editable' ) ).to.be.true;
6373
expect( view.element.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
74+
expect( view.element.getAttribute( 'lang' ) ).to.equal( 'en' );
75+
expect( view.element.getAttribute( 'dir' ) ).to.equal( 'ltr' );
6476
expect( view._hasExternalElement ).to.be.true;
6577
expect( view.isRendered ).to.be.true;
78+
79+
view.destroy();
80+
} );
81+
82+
it( 'sets proper lang and dir attributes (implicit content language)', () => {
83+
const locale = new Locale( { uiLanguage: 'ar' } );
84+
const view = new EditableUIView( locale, editingView );
85+
view.name = editingViewRoot.rootName;
86+
87+
view.render();
88+
89+
expect( view.element.getAttribute( 'lang' ) ).to.equal( 'ar' );
90+
expect( view.element.getAttribute( 'dir' ) ).to.equal( 'rtl' );
91+
92+
view.destroy();
93+
} );
94+
95+
it( 'sets proper lang and dir attributes (explicit content language)', () => {
96+
const locale = new Locale( {
97+
uiLanguage: 'pl',
98+
contentLanguage: 'ar'
99+
} );
100+
const view = new EditableUIView( locale, editingView );
101+
view.name = editingViewRoot.rootName;
102+
103+
view.render();
104+
105+
expect( view.element.getAttribute( 'lang' ) ).to.equal( 'ar' );
106+
expect( view.element.getAttribute( 'dir' ) ).to.equal( 'rtl' );
107+
108+
view.destroy();
66109
} );
67110
} );
68111

@@ -123,19 +166,29 @@ describe( 'EditableUIView', () => {
123166
expect( secondEditingViewRoot.hasClass( 'ck-blurred' ), 12 ).to.be.false;
124167

125168
secondEditableElement.remove();
169+
secondView.destroy();
126170
} );
127171
} );
128172
} );
129173

130174
describe( 'destroy()', () => {
131175
it( 'calls super#destroy()', () => {
132176
const spy = testUtils.sinon.spy( View.prototype, 'destroy' );
177+
const view = new EditableUIView( locale, editingView );
178+
view.name = editingViewRoot.rootName;
133179

180+
view.render();
134181
view.destroy();
182+
135183
sinon.assert.calledOnce( spy );
136184
} );
137185

138186
it( 'can be called multiple times', () => {
187+
const view = new EditableUIView( locale, editingView );
188+
view.name = editingViewRoot.rootName;
189+
190+
view.render();
191+
139192
expect( () => {
140193
view.destroy();
141194
view.destroy();
@@ -144,11 +197,11 @@ describe( 'EditableUIView', () => {
144197

145198
describe( 'when #editableElement as an argument', () => {
146199
it( 'reverts the template of editableElement', () => {
147-
editableElement = document.createElement( 'div' );
200+
const editableElement = document.createElement( 'div' );
148201
editableElement.classList.add( 'foo' );
149202
editableElement.contentEditable = false;
150203

151-
view = new EditableUIView( locale, editingView, editableElement );
204+
const view = new EditableUIView( locale, editingView, editableElement );
152205
view.name = editingViewRoot.rootName;
153206

154207
view.render();

‎tests/editorui/boxed/boxededitoruiview.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe( 'BoxedEditorUIView', () => {
1111
let view, element;
1212

1313
beforeEach( () => {
14-
view = new BoxedEditorUIView( new Locale( 'en' ) );
14+
view = new BoxedEditorUIView( new Locale() );
1515
view.render();
1616
element = view.element;
1717
} );
@@ -31,6 +31,7 @@ describe( 'BoxedEditorUIView', () => {
3131
expect( view.element.classList.contains( 'ck-editor' ) ).to.be.true;
3232
expect( view.element.classList.contains( 'ck-reset' ) ).to.be.true;
3333
expect( view.element.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
34+
expect( view.element.getAttribute( 'dir' ) ).to.equal( 'ltr' );
3435
expect( element.attributes[ 'aria-labelledby' ].value )
3536
.to.equal( view.element.firstChild.id )
3637
.to.match( /^ck-editor__aria-label_\w+$/ );
@@ -59,5 +60,15 @@ describe( 'BoxedEditorUIView', () => {
5960
expect( element.childNodes[ 1 ].attributes.getNamedItem( 'role' ).value ).to.equal( 'presentation' );
6061
expect( element.childNodes[ 2 ].attributes.getNamedItem( 'role' ).value ).to.equal( 'presentation' );
6162
} );
63+
64+
it( 'sets the proper "dir" attribute value when using RTL language', () => {
65+
const view = new BoxedEditorUIView( new Locale( { uiLanguage: 'ar' } ) );
66+
67+
view.render();
68+
69+
expect( view.element.getAttribute( 'dir' ) ).to.equal( 'rtl' );
70+
71+
view.destroy();
72+
} );
6273
} );
6374
} );

‎tests/editorui/editoruiview.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe( 'EditorUIView', () => {
1616
testUtils.createSinonSandbox();
1717

1818
beforeEach( () => {
19-
locale = new Locale( 'en' );
19+
locale = new Locale();
2020
view = new EditorUIView( locale );
2121

2222
view.render();
@@ -46,6 +46,25 @@ describe( 'EditorUIView', () => {
4646
expect( el.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
4747
expect( el.classList.contains( 'ck-reset_all' ) ).to.be.true;
4848
} );
49+
50+
it( 'sets the right dir attribute to the body region (LTR)', () => {
51+
const el = view._bodyCollectionContainer;
52+
53+
expect( el.getAttribute( 'dir' ) ).to.equal( 'ltr' );
54+
} );
55+
56+
it( 'sets the right dir attribute to the body region (RTL)', () => {
57+
const locale = new Locale( { uiLanguage: 'ar' } );
58+
const view = new EditorUIView( locale );
59+
60+
view.render();
61+
62+
const el = view._bodyCollectionContainer;
63+
64+
expect( el.getAttribute( 'dir' ) ).to.equal( 'rtl' );
65+
66+
view.destroy();
67+
} );
4968
} );
5069

5170
describe( 'destroy()', () => {

‎tests/manual/blocktoolbar/blocktoolbar.html

+4
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,8 @@ <h3>Confidence</h3>
5656
.ck-block-toolbar-button {
5757
transform: translateX( -10px );
5858
}
59+
60+
[dir="rtl"] .ck-block-toolbar-button {
61+
transform: translateX( 10px );
62+
}
5963
</style>

‎tests/toolbar/block/blocktoolbar.js

+37
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,43 @@ describe( 'BlockToolbar', () => {
345345
expect( blockToolbar.buttonView.left ).to.equal( 100 );
346346
} );
347347

348+
it( 'should attach the left side of the button to the right side of the editable when language direction is RTL', () => {
349+
editor.locale.uiLanguageDirection = 'rtl';
350+
351+
setData( editor.model, '<paragraph>foo[]bar</paragraph>' );
352+
353+
const target = editor.ui.getEditableElement().querySelector( 'p' );
354+
const styleMock = testUtils.sinon.stub( window, 'getComputedStyle' );
355+
356+
styleMock.withArgs( target ).returns( {
357+
lineHeight: 'normal',
358+
fontSize: '20px',
359+
paddingTop: '10px'
360+
} );
361+
362+
styleMock.callThrough();
363+
364+
testUtils.sinon.stub( editor.ui.getEditableElement(), 'getBoundingClientRect' ).returns( {
365+
left: 200,
366+
right: 600
367+
} );
368+
369+
testUtils.sinon.stub( target, 'getBoundingClientRect' ).returns( {
370+
top: 500,
371+
left: 300
372+
} );
373+
374+
testUtils.sinon.stub( blockToolbar.buttonView.element, 'getBoundingClientRect' ).returns( {
375+
width: 100,
376+
height: 100
377+
} );
378+
379+
editor.ui.fire( 'update' );
380+
381+
expect( blockToolbar.buttonView.top ).to.equal( 472 );
382+
expect( blockToolbar.buttonView.left ).to.equal( 600 );
383+
} );
384+
348385
it( 'should reposition the #panelView when open on ui#update', () => {
349386
blockToolbar.panelView.isVisible = false;
350387

‎theme/mixins/_dir.css

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4+
*/
5+
6+
@define-mixin ck-dir $direction {
7+
@nest [dir="$(direction)"] & {
8+
@mixin-content;
9+
}
10+
}

0 commit comments

Comments
 (0)
This repository has been archived.