Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>code-block-copy-button works!</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {Component, inject} from '@angular/core';
import {MatIconButton} from '@angular/material/button';
import {MatIcon} from '@angular/material/icon';
import {MatSnackBar} from '@angular/material/snack-bar';
import {MatTooltip} from '@angular/material/tooltip';
import {Clipboard} from '@angular/cdk/clipboard';

@Component({
selector: 'code-block-copy-button',
imports: [MatIconButton, MatIcon, MatTooltip],
template: `
<button mat-icon-button matTooltip="Copy code to the clipboard" (click)="copy()">
<mat-icon>content_copy</mat-icon>
</button>
`,
})
export class CodeBlockCopyButton {
private _clipboard = inject(Clipboard);
private _snackbar = inject(MatSnackBar);

/** Code snippet that will be copied */
code = '';

copy(): void {
const message = this._clipboard.copy(this.code)
? 'Copied code snippet'
: 'Failed to copy code snippet';

this._snackbar.open(message, undefined, {duration: 2500});
}
}
26 changes: 26 additions & 0 deletions docs/src/app/shared/doc-viewer/doc-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,28 @@ describe('DocViewer', () => {
expect(clipboardSpy.copy).toHaveBeenCalled();
});

it('should show copy icon button for code blocks', () => {
const fixture = TestBed.createComponent(DocViewerTestComponent);
fixture.componentInstance.documentUrl = `http://material.angular.io/doc-with-code-block.html`;
fixture.detectChanges();

const url = fixture.componentInstance.documentUrl;
http.expectOne(url).flush(FAKE_DOCS[url]);

const docViewer = fixture.debugElement.query(By.directive(DocViewer));
expect(docViewer).not.toBeNull();

// Query all copy buttons within code blocks
const iconButtons = fixture.debugElement.queryAll(By.directive(MatIconButton));
// At least one icon button for copying code should exist
expect(iconButtons.length).toBeGreaterThan(0);

// Click on the first icon button to trigger copying the code
iconButtons[0].nativeNode.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
expect(clipboardSpy.copy).toHaveBeenCalledWith('const example = "test code";');
});

// TODO(mmalerba): Add test that example-viewer is instantiated.
});

Expand Down Expand Up @@ -262,6 +284,10 @@ const FAKE_DOCS: {[key: string]: string} = {
data-docs-api-module-import-button="import {MatIconModule} from '@angular/material/icon';">
</div>
</div>`,
'http://material.angular.io/doc-with-code-block.html': `
<div class="docs-markdown">
<pre><code>const example = "test code";</code></pre>
</div>`,
};

@Component({
Expand Down
30 changes: 30 additions & 0 deletions docs/src/app/shared/doc-viewer/doc-viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {ExampleViewer} from '../example-viewer/example-viewer';
import {HeaderLink} from './header-link';
import {DeprecatedFieldComponent} from './deprecated-tooltip';
import {ModuleImportCopyButton} from './module-import-copy-button';
import {CodeBlockCopyButton} from './code-block-copy-button/code-block-copy-button';

@Injectable({providedIn: 'root'})
class DocFetcher {
Expand Down Expand Up @@ -160,6 +161,9 @@ export class DocViewer implements OnDestroy {
// Create icon buttons to copy module import
this._createCopyIconForModule();

// Create icon button for code block
this._createCopyButtonsForCodeBlocks();

// Resolving and creating components dynamically in Angular happens synchronously, but since
// we want to emit the output if the components are actually rendered completely, we wait
// until the Angular zone becomes stable.
Expand Down Expand Up @@ -267,4 +271,30 @@ export class DocViewer implements OnDestroy {
this._portalHosts.push(elementPortalOutlet);
});
}

_createCopyButtonsForCodeBlocks() {
// Query all <pre> tags that contain <code> elements (markdown code blocks)
const codeBlockElements = this._elementRef.nativeElement.querySelectorAll(
'.docs-markdown pre:has(code)',
);

[...codeBlockElements].forEach((element: HTMLElement) => {
// Extract the text content from the code block
const codeElement = element.querySelector('code');
const codeSnippet = codeElement?.textContent || '';

const elementPortalOutlet = new DomPortalOutlet(element, this._appRef, this._injector);
const codeBlockCopyButtonPortal = new ComponentPortal(
CodeBlockCopyButton,
this._viewContainerRef,
);
const codeBlockCopyButtonOutlet = elementPortalOutlet.attach(codeBlockCopyButtonPortal);

if (codeSnippet) {
codeBlockCopyButtonOutlet.instance.code = codeSnippet;
}

this._portalHosts.push(elementPortalOutlet);
});
}
}
9 changes: 8 additions & 1 deletion docs/src/styles/_markdown.scss
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,22 @@
overflow-x: auto;
padding: 20px;
white-space: pre-wrap;

border: solid 1px var(--mat-sys-outline-variant);
border-radius: 12px;
position: relative;

code {
background: transparent;
padding: 0;
font-size: 100%;
}

code-block-copy-button {
position: absolute;
top: 5px;
right: 5px;
z-index: 2;
}
}

code {
Expand Down
Loading