mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
parent
0b47781c31
commit
8401f8940f
11 changed files with 137 additions and 42 deletions
|
|
@ -109,7 +109,10 @@ describe('DocViewer', () => {
|
|||
expect(exampleViewer).not.toBeNull();
|
||||
expect(exampleViewer.componentInstance.view()).toBe(CodeExampleViewMode.SNIPPET);
|
||||
|
||||
const checkIcon = fixture.debugElement.query(By.directive(IconComponent));
|
||||
const copySourceCodeButton = fixture.debugElement.query(By.directive(CopySourceCodeButton));
|
||||
expect(copySourceCodeButton).not.toBeNull();
|
||||
|
||||
const checkIcon = copySourceCodeButton.query(By.directive(IconComponent));
|
||||
expect((checkIcon.nativeElement as HTMLElement).classList).toContain(
|
||||
`material-symbols-outlined`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ export class DocViewer {
|
|||
path: string,
|
||||
): Promise<void> {
|
||||
const preview = Boolean(placeholder.getAttribute('preview'));
|
||||
const hideCode = Boolean(placeholder.getAttribute('hideCode'));
|
||||
const title = placeholder.getAttribute('header') ?? undefined;
|
||||
const firstCodeSnippetTitle =
|
||||
snippets.length > 0 ? (snippets[0].title ?? snippets[0].name) : undefined;
|
||||
|
|
@ -199,6 +200,7 @@ export class DocViewer {
|
|||
path,
|
||||
files: snippets,
|
||||
preview,
|
||||
hideCode,
|
||||
id: this.countOfExamples,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,35 @@
|
|||
<div class="docs-example-viewer" role="group">
|
||||
<header class="docs-example-viewer-actions">
|
||||
@if (view() === CodeExampleViewMode.SNIPPET) {
|
||||
<span>{{ exampleMetadata()?.title }}</span>
|
||||
}
|
||||
@if (showCode()) {
|
||||
@if (view() === CodeExampleViewMode.SNIPPET) {
|
||||
<span>{{ exampleMetadata()?.title }}</span>
|
||||
}
|
||||
|
||||
@if (view() === CodeExampleViewMode.MULTI_FILE) {
|
||||
<mat-tab-group #codeTabs animationDuration="0ms" mat-stretch-tabs="false">
|
||||
@for (tab of tabs(); track tab) {
|
||||
<mat-tab [label]="tab.name" />
|
||||
}
|
||||
</mat-tab-group>
|
||||
@if (view() === CodeExampleViewMode.MULTI_FILE) {
|
||||
<mat-tab-group #codeTabs animationDuration="0ms" mat-stretch-tabs="false">
|
||||
@for (tab of tabs(); track tab) {
|
||||
<mat-tab [label]="tab.name" />
|
||||
}
|
||||
</mat-tab-group>
|
||||
}
|
||||
} @else {
|
||||
<!-- Title placeholder -->
|
||||
<span aria-hidden="true"> </span>
|
||||
}
|
||||
|
||||
<div class="docs-example-viewer-icons">
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
class="docs-example-code-toggle"
|
||||
[attr.aria-checked]="showCode()"
|
||||
[attr.aria-label]="showCode() ? 'Hide code' : 'Show code'"
|
||||
(click)="showCode.set(!showCode())"
|
||||
[matTooltip]="showCode() ? 'Hide code' : 'Show code'"
|
||||
matTooltipPosition="above"
|
||||
>
|
||||
<docs-icon>{{showCode() ? 'code_off' : 'code'}}</docs-icon>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="docs-example-copy-link"
|
||||
|
|
@ -80,16 +97,18 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="docs-example-viewer-code-wrapper"
|
||||
[class.docs-example-viewer-snippet]="view() === CodeExampleViewMode.SNIPPET"
|
||||
[class.docs-example-viewer-multi-file]="view() === CodeExampleViewMode.MULTI_FILE"
|
||||
>
|
||||
<button docs-copy-source-code></button>
|
||||
@if (snippetCode()?.sanitizedContent; as content) {
|
||||
<div [innerHTML]="content"></div>
|
||||
}
|
||||
</div>
|
||||
@if (showCode()) {
|
||||
<div
|
||||
class="docs-example-viewer-code-wrapper"
|
||||
[class.docs-example-viewer-snippet]="view() === CodeExampleViewMode.SNIPPET"
|
||||
[class.docs-example-viewer-multi-file]="view() === CodeExampleViewMode.MULTI_FILE"
|
||||
>
|
||||
<button docs-copy-source-code></button>
|
||||
@if (snippetCode()?.sanitizedContent; as content) {
|
||||
<div [innerHTML]="content"></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (exampleComponent) {
|
||||
<div class="docs-example-viewer-preview">
|
||||
|
|
|
|||
|
|
@ -57,25 +57,25 @@
|
|||
display: flex;
|
||||
gap: 0.75rem;
|
||||
|
||||
svg {
|
||||
fill: var(--gray-400);
|
||||
}
|
||||
}
|
||||
a,
|
||||
button {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
color: var(--gray-400);
|
||||
|
||||
a,
|
||||
button {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
path {
|
||||
transition: fill 0.3s ease;
|
||||
}
|
||||
path {
|
||||
transition: fill 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
fill: var(--tertiary-contrast);
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--tertiary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -272,6 +272,50 @@ describe('ExampleViewer', () => {
|
|||
button.click();
|
||||
expect(spy.calls.argsFor(0)[0].trim()).toBe(`${window.location.href}#example-1`);
|
||||
});
|
||||
|
||||
it('should hide code content when `hideCode` is true', async () => {
|
||||
componentRef.setInput(
|
||||
'metadata',
|
||||
getMetadata({
|
||||
hideCode: true,
|
||||
}),
|
||||
);
|
||||
|
||||
await component.renderExample();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Initially, the code should be hidden.
|
||||
expect(component.showCode()).toBeFalse();
|
||||
let codeContainer = fixture.debugElement.query(By.css('.docs-example-viewer-code-wrapper'));
|
||||
expect(codeContainer).toBeNull();
|
||||
});
|
||||
|
||||
it('should expand/collapse code content with toggle button.', async () => {
|
||||
componentRef.setInput('metadata', getMetadata());
|
||||
|
||||
await component.renderExample();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Initially, the code should be visible.
|
||||
expect(component.showCode()).toBeTrue();
|
||||
let codeContainer = fixture.debugElement.query(By.css('.docs-example-viewer-code-wrapper'));
|
||||
expect(codeContainer).not.toBeNull();
|
||||
|
||||
const codeToggleButton = fixture.debugElement.query(By.css('.docs-example-code-toggle'));
|
||||
codeToggleButton.nativeElement.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showCode()).toBeFalse();
|
||||
codeContainer = fixture.debugElement.query(By.css('.docs-example-viewer-code-wrapper'));
|
||||
expect(codeContainer).toBeNull();
|
||||
|
||||
codeToggleButton.nativeElement.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.showCode()).toBeTrue();
|
||||
codeContainer = fixture.debugElement.query(By.css('.docs-example-viewer-code-wrapper'));
|
||||
expect(codeContainer).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const getMetadata = (value: Partial<ExampleMetadata> = {}): ExampleMetadata => {
|
||||
|
|
@ -282,6 +326,7 @@ const getMetadata = (value: Partial<ExampleMetadata> = {}): ExampleMetadata => {
|
|||
{name: 'example.css', sanitizedContent: ''},
|
||||
],
|
||||
preview: false,
|
||||
hideCode: false,
|
||||
...value,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {CommonModule, DOCUMENT} from '@angular/common';
|
|||
import {MatTabGroup, MatTabsModule} from '@angular/material/tabs';
|
||||
import {Clipboard} from '@angular/cdk/clipboard';
|
||||
import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-code-button.component';
|
||||
import {IconComponent} from '../../icon/icon.component';
|
||||
import {ExampleMetadata, Snippet} from '../../../interfaces/index';
|
||||
import {EXAMPLE_VIEWER_CONTENT_LOADER} from '../../../providers/index';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
|
|
@ -42,7 +43,7 @@ export const HIDDEN_CLASS_NAME = 'hidden';
|
|||
|
||||
@Component({
|
||||
selector: 'docs-example-viewer',
|
||||
imports: [CommonModule, CopySourceCodeButton, MatTabsModule, MatTooltipModule],
|
||||
imports: [CommonModule, CopySourceCodeButton, MatTabsModule, MatTooltipModule, IconComponent],
|
||||
templateUrl: './example-viewer.component.html',
|
||||
styleUrls: ['./example-viewer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
|
@ -75,6 +76,7 @@ export class ExampleViewer {
|
|||
readonly expandable = signal<boolean>(false);
|
||||
readonly expanded = signal<boolean>(false);
|
||||
readonly snippetCode = signal<Snippet | undefined>(undefined);
|
||||
readonly showCode = signal<boolean>(true);
|
||||
readonly tabs = computed(() =>
|
||||
this.exampleMetadata()?.files.map((file) => ({
|
||||
name:
|
||||
|
|
@ -98,6 +100,10 @@ export class ExampleViewer {
|
|||
|
||||
this.snippetCode.set(this.exampleMetadata()?.files[0]);
|
||||
|
||||
if (this.exampleMetadata()?.hideCode) {
|
||||
this.showCode.set(false);
|
||||
}
|
||||
|
||||
afterNextRender(
|
||||
() => {
|
||||
// Several function below query the DOM directly, we need to wait until the DOM is rendered.
|
||||
|
|
|
|||
|
|
@ -38,4 +38,6 @@ export interface ExampleMetadata {
|
|||
files: Snippet[];
|
||||
/** True when ExampleViewer should have preview */
|
||||
preview: boolean;
|
||||
/** Whether to hide code example by default. */
|
||||
hideCode: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ export interface DocsCodeMultifileToken extends Tokens.Generic {
|
|||
paneTokens: Token[];
|
||||
// True if we should display preview
|
||||
preview: boolean;
|
||||
/** Whether to hide code example by default. */
|
||||
hideCode: boolean;
|
||||
}
|
||||
|
||||
// Capture group 1: all attributes on the opening tag
|
||||
|
|
@ -28,6 +30,7 @@ const multiFileCodeRule = /^\s*<docs-code-multifile(.*?)>(.*?)<\/docs-code-multi
|
|||
|
||||
const pathRule = /path="([^"]*)"/;
|
||||
const previewRule = /preview/;
|
||||
const hideCodeRule = /hideCode/;
|
||||
|
||||
export const docsCodeMultifileExtension = {
|
||||
name: 'docs-code-multifile',
|
||||
|
|
@ -42,6 +45,7 @@ export const docsCodeMultifileExtension = {
|
|||
const attr = match[1].trim();
|
||||
const path = pathRule.exec(attr);
|
||||
const preview = previewRule.exec(attr) ? true : false;
|
||||
const hideCode = hideCodeRule.exec(attr) ? true : false;
|
||||
|
||||
const token: DocsCodeMultifileToken = {
|
||||
type: 'docs-code-multifile',
|
||||
|
|
@ -50,6 +54,7 @@ export const docsCodeMultifileExtension = {
|
|||
panes: match[2].trim(),
|
||||
paneTokens: [],
|
||||
preview: preview,
|
||||
hideCode,
|
||||
};
|
||||
this.lexer.blockTokens(token.panes, token.paneTokens);
|
||||
return token;
|
||||
|
|
@ -69,6 +74,9 @@ export const docsCodeMultifileExtension = {
|
|||
if (token.preview) {
|
||||
el.setAttribute('preview', `${token.preview}`);
|
||||
}
|
||||
if (token.hideCode) {
|
||||
el.setAttribute('hideCode', 'true');
|
||||
}
|
||||
|
||||
return el.outerHTML;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ const languageRule = /language="([^"]*)"/;
|
|||
const visibleLinesRule = /visibleLines="([^"]*)"/;
|
||||
const visibleRegionRule = /visibleRegion="([^"]*)"/;
|
||||
const previewRule = /preview/;
|
||||
const hideCodeRule = /hideCode/;
|
||||
|
||||
export const docsCodeExtension = {
|
||||
name: 'docs-code',
|
||||
|
|
@ -53,6 +54,7 @@ export const docsCodeExtension = {
|
|||
const visibleLines = visibleLinesRule.exec(attr);
|
||||
const visibleRegion = visibleRegionRule.exec(attr);
|
||||
const preview = previewRule.exec(attr) ? true : false;
|
||||
const hideCode = hideCodeRule.exec(attr) ? true : false;
|
||||
const classes = classRule.exec(attr);
|
||||
|
||||
let code = match[2]?.trim() ?? '';
|
||||
|
|
@ -76,6 +78,7 @@ export const docsCodeExtension = {
|
|||
visibleLines: visibleLines?.[1],
|
||||
visibleRegion: visibleRegion?.[1],
|
||||
preview: preview,
|
||||
hideCode,
|
||||
classes: classes?.[1]?.split(' '),
|
||||
};
|
||||
return token;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ export interface CodeToken extends Tokens.Generic {
|
|||
visibleRegion?: string;
|
||||
/* Whether we should display preview */
|
||||
preview?: boolean;
|
||||
/** Whether to hide code example by default. */
|
||||
hideCode?: boolean;
|
||||
/* The lines to display highlighting on */
|
||||
highlight?: string;
|
||||
|
||||
|
|
@ -121,6 +123,9 @@ function applyContainerAttributesAndClasses(el: Element, token: CodeToken) {
|
|||
if (token.preview) {
|
||||
el.setAttribute('preview', 'true');
|
||||
}
|
||||
if (token.hideCode) {
|
||||
el.setAttribute('hideCode', 'true');
|
||||
}
|
||||
if (token.language === 'mermaid') {
|
||||
el.setAttribute('mermaid', 'true');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ We also have styling for the terminal, just set the language as `shell`:
|
|||
| `visibleLines` | `string of number[]` | range of lines for collapse mode |
|
||||
| `visibleRegion` | `string` | **DEPRECATED** FOR `visibleLines` |
|
||||
| `preview` | `boolean` | (False) display preview |
|
||||
| `hideCode` | `boolean` | (False) Whether to collapse code example by default. |
|
||||
|
||||
### Multifile examples
|
||||
|
||||
|
|
@ -198,11 +199,12 @@ You can create multifile examples by wrapping the examples inside a `<docs-code-
|
|||
|
||||
#### `<docs-code-multifile>` Attributes
|
||||
|
||||
| Attributes | Type | Details |
|
||||
|:--- |:--- |:--- |
|
||||
| body contents | `string` | nested tabs of `docs-code` examples |
|
||||
| `path` | `string` | Path to code example for preview and external link |
|
||||
| `preview` | `boolean` | (False) display preview |
|
||||
| Attributes | Type | Details |
|
||||
|:--- |:--- |:--- |
|
||||
| body contents | `string` | nested tabs of `docs-code` examples |
|
||||
| `path` | `string` | Path to code example for preview and external link |
|
||||
| `preview` | `boolean` | (False) display preview |
|
||||
| `hideCode` | `boolean` | (False) Whether to collapse code example by default. |
|
||||
|
||||
### Adding `preview` to your code example
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue