Skip to content

Commit 5e9c7b9

Browse files
committed
Support text block paragraph duplication
Allow duplicating individual paragraphs within text blocks using the content element duplicate action. Text blocks override the default duplication behavior to duplicate only the selected Slate nodes rather than the entire content element. The duplicated nodes are automatically selected afterwards. REDMINE-21205
1 parent e1208e3 commit 5e9c7b9

File tree

7 files changed

+364
-2
lines changed

7 files changed

+364
-2
lines changed

entry_types/scrolled/package/spec/editor/models/contentElementMenuItems-spec.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ describe('ContentElementMenuItems', () => {
3737
});
3838
const contentElement = entry.contentElements.get(1);
3939
entry.duplicateContentElement = jest.fn();
40+
editor.contentElementTypes.register('textBlock', {});
4041

4142
const menuItem = new DuplicateContentElementMenuItem({}, {
4243
contentElement,
@@ -48,6 +49,30 @@ describe('ContentElementMenuItems', () => {
4849

4950
expect(entry.duplicateContentElement).toHaveBeenCalledWith(contentElement);
5051
});
52+
53+
it('calls handleDuplicate instead of duplicateContentElement if defined', () => {
54+
const editor = factories.editorApi();
55+
const entry = factories.entry(ScrolledEntry, {}, {
56+
entryTypeSeed: normalizeSeed({
57+
contentElements: [{id: 1, typeName: 'textBlock'}]
58+
})
59+
});
60+
const contentElement = entry.contentElements.get(1);
61+
const handleDuplicate = jest.fn();
62+
entry.duplicateContentElement = jest.fn();
63+
editor.contentElementTypes.register('textBlock', {handleDuplicate});
64+
65+
const menuItem = new DuplicateContentElementMenuItem({}, {
66+
contentElement,
67+
entry,
68+
editor
69+
});
70+
71+
menuItem.selected();
72+
73+
expect(handleDuplicate).toHaveBeenCalledWith(contentElement);
74+
expect(entry.duplicateContentElement).not.toHaveBeenCalled();
75+
});
5176
});
5277

5378
describe('DestroyContentElementMenuItem', () => {
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
/** @jsx jsx */
2+
import {duplicateNodes} from 'frontend/inlineEditing/EditableText/duplicateNodes';
3+
4+
import {createHyperscript} from 'slate-hyperscript';
5+
6+
const h = createHyperscript({
7+
elements: {
8+
paragraph: {type: 'paragraph'},
9+
heading: {type: 'heading'},
10+
blockQuote: {type: 'block-quote'},
11+
bulletedList: {type: 'bulleted-list'},
12+
listItem: {type: 'list-item'}
13+
},
14+
});
15+
16+
// Strip meta tags to make deep equality checks work
17+
const jsx = (tagName, attributes, ...children) => {
18+
delete attributes.__self;
19+
delete attributes.__source;
20+
return h(tagName, attributes, ...children);
21+
}
22+
23+
describe('duplicateNodes', () => {
24+
it('duplicates single selected paragraph', () => {
25+
const editor = (
26+
<editor>
27+
<paragraph>
28+
Line 1
29+
<cursor />
30+
</paragraph>
31+
<paragraph>
32+
Line 2
33+
</paragraph>
34+
</editor>
35+
);
36+
37+
duplicateNodes(editor);
38+
39+
const output = (
40+
<editor>
41+
<paragraph>
42+
Line 1
43+
</paragraph>
44+
<paragraph>
45+
Line 1
46+
</paragraph>
47+
<paragraph>
48+
Line 2
49+
</paragraph>
50+
</editor>
51+
);
52+
expect(editor.children).toEqual(output.children);
53+
});
54+
55+
it('duplicates multiple selected paragraphs', () => {
56+
const editor = (
57+
<editor>
58+
<paragraph>
59+
<anchor />
60+
Line 1
61+
</paragraph>
62+
<paragraph>
63+
Line 2
64+
<focus />
65+
</paragraph>
66+
<paragraph>
67+
Line 3
68+
</paragraph>
69+
</editor>
70+
);
71+
72+
duplicateNodes(editor);
73+
74+
const output = (
75+
<editor>
76+
<paragraph>
77+
Line 1
78+
</paragraph>
79+
<paragraph>
80+
Line 2
81+
</paragraph>
82+
<paragraph>
83+
Line 1
84+
</paragraph>
85+
<paragraph>
86+
Line 2
87+
</paragraph>
88+
<paragraph>
89+
Line 3
90+
</paragraph>
91+
</editor>
92+
);
93+
expect(editor.children).toEqual(output.children);
94+
});
95+
96+
it('preserves node properties when duplicating', () => {
97+
const editor = (
98+
<editor>
99+
<paragraph variant="highlight" color="#444">
100+
Line 1
101+
<cursor />
102+
</paragraph>
103+
</editor>
104+
);
105+
106+
duplicateNodes(editor);
107+
108+
const output = (
109+
<editor>
110+
<paragraph variant="highlight" color="#444">
111+
Line 1
112+
</paragraph>
113+
<paragraph variant="highlight" color="#444">
114+
Line 1
115+
</paragraph>
116+
</editor>
117+
);
118+
expect(editor.children).toEqual(output.children);
119+
});
120+
121+
it('duplicates heading', () => {
122+
const editor = (
123+
<editor>
124+
<heading>
125+
Title
126+
<cursor />
127+
</heading>
128+
<paragraph>
129+
Text
130+
</paragraph>
131+
</editor>
132+
);
133+
134+
duplicateNodes(editor);
135+
136+
const output = (
137+
<editor>
138+
<heading>
139+
Title
140+
</heading>
141+
<heading>
142+
Title
143+
</heading>
144+
<paragraph>
145+
Text
146+
</paragraph>
147+
</editor>
148+
);
149+
expect(editor.children).toEqual(output.children);
150+
});
151+
152+
it('duplicates list as a whole', () => {
153+
const editor = (
154+
<editor>
155+
<bulletedList>
156+
<listItem>
157+
Item 1
158+
<cursor />
159+
</listItem>
160+
<listItem>
161+
Item 2
162+
</listItem>
163+
</bulletedList>
164+
</editor>
165+
);
166+
167+
duplicateNodes(editor);
168+
169+
const output = (
170+
<editor>
171+
<bulletedList>
172+
<listItem>
173+
Item 1
174+
</listItem>
175+
<listItem>
176+
Item 2
177+
</listItem>
178+
</bulletedList>
179+
<bulletedList>
180+
<listItem>
181+
Item 1
182+
</listItem>
183+
<listItem>
184+
Item 2
185+
</listItem>
186+
</bulletedList>
187+
</editor>
188+
);
189+
expect(editor.children).toEqual(output.children);
190+
});
191+
192+
it('does nothing when no selection', () => {
193+
const editor = (
194+
<editor>
195+
<paragraph>
196+
Line 1
197+
</paragraph>
198+
</editor>
199+
);
200+
editor.selection = null;
201+
202+
duplicateNodes(editor);
203+
204+
const output = (
205+
<editor>
206+
<paragraph>
207+
Line 1
208+
</paragraph>
209+
</editor>
210+
);
211+
expect(editor.children).toEqual(output.children);
212+
});
213+
214+
it('selects duplicated nodes', () => {
215+
const editor = (
216+
<editor>
217+
<paragraph>
218+
Line 1
219+
<cursor />
220+
</paragraph>
221+
<paragraph>
222+
Line 2
223+
</paragraph>
224+
</editor>
225+
);
226+
227+
duplicateNodes(editor);
228+
229+
const output = (
230+
<editor>
231+
<paragraph>
232+
Line 1
233+
</paragraph>
234+
<paragraph>
235+
<anchor />Line 1<focus />
236+
</paragraph>
237+
<paragraph>
238+
Line 2
239+
</paragraph>
240+
</editor>
241+
);
242+
expect(editor.selection).toEqual(output.selection);
243+
});
244+
245+
it('selects all duplicated nodes when multiple were selected', () => {
246+
const editor = (
247+
<editor>
248+
<paragraph>
249+
<anchor />
250+
Line 1
251+
</paragraph>
252+
<paragraph>
253+
Line 2
254+
<focus />
255+
</paragraph>
256+
<paragraph>
257+
Line 3
258+
</paragraph>
259+
</editor>
260+
);
261+
262+
duplicateNodes(editor);
263+
264+
const output = (
265+
<editor>
266+
<paragraph>
267+
Line 1
268+
</paragraph>
269+
<paragraph>
270+
Line 2
271+
</paragraph>
272+
<paragraph>
273+
<anchor />Line 1
274+
</paragraph>
275+
<paragraph>
276+
Line 2<focus />
277+
</paragraph>
278+
<paragraph>
279+
Line 3
280+
</paragraph>
281+
</editor>
282+
);
283+
expect(editor.selection).toEqual(output.selection);
284+
});
285+
});

entry_types/scrolled/package/src/contentElements/textBlock/editor.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ editor.contentElementTypes.register('textBlock', {
141141
contentElement.postCommand({type: 'REMOVE'});
142142
return false;
143143
}
144+
},
145+
146+
handleDuplicate(contentElement) {
147+
contentElement.postCommand({type: 'DUPLICATE'});
144148
}
145149
});
146150

entry_types/scrolled/package/src/editor/models/contentElementMenuItems.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,20 @@ export const DuplicateContentElementMenuItem = Backbone.Model.extend({
66
initialize(attributes, options) {
77
this.contentElement = options.contentElement;
88
this.entry = options.entry;
9+
this.editor = options.editor;
910
this.set('label', I18n.t('pageflow_scrolled.editor.duplicate_content_element_menu_item.label'));
1011
},
1112

1213
selected() {
13-
this.entry.duplicateContentElement(this.contentElement);
14+
const contentElementType =
15+
this.editor.contentElementTypes.findByTypeName(this.contentElement.get('typeName'));
16+
17+
if (contentElementType.handleDuplicate) {
18+
contentElementType.handleDuplicate(this.contentElement);
19+
}
20+
else {
21+
this.entry.duplicateContentElement(this.contentElement);
22+
}
1423
}
1524
});
1625

entry_types/scrolled/package/src/editor/views/EditContentElementView.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export const EditContentElementView = EditConfigurationView.extend({
2222
return [
2323
new DuplicateContentElementMenuItem({}, {
2424
contentElement: this.model,
25-
entry: this.options.entry
25+
entry: this.options.entry,
26+
editor: this.options.editor
2627
}),
2728
new DestroyContentElementMenuItem({}, {
2829
contentElement: this.model,

0 commit comments

Comments
 (0)