Skip to content

Commit 238385b

Browse files
samir-ayoubjhosefmarks
authored andcommitted
feat(rich-text): permite inclusão de links
É possível agora definir links contendo valor de exibição diferenciado ou o próprio link. Fixes DTHFUI-1745
1 parent b9cf19b commit 238385b

16 files changed

+721
-24
lines changed

projects/ui/src/lib/components/po-field/po-rich-text/enums/po-rich-text-modal-type.enum.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ export enum PoRichTextModalType {
1212
/**
1313
* Exibe os dados para inserção de imagens.
1414
*/
15-
Image = 'image'
15+
Image = 'image',
16+
17+
/**
18+
* Exibe os dados para inserção de link e texto customizado para link.
19+
*/
20+
Link = 'link'
1621

1722
}

projects/ui/src/lib/components/po-field/po-rich-text/po-rich-text-body/po-rich-text-body.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
(click)="onClick()"
99
(cut)="update()"
1010
(focus)="onFocus()"
11+
(keydown)="onKeyDown($event)"
1112
(keyup)="onKeyUp()"
1213
(paste)="update()">
1314
</div>

projects/ui/src/lib/components/po-field/po-rich-text/po-rich-text-body/po-rich-text-body.component.spec.ts

Lines changed: 202 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
22

3+
import * as UtilsFunction from '../../../../utils/util';
34
import { configureTestSuite } from './../../../../util-test/util-expect.spec';
45

56
import { PoRichTextBodyComponent } from './po-rich-text-body.component';
@@ -49,7 +50,7 @@ describe('PoRichTextBodyComponent:', () => {
4950
describe('executeCommand:', () => {
5051

5152
it('should call `focus`', () => {
52-
const spyFocus = spyOn(component.bodyElement.nativeElement, <any> 'focus');
53+
const spyFocus = spyOn(component.bodyElement.nativeElement, <any>'focus');
5354
const fakeValue = 'p';
5455

5556
component.executeCommand(fakeValue);
@@ -58,7 +59,7 @@ describe('PoRichTextBodyComponent:', () => {
5859
});
5960

6061
it('should call `execCommand` with string as parameter.', () => {
61-
const spyExecCommand = spyOn(document, <any> 'execCommand');
62+
const spyExecCommand = spyOn(document, <any>'execCommand');
6263
const fakeValue = 'p';
6364

6465
component.executeCommand(fakeValue);
@@ -69,12 +70,29 @@ describe('PoRichTextBodyComponent:', () => {
6970
it('should call `execCommand` with object as parameter.', () => {
7071
const command = 'foreColor';
7172
const value = '#000000';
72-
const spyExecCommand = spyOn(document, <any> 'execCommand');
73-
const fakeValue = { command, value } ;
73+
const spyExecCommand = spyOn(document, <any>'execCommand');
74+
const fakeValue = { command, value };
75+
76+
spyOn(component, <any>'handleCommandLink');
7477

7578
component.executeCommand(fakeValue);
7679

7780
expect(spyExecCommand).toHaveBeenCalledWith(fakeValue.command, false, fakeValue.value);
81+
expect(component['handleCommandLink']).not.toHaveBeenCalled();
82+
});
83+
84+
it('should call `handleCommandLink` with an object as parameter if command value is `InsertHTML`.', () => {
85+
const command = 'InsertHTML';
86+
const value = { urlLink: 'link', urlLinkText: 'link text' };
87+
const spyExecCommand = spyOn(document, <any>'execCommand');
88+
const fakeValue = { command, value };
89+
90+
spyOn(component, <any>'handleCommandLink');
91+
92+
component.executeCommand(fakeValue);
93+
94+
expect(component['handleCommandLink']).toHaveBeenCalledWith(command, value.urlLink, value.urlLinkText);
95+
expect(spyExecCommand).not.toHaveBeenCalled();
7896
});
7997

8098
it('should call `updateModel`', () => {
@@ -152,6 +170,90 @@ describe('PoRichTextBodyComponent:', () => {
152170
expect(component['emitSelectionCommands']).toHaveBeenCalled();
153171
});
154172

173+
it('onKeyDown: should call `event.preventDefault` and `shortcutCommand.emit` if keyCode is `76` and ctrlKey is `true`', () => {
174+
const fakeEvent = {
175+
keyCode: 76,
176+
ctrlKey: true,
177+
preventDefault: () => {},
178+
};
179+
180+
spyOn(component.shortcutCommand, 'emit');
181+
spyOn(fakeEvent, 'preventDefault');
182+
183+
component.onKeyDown(fakeEvent);
184+
185+
expect(fakeEvent.preventDefault).toHaveBeenCalled();
186+
expect(component.shortcutCommand.emit).toHaveBeenCalled();
187+
});
188+
189+
it('onKeyDown: should call `event.preventDefault` and `shortcutCommand.emit` if keyCode is `76` and metaKey is `true`', () => {
190+
const fakeEvent = {
191+
keyCode: 76,
192+
metaKey: true,
193+
preventDefault: () => {},
194+
};
195+
196+
spyOn(component.shortcutCommand, 'emit');
197+
spyOn(fakeEvent, 'preventDefault');
198+
199+
component.onKeyDown(fakeEvent);
200+
201+
expect(fakeEvent.preventDefault).toHaveBeenCalled();
202+
expect(component.shortcutCommand.emit).toHaveBeenCalled();
203+
});
204+
205+
it('onKeyDown: shouldn`t call `event.preventDefault` and `shortcutCommand.emit` if keyCode isn`t `76`', () => {
206+
const fakeEvent = {
207+
keyCode: 18,
208+
cmdKey: true,
209+
preventDefault: () => {},
210+
};
211+
212+
spyOn(component.shortcutCommand, 'emit');
213+
spyOn(fakeEvent, 'preventDefault');
214+
215+
component.onKeyDown(fakeEvent);
216+
217+
expect(fakeEvent.preventDefault).not.toHaveBeenCalled();
218+
expect(component.shortcutCommand.emit).not.toHaveBeenCalled();
219+
});
220+
221+
it('onKeyDown: shouldn`t call `event.preventDefault` and `shortcutCommand.emit` if ctrlKey isn`t true', () => {
222+
const fakeEvent = {
223+
keyCode: 76,
224+
ctrlKey: false,
225+
preventDefault: () => {},
226+
};
227+
228+
spyOn(component.shortcutCommand, 'emit');
229+
spyOn(fakeEvent, 'preventDefault');
230+
231+
component.onKeyDown(fakeEvent);
232+
233+
expect(fakeEvent.preventDefault).not.toHaveBeenCalled();
234+
expect(component.shortcutCommand.emit).not.toHaveBeenCalled();
235+
});
236+
237+
it('cursorPositionedInALink: should return true if tag element is a link', () => {
238+
const fakeSelection = { focusNode: { parentElement: { tagName: 'A' } } };
239+
240+
spyOn(document, 'getSelection').and.returnValue(<any>fakeSelection);
241+
242+
const expectedValue = component['cursorPositionedInALink']();
243+
244+
expect(expectedValue).toBe(true);
245+
});
246+
247+
it('cursorPositionedInALink: should return false if tag element isn`t a link', () => {
248+
const fakeSelection = { focusNode: { parentElement: { tagName: 'B' } } };
249+
250+
spyOn(document, 'getSelection').and.returnValue(<any>fakeSelection);
251+
252+
const expectedValue = component['cursorPositionedInALink']();
253+
254+
expect(expectedValue).toBe(false);
255+
});
256+
155257
it('update: should call `updateModel`', fakeAsync(() => {
156258
spyOn(component, <any>'updateModel');
157259

@@ -177,6 +279,86 @@ describe('PoRichTextBodyComponent:', () => {
177279
expect(component.commands.emit).toHaveBeenCalled();
178280
});
179281

282+
it(`emitSelectionCommands: the object property 'commands'
283+
should contain 'Createlink' if 'cursorPositionedInALink' returns 'true'`, () => {
284+
285+
spyOn(component, <any>'cursorPositionedInALink').and.returnValue(true);
286+
spyOn(document, 'queryCommandState').and.returnValue(false);
287+
spyOn(document, 'queryCommandValue').and.returnValue('rgb');
288+
spyOn(component, <any>'rgbToHex').and.returnValue('hex');
289+
spyOn(component.commands, 'emit');
290+
291+
component['emitSelectionCommands']();
292+
293+
expect(component.commands.emit).toHaveBeenCalledWith({commands: ['Createlink'], hexColor: 'hex'});
294+
});
295+
296+
it(`emitSelectionCommands: the object property 'commands'
297+
shouldn't contain 'Createlink' if 'cursorPositionedInALink' returns 'false'`, () => {
298+
299+
spyOn(component, <any>'cursorPositionedInALink').and.returnValue(false);
300+
spyOn(document, 'queryCommandState').and.returnValue(false);
301+
spyOn(document, 'queryCommandValue').and.returnValue('rgb');
302+
spyOn(component, <any>'rgbToHex').and.returnValue('hex');
303+
spyOn(component.commands, 'emit');
304+
305+
component['emitSelectionCommands']();
306+
307+
expect(component.commands.emit).toHaveBeenCalledWith({commands: [], hexColor: 'hex'});
308+
});
309+
310+
it('handleCommandLink: should call `insertHtmlLinkElement` if isIE returns `true`', () => {
311+
const fakeValue = {
312+
command: 'InsertHTML',
313+
urlLink: 'urlLink',
314+
urlLinkText: 'url link text'
315+
};
316+
317+
spyOn(UtilsFunction, 'isIE').and.returnValue(true);
318+
spyOn(component, <any>'insertHtmlLinkElement');
319+
spyOn(document, <any>'execCommand');
320+
321+
component['handleCommandLink'](fakeValue.command, fakeValue.urlLink, fakeValue.urlLinkText);
322+
323+
expect(document.execCommand).not.toHaveBeenCalled();
324+
expect(UtilsFunction.isIE).toHaveBeenCalled();
325+
expect(component['insertHtmlLinkElement']).toHaveBeenCalledWith(fakeValue.urlLink, fakeValue.urlLinkText);
326+
});
327+
328+
it('handleCommandLink: should call `document.execCommand` with `command`, `false` and linkValue as params if isIE is `false`', () => {
329+
const linkValue = `<a class="po-rich-text-link" href="urlLink" target="_blank">url link text</a>`;
330+
const fakeValue = {
331+
command: 'InsertHTML',
332+
urlLink: 'urlLink',
333+
urlLinkText: 'url link text'
334+
};
335+
336+
spyOn(UtilsFunction, 'isIE').and.returnValue(false);
337+
spyOn(component, <any>'insertHtmlLinkElement');
338+
spyOn(document, <any>'execCommand');
339+
340+
component['handleCommandLink'](fakeValue.command, fakeValue.urlLink, fakeValue.urlLinkText);
341+
342+
expect(document.execCommand).toHaveBeenCalledWith(fakeValue.command, false, linkValue);
343+
expect(component['insertHtmlLinkElement']).not.toHaveBeenCalled();
344+
});
345+
346+
it(`handleCommandLink: the parameter 'linkvalue' should be concatenated with 'urlLink' if 'urlLinkText' is undefined`, () => {
347+
const linkValue = `<a class="po-rich-text-link" href="urlLink" target="_blank">urlLink</a>`;
348+
const fakeValue = {
349+
command: 'InsertHTML',
350+
urlLink: 'urlLink',
351+
urlLinkText: undefined
352+
};
353+
354+
spyOn(UtilsFunction, 'isIE').and.returnValue(false);
355+
spyOn(document, <any>'execCommand');
356+
357+
component['handleCommandLink'](fakeValue.command, fakeValue.urlLink, fakeValue.urlLinkText);
358+
359+
expect(document.execCommand).toHaveBeenCalledWith(fakeValue.command, false, linkValue);
360+
});
361+
180362
it('updateModel: should update `modelValue`', () => {
181363
component.bodyElement.nativeElement.innerHTML = 'teste';
182364
component['updateModel']();
@@ -258,14 +440,27 @@ describe('PoRichTextBodyComponent:', () => {
258440
expect(fakeThis.change.emit).not.toHaveBeenCalled();
259441
}));
260442

443+
it('insertHtmlLinkElement: should contain `po-rich-text-link`', () => {
444+
const urlLink = 'urlLink';
445+
const urlLinkText = 'url link text';
446+
447+
component.focus();
448+
449+
component['insertHtmlLinkElement'](urlLink, urlLinkText);
450+
451+
fixture.detectChanges();
452+
453+
expect(nativeElement.querySelector('.po-rich-text-link')).toBeTruthy();
454+
});
455+
261456
});
262457

263458
describe('Templates:', () => {
264459

265-
it('should contain `po-rich-text-body`', () => {
460+
it('should contain `po-rich-text-body`', () => {
266461

267-
expect(nativeElement.querySelector('.po-rich-text-body')).toBeTruthy();
268-
});
462+
expect(nativeElement.querySelector('.po-rich-text-body')).toBeTruthy();
463+
});
269464

270465
});
271466

projects/ui/src/lib/components/po-field/po-rich-text/po-rich-text-body/po-rich-text-body.component.ts

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
22

3+
import { isIE } from './../../../../utils/util';
4+
import { PoKeyCodeEnum } from './../../../../enums/po-key-code.enum';
5+
36
const poRichTextBodyCommands = [
4-
'bold', 'italic', 'underline', 'justifyleft', 'justifycenter', 'justifyright', 'justifyfull', 'insertUnorderedList'
7+
'bold', 'italic', 'underline', 'justifyleft', 'justifycenter', 'justifyright', 'justifyfull', 'insertUnorderedList', 'Createlink'
58
];
69

710
@Component({
@@ -27,6 +30,8 @@ export class PoRichTextBodyComponent implements OnInit {
2730

2831
@Output('p-commands') commands = new EventEmitter<any>();
2932

33+
@Output('p-shortcut-command') shortcutCommand = new EventEmitter<any>();
34+
3035
@Output('p-value') value = new EventEmitter<any>();
3136

3237
ngOnInit() {
@@ -36,11 +41,18 @@ export class PoRichTextBodyComponent implements OnInit {
3641
setTimeout(() => this.updateValueWithModelValue());
3742
}
3843

39-
executeCommand(command: (string | { command: any, value: string })) {
44+
executeCommand(command: (string | { command: any, value: string | any })) {
4045
this.bodyElement.nativeElement.focus();
4146

4247
if (typeof (command) === 'object') {
43-
document.execCommand(command.command, false, command.value);
48+
49+
if (command.command === 'InsertHTML') {
50+
const { command: linkCommand, value : { urlLink }, value : { urlLinkText} } = command;
51+
52+
this.handleCommandLink(linkCommand, urlLink, urlLinkText);
53+
} else {
54+
document.execCommand(command.command, false, command.value);
55+
}
4456
} else {
4557
document.execCommand(command, false, null);
4658
}
@@ -70,6 +82,15 @@ export class PoRichTextBodyComponent implements OnInit {
7082
this.valueBeforeChange = this.modelValue;
7183
}
7284

85+
onKeyDown(event) {
86+
const keyL = event.keyCode === PoKeyCodeEnum.keyL;
87+
88+
if (keyL && event.ctrlKey || keyL && event.metaKey) {
89+
event.preventDefault();
90+
this.shortcutCommand.emit();
91+
}
92+
}
93+
7394
onKeyUp() {
7495
// Tratamento necessário para eliminar a tag <br> criada no firefox quando o body for limpo.
7596
const bodyElement = this.bodyElement.nativeElement;
@@ -87,13 +108,51 @@ export class PoRichTextBodyComponent implements OnInit {
87108
setTimeout(() => this.onKeyUp());
88109
}
89110

111+
private cursorPositionedInALink() {
112+
const link = document.getSelection();
113+
114+
return link.focusNode.parentElement.tagName === 'A';
115+
}
116+
90117
private emitSelectionCommands() {
91118
const commands = poRichTextBodyCommands.filter(command => document.queryCommandState(command));
92119
const rgbColor = document.queryCommandValue('ForeColor');
93120
const hexColor = this.rgbToHex(rgbColor);
121+
122+
if (this.cursorPositionedInALink()) {
123+
commands.push('Createlink');
124+
}
125+
94126
this.commands.emit({commands, hexColor});
95127
}
96128

129+
private handleCommandLink(linkCommand: string, urlLink: string, urlLinkText: string) {
130+
if (isIE()) {
131+
this.insertHtmlLinkElement(urlLink, urlLinkText);
132+
} else {
133+
// necessário '&nbsp;' no fim pois o Firefox mantém o cursor dentro da tag;
134+
const linkValue = `<a class="po-rich-text-link" href="${urlLink}" target="_blank">${urlLinkText || urlLink}</a>`;
135+
136+
document.execCommand(linkCommand, false, linkValue);
137+
}
138+
}
139+
140+
// tratamento específico para IE pois não suporta o comando 'insertHTML'.
141+
private insertHtmlLinkElement(urlLink: string, urlLinkText: string) {
142+
const selection = document.getSelection();
143+
const selectionRange = selection.getRangeAt(0);
144+
const elementLink = document.createElement('a');
145+
const elementlinkText = document.createTextNode(urlLinkText);
146+
147+
elementLink.appendChild(elementlinkText);
148+
elementLink.href = urlLink;
149+
elementLink.setAttribute('target', '_blank');
150+
elementLink.classList.add('po-rich-text-link');
151+
152+
selectionRange.deleteContents();
153+
selectionRange.insertNode(elementLink);
154+
}
155+
97156
private rgbToHex(rgb) {
98157
// Tratamento necessário para converter o código rgb para hexadecimal.
99158
const sep = rgb.indexOf(',') > -1 ? ',' : ' ';

0 commit comments

Comments
 (0)