Skip to content

Commit 73dc926

Browse files
authoredApr 21, 2024··
Fix uses of clipboard combine with katex directive (#515)
1 parent 8dc1829 commit 73dc926

File tree

2 files changed

+243
-183
lines changed

2 files changed

+243
-183
lines changed
 

‎lib/src/markdown.service.spec.ts

+234-176
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,9 @@ describe('MarkdownService', () => {
466466
const rootNode = document.createElement('button');
467467

468468
const componentRef = {
469+
changeDetectorRef: {
470+
markForCheck: () => {},
471+
},
469472
hostView: {
470473
rootNodes: [rootNode],
471474
onDestroy: (callback) => {},
@@ -486,182 +489,6 @@ describe('MarkdownService', () => {
486489
return { embeddedViewRef, rootNode };
487490
}
488491

489-
it('should render clipboard with default button when clipboard is true and buttonComponent/buttonTemplate is not provided', () => {
490-
491-
const preElement = document.createElement('pre');
492-
preElement.innerText = 'mock-pre-element-text';
493-
const container = document.createElement('div');
494-
container.append(preElement);
495-
496-
const { componentRef, rootNode } = mockComponentRef();
497-
498-
window['ClipboardJS'] = class ClipboardJS {};
499-
500-
const clipboardSpy = spyOn(window, 'ClipboardJS');
501-
502-
viewContainerRefSpy.createComponent.and.returnValue(componentRef);
503-
504-
markdownService.render(container, { clipboard: true }, viewContainerRef);
505-
506-
expect(viewContainerRefSpy.createComponent).toHaveBeenCalledWith(ClipboardButtonComponent as any);
507-
expect(clipboardSpy).toHaveBeenCalledWith(rootNode, { text: jasmine.any(Function) });
508-
expect((clipboardSpy.calls.argsFor(0)[1] as any).text()).toBe(preElement.innerText);
509-
});
510-
511-
it('should render clipboard with buttonComponent when clipboard is true and buttonComponent is provided', () => {
512-
513-
class MockButtonComponent { mockButton = true; }
514-
515-
const preElement = document.createElement('pre');
516-
preElement.innerText = 'mock-pre-element-text';
517-
const container = document.createElement('div');
518-
container.append(preElement);
519-
520-
const { componentRef, rootNode } = mockComponentRef();
521-
522-
window['ClipboardJS'] = class ClipboardJS {};
523-
524-
const clipboardSpy = spyOn(window, 'ClipboardJS');
525-
526-
viewContainerRefSpy.createComponent.and.returnValue(componentRef);
527-
528-
markdownService.render(
529-
container,
530-
{ clipboard: true, clipboardOptions: { buttonComponent: MockButtonComponent } },
531-
viewContainerRef,
532-
);
533-
534-
expect(viewContainerRefSpy.createComponent).toHaveBeenCalledWith(MockButtonComponent as any);
535-
expect(clipboardSpy).toHaveBeenCalledWith(rootNode, { text: jasmine.any(Function) });
536-
expect((clipboardSpy.calls.argsFor(0)[1] as any).text()).toBe(preElement.innerText);
537-
});
538-
539-
it('should render clipboard with buttonTemplate when clipboard is true and buttonTemplate is provided', () => {
540-
541-
const mockTemplateRef = {
542-
elementRef: { nativeElement: 'mock-template-ref' },
543-
} as TemplateRef<unknown>;
544-
545-
const preElement = document.createElement('pre');
546-
preElement.innerText = 'mock-pre-element-text';
547-
const container = document.createElement('div');
548-
container.append(preElement);
549-
550-
const { embeddedViewRef, rootNode } = mockEmbeddedViewRef();
551-
552-
window['ClipboardJS'] = class ClipboardJS {};
553-
554-
const clipboardSpy = spyOn(window, 'ClipboardJS');
555-
556-
viewContainerRefSpy.createEmbeddedView.and.returnValue(embeddedViewRef);
557-
558-
markdownService.render(
559-
container,
560-
{ clipboard: true, clipboardOptions: { buttonTemplate: mockTemplateRef } },
561-
viewContainerRef,
562-
);
563-
564-
expect(viewContainerRefSpy.createEmbeddedView).toHaveBeenCalledWith(mockTemplateRef);
565-
expect(clipboardSpy).toHaveBeenCalledWith(rootNode, { text: jasmine.any(Function) });
566-
expect((clipboardSpy.calls.argsFor(0)[1] as any).text()).toBe(preElement.innerText);
567-
});
568-
569-
it('should destroy clipboard instances when host view is destroyed', () => {
570-
571-
const preElement = document.createElement('pre');
572-
preElement.innerText = 'mock-pre-element-text';
573-
const container = document.createElement('div');
574-
container.append(preElement);
575-
576-
const { componentRef } = mockComponentRef();
577-
const mockClipboardInstance = { destroy: () => {} };
578-
579-
window['ClipboardJS'] = () => {};
580-
581-
spyOn(window, 'ClipboardJS').and.returnValue(mockClipboardInstance);
582-
583-
const hostViewDestroySpy = spyOn(componentRef.hostView, 'onDestroy');
584-
const clipboardDestroySpy = spyOn(mockClipboardInstance, 'destroy');
585-
586-
viewContainerRefSpy.createComponent.and.returnValue(componentRef);
587-
588-
markdownService.render(container, { clipboard: true }, viewContainerRef);
589-
590-
expect(hostViewDestroySpy).toHaveBeenCalled();
591-
592-
const hostViewDestroyCallback = hostViewDestroySpy.calls.argsFor(0)[0];
593-
hostViewDestroyCallback();
594-
595-
expect(clipboardDestroySpy).toHaveBeenCalled();
596-
});
597-
598-
it('should not render clipboard when clipboard is omitted/false/null/undefined', () => {
599-
600-
const preElement = document.createElement('pre');
601-
const container = document.createElement('div');
602-
container.append(preElement);
603-
604-
window['ClipboardJS'] = {
605-
new: () => {},
606-
};
607-
608-
spyOn(window, 'ClipboardJS');
609-
610-
const useCases = [
611-
() => markdownService.render(container),
612-
() => markdownService.render(container, { clipboard: false }, viewContainerRef),
613-
() => markdownService.render(container, { clipboard: null! }, viewContainerRef),
614-
() => markdownService.render(container, { clipboard: undefined }, viewContainerRef),
615-
];
616-
617-
useCases.forEach(func => {
618-
func();
619-
expect(window['ClipboardJS']).not.toHaveBeenCalled();
620-
});
621-
});
622-
623-
it('should not render clipboard or throw when platform is not browser', () => {
624-
625-
const preElement = document.createElement('pre');
626-
const container = document.createElement('div');
627-
container.append(preElement);
628-
629-
window['ClipboardJS'] = {};
630-
631-
spyOn(window, 'ClipboardJS');
632-
633-
markdownService['platform'] = 'server';
634-
635-
expect(() => markdownService.render(container, { clipboard: true })).not.toThrowError();
636-
expect(window['ClipboardJS']).not.toHaveBeenCalled();
637-
});
638-
639-
it('should throw when clipboard is called but not loaded', () => {
640-
641-
const container = document.createElement('div');
642-
643-
window['ClipboardJS'] = undefined;
644-
645-
expect(() => markdownService.render(container, { clipboard: true })).toThrowError(errorClipboardNotLoaded);
646-
});
647-
648-
it('should throw when clipboard is called and viewContainerRef is omitted/null/undefined', () => {
649-
650-
const container = document.createElement('div');
651-
652-
window['ClipboardJS'] = {};
653-
654-
const useCases = [
655-
() => markdownService.render(container, { clipboard: true }),
656-
() => markdownService.render(container, { clipboard: true }, null!),
657-
() => markdownService.render(container, { clipboard: true }, undefined),
658-
];
659-
660-
useCases.forEach(func => {
661-
expect(func).toThrowError(errorClipboardViewContainerRequired);
662-
});
663-
});
664-
665492
it('should render katex when katex is true', () => {
666493

667494
const element = document.createElement('div');
@@ -909,6 +736,237 @@ describe('MarkdownService', () => {
909736
expect(mermaid.run).not.toHaveBeenCalled();
910737
});
911738

739+
it('should render clipboard after katex and mermaid', () => {
740+
741+
const container = document.createElement('div');
742+
const pluginRenderingOrder: string[] = [];
743+
744+
// clipboard
745+
const clipboardPreElement = document.createElement('pre');
746+
clipboardPreElement.innerText = 'mock-pre-element-text';
747+
container.append(clipboardPreElement);
748+
749+
const { componentRef } = mockComponentRef();
750+
viewContainerRefSpy.createComponent.and.returnValue(componentRef);
751+
752+
window['ClipboardJS'] = class ClipboardJS {
753+
constructor() {
754+
pluginRenderingOrder.push('clipboard');
755+
}
756+
};
757+
758+
// katex
759+
const katexElement = document.createElement('div');
760+
katexElement.innerHTML = '$E=mc^2$';
761+
container.append(katexElement);
762+
763+
window['katex'] = {};
764+
window['renderMathInElement'] = (elem: HTMLElement, options?: KatexOptions) => {};
765+
766+
spyOn(window, 'renderMathInElement').and.callFake(() => {
767+
pluginRenderingOrder.push('katex');
768+
});
769+
770+
// mermaid
771+
const mermaidElement = document.createElement('div');
772+
mermaidElement.classList.add('mermaid');
773+
mermaidElement.innerHTML = 'graph TD; A-->B;';
774+
775+
container.append(mermaidElement);
776+
777+
window['mermaid'] = {
778+
initialize: (options: MermaidAPI.Config) => {},
779+
run: (runOptions: MermaidAPI.RunOptions) => {},
780+
};
781+
782+
spyOn(mermaid, 'run').and.callFake(() => {
783+
pluginRenderingOrder.push('mermaid');
784+
});
785+
786+
markdownService.render(container, { clipboard: true, katex: true, mermaid: true }, viewContainerRef);
787+
expect(pluginRenderingOrder).toEqual(['katex', 'mermaid', 'clipboard']);
788+
});
789+
790+
it('should render clipboard with default button when clipboard is true and buttonComponent/buttonTemplate is not provided', () => {
791+
792+
const preElement = document.createElement('pre');
793+
preElement.innerText = 'mock-pre-element-text';
794+
const container = document.createElement('div');
795+
container.append(preElement);
796+
797+
const { componentRef, rootNode } = mockComponentRef();
798+
799+
window['ClipboardJS'] = class ClipboardJS {};
800+
801+
const clipboardSpy = spyOn(window, 'ClipboardJS');
802+
const markForCheckSpy = spyOn(componentRef.changeDetectorRef, 'markForCheck');
803+
804+
viewContainerRefSpy.createComponent.and.returnValue(componentRef);
805+
806+
markdownService.render(container, { clipboard: true }, viewContainerRef);
807+
808+
expect(viewContainerRefSpy.createComponent).toHaveBeenCalledWith(ClipboardButtonComponent as any);
809+
expect(markForCheckSpy).toHaveBeenCalled();
810+
expect(clipboardSpy).toHaveBeenCalledWith(rootNode, { text: jasmine.any(Function) });
811+
expect((clipboardSpy.calls.argsFor(0)[1] as any).text()).toBe(preElement.innerText);
812+
});
813+
814+
it('should render clipboard with buttonComponent when clipboard is true and buttonComponent is provided', () => {
815+
816+
class MockButtonComponent { mockButton = true; }
817+
818+
const preElement = document.createElement('pre');
819+
preElement.innerText = 'mock-pre-element-text';
820+
const container = document.createElement('div');
821+
container.append(preElement);
822+
823+
const { componentRef, rootNode } = mockComponentRef();
824+
825+
window['ClipboardJS'] = class ClipboardJS {};
826+
827+
const clipboardSpy = spyOn(window, 'ClipboardJS');
828+
const markForCheckSpy = spyOn(componentRef.changeDetectorRef, 'markForCheck');
829+
830+
viewContainerRefSpy.createComponent.and.returnValue(componentRef);
831+
832+
markdownService.render(
833+
container,
834+
{ clipboard: true, clipboardOptions: { buttonComponent: MockButtonComponent } },
835+
viewContainerRef,
836+
);
837+
838+
expect(viewContainerRefSpy.createComponent).toHaveBeenCalledWith(MockButtonComponent as any);
839+
expect(markForCheckSpy).toHaveBeenCalled();
840+
expect(clipboardSpy).toHaveBeenCalledWith(rootNode, { text: jasmine.any(Function) });
841+
expect((clipboardSpy.calls.argsFor(0)[1] as any).text()).toBe(preElement.innerText);
842+
});
843+
844+
it('should render clipboard with buttonTemplate when clipboard is true and buttonTemplate is provided', () => {
845+
846+
const mockTemplateRef = {
847+
elementRef: { nativeElement: 'mock-template-ref' },
848+
} as TemplateRef<unknown>;
849+
850+
const preElement = document.createElement('pre');
851+
preElement.innerText = 'mock-pre-element-text';
852+
const container = document.createElement('div');
853+
container.append(preElement);
854+
855+
const { embeddedViewRef, rootNode } = mockEmbeddedViewRef();
856+
857+
window['ClipboardJS'] = class ClipboardJS {};
858+
859+
const clipboardSpy = spyOn(window, 'ClipboardJS');
860+
861+
viewContainerRefSpy.createEmbeddedView.and.returnValue(embeddedViewRef);
862+
863+
markdownService.render(
864+
container,
865+
{ clipboard: true, clipboardOptions: { buttonTemplate: mockTemplateRef } },
866+
viewContainerRef,
867+
);
868+
869+
expect(viewContainerRefSpy.createEmbeddedView).toHaveBeenCalledWith(mockTemplateRef);
870+
expect(clipboardSpy).toHaveBeenCalledWith(rootNode, { text: jasmine.any(Function) });
871+
expect((clipboardSpy.calls.argsFor(0)[1] as any).text()).toBe(preElement.innerText);
872+
});
873+
874+
it('should destroy clipboard instances when host view is destroyed', () => {
875+
876+
const preElement = document.createElement('pre');
877+
preElement.innerText = 'mock-pre-element-text';
878+
const container = document.createElement('div');
879+
container.append(preElement);
880+
881+
const { componentRef } = mockComponentRef();
882+
const mockClipboardInstance = { destroy: () => {} };
883+
884+
window['ClipboardJS'] = () => {};
885+
886+
spyOn(window, 'ClipboardJS').and.returnValue(mockClipboardInstance);
887+
888+
const hostViewDestroySpy = spyOn(componentRef.hostView, 'onDestroy');
889+
const clipboardDestroySpy = spyOn(mockClipboardInstance, 'destroy');
890+
891+
viewContainerRefSpy.createComponent.and.returnValue(componentRef);
892+
893+
markdownService.render(container, { clipboard: true }, viewContainerRef);
894+
895+
expect(hostViewDestroySpy).toHaveBeenCalled();
896+
897+
const hostViewDestroyCallback = hostViewDestroySpy.calls.argsFor(0)[0];
898+
hostViewDestroyCallback();
899+
900+
expect(clipboardDestroySpy).toHaveBeenCalled();
901+
});
902+
903+
it('should not render clipboard when clipboard is omitted/false/null/undefined', () => {
904+
905+
const preElement = document.createElement('pre');
906+
const container = document.createElement('div');
907+
container.append(preElement);
908+
909+
window['ClipboardJS'] = {
910+
new: () => {},
911+
};
912+
913+
spyOn(window, 'ClipboardJS');
914+
915+
const useCases = [
916+
() => markdownService.render(container),
917+
() => markdownService.render(container, { clipboard: false }, viewContainerRef),
918+
() => markdownService.render(container, { clipboard: null! }, viewContainerRef),
919+
() => markdownService.render(container, { clipboard: undefined }, viewContainerRef),
920+
];
921+
922+
useCases.forEach(func => {
923+
func();
924+
expect(window['ClipboardJS']).not.toHaveBeenCalled();
925+
});
926+
});
927+
928+
it('should not render clipboard or throw when platform is not browser', () => {
929+
930+
const preElement = document.createElement('pre');
931+
const container = document.createElement('div');
932+
container.append(preElement);
933+
934+
window['ClipboardJS'] = {};
935+
936+
spyOn(window, 'ClipboardJS');
937+
938+
markdownService['platform'] = 'server';
939+
940+
expect(() => markdownService.render(container, { clipboard: true })).not.toThrowError();
941+
expect(window['ClipboardJS']).not.toHaveBeenCalled();
942+
});
943+
944+
it('should throw when clipboard is called but not loaded', () => {
945+
946+
const container = document.createElement('div');
947+
948+
window['ClipboardJS'] = undefined;
949+
950+
expect(() => markdownService.render(container, { clipboard: true })).toThrowError(errorClipboardNotLoaded);
951+
});
952+
953+
it('should throw when clipboard is called and viewContainerRef is omitted/null/undefined', () => {
954+
955+
const container = document.createElement('div');
956+
957+
window['ClipboardJS'] = {};
958+
959+
const useCases = [
960+
() => markdownService.render(container, { clipboard: true }),
961+
() => markdownService.render(container, { clipboard: true }, null!),
962+
() => markdownService.render(container, { clipboard: true }, undefined),
963+
];
964+
965+
useCases.forEach(func => {
966+
expect(func).toThrowError(errorClipboardViewContainerRequired);
967+
});
968+
});
969+
912970
it('should highlight element', () => {
913971

914972
const element = document.createElement('div');

‎lib/src/markdown.service.ts

+9-7
Original file line numberDiff line numberDiff line change
@@ -203,13 +203,6 @@ export class MarkdownService {
203203
mermaidOptions,
204204
} = options;
205205

206-
if (clipboard) {
207-
this.renderClipboard(element, viewContainerRef, {
208-
...this.DEFAULT_CLIPBOARD_OPTIONS,
209-
...this.clipboardOptions,
210-
...clipboardOptions,
211-
});
212-
}
213206
if (katex) {
214207
this.renderKatex(element, {
215208
...this.DEFAULT_KATEX_OPTIONS,
@@ -222,6 +215,13 @@ export class MarkdownService {
222215
...mermaidOptions,
223216
});
224217
}
218+
if (clipboard) {
219+
this.renderClipboard(element, viewContainerRef, {
220+
...this.DEFAULT_CLIPBOARD_OPTIONS,
221+
...this.clipboardOptions,
222+
...clipboardOptions,
223+
});
224+
}
225225

226226
this.highlight(element);
227227
}
@@ -406,6 +406,7 @@ export class MarkdownService {
406406
if (buttonComponent) {
407407
const componentRef = viewContainerRef.createComponent(buttonComponent);
408408
embeddedViewRef = componentRef.hostView as EmbeddedViewRef<unknown>;
409+
componentRef.changeDetectorRef.markForCheck();
409410
}
410411
// use provided template via input property
411412
else if (buttonTemplate) {
@@ -415,6 +416,7 @@ export class MarkdownService {
415416
else {
416417
const componentRef = viewContainerRef.createComponent(ClipboardButtonComponent);
417418
embeddedViewRef = componentRef.hostView as EmbeddedViewRef<unknown>;
419+
componentRef.changeDetectorRef.markForCheck();
418420
}
419421

420422
// declare clipboard instance variable

0 commit comments

Comments
 (0)
Please sign in to comment.