mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
176 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ecb44d3d65 | |||
| bf3235cdce | |||
| 95af661400 | |||
| f99bb20a10 | |||
| 97e5dcb421 | |||
| db65bc25ca | |||
| 37cf37e3c3 | |||
| 822db64b93 | |||
| 9814e9bd80 | |||
| 93991b4ce2 | |||
| 810f76f574 | |||
| 0e024ecc27 | |||
| cf245ea0af | |||
| e39b22a932 | |||
| 5401332b0e | |||
| bdf8450b7a | |||
| 69540d32ef | |||
| 6a99f83659 | |||
| bae54a1621 | |||
| e366473109 | |||
| 0c12d8e733 | |||
| 8c029c6402 | |||
| 4cbb2793c2 | |||
| 596c25157c | |||
| 6b2a4c083b | |||
| 6eac1b3f22 | |||
| afb05ff1cb | |||
| 183715aedd | |||
| 523f639b76 | |||
| 168eed23b4 | |||
| 6ac209c24f | |||
| 0c729765a1 | |||
| ad29621d30 | |||
| e8794dd1f0 | |||
| de7f5ac9f1 | |||
| be6c246115 | |||
| 69b5a317ec | |||
| 7459d09a23 | |||
| 2ece9509a0 | |||
| 722a4669b9 | |||
| b3cc9e9c3b | |||
| 103a4dccdc | |||
| eadfa39767 | |||
| ec4bac0a3a | |||
| d16071a5a0 | |||
| 1f9e090910 | |||
| 62ade76cf8 | |||
| 0def76f3b3 | |||
| 0fbcd6ed28 | |||
| 0016849ca1 | |||
| e5e5fedf54 | |||
| 352fa3d8ad | |||
| b0657bcce9 | |||
| 130ff94400 | |||
| eaa83f9d27 | |||
| 6489397b99 | |||
| 15e0214037 | |||
| 81b30dcef0 | |||
| 9304c45313 | |||
| f2a97f4f95 | |||
| f7ab04018e | |||
| 61367000e4 | |||
| 6f15006648 | |||
| b9a46bcd64 | |||
| 31dea066d6 | |||
| 481092da4d | |||
| 8df60d4713 | |||
| 58ee039d5e | |||
| 3d0ee73892 | |||
| 8bb5937e71 | |||
| c969422da0 | |||
| 1a606b0bfc | |||
| 288a4fac01 | |||
| 889a02b1c3 | |||
| 23fa43d044 | |||
| c979dfd462 | |||
| 915b0e0b16 | |||
| 463945003d | |||
| 06ae22680e | |||
| f917b6e4d0 | |||
| 87d042ba8a | |||
| 4b561f8f35 | |||
| b9fb98c67c | |||
| 8a005eada5 | |||
| ae15dd94ef | |||
| 7792aad8da | |||
| 142c2dffb0 | |||
| 56f0fc1f9f | |||
| 9e52c1c840 | |||
| e904f34020 | |||
| be16d7fcc3 | |||
| 04c504ab40 | |||
| 91483ab5b3 | |||
| e088c80854 | |||
| 14d442a6c3 | |||
| 1999e31851 | |||
| f1349e1d0f | |||
| f3014c5392 | |||
| bbabf5451a | |||
| edbb403171 | |||
| d3e14b35d3 | |||
| 4bfe707ac2 | |||
| 4f11d6eee2 | |||
| a1cb9dfc0d | |||
| f02f351cda | |||
| e9837c94d0 | |||
| 5682527d94 | |||
| 4c1f3cf843 | |||
| daf0317bdc | |||
| 6cfc4d8c85 | |||
| da9149ea10 | |||
| bbe655ff14 | |||
| 7c0a7a02c0 | |||
| 35b2728994 | |||
| 653e898f78 | |||
| c0855f0e25 | |||
| c94a897248 | |||
| b666d2c20f | |||
| 634d55a4f6 | |||
| 5c08ae68ad | |||
| 60df722d29 | |||
| a531106578 | |||
| d7dca6dbb6 | |||
| f6eec4813f | |||
| 94c6ec0221 | |||
| a7b774c4a2 | |||
| 5673f6b04d | |||
| aedf8322b0 | |||
| e076eb2ae6 | |||
| aa08ecc538 | |||
| 4b82b9815d | |||
| bbbaf7a1b8 | |||
| 729bf5b828 | |||
| 07a0b87a4c | |||
| 551ffbf9f2 | |||
| 4d87b9e899 | |||
| 0da9eae24c | |||
| 15d2e8d54c | |||
| 3a453d6040 | |||
| 8bd4c074af | |||
| 5f97d6aec2 | |||
| 2f8bf933f4 | |||
| ae9505b702 | |||
| 0f4219cf0c | |||
| f689214178 | |||
| aef166f763 | |||
| 02f5e73a8d | |||
| 20f5e1291e | |||
| 8adc6b9425 | |||
| 35fa9b0f77 | |||
| 56ec97d2e1 | |||
| ba325c59c0 | |||
| 447a36ef95 | |||
| 8326e62313 | |||
| 9b35726e42 | |||
| cdebf751e4 | |||
| 66e582551e | |||
| 1efe907881 | |||
| 1074134d4b | |||
| ecc56447b9 | |||
| 85da4b3b2a | |||
| 3fa21cbbc5 | |||
| 1ee64a5c45 | |||
| 3235d6d6dd | |||
| 6ad2fe6ab0 | |||
| 0e0fb7bb61 | |||
| 8a0a00e212 | |||
| 092bfe95cc | |||
| c4384bf269 | |||
| 52852abb56 | |||
| 0f612dcaf8 | |||
| 9acccb11ff | |||
| e914767efb | |||
| 58be8e6bf8 | |||
| 9cc4dbeadd | |||
| 7f5d70a74a |
@@ -5,6 +5,9 @@ inputs:
|
||||
serviceKey:
|
||||
description: 'The service key used for firebase deployments.'
|
||||
required: true
|
||||
githubReleaseTrainReadToken:
|
||||
description: 'GitHub access token for reading release trains without rate limits.'
|
||||
required: true
|
||||
configPath:
|
||||
description: 'The path to the firebase config file.'
|
||||
required: true
|
||||
|
||||
@@ -13,3 +13,8 @@ export function getCredentialFilePath(): string {
|
||||
}
|
||||
return credentialFilePath;
|
||||
}
|
||||
|
||||
/** Github access token. Used for querying the active release trains. */
|
||||
export const githubReleaseTrainReadToken: string = getInput('githubReleaseTrainReadToken', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {fetchLongTermSupportBranchesFromNpm, ActiveReleaseTrains} from '@angular/ng-dev';
|
||||
import {ReleaseConfig} from '@angular/ng-dev';
|
||||
import {GitClient} from '@angular/ng-dev';
|
||||
import {AuthenticatedGitClient} from '@angular/ng-dev';
|
||||
|
||||
export interface Deployment {
|
||||
branch: string;
|
||||
@@ -14,7 +14,7 @@ export interface Deployment {
|
||||
export type Deployments = Map<string, Deployment>;
|
||||
|
||||
export async function getDeployments(): Promise<Deployments> {
|
||||
const {github} = await GitClient.get();
|
||||
const {github} = await AuthenticatedGitClient.get();
|
||||
const releaseTrains = await ActiveReleaseTrains.fetch({
|
||||
api: github,
|
||||
name: 'angular',
|
||||
|
||||
@@ -2,7 +2,8 @@ import {getInput, setFailed} from '@actions/core';
|
||||
import {context} from '@actions/github';
|
||||
import {deployToFirebase, setupRedirect} from './deploy';
|
||||
import {getDeployments} from './deployments';
|
||||
import {GithubConfig, setConfig} from '@angular/ng-dev';
|
||||
import {AuthenticatedGitClient, GithubConfig, setConfig} from '@angular/ng-dev';
|
||||
import {githubReleaseTrainReadToken} from './credential';
|
||||
|
||||
const refMatcher = /refs\/heads\/(.*)/;
|
||||
|
||||
@@ -14,6 +15,9 @@ async function deployDocs() {
|
||||
owner: 'angular',
|
||||
},
|
||||
});
|
||||
|
||||
AuthenticatedGitClient.configure(githubReleaseTrainReadToken);
|
||||
|
||||
if (context.eventName !== 'push') {
|
||||
throw Error();
|
||||
}
|
||||
|
||||
@@ -7433,6 +7433,7 @@ var require_lrucache = __commonJS({
|
||||
//
|
||||
var require_range = __commonJS({
|
||||
""(exports, module) {
|
||||
var SPACE_CHARACTERS = /\s+/g;
|
||||
var Range = class {
|
||||
constructor(range, options) {
|
||||
options = parseOptions(options);
|
||||
@@ -7446,13 +7447,13 @@ var require_range = __commonJS({
|
||||
if (range instanceof Comparator) {
|
||||
this.raw = range.value;
|
||||
this.set = [[range]];
|
||||
this.format();
|
||||
this.formatted = void 0;
|
||||
return this;
|
||||
}
|
||||
this.options = options;
|
||||
this.loose = !!options.loose;
|
||||
this.includePrerelease = !!options.includePrerelease;
|
||||
this.raw = range.trim().split(/\s+/).join(" ");
|
||||
this.raw = range.trim().replace(SPACE_CHARACTERS, " ");
|
||||
this.set = this.raw.split("||").map((r) => this.parseRange(r.trim())).filter((c) => c.length);
|
||||
if (!this.set.length) {
|
||||
throw new TypeError(`Invalid SemVer Range: ${this.raw}`);
|
||||
@@ -7471,10 +7472,27 @@ var require_range = __commonJS({
|
||||
}
|
||||
}
|
||||
}
|
||||
this.format();
|
||||
this.formatted = void 0;
|
||||
}
|
||||
get range() {
|
||||
if (this.formatted === void 0) {
|
||||
this.formatted = "";
|
||||
for (let i = 0; i < this.set.length; i++) {
|
||||
if (i > 0) {
|
||||
this.formatted += "||";
|
||||
}
|
||||
const comps = this.set[i];
|
||||
for (let k = 0; k < comps.length; k++) {
|
||||
if (k > 0) {
|
||||
this.formatted += " ";
|
||||
}
|
||||
this.formatted += comps[k].toString().trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.formatted;
|
||||
}
|
||||
format() {
|
||||
this.range = this.set.map((comps) => comps.join(" ").trim()).join("||").trim();
|
||||
return this.range;
|
||||
}
|
||||
toString() {
|
||||
@@ -11344,6 +11362,9 @@ function getCredentialFilePath() {
|
||||
}
|
||||
return credentialFilePath;
|
||||
}
|
||||
var githubReleaseTrainReadToken = (0, import_core.getInput)("githubReleaseTrainReadToken", {
|
||||
required: true
|
||||
});
|
||||
|
||||
//
|
||||
async function deployToFirebase(deployment, configPath, distDirPath) {
|
||||
@@ -12977,7 +12998,7 @@ AuthenticatedGitClient._authenticatedInstance = null;
|
||||
|
||||
//
|
||||
async function getDeployments() {
|
||||
const { github } = await GitClient.get();
|
||||
const { github } = await AuthenticatedGitClient.get();
|
||||
const releaseTrains = await ActiveReleaseTrains.fetch({
|
||||
api: github,
|
||||
name: "angular",
|
||||
@@ -13033,6 +13054,7 @@ async function deployDocs() {
|
||||
owner: "angular"
|
||||
}
|
||||
});
|
||||
AuthenticatedGitClient.configure(githubReleaseTrainReadToken);
|
||||
if (import_github3.context.eventName !== "push") {
|
||||
throw Error();
|
||||
}
|
||||
|
||||
@@ -197,5 +197,6 @@ jobs:
|
||||
uses: ./.github/actions/deploy-docs-site
|
||||
with:
|
||||
serviceKey: ${{ secrets.ANGULAR_DEV_SITE_DEPLOY }}
|
||||
githubReleaseTrainReadToken: ${{ secrets.DOCS_DEPLOY_GITHUB_RELEASE_TRAIN_TOKEN }}
|
||||
configPath: 'adev/firebase.json'
|
||||
distDir: 'dist/bin/adev/build/browser'
|
||||
|
||||
@@ -39,7 +39,9 @@ jobs:
|
||||
- name: Check code format
|
||||
run: yarn ng-dev format changed --check ${{ github.event.pull_request.base.sha }}
|
||||
- name: Check Package Licenses
|
||||
uses: angular/dev-infra/github-actions/linting/licenses@515b575bd6650a12353b6039b618cdb808280ad7
|
||||
uses: angular/dev-infra/github-actions/linting/licenses@56966c765e97f463c4795fac85d46543d1e6b342
|
||||
with:
|
||||
allow-dependencies-licenses: 'pkg:npm/google-protobuf@'
|
||||
|
||||
devtools:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -93,7 +95,7 @@ jobs:
|
||||
- name: Run CI tests for framework
|
||||
run: yarn tsx ./scripts/build/build-packages-dist.mts
|
||||
- name: Archive build artifacts
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
with:
|
||||
name: pr-artifacts-${{ github.event.number }}
|
||||
path: dist/packages-dist/
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Run analysis'
|
||||
uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3
|
||||
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
# Upload the results as artifacts.
|
||||
- name: 'Upload artifact'
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
@@ -47,6 +47,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: 'Upload to code-scanning'
|
||||
uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
|
||||
uses: github/codeql-action/upload-sarif@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
+2
-1
@@ -597,7 +597,8 @@ groups:
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'adev/**/{.*,*}'
|
||||
'adev/**/{.*,*}',
|
||||
'aio/**/{.*,*}'
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
|
||||
+154
-108
@@ -1,3 +1,157 @@
|
||||
<a name="18.1.5"></a>
|
||||
# 18.1.5 (2024-08-14)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5401332b0e](https://github.com/angular/angular/commit/5401332b0ef1ec398a5e9767ca73cec544635c93) | fix | generate valid TS 5.6 type checking code ([#57303](https://github.com/angular/angular/pull/57303)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e39b22a932](https://github.com/angular/angular/commit/e39b22a932384f228e97414c44d10c7e158dfd2f) | fix | Account for addEventListener to be passed a Window or Document. ([#57282](https://github.com/angular/angular/pull/57282)) |
|
||||
| [db65bc25ca](https://github.com/angular/angular/commit/db65bc25cab413221fef1c2cdaf7c53f569219c8) | fix | Account for addEventListener to be passed a Window or Document. ([#57354](https://github.com/angular/angular/pull/57354)) |
|
||||
| [0e024ecc27](https://github.com/angular/angular/commit/0e024ecc27815c308feef0dbdf36d4d751af4436) | fix | complete post-hydration cleanup in components that use ViewContainerRef ([#57300](https://github.com/angular/angular/pull/57300)) |
|
||||
| [822db64b93](https://github.com/angular/angular/commit/822db64b937db8a581ec9612cf9e3e6e149c820f) | fix | skip hydration for i18n nodes that were not projected ([#57356](https://github.com/angular/angular/pull/57356)) |
|
||||
| [810f76f574](https://github.com/angular/angular/commit/810f76f57416853d5bc006e57bea070416117e79) | fix | take skip hydration flag into account while hydrating i18n blocks ([#57299](https://github.com/angular/angular/pull/57299)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.1.4"></a>
|
||||
# 18.1.4 (2024-08-07)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [6a99f83659](https://github.com/angular/angular/commit/6a99f836593de35989cddc3db849da133814f8fb) | fix | reduce chance of conflicts between generated factory and local variables ([#57181](https://github.com/angular/angular/pull/57181)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [afb05ff1cb](https://github.com/angular/angular/commit/afb05ff1cb008478c98987edfa54ec848ae555d6) | fix | support JIT transforms before other transforms modifying classes ([#57262](https://github.com/angular/angular/pull/57262)) |
|
||||
| [bae54a1621](https://github.com/angular/angular/commit/bae54a1621e1c0427519abfa81fc4796c43f1551) | perf | improve performance of `interpolatedSignalNotInvoked` extended diagnostic ([#57291](https://github.com/angular/angular/pull/57291)) |
|
||||
### language-service
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [6ac209c24f](https://github.com/angular/angular/commit/6ac209c24f46a1240b1a186eb403c79ef596d68a) | fix | avoid generating TS suggestion diagnostics for templates ([#56241](https://github.com/angular/angular/pull/56241)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.1.3"></a>
|
||||
# 18.1.3 (2024-07-31)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [31dea066d6](https://github.com/angular/angular/commit/31dea066d636bb49fa18b1172815b1ef7af4dbe5) | fix | reduce chance of conflicts between generated factory and local variables ([#57181](https://github.com/angular/angular/pull/57181)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1f9e090910](https://github.com/angular/angular/commit/1f9e09091076924a2f2c2d4bd6e5f65303bf8fea) | fix | emitting references to ngtypecheck files ([#57138](https://github.com/angular/angular/pull/57138)) ([#57202](https://github.com/angular/angular/pull/57202)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f7ab04018e](https://github.com/angular/angular/commit/f7ab04018ea0fe12781de987fff40fd1dc30f3f0) | fix | errors during ApplicationRef.tick should be rethrown for zoneless tests ([#56993](https://github.com/angular/angular/pull/56993)) |
|
||||
| [eaa83f9d27](https://github.com/angular/angular/commit/eaa83f9d279855b104597d396e39fe3496470daf) | fix | hydration error in some let declaration setups ([#57173](https://github.com/angular/angular/pull/57173)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.1.2"></a>
|
||||
# 18.1.2 (2024-07-24)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [463945003d](https://github.com/angular/angular/commit/463945003dcf253c64809ffdcddabedb87e78e06) | fix | limit the number of chained instructions ([#57069](https://github.com/angular/angular/pull/57069)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e904f34020](https://github.com/angular/angular/commit/e904f3402053ce9098854a31f2652020dd79e8f9) | fix | add warning for unused let declarations ([#57033](https://github.com/angular/angular/pull/57033)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [9e52c1c840](https://github.com/angular/angular/commit/9e52c1c8402dd5a54cf73caafff4a5b81d73fabd) | fix | `afterNextRender` hooks return that callback value. ([#57031](https://github.com/angular/angular/pull/57031)) |
|
||||
| [b9fb98c67c](https://github.com/angular/angular/commit/b9fb98c67c29d8e5697b72788f09f1263e8130f4) | fix | tree shake dev mode error message ([#57035](https://github.com/angular/angular/pull/57035)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.1.1"></a>
|
||||
# 18.1.1 (2024-07-17)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [a1cb9dfc0d](https://github.com/angular/angular/commit/a1cb9dfc0d3539d16020a53dd4c32311240a6265) | fix | Don't run preconnect assertion on the server. ([#56213](https://github.com/angular/angular/pull/56213)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [daf0317bdc](https://github.com/angular/angular/commit/daf0317bdcef79445eead4a3e0e1be75671f43fb) | fix | JIT mode incorrectly interpreting host directive configuration in partial compilation ([#57002](https://github.com/angular/angular/pull/57002)) |
|
||||
| [d7dca6dbb6](https://github.com/angular/angular/commit/d7dca6dbb6d8afc77a988de0b7471ac4e078762e) | fix | use strict equality for 'code' comparison ([#56944](https://github.com/angular/angular/pull/56944)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c94a897248](https://github.com/angular/angular/commit/c94a8972488f62656f5d0368b8763776e2cd39c6) | fix | avoid emitting references to typecheck files in TS 5.4 ([#56961](https://github.com/angular/angular/pull/56961)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5682527d94](https://github.com/angular/angular/commit/5682527d949b078e92ff7588c7e25dad37fffc52) | fix | not all callbacks running when registered at the same time ([#56981](https://github.com/angular/angular/pull/56981)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b666d2c20f](https://github.com/angular/angular/commit/b666d2c20f932f435fa6c51e1d74d7bca53381f2) | fix | fix common module removal ([#56968](https://github.com/angular/angular/pull/56968)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.1.0"></a>
|
||||
# 18.1.0 (2024-07-10)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f25653e231](https://github.com/angular/angular/commit/f25653e2311152d30b14d25acb0dccb4e2b5ea56) | fix | typo in NgOptimizedImage warning ([#56756](https://github.com/angular/angular/pull/56756)) |
|
||||
| [9b35726e42](https://github.com/angular/angular/commit/9b35726e42ebdeed138a25581e0a7eefff466206) | fix | typo in warning for NgOptimizedDirective ([#56817](https://github.com/angular/angular/pull/56817)) |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [fd6cd0422d](https://github.com/angular/angular/commit/fd6cd0422d2d761d2c6cc0cd41838fbba8a3f010) | feat | Add extended diagnostic to warn when there are uncalled functions in event bindings ([#56295](https://github.com/angular/angular/pull/56295)) |
|
||||
| [341a116d61](https://github.com/angular/angular/commit/341a116d611c095ed414c82612adb529e7be310c) | fix | allow more characters in let declaration name ([#56764](https://github.com/angular/angular/pull/56764)) |
|
||||
| [2a1291e942](https://github.com/angular/angular/commit/2a1291e942a3cd645ee635e72e7d83722383d39b) | fix | give precedence to local let declarations over parent ones ([#56752](https://github.com/angular/angular/pull/56752)) |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [66e582551e](https://github.com/angular/angular/commit/66e582551eb081e422e0df41badce1821c46dc62) | fix | avoid duplicate diagnostics for let declarations read before definition ([#56843](https://github.com/angular/angular/pull/56843)) |
|
||||
| [4d18c5bfd5](https://github.com/angular/angular/commit/4d18c5bfd54c53655955c8cd90472081ade40b34) | fix | flag all conflicts between let declarations and local symbols ([#56752](https://github.com/angular/angular/pull/56752)) |
|
||||
| [9e21582456](https://github.com/angular/angular/commit/9e215824565f0d30da7edb20087c4460069a6660) | fix | Show template syntax errors in local compilation modified ([#55855](https://github.com/angular/angular/pull/55855)) |
|
||||
| [5996502921](https://github.com/angular/angular/commit/599650292107f8856c7cd41791bd0856f9d14eb1) | fix | type check let declarations nested inside nodes ([#56752](https://github.com/angular/angular/pull/56752)) |
|
||||
| [cdebf751e4](https://github.com/angular/angular/commit/cdebf751e4949048b01acc92de2517f46fcd5d37) | fix | used before declared diagnostic not firing for control flow blocks ([#56843](https://github.com/angular/angular/pull/56843)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ea3c802056](https://github.com/angular/angular/commit/ea3c80205653af109c688a4d4808143b34591d54) | feat | Add a schematic to migrate afterRender phase flag ([#55648](https://github.com/angular/angular/pull/55648)) |
|
||||
| [5df3e78c99](https://github.com/angular/angular/commit/5df3e78c9907f522f2f96c087b10ca12d57f7028) | feat | add equality function to rxjs-interop `toSignal` ([#56447](https://github.com/angular/angular/pull/56447)) |
|
||||
| [0a48d584f2](https://github.com/angular/angular/commit/0a48d584f2ffeebb9402032182d4fc13a260c5cf) | feat | add support for let syntax ([#56715](https://github.com/angular/angular/pull/56715)) |
|
||||
| [352e0782ec](https://github.com/angular/angular/commit/352e0782ec37d2adcc662cfc69c83d38058a34bf) | feat | expose signal input metadata in `ComponentMirror` ([#56402](https://github.com/angular/angular/pull/56402)) |
|
||||
| [a655e46447](https://github.com/angular/angular/commit/a655e46447962bf56bf0184e3104328b9f7c1531) | feat | Redesign the `afterRender` & `afterNextRender` phases API ([#55648](https://github.com/angular/angular/pull/55648)) |
|
||||
| [e5a6f91722](https://github.com/angular/angular/commit/e5a6f917225aafa7c5c860f280d2aafe3615727e) | feat | support TypeScript 5.5 ([#56096](https://github.com/angular/angular/pull/56096)) |
|
||||
| [38effcc63e](https://github.com/angular/angular/commit/38effcc63eea360e948dc22860add72d3aa02288) | fix | Add back phase flag option as a deprecated API ([#55648](https://github.com/angular/angular/pull/55648)) |
|
||||
| [86bcfd3e49](https://github.com/angular/angular/commit/86bcfd3e498b8ec1de1a2a1ad0847fe567f7e9d4) | fix | improve docs on afterRender hooks ([#56522](https://github.com/angular/angular/pull/56522)) |
|
||||
| [b2445a0953](https://github.com/angular/angular/commit/b2445a095314aa66da038d3093e6a1b18fe5768b) | fix | link errors to ADEV ([#55554](https://github.com/angular/angular/pull/55554)) ([#56038](https://github.com/angular/angular/pull/56038)) |
|
||||
| [03a2acd2a3](https://github.com/angular/angular/commit/03a2acd2a3bdc87aaeb6b835a7c1016f800b31cb) | fix | properly remove imports in the afterRender phase migration ([#56524](https://github.com/angular/angular/pull/56524)) |
|
||||
| [4d87b9e899](https://github.com/angular/angular/commit/4d87b9e899381894a1de90f251da58613a96eed0) | fix | rename the equality function option in toSignal ([#56769](https://github.com/angular/angular/pull/56769)) ([#56922](https://github.com/angular/angular/pull/56922)) |
|
||||
| [8bd4c074af](https://github.com/angular/angular/commit/8bd4c074afe85b739dff4d3c4dcc19384c42b85e) | fix | toSignal equal option should be passed to inner computed ([#56903](https://github.com/angular/angular/pull/56903)) |
|
||||
### forms
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [00bde8b1c2](https://github.com/angular/angular/commit/00bde8b1c2d1511da40526a374d4e94d31e0d575) | fix | Make `NgControlStatus` host bindings `OnPush` compatible ([#55720](https://github.com/angular/angular/pull/55720)) |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [cc21989132](https://github.com/angular/angular/commit/cc21989132bc64b981df83cb6ff6e1506b42a1d0) | fix | Make `Content-Type` header case insensitive ([#56541](https://github.com/angular/angular/pull/56541)) |
|
||||
### language-service
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b400e2e4d4](https://github.com/angular/angular/commit/b400e2e4d4c27a9c8d8e91b52852ef7b64f7591a) | feat | autocompletion for the component not imported ([#55595](https://github.com/angular/angular/pull/55595)) |
|
||||
| [67b2c336bc](https://github.com/angular/angular/commit/67b2c336bc0bdce3f7ae054c094990a9831f5b20) | fix | import the default exported component correctly ([#56432](https://github.com/angular/angular/pull/56432)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [a13f5da773](https://github.com/angular/angular/commit/a13f5da77303f4ab2f1543df1de1f416216b5a9c) | feat | Allow `UrlTree` as an input to `routerLink` ([#56265](https://github.com/angular/angular/pull/56265)) |
|
||||
| [1d3a7529b4](https://github.com/angular/angular/commit/1d3a7529b4fa3617a5d6a97e742cb13818253a14) | feat | Set a different browser URL from the one for route matching ([#53318](https://github.com/angular/angular/pull/53318)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.0.5"></a>
|
||||
# 18.0.5 (2024-06-26)
|
||||
### core
|
||||
@@ -9,51 +163,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.1.0-next.4"></a>
|
||||
# 18.1.0-next.4 (2024-06-26)
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0a48d584f2](https://github.com/angular/angular/commit/0a48d584f2ffeebb9402032182d4fc13a260c5cf) | feat | add support for let syntax ([#56715](https://github.com/angular/angular/pull/56715)) |
|
||||
| [29ca6d10cc](https://github.com/angular/angular/commit/29ca6d10cc3cd75ebdf64658dafcb3ce579af343) | fix | improve support for i18n hydration of projected content ([#56192](https://github.com/angular/angular/pull/56192)) |
|
||||
| [5be16d06bd](https://github.com/angular/angular/commit/5be16d06bd504b1a72b7fb6f8d6e0c1b93a8188b) | fix | prevent calling devMode only function on `@defer` error. ([#56559](https://github.com/angular/angular/pull/56559)) |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [cc21989132](https://github.com/angular/angular/commit/cc21989132bc64b981df83cb6ff6e1506b42a1d0) | fix | Make `Content-Type` header case insensitive ([#56541](https://github.com/angular/angular/pull/56541)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.1.0-next.3"></a>
|
||||
# 18.1.0-next.3 (2024-06-20)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0b867e83b6](https://github.com/angular/angular/commit/0b867e83b624cbfe449ed3b7daf87642337824f0) | fix | insert constant statements after the first group of imports ([#56431](https://github.com/angular/angular/pull/56431)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5df3e78c99](https://github.com/angular/angular/commit/5df3e78c9907f522f2f96c087b10ca12d57f7028) | feat | add equality function to rxjs-interop `toSignal` ([#56447](https://github.com/angular/angular/pull/56447)) |
|
||||
| [352e0782ec](https://github.com/angular/angular/commit/352e0782ec37d2adcc662cfc69c83d38058a34bf) | feat | expose signal input metadata in `ComponentMirror` ([#56402](https://github.com/angular/angular/pull/56402)) |
|
||||
| [bf6df6f186](https://github.com/angular/angular/commit/bf6df6f18658dd0d477271f7eb969317ce1df024) | fix | do not activate event replay when no events are registered ([#56509](https://github.com/angular/angular/pull/56509)) |
|
||||
| [86bcfd3e49](https://github.com/angular/angular/commit/86bcfd3e498b8ec1de1a2a1ad0847fe567f7e9d4) | fix | improve docs on afterRender hooks ([#56522](https://github.com/angular/angular/pull/56522)) |
|
||||
| [03a2acd2a3](https://github.com/angular/angular/commit/03a2acd2a3bdc87aaeb6b835a7c1016f800b31cb) | fix | properly remove imports in the afterRender phase migration ([#56524](https://github.com/angular/angular/pull/56524)) |
|
||||
### forms
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [00bde8b1c2](https://github.com/angular/angular/commit/00bde8b1c2d1511da40526a374d4e94d31e0d575) | fix | Make `NgControlStatus` host bindings `OnPush` compatible ([#55720](https://github.com/angular/angular/pull/55720)) |
|
||||
### language-service
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [67b2c336bc](https://github.com/angular/angular/commit/67b2c336bc0bdce3f7ae054c094990a9831f5b20) | fix | import the default exported component correctly ([#56432](https://github.com/angular/angular/pull/56432)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1d3a7529b4](https://github.com/angular/angular/commit/1d3a7529b4fa3617a5d6a97e742cb13818253a14) | feat | Set a different browser URL from the one for route matching ([#53318](https://github.com/angular/angular/pull/53318)) |
|
||||
| [0d52c6b182](https://github.com/angular/angular/commit/0d52c6b18275433923a5fe94f3fc24323b73d0be) | fix | Delay the view transition to ensure renders in microtasks complete ([#56494](https://github.com/angular/angular/pull/56494)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.0.4"></a>
|
||||
# 18.0.4 (2024-06-20)
|
||||
### compiler-cli
|
||||
@@ -71,39 +180,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.1.0-next.2"></a>
|
||||
# 18.1.0-next.2 (2024-06-12)
|
||||
### benchpress
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [31d9fa40bdb](https://github.com/angular/angular/commit/31d9fa40bdbd1b815a819527312994b6ab1b532f) | fix | adjust supported browser names for headless chrome ([#56360](https://github.com/angular/angular/pull/56360)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ea3c8020565](https://github.com/angular/angular/commit/ea3c80205653af109c688a4d4808143b34591d54) | feat | Add a schematic to migrate afterRender phase flag ([#55648](https://github.com/angular/angular/pull/55648)) |
|
||||
| [a655e464479](https://github.com/angular/angular/commit/a655e46447962bf56bf0184e3104328b9f7c1531) | feat | Redesign the `afterRender` & `afterNextRender` phases API ([#55648](https://github.com/angular/angular/pull/55648)) |
|
||||
| [38effcc63ee](https://github.com/angular/angular/commit/38effcc63eea360e948dc22860add72d3aa02288) | fix | Add back phase flag option as a deprecated API ([#55648](https://github.com/angular/angular/pull/55648)) |
|
||||
| [d5c6ee432fc](https://github.com/angular/angular/commit/d5c6ee432fcd467c09b4d5d5366e731f5c91e8d4) | fix | async EventEmitter should contribute to app stability ([#56308](https://github.com/angular/angular/pull/56308)) |
|
||||
| [4c7d5d8acd8](https://github.com/angular/angular/commit/4c7d5d8acd8a714fe89366f76dc69f91356f0a06) | fix | signals should be tracked when embeddedViewRef.detectChanges is called ([#55719](https://github.com/angular/angular/pull/55719)) |
|
||||
### language-service
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b400e2e4d4c](https://github.com/angular/angular/commit/b400e2e4d4c27a9c8d8e91b52852ef7b64f7591a) | feat | autocompletion for the component not imported ([#55595](https://github.com/angular/angular/pull/55595)) |
|
||||
### localize
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [81486c2f477](https://github.com/angular/angular/commit/81486c2f4770361b4aecdc891107f70446e43bc3) | fix | add `@angular/localize/init` as polyfill in `angular.json` ([#56300](https://github.com/angular/angular/pull/56300)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [96aa5c8a2c9](https://github.com/angular/angular/commit/96aa5c8a2c9dd607dbcffa5d7923df4c21636510) | fix | resolve error in standalone migration ([#56302](https://github.com/angular/angular/pull/56302)) |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [a13f5da7730](https://github.com/angular/angular/commit/a13f5da77303f4ab2f1543df1de1f416216b5a9c) | feat | Allow `UrlTree` as an input to `routerLink` ([#56265](https://github.com/angular/angular/pull/56265)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.0.3"></a>
|
||||
# 18.0.3 (2024-06-12)
|
||||
### benchpress
|
||||
@@ -126,26 +202,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.1.0-next.1"></a>
|
||||
# 18.1.0-next.1 (2024-06-05)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [9e21582456](https://github.com/angular/angular/commit/9e215824565f0d30da7edb20087c4460069a6660) | fix | Show template syntax errors in local compilation modified ([#55855](https://github.com/angular/angular/pull/55855)) |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [8d75627ddd](https://github.com/angular/angular/commit/8d75627dddb82380dc8b3455a13f128e88309aa6) | fix | Do not migrate `HttpClientModule` imports on components. ([#56067](https://github.com/angular/angular/pull/56067)) |
|
||||
| [1081c8d623](https://github.com/angular/angular/commit/1081c8d6233ba1ff09187b95a09b0644e130cdf8) | fix | don't coerce all producers to consumers on liveness change ([#56140](https://github.com/angular/angular/pull/56140)) |
|
||||
| [b6fb53cdc7](https://github.com/angular/angular/commit/b6fb53cdc7c9f9dad5a113a68e72a2b723626bef) | fix | Fix shouldPreventDefaultBeforeDispatching bug ([#56188](https://github.com/angular/angular/pull/56188)) |
|
||||
| [31f3975e4b](https://github.com/angular/angular/commit/31f3975e4b1f418c1a6b2516618f44093ba20175) | fix | handle missing `withI18nSupport()` call for components that use i18n blocks ([#56175](https://github.com/angular/angular/pull/56175)) |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [75e811c18b](https://github.com/angular/angular/commit/75e811c18b16f42c6f72ac32e44c4bc4d37fc3b1) | fix | do not generate aliased variables with the same name ([#56154](https://github.com/angular/angular/pull/56154)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.0.2"></a>
|
||||
# 18.0.2 (2024-06-05)
|
||||
### core
|
||||
@@ -162,16 +218,6 @@
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.1.0-next.0"></a>
|
||||
# 18.1.0-next.0 (2024-05-29)
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e5a6f91722](https://github.com/angular/angular/commit/e5a6f917225aafa7c5c860f280d2aafe3615727e) | feat | support TypeScript 5.5 ([#56096](https://github.com/angular/angular/pull/56096)) |
|
||||
| [b2445a0953](https://github.com/angular/angular/commit/b2445a095314aa66da038d3093e6a1b18fe5768b) | fix | link errors to ADEV ([#55554](https://github.com/angular/angular/pull/55554)) ([#56038](https://github.com/angular/angular/pull/56038)) |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="18.0.1"></a>
|
||||
# 18.0.1 (2024-05-29)
|
||||
### compiler
|
||||
|
||||
+2
-2
@@ -353,9 +353,9 @@ DEPRECATED: <what is deprecated>
|
||||
Closes #<pr number>
|
||||
```
|
||||
|
||||
Breaking Change section should start with the phrase "BREAKING CHANGE: " followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
|
||||
Breaking Change section should start with the phrase `BREAKING CHANGE: ` followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
|
||||
|
||||
Similarly, a Deprecation section should start with "DEPRECATED: " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
|
||||
Similarly, a Deprecation section should start with `DEPRECATED: ` followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
|
||||
|
||||
|
||||
### Revert commits
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
·
|
||||
<a href="https://github.com/angular/angular/issues">Submit an Issue</a>
|
||||
·
|
||||
<a href="https://blog.angular.io/">Blog</a>
|
||||
<a href="https://blog.angular.dev/">Blog</a>
|
||||
<br>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
@@ -75,7 +75,7 @@ yarn_install(
|
||||
"//:.yarnrc",
|
||||
"//:tools/npm-patches/@bazel+jasmine+5.8.1.patch",
|
||||
"//tools:postinstall-patches.js",
|
||||
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-239d56b71911f9fa1eeefb6e4505dbe7b0cd81a7.patch",
|
||||
"//tools/esm-interop:patches/npm/@angular+build-tooling+0.0.0-d30a56c19bafaac67cf44e605ed8c2c0e45b0a51.patch",
|
||||
"//tools/esm-interop:patches/npm/@bazel+concatjs+5.8.1.patch",
|
||||
"//tools/esm-interop:patches/npm/@bazel+esbuild+5.7.1.patch",
|
||||
"//tools/esm-interop:patches/npm/@bazel+protractor+5.7.1.patch",
|
||||
@@ -143,10 +143,10 @@ cldr_xml_data_repository(
|
||||
# sass rules
|
||||
http_archive(
|
||||
name = "io_bazel_rules_sass",
|
||||
sha256 = "47c50aa960ddf875a2a2dd7efd1c839e5f8598725c00014680122b0e48d0ec7f",
|
||||
strip_prefix = "rules_sass-b222c61b3d3879ec45b66062b2c706a72f3d80bb",
|
||||
sha256 = "cd83736ea65d0df064283aea5922dbaf132dd2b3aa54e7151aae7edaa9572c3e",
|
||||
strip_prefix = "rules_sass-83022b98114c07e9588089c7fe8f76bc0262c7e7",
|
||||
urls = [
|
||||
"https://github.com/bazelbuild/rules_sass/archive/b222c61b3d3879ec45b66062b2c706a72f3d80bb.zip",
|
||||
"https://github.com/bazelbuild/rules_sass/archive/83022b98114c07e9588089c7fe8f76bc0262c7e7.zip",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
+10
-6
@@ -7,6 +7,10 @@ load("//adev/tools/local_deps:index.bzl", "ensure_local_package_deps", "link_loc
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
exports_files([
|
||||
"tsconfig.json",
|
||||
])
|
||||
|
||||
# All source and configuration files required to build the docs app
|
||||
APPLICATION_FILES = [
|
||||
"angular.json",
|
||||
@@ -17,18 +21,15 @@ APPLICATION_FILES = [
|
||||
["src/**/*"],
|
||||
exclude = [
|
||||
"src/**/*.spec.ts",
|
||||
# Temporarily exclude generated sources produced by the non-bazel
|
||||
# build until the whole project is built by bazel and this directory
|
||||
# isn't needed.
|
||||
"src/generated/**/*",
|
||||
],
|
||||
)
|
||||
|
||||
TEST_FILES = APPLICATION_FILES + [
|
||||
"karma.conf.js",
|
||||
"test-main.ts",
|
||||
"tsconfig.spec.json",
|
||||
] + glob(
|
||||
["**/*.spec.ts"],
|
||||
["src/**/*.spec.ts"],
|
||||
)
|
||||
|
||||
APPLICATION_ASSETS = [
|
||||
@@ -42,6 +43,7 @@ APPLICATION_ASSETS = [
|
||||
]
|
||||
|
||||
APPLICATION_DEPS = [
|
||||
"@npm//@angular/docs",
|
||||
"@npm//@angular/build",
|
||||
"@npm//@angular-devkit/build-angular",
|
||||
"@npm//@angular/animations",
|
||||
@@ -50,7 +52,6 @@ APPLICATION_DEPS = [
|
||||
"@npm//@angular/compiler",
|
||||
"@npm//@angular/compiler-cli",
|
||||
"@npm//@angular/core",
|
||||
"@npm//@angular/docs",
|
||||
"@npm//@angular/forms",
|
||||
"@npm//@angular/material",
|
||||
"@npm//@angular/platform-browser",
|
||||
@@ -78,6 +79,9 @@ APPLICATION_DEPS = [
|
||||
"@npm//@lezer/highlight",
|
||||
"@npm//@lezer/javascript",
|
||||
"@npm//@lezer/common",
|
||||
"@npm//@stackblitz/sdk",
|
||||
"@npm//open-in-idx",
|
||||
"@npm//@webcontainer/api",
|
||||
"@npm//@xterm/xterm",
|
||||
"@npm//@xterm/addon-fit",
|
||||
"@npm//algoliasearch",
|
||||
|
||||
+17
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
This site is built with Angular.
|
||||
|
||||
The content is written primarly in Markdown format located in `src/content`. For simple edits, you can directly edit the file on GitHub and generate a Pull Request.
|
||||
The content is written primarily in Markdown format located in `src/content`. For simple edits, you can directly edit the file on GitHub and generate a Pull Request.
|
||||
|
||||
## Local Development
|
||||
|
||||
@@ -24,6 +24,8 @@ yarn
|
||||
yarn docs
|
||||
```
|
||||
|
||||
If you are having issues with the docs building, see the [FAQs](#faqs) section.
|
||||
|
||||
## Contributing
|
||||
|
||||
Want to report a bug, contribute some code, or improve the documentation? Excellent!
|
||||
@@ -35,3 +37,17 @@ And if you're new, check out one of our issues labeled as <kbd>[help wanted](htt
|
||||
### Code of Conduct
|
||||
|
||||
Help us keep Angular open and inclusive. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
## FAQs
|
||||
|
||||
### The build is failing and I'm seeing `bazel:bazel failed: missing input file` messages.
|
||||
|
||||
This is most likely due to a bazel dependency / caching issue. To resolve this, run the following command:
|
||||
|
||||
```
|
||||
# Try this first
|
||||
yarn bazel clean
|
||||
|
||||
# If that doesn't work, try it with the expunge flag
|
||||
yarn bazel clean --expunge
|
||||
```
|
||||
|
||||
+1
-1
@@ -84,10 +84,10 @@
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"include": ["src/app"],
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"main": "test-main.ts",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["@angular/docs/styles/global-styles.scss"],
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"ignore": ["**/.*"],
|
||||
"headers": [
|
||||
{
|
||||
"source": "assets/tutorials/common/*.jpg",
|
||||
"source": "assets/images/tutorials/common/*.jpg",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Resource-Policy",
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
load("//tools:defaults.bzl", "ng_module", "ng_package")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "docs",
|
||||
srcs = [
|
||||
"index.ts",
|
||||
],
|
||||
module_name = "@angular/docs",
|
||||
deps = [
|
||||
"//adev/shared-docs/components",
|
||||
"//adev/shared-docs/constants",
|
||||
"//adev/shared-docs/directives",
|
||||
"//adev/shared-docs/interfaces",
|
||||
"//adev/shared-docs/pipes",
|
||||
"//adev/shared-docs/providers",
|
||||
"//adev/shared-docs/services",
|
||||
"//adev/shared-docs/testing",
|
||||
"//adev/shared-docs/utils",
|
||||
],
|
||||
)
|
||||
|
||||
ng_package(
|
||||
name = "npm_package",
|
||||
srcs = [
|
||||
"package.json",
|
||||
"//adev/shared-docs/icons",
|
||||
"//adev/shared-docs/pipeline:BUILD.bazel",
|
||||
"//adev/shared-docs/pipeline:_guides.bzl",
|
||||
"//adev/shared-docs/pipeline:_playground.bzl",
|
||||
"//adev/shared-docs/pipeline:_stackblitz.bzl",
|
||||
"//adev/shared-docs/pipeline:_tutorial.bzl",
|
||||
"//adev/shared-docs/pipeline:guides.mjs",
|
||||
"//adev/shared-docs/pipeline:guides-no-mermaid.mjs",
|
||||
"//adev/shared-docs/pipeline:playground.mjs",
|
||||
"//adev/shared-docs/pipeline:stackblitz.mjs",
|
||||
"//adev/shared-docs/pipeline:tutorial.mjs",
|
||||
"//adev/shared-docs/pipeline/examples/template:files",
|
||||
"//adev/shared-docs/pipeline/tutorials/common:files",
|
||||
"//adev/shared-docs/styles",
|
||||
],
|
||||
visibility = [
|
||||
"//adev:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
":docs",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ts_library(
|
||||
name = "components",
|
||||
srcs = [
|
||||
"index.ts",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//adev/shared-docs/components/algolia-icon",
|
||||
"//adev/shared-docs/components/breadcrumb",
|
||||
"//adev/shared-docs/components/cookie-popup",
|
||||
"//adev/shared-docs/components/copy-source-code-button",
|
||||
"//adev/shared-docs/components/icon",
|
||||
"//adev/shared-docs/components/navigation-list",
|
||||
"//adev/shared-docs/components/search-dialog",
|
||||
"//adev/shared-docs/components/select",
|
||||
"//adev/shared-docs/components/slide-toggle",
|
||||
"//adev/shared-docs/components/table-of-contents",
|
||||
"//adev/shared-docs/components/text-field",
|
||||
"//adev/shared-docs/components/viewers",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
load("//tools:defaults.bzl", "ng_module")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "algolia-icon",
|
||||
srcs = [
|
||||
"algolia-icon.component.ts",
|
||||
],
|
||||
assets = [
|
||||
"algolia-icon.component.html",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/components:__pkg__",
|
||||
"//adev/shared-docs/components/search-dialog:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//packages/core",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
<!-- Algolia logo -->
|
||||
<svg
|
||||
id="Layer_1"
|
||||
class="docs-algolia-logo"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 2196.2 500"
|
||||
>
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1,
|
||||
.cls-2 {
|
||||
fill: #003dff;
|
||||
}
|
||||
.cls-2 {
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M1070.38,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
|
||||
/>
|
||||
<rect class="cls-1" x="1845.88" y="104.73" width="62.58" height="277.9" rx="5.9" ry="5.9" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M1851.78,71.38h50.77c3.26,0,5.9-2.64,5.9-5.9V5.9c0-3.62-3.24-6.39-6.82-5.83l-50.77,7.95c-2.87,.45-4.99,2.92-4.99,5.83v51.62c0,3.26,2.64,5.9,5.9,5.9Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M1764.03,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M1631.95,142.72c-11.14-12.25-24.83-21.65-40.78-28.31-15.92-6.53-33.26-9.85-52.07-9.85-18.78,0-36.15,3.17-51.92,9.85-15.59,6.66-29.29,16.05-40.76,28.31-11.47,12.23-20.38,26.87-26.76,44.03-6.38,17.17-9.24,37.37-9.24,58.36,0,20.99,3.19,36.87,9.55,54.21,6.38,17.32,15.14,32.11,26.45,44.36,11.29,12.23,24.83,21.62,40.6,28.46,15.77,6.83,40.12,10.33,52.4,10.48,12.25,0,36.78-3.82,52.7-10.48,15.92-6.68,29.46-16.23,40.78-28.46,11.29-12.25,20.05-27.04,26.25-44.36,6.22-17.34,9.24-33.22,9.24-54.21,0-20.99-3.34-41.19-10.03-58.36-6.38-17.17-15.14-31.8-26.43-44.03Zm-44.43,163.75c-11.47,15.75-27.56,23.7-48.09,23.7-20.55,0-36.63-7.8-48.1-23.7-11.47-15.75-17.21-34.01-17.21-61.2,0-26.89,5.59-49.14,17.06-64.87,11.45-15.75,27.54-23.52,48.07-23.52,20.55,0,36.63,7.78,48.09,23.52,11.47,15.57,17.36,37.98,17.36,64.87,0,27.19-5.72,45.3-17.19,61.2Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M894.42,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M2133.97,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M1314.05,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-11.79,18.34-19.6,39.64-22.11,62.59-.58,5.3-.88,10.68-.88,16.14s.31,11.15,.93,16.59c4.28,38.09,23.14,71.61,50.66,94.52,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47h0c17.99,0,34.61-5.93,48.16-15.97,16.29-11.58,28.88-28.54,34.48-47.75v50.26h-.11v11.08c0,21.84-5.71,38.27-17.34,49.36-11.61,11.08-31.04,16.63-58.25,16.63-11.12,0-28.79-.59-46.6-2.41-2.83-.29-5.46,1.5-6.27,4.22l-12.78,43.11c-1.02,3.46,1.27,7.02,4.83,7.53,21.52,3.08,42.52,4.68,54.65,4.68,48.91,0,85.16-10.75,108.89-32.21,21.48-19.41,33.15-48.89,35.2-88.52V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,64.1s.65,139.13,0,143.36c-12.08,9.77-27.11,13.59-43.49,14.7-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-1.32,0-2.63-.03-3.94-.1-40.41-2.11-74.52-37.26-74.52-79.38,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M249.83,0C113.3,0,2,110.09,.03,246.16c-2,138.19,110.12,252.7,248.33,253.5,42.68,.25,83.79-10.19,120.3-30.03,3.56-1.93,4.11-6.83,1.08-9.51l-23.38-20.72c-4.75-4.21-11.51-5.4-17.36-2.92-25.48,10.84-53.17,16.38-81.71,16.03-111.68-1.37-201.91-94.29-200.13-205.96,1.76-110.26,92-199.41,202.67-199.41h202.69V407.41l-115-102.18c-3.72-3.31-9.42-2.66-12.42,1.31-18.46,24.44-48.53,39.64-81.93,37.34-46.33-3.2-83.87-40.5-87.34-86.81-4.15-55.24,39.63-101.52,94-101.52,49.18,0,89.68,37.85,93.91,85.95,.38,4.28,2.31,8.27,5.52,11.12l29.95,26.55c3.4,3.01,8.79,1.17,9.63-3.3,2.16-11.55,2.92-23.58,2.07-35.92-4.82-70.34-61.8-126.93-132.17-131.26-80.68-4.97-148.13,58.14-150.27,137.25-2.09,77.1,61.08,143.56,138.19,145.26,32.19,.71,62.03-9.41,86.14-26.95l150.26,133.2c6.44,5.71,16.61,1.14,16.61-7.47V9.48C499.66,4.25,495.42,0,490.18,0H249.83Z"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
@@ -0,0 +1,18 @@
|
||||
/*!
|
||||
* @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 {ChangeDetectionStrategy, Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-algolia-icon',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [],
|
||||
templateUrl: './algolia-icon.component.html',
|
||||
})
|
||||
export class AlgoliaIcon {}
|
||||
@@ -0,0 +1,54 @@
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "breadcrumb",
|
||||
srcs = [
|
||||
"breadcrumb.component.ts",
|
||||
],
|
||||
assets = [
|
||||
":breadcrumb.component.css",
|
||||
"breadcrumb.component.html",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/components:__pkg__",
|
||||
"//adev/shared-docs/components/viewers:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//adev/shared-docs/interfaces",
|
||||
"//adev/shared-docs/services",
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"//packages/router",
|
||||
],
|
||||
)
|
||||
|
||||
sass_binary(
|
||||
name = "style",
|
||||
src = "breadcrumb.component.scss",
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
":breadcrumb",
|
||||
"//adev/shared-docs/interfaces",
|
||||
"//adev/shared-docs/services",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
"//packages/platform-browser",
|
||||
"//packages/router",
|
||||
"//packages/router/testing",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
@for (breadcrumb of breadcrumbItems(); track breadcrumb) {
|
||||
<div class="docs-breadcrumb">
|
||||
@if (breadcrumb.path) {
|
||||
@if (breadcrumb.isExternal) {
|
||||
<a [href]="breadcrumb.path">{{ breadcrumb.label }}</a>
|
||||
} @else {
|
||||
<a [routerLink]="'/' + breadcrumb.path">{{ breadcrumb.label }}</a>
|
||||
}
|
||||
} @else {
|
||||
<span>{{ breadcrumb.label }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-block-end: 1.5rem;
|
||||
}
|
||||
|
||||
.docs-breadcrumb {
|
||||
span {
|
||||
color: var(--quaternary-contrast);
|
||||
font-size: 0.875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
span {
|
||||
&::after {
|
||||
content: 'chevron_right';
|
||||
font-family: var(--icons);
|
||||
margin-inline: 0.5rem;
|
||||
color: var(--quinary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*!
|
||||
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {Breadcrumb} from './breadcrumb.component';
|
||||
import {NavigationState} from '../../services';
|
||||
import {NavigationItem} from '../../interfaces';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {provideExperimentalZonelessChangeDetection} from '@angular/core';
|
||||
|
||||
describe('Breadcrumb', () => {
|
||||
let fixture: ComponentFixture<Breadcrumb>;
|
||||
let navigationStateSpy: jasmine.SpyObj<NavigationState>;
|
||||
|
||||
beforeEach(() => {
|
||||
navigationStateSpy = jasmine.createSpyObj('NavigationState', ['activeNavigationItem']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [Breadcrumb, RouterTestingModule],
|
||||
providers: [
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
{
|
||||
provide: NavigationState,
|
||||
useValue: navigationStateSpy,
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture = TestBed.createComponent(Breadcrumb);
|
||||
});
|
||||
|
||||
it('should display proper breadcrumb structure based on navigation state', () => {
|
||||
navigationStateSpy.activeNavigationItem.and.returnValue(item);
|
||||
|
||||
fixture.detectChanges();
|
||||
const breadcrumbs = fixture.debugElement.queryAll(By.css('.docs-breadcrumb span'));
|
||||
|
||||
expect(breadcrumbs.length).toBe(2);
|
||||
expect(breadcrumbs[0].nativeElement.innerText).toEqual('Grandparent');
|
||||
expect(breadcrumbs[1].nativeElement.innerText).toEqual('Parent');
|
||||
});
|
||||
|
||||
it('should display breadcrumb links when navigation item has got path', () => {
|
||||
navigationStateSpy.activeNavigationItem.and.returnValue(exampleItemWithPath);
|
||||
|
||||
fixture.detectChanges();
|
||||
const breadcrumbs = fixture.debugElement.queryAll(By.css('.docs-breadcrumb a'));
|
||||
|
||||
expect(breadcrumbs.length).toBe(1);
|
||||
expect(breadcrumbs[0].nativeElement.innerText).toEqual('Parent');
|
||||
expect(breadcrumbs[0].nativeElement.href).toEqual(`${window.origin}/example`);
|
||||
});
|
||||
});
|
||||
|
||||
const grandparent: NavigationItem = {
|
||||
label: 'Grandparent',
|
||||
};
|
||||
|
||||
const parent: NavigationItem = {
|
||||
label: 'Parent',
|
||||
parent: grandparent,
|
||||
};
|
||||
|
||||
const item: NavigationItem = {
|
||||
label: 'Active Item',
|
||||
parent: parent,
|
||||
};
|
||||
|
||||
const parentWithPath: NavigationItem = {
|
||||
label: 'Parent',
|
||||
path: '/example',
|
||||
};
|
||||
|
||||
const exampleItemWithPath: NavigationItem = {
|
||||
label: 'Active Item',
|
||||
parent: parentWithPath,
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/*!
|
||||
* @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 {ChangeDetectionStrategy, Component, OnInit, inject, signal} from '@angular/core';
|
||||
import {NavigationState} from '../../services/index';
|
||||
import {NavigationItem} from '../../interfaces/index';
|
||||
import {NgFor, NgIf} from '@angular/common';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-breadcrumb',
|
||||
standalone: true,
|
||||
imports: [NgIf, NgFor, RouterLink],
|
||||
templateUrl: './breadcrumb.component.html',
|
||||
styleUrls: ['./breadcrumb.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class Breadcrumb implements OnInit {
|
||||
private readonly navigationState = inject(NavigationState);
|
||||
|
||||
breadcrumbItems = signal<NavigationItem[]>([]);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setBreadcrumbItemsBasedOnNavigationStructure();
|
||||
}
|
||||
|
||||
private setBreadcrumbItemsBasedOnNavigationStructure(): void {
|
||||
let breadcrumbs: NavigationItem[] = [];
|
||||
|
||||
const traverse = (node: NavigationItem | null) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.parent) {
|
||||
breadcrumbs = [node.parent, ...breadcrumbs];
|
||||
traverse(node.parent);
|
||||
}
|
||||
};
|
||||
|
||||
traverse(this.navigationState.activeNavigationItem());
|
||||
|
||||
this.breadcrumbItems.set(breadcrumbs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "cookie-popup",
|
||||
srcs = [
|
||||
"cookie-popup.component.ts",
|
||||
],
|
||||
assets = [
|
||||
":cookie-popup.component.css",
|
||||
"cookie-popup.component.html",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/components:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//adev/shared-docs/providers",
|
||||
"//adev/shared-docs/utils",
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
],
|
||||
)
|
||||
|
||||
sass_binary(
|
||||
name = "style",
|
||||
src = "cookie-popup.component.scss",
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
":cookie-popup",
|
||||
"//adev/shared-docs/providers",
|
||||
"//adev/shared-docs/testing",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
@if (!hasAccepted()) {
|
||||
<div class="docs-cookies-popup docs-invert-mode">
|
||||
<p>This site uses cookies from Google to deliver its services and to analyze traffic.</p>
|
||||
|
||||
<div>
|
||||
<a href="https://policies.google.com/technologies/cookies" target="_blank" rel="noopener">
|
||||
<button class="docs-primary-btn" [attr.text]="'Learn more'" aria-label="Learn More">
|
||||
Learn more
|
||||
</button>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
(click)="accept()"
|
||||
class="docs-primary-btn"
|
||||
[attr.text]="'Ok, Got it'"
|
||||
aria-label="Ok, Got it"
|
||||
>
|
||||
Ok, Got it
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
:host {
|
||||
position: fixed;
|
||||
bottom: 0.5rem;
|
||||
right: 0.5rem;
|
||||
z-index: var(--z-index-cookie-consent);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
animation: 1s linear forwards 0.5s fadeIn;
|
||||
}
|
||||
|
||||
.docs-cookies-popup {
|
||||
padding: 1rem;
|
||||
background-color: var(--page-background);
|
||||
border: 1px solid var(--senary-contrast);
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
max-width: 265px;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-block-start: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-block: 0;
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
100% {
|
||||
opacity: 100%;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*!
|
||||
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {CookiePopup, STORAGE_KEY} from './cookie-popup.component';
|
||||
import {LOCAL_STORAGE} from '../../providers/index';
|
||||
import {MockLocalStorage} from '../../testing/index';
|
||||
import {provideExperimentalZonelessChangeDetection} from '@angular/core';
|
||||
|
||||
describe('CookiePopup', () => {
|
||||
let fixture: ComponentFixture<CookiePopup>;
|
||||
let mockLocalStorage = new MockLocalStorage();
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CookiePopup],
|
||||
providers: [
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
{
|
||||
provide: LOCAL_STORAGE,
|
||||
useValue: mockLocalStorage,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should make the popup visible by default', () => {
|
||||
initComponent(false);
|
||||
|
||||
expect(getCookiesPopup()).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should hide the cookies popup if the user has already accepted cookies', () => {
|
||||
initComponent(true);
|
||||
|
||||
expect(getCookiesPopup()).toBeNull();
|
||||
});
|
||||
|
||||
it('should hide the cookies popup', () => {
|
||||
initComponent(false);
|
||||
|
||||
accept();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getCookiesPopup()).toBeNull();
|
||||
});
|
||||
|
||||
it('should store the user confirmation', () => {
|
||||
initComponent(false);
|
||||
|
||||
expect(mockLocalStorage.getItem(STORAGE_KEY)).toBeNull();
|
||||
|
||||
accept();
|
||||
|
||||
expect(mockLocalStorage.getItem(STORAGE_KEY)).toBe('true');
|
||||
});
|
||||
|
||||
// Helpers
|
||||
function getCookiesPopup() {
|
||||
return (fixture.nativeElement as HTMLElement).querySelector('.docs-cookies-popup');
|
||||
}
|
||||
|
||||
function accept() {
|
||||
(fixture.nativeElement as HTMLElement)
|
||||
.querySelector<HTMLButtonElement>('button[text="Ok, Got it"]')
|
||||
?.click();
|
||||
}
|
||||
|
||||
function initComponent(cookiesAccepted: boolean) {
|
||||
mockLocalStorage.setItem(STORAGE_KEY, cookiesAccepted ? 'true' : null);
|
||||
fixture = TestBed.createComponent(CookiePopup);
|
||||
|
||||
fixture.detectChanges();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/*!
|
||||
* @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 {ChangeDetectionStrategy, Component, inject, signal} from '@angular/core';
|
||||
import {NgIf} from '@angular/common';
|
||||
import {LOCAL_STORAGE} from '../../providers/index';
|
||||
import {setCookieConsent} from '../../utils';
|
||||
|
||||
/**
|
||||
* Decelare gtag as part of the window in this file as gtag is expected to already be loaded.
|
||||
*/
|
||||
declare const window: Window & typeof globalThis & {gtag?: Function};
|
||||
|
||||
export const STORAGE_KEY = 'docs-accepts-cookies';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-cookie-popup',
|
||||
standalone: true,
|
||||
imports: [NgIf],
|
||||
templateUrl: './cookie-popup.component.html',
|
||||
styleUrls: ['./cookie-popup.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CookiePopup {
|
||||
private readonly localStorage = inject(LOCAL_STORAGE);
|
||||
|
||||
/** Whether the user has accepted the cookie disclaimer. */
|
||||
hasAccepted = signal<boolean>(false);
|
||||
|
||||
constructor() {
|
||||
// Needs to be in a try/catch, because some browsers will
|
||||
// throw when using `localStorage` in private mode.
|
||||
try {
|
||||
this.hasAccepted.set(this.localStorage?.getItem(STORAGE_KEY) === 'true');
|
||||
} catch {
|
||||
this.hasAccepted.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/** Accepts the cookie disclaimer. */
|
||||
protected accept(): void {
|
||||
try {
|
||||
this.localStorage?.setItem(STORAGE_KEY, 'true');
|
||||
} catch {}
|
||||
|
||||
this.hasAccepted.set(true);
|
||||
|
||||
// Enable Google Analytics consent properties
|
||||
setCookieConsent('granted');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "copy-source-code-button",
|
||||
srcs = [
|
||||
"copy-source-code-button.component.ts",
|
||||
],
|
||||
assets = [
|
||||
"copy-source-code-button.component.html",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/components:__pkg__",
|
||||
"//adev/shared-docs/components/viewers:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//adev/shared-docs/components/icon",
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"@npm//@angular/cdk",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
":copy-source-code-button",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
"//packages/platform-browser",
|
||||
"@npm//@angular/cdk",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<i>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="docs-copy"
|
||||
>
|
||||
<path
|
||||
d="M5 22C4.45 22 3.97917 21.8042 3.5875 21.4125C3.19583 21.0208 3 20.55 3 20V6H5V20H16V22H5ZM9 18C8.45 18 7.97917 17.8042 7.5875 17.4125C7.19583 17.0208 7 16.55 7 16V4C7 3.45 7.19583 2.97917 7.5875 2.5875C7.97917 2.19583 8.45 2 9 2H18C18.55 2 19.0208 2.19583 19.4125 2.5875C19.8042 2.97917 20 3.45 20 4V16C20 16.55 19.8042 17.0208 19.4125 17.4125C19.0208 17.8042 18.55 18 18 18H9ZM9 16H18V4H9V16Z"
|
||||
fill="#A39FA9"
|
||||
/>
|
||||
</svg>
|
||||
</i>
|
||||
|
||||
<docs-icon class="docs-check">check</docs-icon>
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
/*!
|
||||
* @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 {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
|
||||
|
||||
import {
|
||||
CONFIRMATION_DISPLAY_TIME_MS,
|
||||
CopySourceCodeButton,
|
||||
} from './copy-source-code-button.component';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
provideExperimentalZonelessChangeDetection,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {Clipboard} from '@angular/cdk/clipboard';
|
||||
|
||||
const SUCCESSFULLY_COPY_CLASS_NAME = 'docs-copy-source-code-button-success';
|
||||
const FAILED_COPY_CLASS_NAME = 'docs-copy-source-code-button-failed';
|
||||
|
||||
describe('CopySourceCodeButton', () => {
|
||||
let component: CodeSnippetWrapper;
|
||||
let fixture: ComponentFixture<CodeSnippetWrapper>;
|
||||
let copySpy: jasmine.Spy<(text: string) => boolean>;
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CodeSnippetWrapper],
|
||||
providers: [provideExperimentalZonelessChangeDetection()],
|
||||
});
|
||||
fixture = TestBed.createComponent(CodeSnippetWrapper);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const clipboardService = TestBed.inject(Clipboard);
|
||||
copySpy = spyOn(clipboardService, 'copy');
|
||||
});
|
||||
|
||||
it('should call clipboard service when clicked on copy source code', async () => {
|
||||
const expectedCodeToBeCopied = 'npm install -g @angular/cli';
|
||||
component.code.set(expectedCodeToBeCopied);
|
||||
|
||||
await fixture.whenStable();
|
||||
|
||||
const button = fixture.debugElement.query(By.directive(CopySourceCodeButton)).nativeElement;
|
||||
button.click();
|
||||
|
||||
expect(copySpy.calls.argsFor(0)[0].trim()).toBe(expectedCodeToBeCopied);
|
||||
});
|
||||
|
||||
it('should not copy lines marked as deleted when code snippet contains diff', async () => {
|
||||
const codeInHtmlFormat = `
|
||||
<code>
|
||||
<div class="line remove"><span class="hljs-tag"><<span class="hljs-name">div</span> *<span class="hljs-attr">ngFor</span>=<span class="hljs-string">"let product of products"</span>></span></div>
|
||||
<div class="line add"><span class="hljs-tag"><<span class="hljs-name">div</span> *<span class="hljs-attr">ngFor</span>=<span class="hljs-string">"let product of products()"</span>></span></div>
|
||||
</code>
|
||||
`;
|
||||
const expectedCodeToBeCopied = `<div *ngFor="let product of products()">`;
|
||||
component.code.set(codeInHtmlFormat);
|
||||
|
||||
await fixture.whenStable();
|
||||
|
||||
const button = fixture.debugElement.query(By.directive(CopySourceCodeButton)).nativeElement;
|
||||
button.click();
|
||||
|
||||
expect(copySpy.calls.argsFor(0)[0].trim()).toBe(expectedCodeToBeCopied);
|
||||
});
|
||||
|
||||
it(`should set ${SUCCESSFULLY_COPY_CLASS_NAME} for ${CONFIRMATION_DISPLAY_TIME_MS} ms when copy was executed properly`, fakeAsync(() => {
|
||||
component.code.set('example');
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const button = fixture.debugElement.query(By.directive(CopySourceCodeButton)).nativeElement;
|
||||
button.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(button).toHaveClass(SUCCESSFULLY_COPY_CLASS_NAME);
|
||||
|
||||
tick(CONFIRMATION_DISPLAY_TIME_MS);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(button).not.toHaveClass(SUCCESSFULLY_COPY_CLASS_NAME);
|
||||
}));
|
||||
|
||||
it(`should set ${FAILED_COPY_CLASS_NAME} for ${CONFIRMATION_DISPLAY_TIME_MS} ms when copy failed`, fakeAsync(() => {
|
||||
component.code.set('example');
|
||||
copySpy.and.throwError('Fake copy error');
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const button = fixture.debugElement.query(By.directive(CopySourceCodeButton)).nativeElement;
|
||||
button.click();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(button).toHaveClass(FAILED_COPY_CLASS_NAME);
|
||||
|
||||
tick(CONFIRMATION_DISPLAY_TIME_MS);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(button).not.toHaveClass(FAILED_COPY_CLASS_NAME);
|
||||
}));
|
||||
});
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<pre>
|
||||
<code [innerHtml]="code()"></code>
|
||||
</pre>
|
||||
<button docs-copy-source-code></button>
|
||||
`,
|
||||
imports: [CopySourceCodeButton],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
})
|
||||
class CodeSnippetWrapper {
|
||||
code = signal('');
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*!
|
||||
* @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 {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
WritableSignal,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Clipboard} from '@angular/cdk/clipboard';
|
||||
import {IconComponent} from '../icon/icon.component';
|
||||
|
||||
export const REMOVED_LINE_CLASS_NAME = '.line.remove';
|
||||
export const CONFIRMATION_DISPLAY_TIME_MS = 2000;
|
||||
|
||||
@Component({
|
||||
selector: 'button[docs-copy-source-code]',
|
||||
standalone: true,
|
||||
imports: [CommonModule, IconComponent],
|
||||
templateUrl: './copy-source-code-button.component.html',
|
||||
host: {
|
||||
'type': 'button',
|
||||
'aria-label': 'Copy example source to clipboard',
|
||||
'title': 'Copy example source',
|
||||
'(click)': 'copySourceCode()',
|
||||
'[class.docs-copy-source-code-button-success]': 'showCopySuccess()',
|
||||
'[class.docs-copy-source-code-button-failed]': 'showCopyFailure()',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CopySourceCodeButton {
|
||||
private readonly changeDetector = inject(ChangeDetectorRef);
|
||||
private readonly clipboard = inject(Clipboard);
|
||||
private readonly elementRef = inject(ElementRef);
|
||||
|
||||
protected readonly showCopySuccess = signal(false);
|
||||
protected readonly showCopyFailure = signal(false);
|
||||
|
||||
copySourceCode(): void {
|
||||
try {
|
||||
const codeElement = this.elementRef.nativeElement.parentElement.querySelector(
|
||||
'code',
|
||||
) as HTMLElement;
|
||||
const sourceCode = this.getSourceCode(codeElement);
|
||||
this.clipboard.copy(sourceCode);
|
||||
this.showResult(this.showCopySuccess);
|
||||
} catch {
|
||||
this.showResult(this.showCopyFailure);
|
||||
}
|
||||
}
|
||||
|
||||
private getSourceCode(codeElement: HTMLElement): string {
|
||||
this.showCopySuccess.set(false);
|
||||
this.showCopyFailure.set(false);
|
||||
|
||||
const removedLines: NodeList = codeElement.querySelectorAll(REMOVED_LINE_CLASS_NAME);
|
||||
|
||||
if (removedLines.length) {
|
||||
// Get only those lines which are not marked as removed
|
||||
const formattedText = Array.from(codeElement.querySelectorAll('.line:not(.remove)'))
|
||||
.map((line) => (line as HTMLDivElement).innerText)
|
||||
.join('\n');
|
||||
|
||||
return formattedText.trim();
|
||||
} else {
|
||||
const text: string = codeElement.innerText || '';
|
||||
return text.replace(/\n\n\n/g, ``).trim();
|
||||
}
|
||||
}
|
||||
|
||||
private showResult(messageState: WritableSignal<boolean>) {
|
||||
messageState.set(true);
|
||||
|
||||
setTimeout(() => {
|
||||
messageState.set(false);
|
||||
// It's required for code snippets embedded in the ExampleViewer.
|
||||
this.changeDetector.markForCheck();
|
||||
}, CONFIRMATION_DISPLAY_TIME_MS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
load("//tools:defaults.bzl", "ng_module")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "icon",
|
||||
srcs = [
|
||||
"icon.component.ts",
|
||||
],
|
||||
assets = [
|
||||
":icon.component.css",
|
||||
"icon.component.html",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/components:__pkg__",
|
||||
"//adev/shared-docs/components/copy-source-code-button:__pkg__",
|
||||
"//adev/shared-docs/components/navigation-list:__pkg__",
|
||||
"//adev/shared-docs/components/table-of-contents:__pkg__",
|
||||
"//adev/shared-docs/components/text-field:__pkg__",
|
||||
"//adev/shared-docs/components/viewers:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
],
|
||||
)
|
||||
|
||||
sass_binary(
|
||||
name = "style",
|
||||
src = "icon.component.scss",
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
<ng-content></ng-content>
|
||||
@@ -0,0 +1,3 @@
|
||||
.docs-icon_high-contrast {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*!
|
||||
* @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 {DOCUMENT} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
afterNextRender,
|
||||
computed,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-icon',
|
||||
standalone: true,
|
||||
templateUrl: './icon.component.html',
|
||||
styleUrl: './icon.component.scss',
|
||||
host: {
|
||||
'[class]': 'MATERIAL_SYMBOLS_OUTLINED',
|
||||
'[style.font-size.px]': 'fontSize()',
|
||||
'aria-hidden': 'true',
|
||||
'translate': 'no',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class IconComponent {
|
||||
fontSize = computed(() => {
|
||||
return IconComponent.isFontLoaded() ? null : 0;
|
||||
});
|
||||
|
||||
protected readonly MATERIAL_SYMBOLS_OUTLINED = 'material-symbols-outlined';
|
||||
|
||||
private static isFontLoaded = signal(false);
|
||||
/** Share the same promise across different instances of the component */
|
||||
private static whenFontLoad?: Promise<FontFace[]> | undefined;
|
||||
|
||||
constructor() {
|
||||
if (IconComponent.isFontLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const document = inject(DOCUMENT);
|
||||
afterNextRender(async () => {
|
||||
IconComponent.whenFontLoad ??= document.fonts.load('normal 1px "Material Symbols Outlined"');
|
||||
await IconComponent.whenFontLoad;
|
||||
IconComponent.isFontLoaded.set(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*!
|
||||
* @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
|
||||
*/
|
||||
|
||||
export * from './viewers/docs-viewer/docs-viewer.component';
|
||||
export * from './cookie-popup/cookie-popup.component';
|
||||
export * from './navigation-list/navigation-list.component';
|
||||
export * from './select/select.component';
|
||||
export * from './slide-toggle/slide-toggle.component';
|
||||
export * from './table-of-contents/table-of-contents.component';
|
||||
export * from './text-field/text-field.component';
|
||||
export * from './icon/icon.component';
|
||||
export * from './search-dialog/search-dialog.component';
|
||||
@@ -0,0 +1,58 @@
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "navigation-list",
|
||||
srcs = [
|
||||
"navigation-list.component.ts",
|
||||
],
|
||||
assets = [
|
||||
":navigation-list.component.css",
|
||||
"navigation-list.component.html",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/components:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//adev/shared-docs/components/icon",
|
||||
"//adev/shared-docs/interfaces",
|
||||
"//adev/shared-docs/pipes",
|
||||
"//adev/shared-docs/services",
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"//packages/router",
|
||||
],
|
||||
)
|
||||
|
||||
sass_binary(
|
||||
name = "style",
|
||||
src = "navigation-list.component.scss",
|
||||
deps = [
|
||||
"//adev/shared-docs/styles",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
":navigation-list",
|
||||
"//adev/shared-docs/interfaces",
|
||||
"//adev/shared-docs/services",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
"//packages/platform-browser",
|
||||
"//packages/router",
|
||||
"//packages/router/testing",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
<ng-template #navigationList let-navigationItems>
|
||||
<ul
|
||||
class="docs-navigation-list docs-faceted-list"
|
||||
[class.docs-navigation-list-dropdown]="isDropdownView"
|
||||
>
|
||||
@for (item of navigationItems; track $index) {
|
||||
<li
|
||||
class="docs-faceted-list-item"
|
||||
[class.docs-navigation-link-hidden]="displayItemsToLevel && item.level > displayItemsToLevel"
|
||||
>
|
||||
@if (item.path) { @if (item.isExternal) {
|
||||
<a [href]="item.path" target="_blank">
|
||||
<span [class.docs-external-link]="item.isExternal">{{ item.label }}</span>
|
||||
@if (item.children && item.level! > 1 && !item.isExpanded) {
|
||||
<docs-icon>chevron_right</docs-icon>
|
||||
}
|
||||
</a>
|
||||
} @else {
|
||||
<a
|
||||
[routerLink]="'/' + item.path"
|
||||
[routerLinkActiveOptions]="{
|
||||
queryParams: 'ignored',
|
||||
fragment: 'ignored',
|
||||
matrixParams: 'exact',
|
||||
paths: 'exact',
|
||||
exact: false
|
||||
}"
|
||||
routerLinkActive="docs-faceted-list-item-active"
|
||||
(click)="emitClickOnLink()"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
@if (item.children && !item.isExpanded) {
|
||||
<docs-icon>chevron_right</docs-icon>
|
||||
}
|
||||
</a>
|
||||
} } @else {
|
||||
<!-- Nav Section Header -->
|
||||
@if (item.level !== collapsableLevel && item.level !== expandableLevel) {
|
||||
<div class="docs-secondary-nav-header">
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Nav Button Expand/Collapse -->
|
||||
@if ((item.children && item.level === expandableLevel) || item.level === collapsableLevel) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="toggle(item)"
|
||||
attr.aria-label="{{ item.isExpanded ? 'Collapse' : 'Expand' }} {{ item.label }}"
|
||||
[attr.aria-expanded]="item.isExpanded"
|
||||
class="docs-secondary-nav-button"
|
||||
[class.docs-faceted-list-item-active]="item | isActiveNavigationItem: activeItem()"
|
||||
[class.docs-expanded-button]="item.children && item.level == collapsableLevel"
|
||||
[class.docs-not-expanded-button]="item.children && item.level === expandableLevel"
|
||||
[class.docs-nav-item-has-icon]="
|
||||
item.children && item.level === expandableLevel && !item.isExpanded
|
||||
"
|
||||
>
|
||||
@if (item.children && item.level === collapsableLevel) {
|
||||
<docs-icon>arrow_back</docs-icon>
|
||||
}
|
||||
<span>{{ item.label }}</span>
|
||||
</button>
|
||||
} } @if (item.children?.length > 0) {
|
||||
<ng-container
|
||||
*ngTemplateOutlet="navigationList; context: {$implicit: item.children}"
|
||||
></ng-container>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</ng-template>
|
||||
|
||||
<ng-container
|
||||
*ngTemplateOutlet="navigationList; context: {$implicit: navigationItems}"
|
||||
></ng-container>
|
||||
@@ -0,0 +1,148 @@
|
||||
@use '../../styles/media-queries' as mq;
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
min-width: var(--secondary-nav-width);
|
||||
list-style: none;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: 100vh;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-block: 1.5rem;
|
||||
font-size: 0.875rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--septenary-contrast);
|
||||
@include mq.for-tablet-landscape-down {
|
||||
background-color: var(--quinary-contrast);
|
||||
}
|
||||
|
||||
border-radius: 10px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--quinary-contrast);
|
||||
}
|
||||
|
||||
.docs-nav-secondary & {
|
||||
padding-block: 2rem;
|
||||
}
|
||||
|
||||
> .docs-faceted-list {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.docs-navigation-link-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.docs-nav-item-has-icon {
|
||||
&::after {
|
||||
// FIXME: for some reason this disappears when transformed
|
||||
content: 'chevron_right';
|
||||
font-size: 1.25rem;
|
||||
font-family: var(--icons);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.docs-secondary-nav-header {
|
||||
padding-block: 1.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.docs-secondary-nav-button {
|
||||
width: 15rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: none;
|
||||
padding-block: 1.25rem;
|
||||
padding-inline-start: 0;
|
||||
color: var(--primary-contrast);
|
||||
font-size: 0.875rem;
|
||||
font-family: var(--inter-font);
|
||||
line-height: 160%;
|
||||
letter-spacing: -0.00875rem;
|
||||
transition: color 0.3s ease, background 0.3s ease;
|
||||
text-align: left; // forces left alignment of text in button
|
||||
|
||||
&.docs-secondary-nav-button-active {
|
||||
// font gradient
|
||||
background-image: var(--pink-to-purple-vertical-gradient);
|
||||
&::before {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
background: var(--pink-to-purple-vertical-gradient);
|
||||
}
|
||||
&:hover {
|
||||
&::before {
|
||||
opacity: 1;
|
||||
transform: scaleY(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.docs-expanded-button {
|
||||
justify-content: start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
a,
|
||||
.docs-not-expanded-button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
line-height: 1.4rem;
|
||||
letter-spacing: -0.00875rem;
|
||||
padding: 0.5rem;
|
||||
padding-inline-start: 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
// Add padding-bottom to last item in the list
|
||||
.docs-navigation-list {
|
||||
width: 100%;
|
||||
|
||||
li:last-of-type {
|
||||
ul:last-of-type {
|
||||
li:last-of-type {
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:first-child {
|
||||
margin-inline-start: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-external-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 0.5rem;
|
||||
&::after {
|
||||
content: 'open_in_new';
|
||||
font-family: var(--icons);
|
||||
font-size: 1.1rem;
|
||||
color: var(--quinary-contrast);
|
||||
transition: color 0.3s ease;
|
||||
margin-inline-end: 0.4rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*!
|
||||
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {NavigationList} from './navigation-list.component';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {NavigationItem} from '../../interfaces';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {provideExperimentalZonelessChangeDetection, signal} from '@angular/core';
|
||||
import {NavigationState} from '../../services';
|
||||
|
||||
const navigationItems: NavigationItem[] = [
|
||||
{
|
||||
label: 'Introduction',
|
||||
path: 'guide',
|
||||
level: 1,
|
||||
},
|
||||
{
|
||||
label: 'Getting Started',
|
||||
level: 1,
|
||||
children: [
|
||||
{label: 'What is Angular?', path: 'guide/what-is-angular', level: 2},
|
||||
{label: 'Setup', path: 'guide/setup', level: 2},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe('NavigationList', () => {
|
||||
let component: NavigationList;
|
||||
let fixture: ComponentFixture<NavigationList>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NavigationList, RouterTestingModule],
|
||||
providers: [
|
||||
{provide: NavigationState, useClass: FakeNavigationListState},
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
],
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(NavigationList);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should display provided navigation structure', () => {
|
||||
component.navigationItems = [...navigationItems];
|
||||
fixture.detectChanges();
|
||||
|
||||
const links = fixture.debugElement.queryAll(By.css('a'));
|
||||
const nonClickableItem = fixture.debugElement.queryAll(By.css('.docs-secondary-nav-header'));
|
||||
|
||||
expect(links.length).toBe(3);
|
||||
expect(nonClickableItem.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should append `docs-navigation-list-dropdown` when isDropdownView is true', () => {
|
||||
component.isDropdownView = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const ulElement = fixture.debugElement.query(By.css('ul.docs-navigation-list-dropdown'));
|
||||
|
||||
expect(ulElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not append `docs-navigation-list-dropdown` when isDropdownView is false', () => {
|
||||
component.isDropdownView = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
const ulElement = fixture.debugElement.query(By.css('ul.docs-navigation-list-dropdown'));
|
||||
|
||||
expect(ulElement).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should emit linkClicked when user clicked on link', () => {
|
||||
const emitClickOnLinkSpy = spyOn(component, 'emitClickOnLink');
|
||||
component.navigationItems = [...navigationItems];
|
||||
fixture.detectChanges(true);
|
||||
|
||||
const guideLink = fixture.debugElement.query(By.css('a[href="/guide"]'));
|
||||
guideLink.nativeElement.click();
|
||||
|
||||
expect(emitClickOnLinkSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it(`should not call navigationState.toggleItem() when item's level is equal to 1 and is not neither expandable or collapsable level`, () => {
|
||||
const navigationState = TestBed.inject(NavigationState);
|
||||
const toggleItemSpy = spyOn(navigationState, 'toggleItem');
|
||||
const itemToToggle = navigationItems[1];
|
||||
|
||||
component.toggle(itemToToggle);
|
||||
|
||||
expect(toggleItemSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should call navigationState.toggleItem() when item's level is expandable`, () => {
|
||||
const navigationState = TestBed.inject(NavigationState);
|
||||
const toggleItemSpy = spyOn(navigationState, 'toggleItem');
|
||||
const itemToToggle = navigationItems[1];
|
||||
|
||||
component.expandableLevel = 1;
|
||||
component.toggle(itemToToggle);
|
||||
|
||||
expect(toggleItemSpy).toHaveBeenCalledOnceWith(itemToToggle);
|
||||
});
|
||||
|
||||
it(`should call navigationState.toggleItem() when item's level is collapsable`, () => {
|
||||
const navigationState = TestBed.inject(NavigationState);
|
||||
const toggleItemSpy = spyOn(navigationState, 'toggleItem');
|
||||
const itemToToggle = navigationItems[1].children![1];
|
||||
|
||||
component.collapsableLevel = 2;
|
||||
component.toggle(itemToToggle);
|
||||
|
||||
expect(toggleItemSpy).toHaveBeenCalledOnceWith(itemToToggle);
|
||||
});
|
||||
|
||||
it('should display items to provided level', () => {
|
||||
component.navigationItems = [...navigationItems];
|
||||
component.displayItemsToLevel = 1;
|
||||
fixture.detectChanges(true);
|
||||
|
||||
const visibleItems = fixture.debugElement.queryAll(
|
||||
By.css('li.docs-faceted-list-item:not(.docs-navigation-link-hidden)'),
|
||||
);
|
||||
const hiddenItems = fixture.debugElement.queryAll(
|
||||
By.css('li.docs-faceted-list-item.docs-navigation-link-hidden'),
|
||||
);
|
||||
|
||||
expect(visibleItems.length).toBe(2);
|
||||
expect(visibleItems[0].nativeElement.innerText).toBe(navigationItems[0].label);
|
||||
expect(visibleItems[1].nativeElement.innerText).toBe(navigationItems[1].label);
|
||||
expect(hiddenItems.length).toBe(2);
|
||||
expect(hiddenItems[0].nativeElement.innerText).toBe(navigationItems[1].children![0].label);
|
||||
expect(hiddenItems[1].nativeElement.innerText).toBe(navigationItems[1].children![1].label);
|
||||
});
|
||||
});
|
||||
|
||||
class FakeNavigationListState {
|
||||
isOpened = signal(true);
|
||||
activeNavigationItem = signal(navigationItems.at(1));
|
||||
toggleItem(item: NavigationItem) {}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*!
|
||||
* @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 {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import {NavigationItem} from '../../interfaces/index';
|
||||
import {NavigationState} from '../../services/index';
|
||||
import {RouterLink, RouterLinkActive} from '@angular/router';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {IconComponent} from '../icon/icon.component';
|
||||
import {IsActiveNavigationItem} from '../../pipes/is-active-navigation-item.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-navigation-list',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink, RouterLinkActive, IconComponent, IsActiveNavigationItem],
|
||||
templateUrl: './navigation-list.component.html',
|
||||
styleUrls: ['./navigation-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NavigationList {
|
||||
@Input({required: true}) navigationItems: NavigationItem[] = [];
|
||||
@Input() displayItemsToLevel: number = 2;
|
||||
@Input() collapsableLevel: number | undefined = undefined;
|
||||
@Input() expandableLevel: number = 2;
|
||||
@Input() isDropdownView = false;
|
||||
|
||||
@Output() linkClicked = new EventEmitter<void>();
|
||||
|
||||
private readonly navigationState = inject(NavigationState);
|
||||
|
||||
expandedItems = this.navigationState.expandedItems;
|
||||
activeItem = this.navigationState.activeNavigationItem;
|
||||
|
||||
toggle(item: NavigationItem): void {
|
||||
if (
|
||||
item.level === 1 &&
|
||||
item.level !== this.expandableLevel &&
|
||||
item.level !== this.collapsableLevel
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.navigationState.toggleItem(item);
|
||||
}
|
||||
|
||||
emitClickOnLink(): void {
|
||||
this.linkClicked.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "search-dialog",
|
||||
srcs = [
|
||||
"search-dialog.component.ts",
|
||||
],
|
||||
assets = [
|
||||
":search-dialog.component.css",
|
||||
"search-dialog.component.html",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/components:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//adev/shared-docs/components/algolia-icon",
|
||||
"//adev/shared-docs/components/text-field",
|
||||
"//adev/shared-docs/directives",
|
||||
"//adev/shared-docs/interfaces",
|
||||
"//adev/shared-docs/pipes",
|
||||
"//adev/shared-docs/services",
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"//packages/forms",
|
||||
"//packages/router",
|
||||
],
|
||||
)
|
||||
|
||||
sass_binary(
|
||||
name = "style",
|
||||
src = "search-dialog.component.scss",
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
":search-dialog",
|
||||
"//adev/shared-docs/components/algolia-icon",
|
||||
"//adev/shared-docs/providers",
|
||||
"//adev/shared-docs/services",
|
||||
"//adev/shared-docs/testing",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
"//packages/platform-browser",
|
||||
"//packages/router",
|
||||
"//packages/router/testing",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
@@ -0,0 +1,89 @@
|
||||
<dialog #searchDialog>
|
||||
<div class="docs-search-container" (docsClickOutside)="closeSearchDialog()">
|
||||
<docs-text-field
|
||||
[autofocus]="true"
|
||||
[hideIcon]="true"
|
||||
[ngModel]="searchQuery()"
|
||||
(ngModelChange)="updateSearchQuery($event)"
|
||||
class="docs-search-input"
|
||||
placeholder="Search docs"
|
||||
></docs-text-field>
|
||||
|
||||
@if (searchResults() && searchResults()!.length > 0) {
|
||||
<ul class="docs-search-results docs-mini-scroll-track">
|
||||
@for (result of searchResults(); track result.objectID) {
|
||||
<li docsSearchItem [item]="result">
|
||||
<a
|
||||
[routerLink]="'/' + result.url | relativeLink: 'pathname'"
|
||||
[fragment]="result.url | relativeLink: 'hash'"
|
||||
>
|
||||
<div>
|
||||
<div class="docs-result-icon-and-type">
|
||||
<!-- Icon -->
|
||||
<span class="docs-search-result-icon" aria-hidden="true">
|
||||
<i role="presentation" class="material-symbols-outlined docs-icon-small">
|
||||
{{ result.hierarchy.lvl0 === 'Tutorials' ? 'code' : 'description'}}
|
||||
</i>
|
||||
</span>
|
||||
<!-- Results type -->
|
||||
<span class="docs-search-results__type">
|
||||
@let snippet = result._snippetResult.hierarchy?.lvl1?.value ?? '';
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="highlightSnippet"
|
||||
[ngTemplateOutletContext]="{snippet}"
|
||||
></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@let content = result._snippetResult.content;
|
||||
@let hierarchy = result._snippetResult.hierarchy;
|
||||
@if (content || hierarchy?.lvl2 || hierarchy?.lvl3 || hierarchy?.lvl4) {
|
||||
<span class="docs-search-results__type docs-search-results__lvl2">
|
||||
@let snippet = getBestSnippetForMatch(result);
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="highlightSnippet"
|
||||
[ngTemplateOutletContext]="{snippet}"
|
||||
></ng-container>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Page title -->
|
||||
<span class="docs-result-page-title">{{ result.hierarchy?.lvl0 }}</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
} @else {
|
||||
<div class="docs-search-results docs-mini-scroll-track">
|
||||
@if (searchResults() === undefined) {
|
||||
<div class="docs-search-results__start-typing">
|
||||
<span>Start typing to see results</span>
|
||||
</div>
|
||||
} @else if (searchResults()?.length === 0) {
|
||||
<div class="docs-search-results__no-results">
|
||||
<span>No results found</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="docs-algolia">
|
||||
<span>Search by</span>
|
||||
<a href="https://www.algolia.com/developers/" target="_blank" rel="noopener">
|
||||
<docs-algolia-icon />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<ng-template #highlightSnippet let-snippet="snippet">
|
||||
@let parts = splitMarkedText(snippet);
|
||||
@for (part of parts; track $index) {
|
||||
@if (part.highlight) {
|
||||
<mark>{{part.text}}</mark>
|
||||
} @else {
|
||||
<span>{{part.text}}</span>
|
||||
}
|
||||
}
|
||||
</ng-template>
|
||||
@@ -0,0 +1,155 @@
|
||||
dialog {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding-block-end: 3rem;
|
||||
|
||||
&::backdrop {
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
}
|
||||
|
||||
.docs-search-container {
|
||||
width: 750px;
|
||||
max-width: 90vw;
|
||||
background-color: var(--page-background);
|
||||
border: 1px solid var(--senary-contrast);
|
||||
border-radius: 0.25rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
.docs-search-input {
|
||||
border-radius: 0.25rem 0.25rem 0 0;
|
||||
border: none;
|
||||
border-block-end: 1px solid var(--senary-contrast);
|
||||
height: 2.6875rem; // 43px;
|
||||
padding-inline-start: 1rem;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: 'Esc';
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
color: var(--gray-400);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
list-style-type: none;
|
||||
padding-inline: 0;
|
||||
padding-block-start: 1rem;
|
||||
margin: 0;
|
||||
border-block-end: 1px solid var(--senary-contrast);
|
||||
|
||||
li {
|
||||
border-inline-start: 2px solid var(--senary-contrast);
|
||||
margin-inline-start: 1rem;
|
||||
padding-inline-end: 1rem;
|
||||
padding-block: 0.25rem;
|
||||
|
||||
mark {
|
||||
background: #e62600;
|
||||
background: var(--red-to-orange-horizontal-gradient);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--secondary-contrast);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
|
||||
.docs-search-result-icon {
|
||||
i {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--septenary-contrast); // stylelint-disable-line
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: var(--octonary-contrast); // stylelint-disable-line
|
||||
border-inline-start: 2px solid var(--primary-contrast);
|
||||
a {
|
||||
span:not(.docs-result-page-title),
|
||||
.docs-search-results__type {
|
||||
color: var(--primary-contrast);
|
||||
i {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.docs-search-result-icon,
|
||||
.docs-search-results__type,
|
||||
.docs-result-page-title {
|
||||
color: var(--quaternary-contrast);
|
||||
display: inline-block;
|
||||
font-size: 0.875rem;
|
||||
transition: color 0.3s ease;
|
||||
padding: 0.75rem;
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
|
||||
.docs-search-results__lvl2 {
|
||||
display: inline-block;
|
||||
margin-inline-start: 2rem;
|
||||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
.docs-search-results__lvl3 {
|
||||
margin-inline-start: 2rem;
|
||||
padding-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-result-page-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-search-results__start-typing,
|
||||
.docs-search-results__no-results {
|
||||
padding: 0.75rem;
|
||||
color: var(--gray-400);
|
||||
}
|
||||
|
||||
.docs-result-icon-and-type {
|
||||
display: flex;
|
||||
|
||||
.docs-search-results__type {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-algolia {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
color: var(--gray-400);
|
||||
padding: 1rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
gap: 0.25rem;
|
||||
background-color: var(--page-background);
|
||||
border-radius: 0 0 0.25rem 0.25rem;
|
||||
|
||||
docs-algolia-icon {
|
||||
display: inline-flex;
|
||||
margin-block-start: 0.12rem;
|
||||
margin-inline-start: 0.15rem;
|
||||
width: 4rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*!
|
||||
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {SearchDialog} from './search-dialog.component';
|
||||
import {WINDOW} from '../../providers';
|
||||
import {Search} from '../../services';
|
||||
import {FakeEventTarget} from '../../testing/index';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {AlgoliaIcon} from '../algolia-icon/algolia-icon.component';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {Router} from '@angular/router';
|
||||
import {provideExperimentalZonelessChangeDetection} from '@angular/core';
|
||||
|
||||
describe('SearchDialog', () => {
|
||||
let fixture: ComponentFixture<SearchDialog>;
|
||||
|
||||
const fakeSearch = {
|
||||
searchQuery: jasmine.createSpy(),
|
||||
searchResults: jasmine.createSpy(),
|
||||
};
|
||||
const fakeWindow = new FakeEventTarget();
|
||||
|
||||
beforeEach(async () => {
|
||||
fakeSearch.searchResults.and.returnValue([]);
|
||||
fakeSearch.searchQuery.and.returnValue('');
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SearchDialog, RouterTestingModule],
|
||||
providers: [
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
{
|
||||
provide: Search,
|
||||
useValue: fakeSearch,
|
||||
},
|
||||
{
|
||||
provide: WINDOW,
|
||||
useValue: fakeWindow,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SearchDialog);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should navigate to active item when user pressed Enter', () => {
|
||||
const router = TestBed.inject(Router);
|
||||
const navigateByUrlSpy = spyOn(router, 'navigateByUrl');
|
||||
|
||||
fakeSearch.searchResults.and.returnValue(fakeSearchResults);
|
||||
fixture.detectChanges();
|
||||
|
||||
fakeWindow.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
code: 'Enter',
|
||||
key: 'Enter',
|
||||
charCode: 13,
|
||||
keyCode: 13,
|
||||
view: window,
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(navigateByUrlSpy).toHaveBeenCalledOnceWith('fakeUrl1#h1');
|
||||
});
|
||||
|
||||
it('should always display algolia logo', () => {
|
||||
const algoliaIcon = fixture.debugElement.query(By.directive(AlgoliaIcon));
|
||||
|
||||
expect(algoliaIcon).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display `No results found` message when there are no results for provided query', () => {
|
||||
fakeSearch.searchResults.and.returnValue([]);
|
||||
fixture.detectChanges();
|
||||
|
||||
const noResultsContainer = fixture.debugElement.query(
|
||||
By.css('.docs-search-results__no-results'),
|
||||
);
|
||||
|
||||
expect(noResultsContainer).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display `Start typing to see results` message when there are no provided query', () => {
|
||||
fakeSearch.searchResults.and.returnValue(undefined);
|
||||
fixture.detectChanges();
|
||||
|
||||
const startTypingContainer = fixture.debugElement.query(
|
||||
By.css('.docs-search-results__start-typing'),
|
||||
);
|
||||
|
||||
expect(startTypingContainer).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display list of the search results when results exist', () => {
|
||||
fakeSearch.searchResults.and.returnValue(fakeSearchResults);
|
||||
fixture.detectChanges();
|
||||
|
||||
const resultListContainer = fixture.debugElement.query(By.css('ul.docs-search-results'));
|
||||
const resultItems = fixture.debugElement.queryAll(By.css('ul.docs-search-results li a'));
|
||||
|
||||
expect(resultListContainer).toBeTruthy();
|
||||
expect(resultItems.length).toBe(2);
|
||||
expect(resultItems[0].nativeElement.href).toBe(`${window.origin}/fakeUrl1#h1`);
|
||||
expect(resultItems[1].nativeElement.href).toBe(`${window.origin}/fakeUrl2#h1`);
|
||||
});
|
||||
|
||||
it('should close search dialog when user clicked outside `.docs-search-container`', () => {
|
||||
const dialogContainer = fixture.debugElement.query(By.css('dialog'));
|
||||
const closeSearchDialogSpy = spyOn(fixture.componentInstance, 'closeSearchDialog');
|
||||
|
||||
dialogContainer.nativeElement.click();
|
||||
|
||||
expect(closeSearchDialogSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
const fakeSearchResults = [
|
||||
{
|
||||
'url': 'https://angular.dev/fakeUrl1#h1',
|
||||
'hierarchy': {
|
||||
'lvl0': 'FakeLvl0',
|
||||
'lvl1': 'FakeLvl1',
|
||||
'lvl2': 'FakeLvl2',
|
||||
'lvl3': null,
|
||||
'lvl4': null,
|
||||
'lvl5': null,
|
||||
'lvl6': null,
|
||||
},
|
||||
'objectID': 'fakeObjectId1',
|
||||
},
|
||||
{
|
||||
'url': 'https://angular.dev/fakeUrl2#h1',
|
||||
'hierarchy': {
|
||||
'lvl0': 'FakeLvl0',
|
||||
'lvl1': 'FakeLvl1',
|
||||
'lvl2': 'FakeLvl2',
|
||||
'lvl3': null,
|
||||
'lvl4': null,
|
||||
'lvl5': null,
|
||||
'lvl6': null,
|
||||
},
|
||||
'objectID': 'fakeObjectId2',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,173 @@
|
||||
/*!
|
||||
* @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 {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
Injector,
|
||||
OnDestroy,
|
||||
Signal,
|
||||
afterNextRender,
|
||||
effect,
|
||||
inject,
|
||||
output,
|
||||
viewChild,
|
||||
viewChildren,
|
||||
} from '@angular/core';
|
||||
import {NgTemplateOutlet} from '@angular/common';
|
||||
|
||||
import {WINDOW} from '../../providers/index';
|
||||
import {ClickOutside} from '../../directives/index';
|
||||
import {Search} from '../../services/index';
|
||||
|
||||
import {TextField} from '../text-field/text-field.component';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {ActiveDescendantKeyManager} from '@angular/cdk/a11y';
|
||||
import {SearchItem} from '../../directives/search-item/search-item.directive';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {Router, RouterLink} from '@angular/router';
|
||||
import {filter, fromEvent} from 'rxjs';
|
||||
import {AlgoliaIcon} from '../algolia-icon/algolia-icon.component';
|
||||
import {RelativeLink} from '../../pipes/relative-link.pipe';
|
||||
import {SearchResult, SnippetResult} from '../../interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-search-dialog',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
ClickOutside,
|
||||
TextField,
|
||||
FormsModule,
|
||||
SearchItem,
|
||||
AlgoliaIcon,
|
||||
RelativeLink,
|
||||
RouterLink,
|
||||
NgTemplateOutlet,
|
||||
],
|
||||
templateUrl: './search-dialog.component.html',
|
||||
styleUrls: ['./search-dialog.component.scss'],
|
||||
})
|
||||
export class SearchDialog implements OnDestroy {
|
||||
onClose = output();
|
||||
dialog = viewChild.required<ElementRef<HTMLDialogElement>>('searchDialog');
|
||||
items = viewChildren(SearchItem);
|
||||
|
||||
private readonly search = inject(Search);
|
||||
private readonly relativeLink = new RelativeLink();
|
||||
private readonly router = inject(Router);
|
||||
private readonly window = inject(WINDOW);
|
||||
private readonly injector = inject(Injector);
|
||||
private readonly keyManager = new ActiveDescendantKeyManager(
|
||||
this.items,
|
||||
this.injector,
|
||||
).withWrap();
|
||||
|
||||
searchQuery = this.search.searchQuery;
|
||||
searchResults = this.search.searchResults;
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.items();
|
||||
afterNextRender(
|
||||
{
|
||||
write: () => this.keyManager.setFirstItemActive(),
|
||||
},
|
||||
{injector: this.injector},
|
||||
);
|
||||
});
|
||||
|
||||
this.keyManager.change.pipe(takeUntilDestroyed()).subscribe(() => {
|
||||
this.keyManager.activeItem?.scrollIntoView();
|
||||
});
|
||||
|
||||
afterNextRender({
|
||||
write: () => {
|
||||
if (!this.dialog().nativeElement.open) {
|
||||
this.dialog().nativeElement.showModal?.();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
fromEvent<KeyboardEvent>(this.window, 'keydown')
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe((event) => {
|
||||
// When user presses Enter we can navigate to currently selected item in the search result list.
|
||||
if (event.key === 'Enter') {
|
||||
this.navigateToTheActiveItem();
|
||||
} else {
|
||||
this.keyManager.onKeydown(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
splitMarkedText(snippet: string): Array<{highlight: boolean; text: string}> {
|
||||
const parts: Array<{highlight: boolean; text: string}> = [];
|
||||
while (snippet.indexOf('<ɵ>') !== -1) {
|
||||
const beforeMatch = snippet.substring(0, snippet.indexOf('<ɵ>'));
|
||||
const match = snippet.substring(snippet.indexOf('<ɵ>') + 3, snippet.indexOf('</ɵ>'));
|
||||
parts.push({highlight: false, text: beforeMatch});
|
||||
parts.push({highlight: true, text: match});
|
||||
snippet = snippet.substring(snippet.indexOf('</ɵ>') + 4);
|
||||
}
|
||||
parts.push({highlight: false, text: snippet});
|
||||
return parts;
|
||||
}
|
||||
|
||||
getBestSnippetForMatch(result: SearchResult): string {
|
||||
// if there is content, return it
|
||||
if (result._snippetResult.content !== undefined) {
|
||||
return result._snippetResult.content.value;
|
||||
}
|
||||
|
||||
const hierarchy = result._snippetResult.hierarchy;
|
||||
if (hierarchy === undefined) {
|
||||
return '';
|
||||
}
|
||||
function matched(snippet: SnippetResult | undefined) {
|
||||
return snippet?.matchLevel !== undefined && snippet.matchLevel !== 'none';
|
||||
}
|
||||
// return the most specific subheader match
|
||||
if (matched(hierarchy.lvl4)) {
|
||||
return hierarchy.lvl4!.value;
|
||||
}
|
||||
if (matched(hierarchy.lvl3)) {
|
||||
return hierarchy.lvl3!.value;
|
||||
}
|
||||
if (matched(hierarchy.lvl2)) {
|
||||
return hierarchy.lvl2!.value;
|
||||
}
|
||||
// if no subheader matched the query, fall back to just returning the most specific one
|
||||
return hierarchy.lvl3?.value ?? hierarchy.lvl2?.value ?? '';
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.keyManager.destroy();
|
||||
}
|
||||
|
||||
closeSearchDialog() {
|
||||
this.dialog().nativeElement.close();
|
||||
this.onClose.emit();
|
||||
}
|
||||
|
||||
updateSearchQuery(query: string) {
|
||||
this.search.updateSearchQuery(query);
|
||||
}
|
||||
|
||||
private navigateToTheActiveItem(): void {
|
||||
const activeItemLink: string | undefined = this.keyManager.activeItem?.item?.url;
|
||||
|
||||
if (!activeItemLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.router.navigateByUrl(this.relativeLink.transform(activeItemLink));
|
||||
this.onClose.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "select",
|
||||
srcs = [
|
||||
"select.component.ts",
|
||||
],
|
||||
assets = [
|
||||
":select.component.css",
|
||||
"select.component.html",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/components:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"//packages/forms",
|
||||
],
|
||||
)
|
||||
|
||||
sass_binary(
|
||||
name = "style",
|
||||
src = "select.component.scss",
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
":select",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
<select [attr.id]="id" [attr.name]="name" [ngModel]="selectedOption()" (ngModelChange)="setOption($event)">
|
||||
@for (item of options; track item) {
|
||||
<option [value]="item.value">{{ item.label }}</option>
|
||||
}
|
||||
</select>
|
||||
@@ -0,0 +1,31 @@
|
||||
/*!
|
||||
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {Select} from './select.component';
|
||||
import {provideExperimentalZonelessChangeDetection} from '@angular/core';
|
||||
|
||||
describe('Select', () => {
|
||||
let component: Select;
|
||||
let fixture: ComponentFixture<Select>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [Select],
|
||||
providers: [provideExperimentalZonelessChangeDetection()],
|
||||
});
|
||||
fixture = TestBed.createComponent(Select);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/*!
|
||||
* @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 {ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule} from '@angular/forms';
|
||||
import {ChangeDetectionStrategy, Component, Input, forwardRef, signal} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
|
||||
type SelectOptionValue = string | number | boolean;
|
||||
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
value: SelectOptionValue;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'docs-select',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, FormsModule],
|
||||
templateUrl: './select.component.html',
|
||||
styleUrls: ['./select.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => Select),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
host: {
|
||||
class: 'docs-form-element',
|
||||
},
|
||||
})
|
||||
export class Select implements ControlValueAccessor {
|
||||
@Input({required: true, alias: 'selectId'}) id!: string;
|
||||
@Input({required: true}) name!: string;
|
||||
@Input({required: true}) options!: SelectOption[];
|
||||
@Input() disabled = false;
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
private onChange: (value: SelectOptionValue) => void = (_: SelectOptionValue) => {};
|
||||
private onTouched: () => void = () => {};
|
||||
|
||||
protected readonly selectedOption = signal<SelectOptionValue | null>(null);
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
writeValue(value: SelectOptionValue): void {
|
||||
this.selectedOption.set(value);
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
setDisabledState?(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
setOption($event: SelectOptionValue): void {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedOption.set($event);
|
||||
this.onChange($event);
|
||||
this.onTouched();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "slide-toggle",
|
||||
srcs = [
|
||||
"slide-toggle.component.ts",
|
||||
],
|
||||
assets = [
|
||||
":slide-toggle.component.css",
|
||||
"slide-toggle.component.html",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/components:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"//packages/forms",
|
||||
],
|
||||
)
|
||||
|
||||
sass_binary(
|
||||
name = "style",
|
||||
src = "slide-toggle.component.scss",
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
":slide-toggle",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
<label [attr.for]="buttonId">
|
||||
<span class="docs-label">{{ label }}</span>
|
||||
<div class="docs-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
[id]="buttonId"
|
||||
role="switch"
|
||||
(click)="toggle()"
|
||||
[class.docs-toggle-active]="checked()"
|
||||
[checked]="checked()"
|
||||
/>
|
||||
<span class="docs-slider"></span>
|
||||
</div>
|
||||
</label>
|
||||
@@ -0,0 +1,78 @@
|
||||
:host,
|
||||
label {
|
||||
display: inline-flex;
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.docs-label {
|
||||
font-size: 0.875rem;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 160%; // 1.4rem
|
||||
letter-spacing: -0.00875rem;
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
.docs-toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
width: 3rem;
|
||||
height: 1.5rem;
|
||||
border: 1px solid var(--senary-contrast);
|
||||
border-radius: 34px;
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
border-radius: 34px;
|
||||
inset: 0;
|
||||
background-color: var(--septenary-contrast);
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
|
||||
// background
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 34px;
|
||||
background: var(--pink-to-purple-horizontal-gradient);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
// toggle knob
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
left: 0.125rem;
|
||||
bottom: 0.125rem;
|
||||
background-color: var(--page-background);
|
||||
transition: transform 0.3s ease, background-color 0.3s ease;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
&:checked + .docs-slider {
|
||||
// background
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// toggle knob
|
||||
&::after {
|
||||
transform: translateX(1.5rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*!
|
||||
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {SlideToggle} from './slide-toggle.component';
|
||||
import {provideExperimentalZonelessChangeDetection} from '@angular/core';
|
||||
|
||||
describe('SlideToggle', () => {
|
||||
let component: SlideToggle;
|
||||
let fixture: ComponentFixture<SlideToggle>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [SlideToggle],
|
||||
providers: [provideExperimentalZonelessChangeDetection()],
|
||||
});
|
||||
fixture = TestBed.createComponent(SlideToggle);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should toggle the value when clicked', () => {
|
||||
expect(component['checked']()).toBeFalse();
|
||||
|
||||
const buttonElement = fixture.nativeElement.querySelector('input');
|
||||
buttonElement.click();
|
||||
|
||||
expect(component['checked']()).toBeTrue();
|
||||
});
|
||||
|
||||
it('should call onChange and onTouched when toggled', () => {
|
||||
const onChangeSpy = jasmine.createSpy('onChangeSpy');
|
||||
const onTouchedSpy = jasmine.createSpy('onTouchedSpy');
|
||||
component.registerOnChange(onChangeSpy);
|
||||
component.registerOnTouched(onTouchedSpy);
|
||||
|
||||
component.toggle();
|
||||
|
||||
expect(onChangeSpy).toHaveBeenCalled();
|
||||
expect(onChangeSpy).toHaveBeenCalledWith(true);
|
||||
expect(onTouchedSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set active class for button when is checked', () => {
|
||||
component.writeValue(true);
|
||||
fixture.detectChanges();
|
||||
const buttonElement: HTMLButtonElement = fixture.nativeElement.querySelector('input');
|
||||
expect(buttonElement.classList.contains('docs-toggle-active')).toBeTrue();
|
||||
|
||||
component.writeValue(false);
|
||||
fixture.detectChanges();
|
||||
expect(buttonElement.classList.contains('docs-toggle-active')).toBeFalse();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
/*!
|
||||
* @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 {ChangeDetectionStrategy, Component, Input, forwardRef, signal} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-slide-toggle',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './slide-toggle.component.html',
|
||||
styleUrls: ['./slide-toggle.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => SlideToggle),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class SlideToggle implements ControlValueAccessor {
|
||||
@Input({required: true}) buttonId!: string;
|
||||
@Input({required: true}) label!: string;
|
||||
@Input() disabled = false;
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
private onChange: (value: boolean) => void = (_: boolean) => {};
|
||||
private onTouched: () => void = () => {};
|
||||
|
||||
protected readonly checked = signal(false);
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
writeValue(value: boolean): void {
|
||||
this.checked.set(value);
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
// Toggles the checked state of the slide-toggle.
|
||||
toggle(): void {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.checked.update((checked) => !checked);
|
||||
this.onChange(this.checked());
|
||||
this.onTouched();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "table-of-contents",
|
||||
srcs = [
|
||||
"table-of-contents.component.ts",
|
||||
],
|
||||
assets = [
|
||||
":table-of-contents.component.css",
|
||||
"table-of-contents.component.html",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/components:__pkg__",
|
||||
"//adev/shared-docs/components/viewers:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//adev/shared-docs/components/icon",
|
||||
"//adev/shared-docs/interfaces",
|
||||
"//adev/shared-docs/services",
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"//packages/router",
|
||||
],
|
||||
)
|
||||
|
||||
sass_binary(
|
||||
name = "style",
|
||||
src = "table-of-contents.component.scss",
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
":table-of-contents",
|
||||
"//adev/shared-docs/interfaces",
|
||||
"//adev/shared-docs/providers",
|
||||
"//adev/shared-docs/services",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
"//packages/router",
|
||||
"//packages/router/testing",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
<aside>
|
||||
<nav>
|
||||
<header>
|
||||
<h2 class="docs-title">On this page</h2>
|
||||
</header>
|
||||
<ul class="docs-faceted-list">
|
||||
<!-- TODO: Hide li elements with class docs-toc-item-h3 for laptop, table and phone screen resolutions -->
|
||||
@for (item of tableOfContentItems(); track item.id) {
|
||||
<li
|
||||
class="docs-faceted-list-item"
|
||||
[class.docs-toc-item-h2]="item.level === TableOfContentsLevel.H2"
|
||||
[class.docs-toc-item-h3]="item.level === TableOfContentsLevel.H3"
|
||||
>
|
||||
<a
|
||||
routerLink="."
|
||||
[fragment]="item.id"
|
||||
[class.docs-faceted-list-item-active]="item.id === activeItemId()"
|
||||
>
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
@if (shouldDisplayScrollToTop()) {
|
||||
<button type="button" (click)="scrollToTop()">
|
||||
<docs-icon role="presentation">arrow_upward_alt</docs-icon>
|
||||
Back to the top
|
||||
</button>
|
||||
}
|
||||
</aside>
|
||||
@@ -0,0 +1,96 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
top: 0;
|
||||
height: fit-content;
|
||||
width: 14rem;
|
||||
padding-inline: 1rem;
|
||||
max-height: 100vh;
|
||||
overflow-y: scroll;
|
||||
|
||||
aside {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
& :has(ul li:only-child) {
|
||||
// Hide the entire TOC is there's only one item
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1430px) {
|
||||
position: relative;
|
||||
right: 0;
|
||||
max-height: min-content;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.docs-title {
|
||||
font-size: 1.25rem;
|
||||
margin-block-start: var(--layout-padding);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--septenary-contrast);
|
||||
border-radius: 10px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--quinary-contrast);
|
||||
}
|
||||
|
||||
.docs-faceted-list-item {
|
||||
font-size: 0.875rem;
|
||||
|
||||
a {
|
||||
display: block; // to prevent overflow from the li parent
|
||||
padding: 0.5rem 0.5rem 0.5rem 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.docs-toc-item-h3 a {
|
||||
padding-inline-start: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 0.875rem;
|
||||
font-family: var(--inter-font);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0.5rem 0;
|
||||
color: var(--tertiary-contrast);
|
||||
transition: color 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
docs-icon {
|
||||
margin-inline-end: 0.35rem;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
docs-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1430px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*!
|
||||
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {TableOfContents} from './table-of-contents.component';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {TableOfContentsItem, TableOfContentsLevel} from '../../interfaces/index';
|
||||
import {TableOfContentsScrollSpy, TableOfContentsLoader} from '../../services/index';
|
||||
import {WINDOW} from '../../providers/index';
|
||||
import {provideExperimentalZonelessChangeDetection, signal} from '@angular/core';
|
||||
|
||||
describe('TableOfContents', () => {
|
||||
let component: TableOfContents;
|
||||
let fixture: ComponentFixture<TableOfContents>;
|
||||
let scrollSpy: jasmine.SpyObj<TableOfContentsScrollSpy>;
|
||||
const items: TableOfContentsItem[] = [
|
||||
{
|
||||
title: 'Heading 2',
|
||||
top: 0,
|
||||
id: 'item-heading-2',
|
||||
level: TableOfContentsLevel.H2,
|
||||
},
|
||||
{
|
||||
title: 'First Heading 3',
|
||||
top: 100,
|
||||
id: 'first-item-heading-3',
|
||||
level: TableOfContentsLevel.H3,
|
||||
},
|
||||
{
|
||||
title: 'Second Heading 3',
|
||||
top: 200,
|
||||
id: 'second-item-heading-3',
|
||||
level: TableOfContentsLevel.H3,
|
||||
},
|
||||
];
|
||||
const fakeWindow = {
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
scrollSpy = jasmine.createSpyObj<TableOfContentsScrollSpy>('TableOfContentsScrollSpy', [
|
||||
'startListeningToScroll',
|
||||
'activeItemId',
|
||||
'scrollbarThumbOnTop',
|
||||
]);
|
||||
scrollSpy.startListeningToScroll.and.returnValue();
|
||||
scrollSpy.activeItemId.and.returnValue(items[0].id);
|
||||
scrollSpy.scrollbarThumbOnTop.and.returnValue(false);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TableOfContents, RouterTestingModule],
|
||||
providers: [
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
{
|
||||
provide: WINDOW,
|
||||
useValue: fakeWindow,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
TestBed.overrideProvider(TableOfContentsScrollSpy, {
|
||||
useValue: scrollSpy,
|
||||
});
|
||||
|
||||
const tableOfContentsLoaderSpy = TestBed.inject(TableOfContentsLoader);
|
||||
spyOn(tableOfContentsLoaderSpy, 'buildTableOfContent').and.returnValue();
|
||||
tableOfContentsLoaderSpy.tableOfContentItems.set(items);
|
||||
fixture = TestBed.createComponent(TableOfContents);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call scrollToTop when user click on Back to the top button', () => {
|
||||
const spy = spyOn(component, 'scrollToTop');
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
|
||||
button.click();
|
||||
|
||||
expect(spy).toHaveBeenCalledOnceWith();
|
||||
});
|
||||
|
||||
it('should render items when tableOfContentItems has value', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const renderedItems = fixture.nativeElement.querySelectorAll('li');
|
||||
|
||||
expect(renderedItems.length).toBe(3);
|
||||
expect(component.tableOfContentItems().length).toBe(3);
|
||||
});
|
||||
|
||||
it('should append level class to element', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const h2Items = fixture.nativeElement.querySelectorAll('li.docs-toc-item-h2');
|
||||
const h3Items = fixture.nativeElement.querySelectorAll('li.docs-toc-item-h3');
|
||||
|
||||
expect(h2Items.length).toBe(1);
|
||||
expect(h3Items.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should append active class when item is active', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const activeItem = fixture.nativeElement.querySelector('.docs-faceted-list-item-active');
|
||||
|
||||
expect(activeItem).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*!
|
||||
* @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 {NgFor, NgIf} from '@angular/common';
|
||||
import {ChangeDetectionStrategy, Component, Input, computed, inject} from '@angular/core';
|
||||
import {RouterLink} from '@angular/router';
|
||||
import {TableOfContentsLevel} from '../../interfaces/index';
|
||||
import {TableOfContentsLoader} from '../../services/table-of-contents-loader.service';
|
||||
import {TableOfContentsScrollSpy} from '../../services/table-of-contents-scroll-spy.service';
|
||||
import {IconComponent} from '../icon/icon.component';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-table-of-contents',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './table-of-contents.component.html',
|
||||
styleUrls: ['./table-of-contents.component.scss'],
|
||||
imports: [NgIf, NgFor, RouterLink, IconComponent],
|
||||
})
|
||||
export class TableOfContents {
|
||||
// Element that contains the content from which the Table of Contents is built
|
||||
@Input({required: true}) contentSourceElement!: HTMLElement;
|
||||
|
||||
private readonly scrollSpy = inject(TableOfContentsScrollSpy);
|
||||
private readonly tableOfContentsLoader = inject(TableOfContentsLoader);
|
||||
tableOfContentItems = this.tableOfContentsLoader.tableOfContentItems;
|
||||
|
||||
activeItemId = this.scrollSpy.activeItemId;
|
||||
shouldDisplayScrollToTop = computed(() => !this.scrollSpy.scrollbarThumbOnTop());
|
||||
TableOfContentsLevel = TableOfContentsLevel;
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.tableOfContentsLoader.buildTableOfContent(this.contentSourceElement);
|
||||
this.scrollSpy.startListeningToScroll(this.contentSourceElement);
|
||||
}
|
||||
|
||||
scrollToTop(): void {
|
||||
this.scrollSpy.scrollToTop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "text-field",
|
||||
srcs = [
|
||||
"text-field.component.ts",
|
||||
],
|
||||
assets = [
|
||||
":text-field.component.css",
|
||||
"text-field.component.html",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/components:__pkg__",
|
||||
"//adev/shared-docs/components/search-dialog:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//adev/shared-docs/components/icon",
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"//packages/forms",
|
||||
],
|
||||
)
|
||||
|
||||
sass_binary(
|
||||
name = "style",
|
||||
src = "text-field.component.scss",
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
":text-field",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
@if (!hideIcon) {
|
||||
<docs-icon class="docs-icon_high-contrast">search</docs-icon>
|
||||
}
|
||||
<input
|
||||
#inputRef
|
||||
type="text"
|
||||
[attr.placeholder]="placeholder"
|
||||
[attr.name]="name"
|
||||
[ngModel]="value()"
|
||||
(ngModelChange)="setValue($event)"
|
||||
class="docs-text-field"
|
||||
/>
|
||||
@@ -0,0 +1,9 @@
|
||||
// search field
|
||||
.docs-text-field {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
// filter field on api reference list
|
||||
docs-icon + .docs-text-field {
|
||||
font-size: 1rem;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*!
|
||||
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {TextField} from './text-field.component';
|
||||
import {provideExperimentalZonelessChangeDetection} from '@angular/core';
|
||||
|
||||
describe('TextField', () => {
|
||||
let component: TextField;
|
||||
let fixture: ComponentFixture<TextField>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TextField],
|
||||
providers: [provideExperimentalZonelessChangeDetection()],
|
||||
});
|
||||
fixture = TestBed.createComponent(TextField);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
/*!
|
||||
* @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 {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
Input,
|
||||
ViewChild,
|
||||
afterNextRender,
|
||||
forwardRef,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR} from '@angular/forms';
|
||||
import {IconComponent} from '../icon/icon.component';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-text-field',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, FormsModule, IconComponent],
|
||||
templateUrl: './text-field.component.html',
|
||||
styleUrls: ['./text-field.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => TextField),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
host: {
|
||||
class: 'docs-form-element',
|
||||
},
|
||||
})
|
||||
export class TextField implements ControlValueAccessor {
|
||||
@ViewChild('inputRef') private input?: ElementRef<HTMLInputElement>;
|
||||
|
||||
@Input() name: string | null = null;
|
||||
@Input() placeholder: string | null = null;
|
||||
@Input() disabled = false;
|
||||
@Input() hideIcon = false;
|
||||
@Input() autofocus = false;
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
private onChange: (value: string) => void = (_: string) => {};
|
||||
private onTouched: () => void = () => {};
|
||||
|
||||
protected readonly value = signal<string | null>(null);
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
if (this.autofocus) {
|
||||
this.input?.nativeElement.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
writeValue(value: string): void {
|
||||
this.value.set(value);
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
setDisabledState?(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
setValue(value: string): void {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.value.set(value);
|
||||
this.onChange(value);
|
||||
this.onTouched();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "viewers",
|
||||
srcs = [
|
||||
"docs-viewer/docs-viewer.component.ts",
|
||||
"example-viewer/example-viewer.component.ts",
|
||||
],
|
||||
assets = [
|
||||
":docs-viewer/docs-viewer.component.css",
|
||||
":example-viewer/example-viewer.component.css",
|
||||
"example-viewer/example-viewer.component.html",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/components:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//adev/shared-docs/components/breadcrumb",
|
||||
"//adev/shared-docs/components/copy-source-code-button",
|
||||
"//adev/shared-docs/components/table-of-contents",
|
||||
"//adev/shared-docs/interfaces",
|
||||
"//adev/shared-docs/providers",
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"//packages/router",
|
||||
"@npm//@angular/cdk",
|
||||
"@npm//@angular/material",
|
||||
"@npm//@types/dom-view-transitions",
|
||||
],
|
||||
)
|
||||
|
||||
sass_binary(
|
||||
name = "example-viewer-style",
|
||||
src = "example-viewer/example-viewer.component.scss",
|
||||
)
|
||||
|
||||
sass_binary(
|
||||
name = "docs-viewer-style",
|
||||
src = "docs-viewer/docs-viewer.component.scss",
|
||||
deps = [
|
||||
"//adev/shared-docs/styles",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["**/*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
":viewers",
|
||||
"//adev/shared-docs/components/breadcrumb",
|
||||
"//adev/shared-docs/components/copy-source-code-button",
|
||||
"//adev/shared-docs/components/icon",
|
||||
"//adev/shared-docs/components/table-of-contents",
|
||||
"//adev/shared-docs/interfaces",
|
||||
"//adev/shared-docs/providers",
|
||||
"//adev/shared-docs/services",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
"//packages/platform-browser",
|
||||
"//packages/platform-browser/animations",
|
||||
"//packages/router",
|
||||
"//packages/router/testing",
|
||||
"@npm//@angular/cdk",
|
||||
"@npm//@angular/material",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
@@ -0,0 +1,129 @@
|
||||
@use '../../../styles/links' as links;
|
||||
@use '../../../styles/anchor' as anchor;
|
||||
|
||||
:host {
|
||||
--translate-y: clamp(5px, 0.25em, 7px);
|
||||
}
|
||||
|
||||
.docs-viewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--layout-padding);
|
||||
max-width: var(--page-width);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media only screen and (max-width: 1430px) {
|
||||
container: docs-content / inline-size;
|
||||
}
|
||||
|
||||
// If rendered on the docs page, accommodate width for TOC
|
||||
docs-docs & {
|
||||
@media only screen and (min-width: 1430px) and (max-width: 1550px) {
|
||||
width: calc(100% - 195px - var(--layout-padding));
|
||||
max-width: var(--page-width);
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
.docs-anchor {
|
||||
margin-block-start: 2.5rem;
|
||||
display: inline-block;
|
||||
color: inherit;
|
||||
|
||||
@include anchor.docs-anchor();
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
margin-block-end: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-block-end: 0.5rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.25rem;
|
||||
margin-block-end: 0.5rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1rem;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.875rem;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
a:not(.docs-github-links):not(.docs-card):not(.docs-pill):not(.docs-example-github-link) {
|
||||
&[href^='http:'],
|
||||
&[href^='https:'] {
|
||||
@include links.external-link-with-icon();
|
||||
}
|
||||
}
|
||||
|
||||
&-scroll-margin-large {
|
||||
h2,
|
||||
h3 {
|
||||
scroll-margin: 5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.docs-header {
|
||||
margin-block-end: 1rem;
|
||||
& > p:first-child {
|
||||
color: var(--quaternary-contrast);
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-page-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
h1 {
|
||||
margin-block: 0;
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-contrast);
|
||||
height: fit-content;
|
||||
|
||||
docs-icon {
|
||||
color: var(--gray-400);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
docs-icon {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/*!
|
||||
* @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 {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {ExampleViewerContentLoader} from '../../../interfaces';
|
||||
import {EXAMPLE_VIEWER_CONTENT_LOADER} from '../../../providers';
|
||||
import {CodeExampleViewMode, ExampleViewer} from '../example-viewer/example-viewer.component';
|
||||
import {DocViewer} from './docs-viewer.component';
|
||||
import {IconComponent} from '../../icon/icon.component';
|
||||
import {Breadcrumb} from '../../breadcrumb/breadcrumb.component';
|
||||
import {NavigationState} from '../../../services';
|
||||
import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-code-button.component';
|
||||
import {TableOfContents} from '../../table-of-contents/table-of-contents.component';
|
||||
import {provideExperimentalZonelessChangeDetection} from '@angular/core';
|
||||
|
||||
describe('DocViewer', () => {
|
||||
let fixture: ComponentFixture<DocViewer>;
|
||||
let exampleContentSpy: jasmine.SpyObj<ExampleViewerContentLoader>;
|
||||
let navigationStateSpy: jasmine.SpyObj<NavigationState>;
|
||||
|
||||
const exampleDocContentWithExampleViewerPlaceholders = `<div class="docs-code linenums" visibleLines="[12, 31]" expanded="true" path="hello-world/hello-world-new.ts">
|
||||
<div class="docs-code-header">A styled code example</div>
|
||||
<pre>
|
||||
<code><div class="hljs-ln-line"><span class="hljs-comment">/*!</div><div class="hljs-ln-line"> * @license</div><div class="hljs-ln-line"> * Copyright Google LLC All Rights Reserved.</div><div class="hljs-ln-line"> *</div><div class="hljs-ln-line"> * Use of this source code is governed by an MIT-style license that can be</div><div class="hljs-ln-line"> * found in the LICENSE file at https://angular.dev/license</div><div class="hljs-ln-line"> */</span></div><div class="hljs-ln-line"></div><div class="hljs-ln-line remove"><span class="hljs-keyword">import</span> {ChangeDetectorRef, Component, <span class="hljs-keyword">inject</span>, signal} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;</div><div class="hljs-ln-line add"><span class="hljs-keyword">import</span> {Component, signal} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;</div><div class="hljs-ln-line"><span class="hljs-keyword">import</span> {CommonModule} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/common'</span>;</div><div class="hljs-ln-line"></div><div class="hljs-ln-line highlighted">@Component({</div><div class="hljs-ln-line highlighted"> selector: <span class="hljs-string">'hello-world'</span>,</div><div class="hljs-ln-line highlighted"> standalone: <span class="hljs-keyword">true</span>,</div><div class="hljs-ln-line highlighted"> imports: [CommonModule],</div><div class="hljs-ln-line highlighted"> templateUrl: <span class="hljs-string">'./hello-world.html'</span>,</div><div class="hljs-ln-line highlighted"> styleUrls: [<span class="hljs-string">'./hello-world.css'</span>],</div><div class="hljs-ln-line highlighted">})</div><div class="hljs-ln-line">export <span class="hljs-keyword">default</span> <span class="hljs-keyword">class</span> HelloWorldComponent {</div><div class="hljs-ln-line remove"> world = <span class="hljs-string">'World'</span>;</div><div class="hljs-ln-line add"> world = <span class="hljs-string">'World!!!'</span>;</div><div class="hljs-ln-line"> <span class="hljs-keyword">count</span> = signal(<span class="hljs-number">0</span>);</div><div class="hljs-ln-line remove"> changeDetector = <span class="hljs-keyword">inject</span>(ChangeDetectorRef);</div><div class="hljs-ln-line"></div><div class="hljs-ln-line"> increase(): <span class="hljs-keyword">void</span> {</div><div class="hljs-ln-line"> <span class="hljs-keyword">this</span>.<span class="hljs-keyword">count</span>.update((<span class="hljs-keyword">previous</span>) => {</div><div class="hljs-ln-line highlighted"> <span class="hljs-keyword">return</span> <span class="hljs-keyword">previous</span> + <span class="hljs-number">1</span>;</div><div class="hljs-ln-line"> });</div><div class="hljs-ln-line remove"> <span class="hljs-keyword">this</span>.changeDetector.detectChanges();</div><div class="hljs-ln-line"> }</div><div class="hljs-ln-line">}</div><div class="hljs-ln-line"></div></code>
|
||||
</pre>
|
||||
</div>`;
|
||||
|
||||
const exampleDocContentWithExpandedExampleViewerPlaceholders = `<div class="docs-code-multifile" expanded="true" path="hello-world/hello-world-new.ts">
|
||||
<div class="docs-code" visibleLines="[12, 31]" path="hello-world/hello-world-new.ts">
|
||||
<pre>
|
||||
<code><div class="hljs-ln-line"><span class="hljs-comment">/*!</div><div class="hljs-ln-line"> * @license</div><div class="hljs-ln-line"> * Copyright Google LLC All Rights Reserved.</div><div class="hljs-ln-line"> *</div><div class="hljs-ln-line"> * Use of this source code is governed by an MIT-style license that can be</div><div class="hljs-ln-line"> * found in the LICENSE file at https://angular.dev/license</div><div class="hljs-ln-line"> */</span></div><div class="hljs-ln-line"></div><div class="hljs-ln-line remove"><span class="hljs-keyword">import</span> {ChangeDetectorRef, Component, <span class="hljs-keyword">inject</span>, signal} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;</div><div class="hljs-ln-line add"><span class="hljs-keyword">import</span> {Component, signal} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;</div><div class="hljs-ln-line"><span class="hljs-keyword">import</span> {CommonModule} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/common'</span>;</div><div class="hljs-ln-line"></div><div class="hljs-ln-line">@Component({</div><div class="hljs-ln-line"> selector: <span class="hljs-string">'hello-world'</span>,</div><div class="hljs-ln-line"> standalone: <span class="hljs-keyword">true</span>,</div><div class="hljs-ln-line"> imports: [CommonModule],</div><div class="hljs-ln-line"> templateUrl: <span class="hljs-string">'./hello-world.html'</span>,</div><div class="hljs-ln-line"> styleUrls: [<span class="hljs-string">'./hello-world.css'</span>],</div><div class="hljs-ln-line">})</div><div class="hljs-ln-line">export <span class="hljs-keyword">default</span> <span class="hljs-keyword">class</span> HelloWorldComponent {</div><div class="hljs-ln-line remove"> world = <span class="hljs-string">'World'</span>;</div><div class="hljs-ln-line add"> world = <span class="hljs-string">'World!!!'</span>;</div><div class="hljs-ln-line"> <span class="hljs-keyword">count</span> = signal(<span class="hljs-number">0</span>);</div><div class="hljs-ln-line remove"> changeDetector = <span class="hljs-keyword">inject</span>(ChangeDetectorRef);</div><div class="hljs-ln-line"></div><div class="hljs-ln-line"> increase(): <span class="hljs-keyword">void</span> {</div><div class="hljs-ln-line"> <span class="hljs-keyword">this</span>.<span class="hljs-keyword">count</span>.update((<span class="hljs-keyword">previous</span>) => {</div><div class="hljs-ln-line"> <span class="hljs-keyword">return</span> <span class="hljs-keyword">previous</span> + <span class="hljs-number">1</span>;</div><div class="hljs-ln-line"> });</div><div class="hljs-ln-line remove"> <span class="hljs-keyword">this</span>.changeDetector.detectChanges();</div><div class="hljs-ln-line"> }</div><div class="hljs-ln-line">}</div><div class="hljs-ln-line"></div></code>
|
||||
</pre>
|
||||
</div>
|
||||
<div class="docs-code linenums" path="hello-world/hello-world.html">
|
||||
<pre>
|
||||
<code><div class="hljs-ln-line"><span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">h2</span>></span>Hello </span><span class="hljs-template-variable">{{ <span class="hljs-name">world</span> }}</span><span class="language-xml"><span class="hljs-tag"></<span class="hljs-name">h2</span>></span></div><div class="hljs-ln-line"><span class="hljs-tag"><<span class="hljs-name">button</span> (<span class="hljs-attr">click</span>)=<span class="hljs-string">"increase()"</span>></span>Increase<span class="hljs-tag"></<span class="hljs-name">button</span>></span></div><div class="hljs-ln-line"><span class="hljs-tag"><<span class="hljs-name">p</span>></span>Counter: </span><span class="hljs-template-variable">{{ <span class="hljs-name">count</span>() }}</span><span class="language-xml"><span class="hljs-tag"></<span class="hljs-name">p</span>></span></div><div class="hljs-ln-line"></span></div></code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const exampleContentWithIcons = `
|
||||
<p>Content</p>
|
||||
<docs-icon>light_mode</docs-icon>
|
||||
<p>More content</p>
|
||||
<docs-icon>dark_mode</docs-icon>
|
||||
`;
|
||||
|
||||
const exampleContentWithBreadcrumbPlaceholder = `
|
||||
<docs-breadcrumb></docs-breadcrumb>
|
||||
<p>Content</p>
|
||||
`;
|
||||
|
||||
const exampleContentWithCodeSnippet = `
|
||||
<div class="docs-code" path="forms/src/app/actor.ts" header="src/app/actor.ts">
|
||||
<pre class="docs-mini-scroll-track">
|
||||
<code>
|
||||
<div class="hljs-ln-line"></div>
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const exampleContentWithHeadings = `
|
||||
<h2>Heading h2</h2>
|
||||
<h3>Heading h3</h3>
|
||||
`;
|
||||
|
||||
beforeEach(() => {
|
||||
exampleContentSpy = jasmine.createSpyObj('ExampleViewerContentLoader', ['getCodeExampleData']);
|
||||
navigationStateSpy = jasmine.createSpyObj(NavigationState, ['activeNavigationItem']);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DocViewer, NoopAnimationsModule, RouterTestingModule],
|
||||
providers: [
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
{provide: EXAMPLE_VIEWER_CONTENT_LOADER, useValue: exampleContentSpy},
|
||||
{provide: NavigationState, useValue: navigationStateSpy},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DocViewer);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should load doc into innerHTML', () => {
|
||||
const fixture = TestBed.createComponent(DocViewer);
|
||||
fixture.componentRef.setInput('docContent', 'hello world');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.innerHTML).toBe('hello world');
|
||||
});
|
||||
|
||||
it('should instantiate example viewer in snippet view mode', async () => {
|
||||
const fixture = TestBed.createComponent(DocViewer);
|
||||
fixture.componentRef.setInput('docContent', exampleDocContentWithExampleViewerPlaceholders);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const exampleViewer = fixture.debugElement.query(By.directive(ExampleViewer));
|
||||
|
||||
expect(exampleViewer).not.toBeNull();
|
||||
expect(exampleViewer.componentInstance.view()).toBe(CodeExampleViewMode.SNIPPET);
|
||||
});
|
||||
|
||||
it('should display example viewer in multi file mode when user clicks expand', async () => {
|
||||
const fixture = TestBed.createComponent(DocViewer);
|
||||
fixture.componentRef.setInput(
|
||||
'docContent',
|
||||
exampleDocContentWithExpandedExampleViewerPlaceholders,
|
||||
);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const exampleViewer = fixture.debugElement.query(By.directive(ExampleViewer));
|
||||
const expandButton = fixture.debugElement.query(
|
||||
By.css('button[aria-label="Expand code example"]'),
|
||||
);
|
||||
expandButton.nativeElement.click();
|
||||
|
||||
expect(exampleViewer).not.toBeNull();
|
||||
expect(exampleViewer.componentInstance.view()).toBe(CodeExampleViewMode.MULTI_FILE);
|
||||
expect(exampleViewer.componentInstance.tabs().length).toBe(2);
|
||||
});
|
||||
|
||||
it('should render Icon component when content has <docs-icon> element', async () => {
|
||||
const fixture = TestBed.createComponent(DocViewer);
|
||||
const renderComponentSpy = spyOn(fixture.componentInstance, 'renderComponent' as any);
|
||||
fixture.componentRef.setInput('docContent', exampleContentWithIcons);
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(renderComponentSpy).toHaveBeenCalledTimes(2);
|
||||
expect(renderComponentSpy.calls.allArgs()[0][0]).toBe(IconComponent);
|
||||
expect((renderComponentSpy.calls.allArgs()[0][1] as HTMLElement).innerText).toEqual(
|
||||
`light_mode`,
|
||||
);
|
||||
expect(renderComponentSpy.calls.allArgs()[1][0]).toBe(IconComponent);
|
||||
expect((renderComponentSpy.calls.allArgs()[1][1] as HTMLElement).innerText).toEqual(
|
||||
`dark_mode`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Breadcrumb component when content has <docs-breadcrumb> element', async () => {
|
||||
navigationStateSpy.activeNavigationItem.and.returnValue({
|
||||
label: 'Active Item',
|
||||
parent: {
|
||||
label: 'Parent Item',
|
||||
},
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(DocViewer);
|
||||
const renderComponentSpy = spyOn(fixture.componentInstance, 'renderComponent' as any);
|
||||
fixture.componentRef.setInput('docContent', exampleContentWithBreadcrumbPlaceholder);
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(renderComponentSpy).toHaveBeenCalledTimes(1);
|
||||
expect(renderComponentSpy.calls.allArgs()[0][0]).toBe(Breadcrumb);
|
||||
});
|
||||
|
||||
it('should render copy source code buttons', async () => {
|
||||
const fixture = TestBed.createComponent(DocViewer);
|
||||
fixture.componentRef.setInput('docContent', exampleContentWithCodeSnippet);
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const copySourceCodeButton = fixture.debugElement.query(By.directive(CopySourceCodeButton));
|
||||
|
||||
expect(copySourceCodeButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render ToC', async () => {
|
||||
const fixture = TestBed.createComponent(DocViewer);
|
||||
const renderComponentSpy = spyOn(fixture.componentInstance, 'renderComponent' as any);
|
||||
fixture.componentRef.setInput('docContent', exampleContentWithHeadings);
|
||||
fixture.componentRef.setInput('hasToc', true);
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(renderComponentSpy).toHaveBeenCalled();
|
||||
expect(renderComponentSpy.calls.allArgs()[0][0]).toBe(TableOfContents);
|
||||
});
|
||||
|
||||
it('should not render ToC when hasToc is false', async () => {
|
||||
const fixture = TestBed.createComponent(DocViewer);
|
||||
const renderComponentSpy = spyOn(fixture.componentInstance, 'renderComponent' as any);
|
||||
fixture.componentRef.setInput('docContent', exampleContentWithHeadings);
|
||||
fixture.componentRef.setInput('hasToc', false);
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(renderComponentSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,349 @@
|
||||
/*!
|
||||
* @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 {CommonModule, DOCUMENT, isPlatformBrowser, Location} from '@angular/common';
|
||||
import {
|
||||
ApplicationRef,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ComponentRef,
|
||||
createComponent,
|
||||
DestroyRef,
|
||||
ElementRef,
|
||||
EnvironmentInjector,
|
||||
inject,
|
||||
Injector,
|
||||
Input,
|
||||
OnChanges,
|
||||
PLATFORM_ID,
|
||||
SimpleChanges,
|
||||
Type,
|
||||
ViewContainerRef,
|
||||
ViewEncapsulation,
|
||||
ɵPendingTasks as PendingTasks,
|
||||
EventEmitter,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {TOC_SKIP_CONTENT_MARKER, NavigationState} from '../../../services/index';
|
||||
import {TableOfContents} from '../../table-of-contents/table-of-contents.component';
|
||||
import {IconComponent} from '../../icon/icon.component';
|
||||
import {handleHrefClickEventWithRouter} from '../../../utils/index';
|
||||
import {Snippet} from '../../../interfaces/index';
|
||||
import {Router} from '@angular/router';
|
||||
import {fromEvent} from 'rxjs';
|
||||
|
||||
import {Breadcrumb} from '../../breadcrumb/breadcrumb.component';
|
||||
import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-code-button.component';
|
||||
import {ExampleViewer} from '../example-viewer/example-viewer.component';
|
||||
|
||||
/// <reference types="@types/dom-view-transitions" />
|
||||
|
||||
const TOC_HOST_ELEMENT_NAME = 'docs-table-of-contents';
|
||||
export const ASSETS_EXAMPLES_PATH = 'assets/content/examples';
|
||||
export const DOCS_VIEWER_SELECTOR = 'docs-viewer';
|
||||
export const DOCS_CODE_SELECTOR = '.docs-code';
|
||||
export const DOCS_CODE_MUTLIFILE_SELECTOR = '.docs-code-multifile';
|
||||
// TODO: Update the branch/sha
|
||||
export const GITHUB_CONTENT_URL =
|
||||
'https://github.com/angular/angular/blob/main/adev/src/content/examples/';
|
||||
|
||||
@Component({
|
||||
selector: DOCS_VIEWER_SELECTOR,
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: '',
|
||||
styleUrls: ['docs-viewer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
host: {
|
||||
'[class.docs-animate-content]': 'animateContent',
|
||||
},
|
||||
})
|
||||
export class DocViewer implements OnChanges {
|
||||
@Input() docContent?: string;
|
||||
@Input() hasToc = false;
|
||||
@Output() contentLoaded = new EventEmitter<void>();
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly elementRef = inject(ElementRef);
|
||||
private readonly location = inject(Location);
|
||||
private readonly navigationState = inject(NavigationState);
|
||||
private readonly platformId = inject(PLATFORM_ID);
|
||||
private readonly router = inject(Router);
|
||||
private readonly viewContainer = inject(ViewContainerRef);
|
||||
private readonly environmentInjector = inject(EnvironmentInjector);
|
||||
private readonly injector = inject(Injector);
|
||||
private readonly appRef = inject(ApplicationRef);
|
||||
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
private animateContent = false;
|
||||
private readonly pendingRenderTasks = inject(PendingTasks);
|
||||
|
||||
private countOfExamples = 0;
|
||||
|
||||
async ngOnChanges(changes: SimpleChanges): Promise<void> {
|
||||
const taskId = this.pendingRenderTasks.add();
|
||||
if ('docContent' in changes) {
|
||||
await this.renderContentsAndRunClientSetup(this.docContent!);
|
||||
}
|
||||
this.pendingRenderTasks.remove(taskId);
|
||||
}
|
||||
|
||||
async renderContentsAndRunClientSetup(content?: string): Promise<void> {
|
||||
const isBrowser = isPlatformBrowser(this.platformId);
|
||||
const contentContainer = this.elementRef.nativeElement;
|
||||
|
||||
if (content) {
|
||||
if (isBrowser && !(this.document as any).startViewTransition) {
|
||||
// Apply a special class to the host node to trigger animation.
|
||||
// Note: when a page is hydrated, the `content` would be empty,
|
||||
// so we don't trigger an animation to avoid a content flickering
|
||||
// visual effect. In addition, if the browser supports view transitions (startViewTransition is present), the animation is handled by the native View Transition API so it does not need to be done here.
|
||||
this.animateContent = true;
|
||||
}
|
||||
|
||||
contentContainer.innerHTML = content;
|
||||
}
|
||||
|
||||
if (isBrowser) {
|
||||
// First we setup event listeners on the HTML we just loaded.
|
||||
// We want to do this before things like the example viewers are loaded.
|
||||
this.setupAnchorListeners(contentContainer);
|
||||
// Rewrite relative anchors (hrefs starting with `#`) because relative hrefs are relative to the base URL, which is '/'
|
||||
this.rewriteRelativeAnchors(contentContainer);
|
||||
// In case when content contains placeholders for executable examples, create ExampleViewer components.
|
||||
await this.loadExamples();
|
||||
// In case when content contains static code snippets, then create buttons
|
||||
// responsible for copy source code.
|
||||
this.loadCopySourceCodeButtons();
|
||||
}
|
||||
|
||||
// Display Breadcrumb component if the `<docs-breadcrumb>` element exists
|
||||
this.loadBreadcrumb(contentContainer);
|
||||
|
||||
// Display Icon component if the `<docs-icon>` element exists
|
||||
this.loadIcons(contentContainer);
|
||||
|
||||
// Render ToC
|
||||
this.renderTableOfContents(contentContainer);
|
||||
|
||||
this.contentLoaded.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load ExampleViewer component when:
|
||||
* - exists docs-code-multifile element with multiple files OR
|
||||
* - exists docs-code element with single file AND
|
||||
* - 'preview' attribute was provided OR
|
||||
* - 'visibleLines' attribute was provided
|
||||
*/
|
||||
private async loadExamples(): Promise<void> {
|
||||
const multifileCodeExamples = <HTMLElement[]>(
|
||||
Array.from(this.elementRef.nativeElement.querySelectorAll(DOCS_CODE_MUTLIFILE_SELECTOR))
|
||||
);
|
||||
|
||||
for (let placeholder of multifileCodeExamples) {
|
||||
const path = placeholder.getAttribute('path')!;
|
||||
const snippets = this.getCodeSnippetsFromMultifileWrapper(placeholder);
|
||||
await this.renderExampleViewerComponents(placeholder, snippets, path);
|
||||
}
|
||||
|
||||
const docsCodeElements = this.elementRef.nativeElement.querySelectorAll(DOCS_CODE_SELECTOR);
|
||||
|
||||
for (const placeholder of docsCodeElements) {
|
||||
const snippet = this.getStandaloneCodeSnippet(placeholder);
|
||||
if (snippet) {
|
||||
await this.renderExampleViewerComponents(placeholder, [snippet], snippet.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderTableOfContents(element: HTMLElement): void {
|
||||
if (!this.hasToc) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstHeading = element.querySelector<HTMLHeadingElement>('h2,h3[id]');
|
||||
if (!firstHeading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Since the content of the main area is dynamically created and there is
|
||||
// no host element for a ToC component, we create it manually.
|
||||
let tocHostElement: HTMLElement | null = element.querySelector(TOC_HOST_ELEMENT_NAME);
|
||||
if (!tocHostElement) {
|
||||
tocHostElement = this.document.createElement(TOC_HOST_ELEMENT_NAME);
|
||||
tocHostElement.setAttribute(TOC_SKIP_CONTENT_MARKER, 'true');
|
||||
firstHeading?.parentNode?.insertBefore(tocHostElement, firstHeading);
|
||||
}
|
||||
|
||||
this.renderComponent(TableOfContents, tocHostElement, {contentSourceElement: element});
|
||||
}
|
||||
|
||||
private async renderExampleViewerComponents(
|
||||
placeholder: HTMLElement,
|
||||
snippets: Snippet[],
|
||||
path: string,
|
||||
): Promise<void> {
|
||||
const preview = Boolean(placeholder.getAttribute('preview'));
|
||||
const title = placeholder.getAttribute('header') ?? undefined;
|
||||
const firstCodeSnippetTitle =
|
||||
snippets.length > 0 ? snippets[0].title ?? snippets[0].name : undefined;
|
||||
const exampleRef = this.viewContainer.createComponent(ExampleViewer);
|
||||
|
||||
this.countOfExamples++;
|
||||
exampleRef.instance.metadata = {
|
||||
title: title ?? firstCodeSnippetTitle,
|
||||
path,
|
||||
files: snippets,
|
||||
preview,
|
||||
id: this.countOfExamples,
|
||||
};
|
||||
|
||||
exampleRef.instance.githubUrl = `${GITHUB_CONTENT_URL}/${snippets[0].name}`;
|
||||
exampleRef.instance.stackblitzUrl = `${ASSETS_EXAMPLES_PATH}/${snippets[0].name}.html`;
|
||||
|
||||
placeholder.parentElement!.replaceChild(exampleRef.location.nativeElement, placeholder);
|
||||
|
||||
await exampleRef.instance.renderExample();
|
||||
}
|
||||
|
||||
private getCodeSnippetsFromMultifileWrapper(element: HTMLElement): Snippet[] {
|
||||
const tabs = <Element[]>Array.from(element.querySelectorAll(DOCS_CODE_SELECTOR));
|
||||
|
||||
return tabs.map((tab) => ({
|
||||
name: tab.getAttribute('path') ?? tab.getAttribute('header') ?? '',
|
||||
content: tab.innerHTML,
|
||||
visibleLinesRange: tab.getAttribute('visibleLines') ?? undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
private getStandaloneCodeSnippet(element: HTMLElement): Snippet | null {
|
||||
const visibleLines = element.getAttribute('visibleLines') ?? undefined;
|
||||
const preview = element.getAttribute('preview');
|
||||
|
||||
if (!visibleLines && !preview) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = element.querySelector('pre')!;
|
||||
const path = element.getAttribute('path')!;
|
||||
const title = element.getAttribute('header') ?? undefined;
|
||||
|
||||
return {
|
||||
title,
|
||||
name: path,
|
||||
content: content?.outerHTML,
|
||||
visibleLinesRange: visibleLines,
|
||||
};
|
||||
}
|
||||
|
||||
// If the content contains static code snippets, we should add buttons to copy
|
||||
// the code
|
||||
private loadCopySourceCodeButtons(): void {
|
||||
const staticCodeSnippets = <Element[]>(
|
||||
Array.from(this.elementRef.nativeElement.querySelectorAll('.docs-code:not([mermaid])'))
|
||||
);
|
||||
|
||||
for (let codeSnippet of staticCodeSnippets) {
|
||||
const copySourceCodeButton = this.viewContainer.createComponent(CopySourceCodeButton);
|
||||
codeSnippet.appendChild(copySourceCodeButton.location.nativeElement);
|
||||
}
|
||||
}
|
||||
|
||||
private loadBreadcrumb(element: HTMLElement): void {
|
||||
const breadcrumbPlaceholder = element.querySelector('docs-breadcrumb') as HTMLElement;
|
||||
const activeNavigationItem = this.navigationState.activeNavigationItem();
|
||||
|
||||
if (breadcrumbPlaceholder && !!activeNavigationItem?.parent) {
|
||||
this.renderComponent(Breadcrumb, breadcrumbPlaceholder);
|
||||
}
|
||||
}
|
||||
|
||||
private loadIcons(element: HTMLElement): void {
|
||||
element.querySelectorAll('docs-icon').forEach((iconsPlaceholder) => {
|
||||
this.renderComponent(IconComponent, iconsPlaceholder as HTMLElement);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to render a component dynamically in a context of this class.
|
||||
*/
|
||||
private renderComponent<T>(
|
||||
type: Type<T>,
|
||||
hostElement: HTMLElement,
|
||||
inputs?: {[key: string]: unknown},
|
||||
): ComponentRef<T> {
|
||||
const componentRef = createComponent(type, {
|
||||
hostElement,
|
||||
elementInjector: this.injector,
|
||||
environmentInjector: this.environmentInjector,
|
||||
});
|
||||
|
||||
if (inputs) {
|
||||
for (const [name, value] of Object.entries(inputs)) {
|
||||
componentRef.setInput(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger change detection after setting inputs.
|
||||
componentRef.changeDetectorRef.detectChanges();
|
||||
|
||||
// Attach a view to the ApplicationRef for change detection
|
||||
// purposes and for hydration serialization to pick it up
|
||||
// during SSG.
|
||||
this.appRef.attachView(componentRef.hostView);
|
||||
|
||||
return componentRef;
|
||||
}
|
||||
|
||||
private setupAnchorListeners(element: HTMLElement): void {
|
||||
element.querySelectorAll(`a[href]`).forEach((anchor) => {
|
||||
// Get the target element's ID from the href attribute
|
||||
const url = new URL((anchor as HTMLAnchorElement).href);
|
||||
const isExternalLink = url.origin !== this.document.location.origin;
|
||||
if (isExternalLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
fromEvent(anchor, 'click')
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((e) => {
|
||||
const closestAnchor = (e.target as Element).closest('a');
|
||||
if (closestAnchor?.target && closestAnchor.target !== 'self') {
|
||||
return;
|
||||
}
|
||||
|
||||
const hrefAttr = closestAnchor?.getAttribute?.('href');
|
||||
if (!hrefAttr) {
|
||||
return;
|
||||
}
|
||||
|
||||
let relativeUrl: string;
|
||||
if (hrefAttr.startsWith('http')) {
|
||||
// Url is absolute but we're targeting the same domain
|
||||
const url = new URL(hrefAttr);
|
||||
relativeUrl = `${url.pathname}${url.hash}${url.search}`;
|
||||
} else {
|
||||
relativeUrl = hrefAttr;
|
||||
}
|
||||
|
||||
handleHrefClickEventWithRouter(e, this.router, relativeUrl);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private rewriteRelativeAnchors(element: HTMLElement) {
|
||||
for (const anchor of Array.from(element.querySelectorAll(`a[href^="#"]:not(a[download])`))) {
|
||||
const url = new URL((anchor as HTMLAnchorElement).href);
|
||||
(anchor as HTMLAnchorElement).href = this.location.path() + url.hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<div class="docs-example-viewer" role="group">
|
||||
<header class="docs-example-viewer-actions">
|
||||
@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>
|
||||
}
|
||||
</mat-tab-group>
|
||||
}
|
||||
|
||||
<div class="docs-example-viewer-icons">
|
||||
<button
|
||||
type="button"
|
||||
class="docs-example-copy-link"
|
||||
[attr.aria-label]="'Copy link to ' + exampleMetadata()?.title + ' example to the clipboard'"
|
||||
(click)="copyLink()"
|
||||
>
|
||||
<i aria-hidden="true">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="inherit"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<!-- link icon -->
|
||||
<path
|
||||
d="M11 17H7C5.61667 17 4.4375 16.5125 3.4625 15.5375C2.4875 14.5625 2 13.3833 2 12C2 10.6167 2.4875 9.4375 3.4625 8.4625C4.4375 7.4875 5.61667 7 7 7H11V9H7C6.16667 9 5.45833 9.29167 4.875 9.875C4.29167 10.4583 4 11.1667 4 12C4 12.8333 4.29167 13.5417 4.875 14.125C5.45833 14.7083 6.16667 15 7 15H11V17ZM8 13V11H16V13H8ZM13 17V15H17C17.8333 15 18.5417 14.7083 19.125 14.125C19.7083 13.5417 20 12.8333 20 12C20 11.1667 19.7083 10.4583 19.125 9.875C18.5417 9.29167 17.8333 9 17 9H13V7H17C18.3833 7 19.5625 7.4875 20.5375 8.4625C21.5125 9.4375 22 10.6167 22 12C22 13.3833 21.5125 14.5625 20.5375 15.5375C19.5625 16.5125 18.3833 17 17 17H13Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</i>
|
||||
</button>
|
||||
<ng-container *ngTemplateOutlet="openCodeInExternalProvider" />
|
||||
@if (expandable()) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="toggleExampleVisibility()"
|
||||
[attr.title]="(expanded() ? 'Collapse' : 'Expand') + ' example'"
|
||||
[attr.aria-label]="(expanded() ? 'Collapse' : 'Expand') + ' code example'"
|
||||
>
|
||||
<i aria-hidden="true">
|
||||
@if (!expanded()) {
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<!-- Expand arrow -->
|
||||
<path d="M3 21v-8h2v4.6L17.6 5H13V3h8v8h-2V6.4L6.4 19H11v2H3Z" />
|
||||
</svg>
|
||||
} @else {
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill="var(--gray-400)"
|
||||
d="M3.4 22 2 20.6 8.6 14H4v-2h8v8h-2v-4.6L3.4 22ZM12 12V4h2v4.6L20.6 2 22 3.4 15.4 10H20v2h-8Z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
</i>
|
||||
</button>
|
||||
}
|
||||
</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>
|
||||
<docs-viewer [docContent]="snippetCode()?.content" />
|
||||
</div>
|
||||
|
||||
@if (exampleComponent) {
|
||||
<div class="docs-example-viewer-preview">
|
||||
<ng-container *ngComponentOutlet="exampleComponent" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<ng-template #openCodeInExternalProvider>
|
||||
@if (exampleComponent) {
|
||||
@if (githubUrl) {
|
||||
<a
|
||||
[href]="githubUrl"
|
||||
target="_blank"
|
||||
title="Open example on GitHub"
|
||||
class="docs-example-github-link"
|
||||
aria-label="Open example on GitHub"
|
||||
>
|
||||
<i aria-hidden="true">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="inherit"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.16141 22.8681C9.16141 22.5894 9.15159 21.8509 9.14614 20.8707C5.96014 21.5798 5.28759 19.296 5.28759 19.296C4.76668 17.9389 4.01559 17.5778 4.01559 17.5778C2.97541 16.8485 4.09414 16.8638 4.09414 16.8638C5.24396 16.9467 5.84886 18.0747 5.84886 18.0747C6.8705 19.8692 8.52923 19.3516 9.18268 19.0505C9.28686 18.2912 9.5825 17.7736 9.90977 17.4801C7.36632 17.184 4.69196 16.176 4.69196 11.6754C4.69196 10.3936 5.13868 9.34523 5.87123 8.52377C5.75396 8.22705 5.36014 7.03305 5.98359 5.41577C5.98359 5.41577 6.94577 5.09996 9.13359 6.61959C10.0467 6.35941 11.0269 6.2285 12.0016 6.22414C12.9741 6.2285 13.9538 6.35941 14.869 6.61959C17.0558 5.09996 18.0163 5.41577 18.0163 5.41577C18.6414 7.0325 18.2481 8.2265 18.1298 8.52377C18.864 9.34523 19.3069 10.3936 19.3069 11.6754C19.3069 16.1874 16.6287 17.1801 14.077 17.4709C14.4889 17.8336 14.8543 18.5503 14.8543 19.6461C14.8543 21.2165 14.8396 22.4836 14.8396 22.8681C14.8396 23.1829 15.0463 23.5478 15.6278 23.4327C20.1758 21.877 23.4545 17.4774 23.4545 12.2907C23.4545 5.80359 18.3256 0.54541 11.9994 0.54541C5.67432 0.54541 0.54541 5.80359 0.54541 12.2907C0.545956 17.479 3.82796 21.8814 8.37977 23.4343C8.95196 23.5418 9.16141 23.179 9.16141 22.8681Z"
|
||||
fill="inherit"
|
||||
/>
|
||||
</svg>
|
||||
</i>
|
||||
</a>
|
||||
}
|
||||
@if (stackblitzUrl) {
|
||||
<a
|
||||
[href]="stackblitzUrl"
|
||||
target="_blank"
|
||||
class="docs-example-stackblitz-link"
|
||||
title="Edit this example in StackBlitz"
|
||||
aria-label="Edit this example in StackBlitz"
|
||||
>
|
||||
<i aria-hidden="true">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 356 511"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M138.719 150.22C62.6928 232.614 0.340573 300.4 0.158928 300.856C-0.0227172 301.311 33.9559 301.799 75.6665 301.939L151.505 302.195L117.656 396.511C74.7852 515.966 76.7972 510.288 77.3522 510.288C78.2145 510.288 355.296 209.735 355.296 208.799C355.296 208.245 325.263 207.879 279.943 207.879C233.709 207.879 204.591 207.518 204.591 206.943C204.591 206.428 220.136 162.751 239.137 109.883C279.06 -1.20153 278.545 0.264614 277.638 0.347453C277.26 0.382384 214.746 67.8247 138.719 150.22Z"
|
||||
/>
|
||||
</svg>
|
||||
</i>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -0,0 +1,129 @@
|
||||
:host {
|
||||
.docs-example-viewer-preview {
|
||||
.docs-dark-mode & {
|
||||
background: var(--gray-100);
|
||||
}
|
||||
@media screen and (prefers-color-scheme: dark) {
|
||||
background: var(--gray-100);
|
||||
}
|
||||
.docs-light-mode & {
|
||||
background: var(--page-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.docs-example-viewer {
|
||||
border: 1px solid var(--senary-contrast);
|
||||
border-radius: 0.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Example viewer header
|
||||
.docs-example-viewer-actions {
|
||||
background: var(--subtle-purple);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid var(--senary-contrast);
|
||||
transition: background 0.3s ease, border-color 0.3s ease;
|
||||
padding-inline-end: 0.65rem;
|
||||
font-family: var(--inter-tight-font);
|
||||
|
||||
mat-tab-group {
|
||||
max-width: calc(100% - 140px);
|
||||
}
|
||||
|
||||
span:first-of-type {
|
||||
background-image: var(--purple-to-blue-horizontal-gradient);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
|
||||
padding: 0.7rem 1.1rem;
|
||||
font-size: 0.875rem;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 1.4rem;
|
||||
letter-spacing: -0.00875rem;
|
||||
margin: 0;
|
||||
word-wrap: break-word;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.docs-example-viewer-icons {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
|
||||
svg {
|
||||
fill: var(--gray-400);
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
path {
|
||||
transition: fill 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
fill: var(--tertiary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example viewer code
|
||||
.docs-example-viewer-code-wrapper {
|
||||
position: relative;
|
||||
font-size: 0.875rem;
|
||||
// TODO: only show this if there is a preview
|
||||
// border-block-end: 1px solid var(--senary-contrast);
|
||||
transition: border-color 0.3s ease;
|
||||
container: viewerblock / inline-size;
|
||||
background-color: var(--octonary-contrast);
|
||||
|
||||
button[docs-copy-source-code] {
|
||||
top: 0.31rem;
|
||||
}
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line
|
||||
::ng-deep {
|
||||
.docs-example-viewer-preview {
|
||||
// stylelint-disable-next-line
|
||||
all: initial;
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
border-block-start: 1px solid var(--senary-contrast);
|
||||
|
||||
*,
|
||||
code::before,
|
||||
code,
|
||||
pre,
|
||||
a,
|
||||
i,
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
ol,
|
||||
ul,
|
||||
li,
|
||||
hr,
|
||||
input,
|
||||
select,
|
||||
table {
|
||||
all: revert;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
/*!
|
||||
* @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 {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
|
||||
import {ExampleViewer} from './example-viewer.component';
|
||||
import {DocsContentLoader, ExampleMetadata, ExampleViewerContentLoader} from '../../../interfaces';
|
||||
import {DOCS_CONTENT_LOADER, EXAMPLE_VIEWER_CONTENT_LOADER} from '../../../providers';
|
||||
import {Component, provideExperimentalZonelessChangeDetection} from '@angular/core';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {HarnessLoader} from '@angular/cdk/testing';
|
||||
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
|
||||
import {Clipboard} from '@angular/cdk/clipboard';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {MatTabGroupHarness} from '@angular/material/tabs/testing';
|
||||
import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-code-button.component';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
|
||||
describe('ExampleViewer', () => {
|
||||
let component: ExampleViewer;
|
||||
let fixture: ComponentFixture<ExampleViewer>;
|
||||
let loader: HarnessLoader;
|
||||
let exampleContentSpy: jasmine.SpyObj<ExampleViewerContentLoader>;
|
||||
let contentServiceSpy: jasmine.SpyObj<DocsContentLoader>;
|
||||
|
||||
beforeEach(() => {
|
||||
exampleContentSpy = jasmine.createSpyObj('ExampleContentLoader', ['loadPreview']);
|
||||
contentServiceSpy = jasmine.createSpyObj('ContentLoader', ['getContent']);
|
||||
contentServiceSpy.getContent.and.returnValue(Promise.resolve(undefined));
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ExampleViewer, NoopAnimationsModule],
|
||||
providers: [
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
{provide: EXAMPLE_VIEWER_CONTENT_LOADER, useValue: exampleContentSpy},
|
||||
{provide: DOCS_CONTENT_LOADER, useValue: contentServiceSpy},
|
||||
{provide: ActivatedRoute, useValue: {snapshot: {fragment: 'fragment'}}},
|
||||
],
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(ExampleViewer);
|
||||
component = fixture.componentInstance;
|
||||
loader = TestbedHarnessEnvironment.loader(fixture);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should set file extensions as tab names when all files have different extension', waitForAsync(async () => {
|
||||
component.metadata = getMetadata({
|
||||
files: [
|
||||
{name: 'file.ts', content: ''},
|
||||
{name: 'file.html', content: ''},
|
||||
{name: 'file.css', content: ''},
|
||||
],
|
||||
});
|
||||
|
||||
await component.renderExample();
|
||||
|
||||
expect(component.tabs()!.length).toBe(3);
|
||||
expect(component.tabs()![0].name).toBe('TS');
|
||||
expect(component.tabs()![1].name).toBe('HTML');
|
||||
expect(component.tabs()![2].name).toBe('CSS');
|
||||
}));
|
||||
|
||||
it('should generate correct code content for multi file mode when it is expanded', waitForAsync(async () => {
|
||||
component.metadata = getMetadata({
|
||||
files: [
|
||||
{name: 'file.ts', content: 'typescript file'},
|
||||
{name: 'file.html', content: 'html file'},
|
||||
{name: 'file.css', content: 'css file'},
|
||||
],
|
||||
});
|
||||
|
||||
await component.renderExample();
|
||||
|
||||
expect(component.tabs()!.length).toBe(3);
|
||||
expect(component.tabs()![0].code).toBe('typescript file');
|
||||
expect(component.tabs()![1].code).toBe('html file');
|
||||
expect(component.tabs()![2].code).toBe('css file');
|
||||
}));
|
||||
|
||||
it('should set file names as tab names when there is at least one duplication', async () => {
|
||||
component.metadata = getMetadata({
|
||||
files: [
|
||||
{name: 'example.ts', content: 'typescript file'},
|
||||
{name: 'example.html', content: 'html file'},
|
||||
{name: 'another-example.ts', content: 'css file'},
|
||||
],
|
||||
});
|
||||
|
||||
await component.renderExample();
|
||||
expect(component.tabs()!.length).toBe(3);
|
||||
expect(component.tabs()![0].name).toBe('example.ts');
|
||||
expect(component.tabs()![1].name).toBe('example.html');
|
||||
expect(component.tabs()![2].name).toBe('another-example.ts');
|
||||
});
|
||||
|
||||
it('should expandable be false when none of the example files have defined visibleLinesRange ', waitForAsync(async () => {
|
||||
component.metadata = getMetadata();
|
||||
await component.renderExample();
|
||||
expect(component.expandable()).toBeFalse();
|
||||
}));
|
||||
|
||||
it('should expandable be true when at least one example file has defined visibleLinesRange ', waitForAsync(async () => {
|
||||
component.metadata = getMetadata({
|
||||
files: [
|
||||
{name: 'example.ts', content: 'typescript file'},
|
||||
{
|
||||
name: 'example.html',
|
||||
content: 'html file',
|
||||
visibleLinesRange: '[1, 2]',
|
||||
},
|
||||
{name: 'another-example.ts', content: 'css file'},
|
||||
],
|
||||
});
|
||||
await component.renderExample();
|
||||
expect(component.expandable()).toBeTrue();
|
||||
}));
|
||||
|
||||
it('should set exampleComponent when metadata contains path and preview is true', waitForAsync(async () => {
|
||||
exampleContentSpy.loadPreview.and.resolveTo(ExampleComponent);
|
||||
component.metadata = getMetadata({
|
||||
path: 'example.ts',
|
||||
preview: true,
|
||||
});
|
||||
await component.renderExample();
|
||||
expect(component.exampleComponent).toBe(ExampleComponent);
|
||||
}));
|
||||
|
||||
it('should display GitHub button when githubUrl is provided and there is preview', waitForAsync(async () => {
|
||||
exampleContentSpy.loadPreview.and.resolveTo(ExampleComponent);
|
||||
component.metadata = getMetadata({
|
||||
path: 'example.ts',
|
||||
preview: true,
|
||||
});
|
||||
component.githubUrl = 'https://github.com/';
|
||||
await component.renderExample();
|
||||
const githubButton = fixture.debugElement.query(
|
||||
By.css('a[aria-label="Open example on GitHub"]'),
|
||||
);
|
||||
expect(githubButton).toBeTruthy();
|
||||
expect(githubButton.nativeElement.href).toBe(component.githubUrl);
|
||||
}));
|
||||
|
||||
it('should display StackBlitz button when stackblitzUrl is provided and there is preview', waitForAsync(async () => {
|
||||
exampleContentSpy.loadPreview.and.resolveTo(ExampleComponent);
|
||||
component.metadata = getMetadata({
|
||||
path: 'example.ts',
|
||||
preview: true,
|
||||
});
|
||||
component.stackblitzUrl = 'https://stackblitz.com/';
|
||||
await component.renderExample();
|
||||
const stackblitzButton = fixture.debugElement.query(
|
||||
By.css('a[aria-label="Edit this example in StackBlitz"]'),
|
||||
);
|
||||
expect(stackblitzButton).toBeTruthy();
|
||||
expect(stackblitzButton.nativeElement.href).toBe(component.stackblitzUrl);
|
||||
}));
|
||||
|
||||
it('should set expanded flag in metadata after toggleExampleVisibility', waitForAsync(async () => {
|
||||
component.metadata = getMetadata();
|
||||
await component.renderExample();
|
||||
component.toggleExampleVisibility();
|
||||
expect(component.expanded()).toBeTrue();
|
||||
const tabGroup = await loader.getHarness(MatTabGroupHarness);
|
||||
const tab = await tabGroup.getSelectedTab();
|
||||
expect(await tab.getLabel()).toBe('TS');
|
||||
component.toggleExampleVisibility();
|
||||
expect(component.expanded()).toBeFalse();
|
||||
}));
|
||||
|
||||
// TODO(josephperrott): enable once the docs-viewer/example-viewer circle is sorted out.
|
||||
xit('should call clipboard service when clicked on copy source code', waitForAsync(async () => {
|
||||
const expectedCodeSnippetContent = 'typescript code';
|
||||
component.metadata = getMetadata({
|
||||
files: [
|
||||
{
|
||||
name: 'example.ts',
|
||||
content: `<pre><code>${expectedCodeSnippetContent}</code></pre>`,
|
||||
},
|
||||
{name: 'example.css', content: ''},
|
||||
],
|
||||
});
|
||||
const clipboardService = TestBed.inject(Clipboard);
|
||||
const spy = spyOn(clipboardService, 'copy');
|
||||
|
||||
await component.renderExample();
|
||||
const button = fixture.debugElement.query(By.directive(CopySourceCodeButton)).nativeElement;
|
||||
button.click();
|
||||
|
||||
expect(spy.calls.argsFor(0)[0]?.trim()).toBe(expectedCodeSnippetContent);
|
||||
}));
|
||||
|
||||
it('should call clipboard service when clicked on copy example link', waitForAsync(async () => {
|
||||
component.metadata = getMetadata();
|
||||
component.expanded.set(true);
|
||||
fixture.detectChanges();
|
||||
|
||||
const clipboardService = TestBed.inject(Clipboard);
|
||||
const spy = spyOn(clipboardService, 'copy');
|
||||
await component.renderExample();
|
||||
const button = fixture.debugElement.query(
|
||||
By.css('button.docs-example-copy-link'),
|
||||
).nativeElement;
|
||||
button.click();
|
||||
expect(spy.calls.argsFor(0)[0].trim()).toBe(`${window.origin}/context.html#example-1`);
|
||||
}));
|
||||
});
|
||||
|
||||
const getMetadata = (value: Partial<ExampleMetadata> = {}): ExampleMetadata => {
|
||||
return {
|
||||
id: 1,
|
||||
files: [
|
||||
{name: 'example.ts', content: ''},
|
||||
{name: 'example.css', content: ''},
|
||||
],
|
||||
preview: false,
|
||||
...value,
|
||||
};
|
||||
};
|
||||
|
||||
@Component({
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
class ExampleComponent {}
|
||||
@@ -0,0 +1,243 @@
|
||||
/*!
|
||||
* @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 {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
Input,
|
||||
Type,
|
||||
computed,
|
||||
inject,
|
||||
ChangeDetectorRef,
|
||||
ViewChild,
|
||||
signal,
|
||||
ElementRef,
|
||||
forwardRef,
|
||||
} from '@angular/core';
|
||||
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 {ExampleMetadata, Snippet} from '../../../interfaces/index';
|
||||
import {EXAMPLE_VIEWER_CONTENT_LOADER} from '../../../providers/index';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {DocViewer} from '../docs-viewer/docs-viewer.component';
|
||||
|
||||
export enum CodeExampleViewMode {
|
||||
SNIPPET = 'snippet',
|
||||
MULTI_FILE = 'multi',
|
||||
}
|
||||
|
||||
export const CODE_LINE_NUMBER_CLASS_NAME = 'shiki-ln-number';
|
||||
export const CODE_LINE_CLASS_NAME = 'line';
|
||||
export const GAP_CODE_LINE_CLASS_NAME = 'gap';
|
||||
export const HIDDEN_CLASS_NAME = 'hidden';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-example-viewer',
|
||||
standalone: true,
|
||||
imports: [CommonModule, forwardRef(() => DocViewer), CopySourceCodeButton, MatTabsModule],
|
||||
templateUrl: './example-viewer.component.html',
|
||||
styleUrls: ['./example-viewer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ExampleViewer {
|
||||
// TODO: replace by signal-based input when it'll be available
|
||||
@Input({required: true}) set metadata(value: ExampleMetadata) {
|
||||
this.exampleMetadata.set(value);
|
||||
}
|
||||
|
||||
@Input() githubUrl: string | null = null;
|
||||
@Input() stackblitzUrl: string | null = null;
|
||||
@ViewChild('codeTabs') matTabGroup?: MatTabGroup;
|
||||
|
||||
private readonly changeDetector = inject(ChangeDetectorRef);
|
||||
private readonly clipboard = inject(Clipboard);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly elementRef = inject(ElementRef<HTMLElement>);
|
||||
private readonly exampleViewerContentLoader = inject(EXAMPLE_VIEWER_CONTENT_LOADER);
|
||||
|
||||
private readonly shouldDisplayFullName = computed(() => {
|
||||
const fileExtensions =
|
||||
this.exampleMetadata()?.files.map((file) => this.getFileExtension(file.name)) ?? [];
|
||||
|
||||
// Display full file names only when exist files with the same extension
|
||||
return new Set(fileExtensions).size !== fileExtensions.length;
|
||||
});
|
||||
|
||||
CodeExampleViewMode = CodeExampleViewMode;
|
||||
exampleComponent?: Type<unknown>;
|
||||
|
||||
expanded = signal<boolean>(false);
|
||||
exampleMetadata = signal<ExampleMetadata | null>(null);
|
||||
snippetCode = signal<Snippet | undefined>(undefined);
|
||||
tabs = computed(() =>
|
||||
this.exampleMetadata()?.files.map((file) => ({
|
||||
name:
|
||||
file.title ?? (this.shouldDisplayFullName() ? file.name : this.getFileExtension(file.name)),
|
||||
code: file.content,
|
||||
})),
|
||||
);
|
||||
view = computed(() =>
|
||||
this.exampleMetadata()?.files.length === 1
|
||||
? CodeExampleViewMode.SNIPPET
|
||||
: CodeExampleViewMode.MULTI_FILE,
|
||||
);
|
||||
expandable = computed(() =>
|
||||
this.exampleMetadata()?.files.some((file) => !!file.visibleLinesRange),
|
||||
);
|
||||
|
||||
async renderExample(): Promise<void> {
|
||||
// Lazy load live example component
|
||||
if (this.exampleMetadata()?.path && this.exampleMetadata()?.preview) {
|
||||
this.exampleComponent = await this.exampleViewerContentLoader.loadPreview(
|
||||
this.exampleMetadata()?.path!,
|
||||
);
|
||||
}
|
||||
|
||||
this.snippetCode.set(this.exampleMetadata()?.files[0]);
|
||||
|
||||
this.changeDetector.detectChanges();
|
||||
|
||||
this.setCodeLinesVisibility();
|
||||
|
||||
this.elementRef.nativeElement.setAttribute(
|
||||
'id',
|
||||
`example-${this.exampleMetadata()?.id.toString()!}`,
|
||||
);
|
||||
|
||||
this.matTabGroup?.realignInkBar();
|
||||
|
||||
this.listenToMatTabIndexChange();
|
||||
}
|
||||
|
||||
toggleExampleVisibility(): void {
|
||||
this.expanded.update((expanded) => !expanded);
|
||||
|
||||
this.setCodeLinesVisibility();
|
||||
}
|
||||
|
||||
copyLink(): void {
|
||||
// Reconstruct the URL using `origin + pathname` so we drop any pre-existing hash.
|
||||
const fullUrl =
|
||||
location.origin +
|
||||
location.pathname +
|
||||
location.search +
|
||||
'#example-' +
|
||||
this.exampleMetadata()?.id;
|
||||
this.clipboard.copy(fullUrl);
|
||||
}
|
||||
|
||||
private listenToMatTabIndexChange(): void {
|
||||
this.matTabGroup?.realignInkBar();
|
||||
this.matTabGroup?.selectedIndexChange
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((index) => {
|
||||
this.snippetCode.set(this.exampleMetadata()?.files[index]);
|
||||
this.changeDetector.detectChanges();
|
||||
this.setCodeLinesVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
private getFileExtension(name: string): string {
|
||||
const segments = name.split('.');
|
||||
return segments.length ? segments[segments.length - 1].toLocaleUpperCase() : '';
|
||||
}
|
||||
|
||||
private setCodeLinesVisibility(): void {
|
||||
this.expanded()
|
||||
? this.handleExpandedStateForCodeBlock()
|
||||
: this.handleCollapsedStateForCodeBlock();
|
||||
}
|
||||
|
||||
private handleExpandedStateForCodeBlock(): void {
|
||||
const lines = <HTMLDivElement[]>(
|
||||
Array.from(
|
||||
this.elementRef.nativeElement.querySelectorAll(
|
||||
`.${CODE_LINE_CLASS_NAME}.${HIDDEN_CLASS_NAME}`,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
const lineNumbers = <HTMLSpanElement[]>(
|
||||
Array.from(
|
||||
this.elementRef.nativeElement.querySelectorAll(
|
||||
`.${CODE_LINE_NUMBER_CLASS_NAME}.${HIDDEN_CLASS_NAME}`,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
const gapLines = <HTMLDivElement[]>(
|
||||
Array.from(
|
||||
this.elementRef.nativeElement.querySelectorAll(
|
||||
`.${CODE_LINE_CLASS_NAME}.${GAP_CODE_LINE_CLASS_NAME}`,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
for (const line of lines) {
|
||||
line.classList.remove(HIDDEN_CLASS_NAME);
|
||||
}
|
||||
|
||||
for (const lineNumber of lineNumbers) {
|
||||
lineNumber.classList.remove(HIDDEN_CLASS_NAME);
|
||||
}
|
||||
|
||||
for (const expandLine of gapLines) {
|
||||
expandLine.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private handleCollapsedStateForCodeBlock(): void {
|
||||
const visibleLinesRange = this.snippetCode()?.visibleLinesRange;
|
||||
|
||||
if (!visibleLinesRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const linesToDisplay = (visibleLinesRange?.split(',') ?? []).map((line) => Number(line));
|
||||
const lines = <HTMLDivElement[]>(
|
||||
Array.from(this.elementRef.nativeElement.querySelectorAll(`.${CODE_LINE_CLASS_NAME}`))
|
||||
);
|
||||
const lineNumbers = <HTMLSpanElement[]>(
|
||||
Array.from(this.elementRef.nativeElement.querySelectorAll(`.${CODE_LINE_NUMBER_CLASS_NAME}`))
|
||||
);
|
||||
const appendGapBefore = [];
|
||||
|
||||
for (const [index, line] of lines.entries()) {
|
||||
if (!linesToDisplay.includes(index)) {
|
||||
line.classList.add(HIDDEN_CLASS_NAME);
|
||||
} else if (!linesToDisplay.includes(index - 1)) {
|
||||
appendGapBefore.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [index, lineNumber] of lineNumbers.entries()) {
|
||||
if (!linesToDisplay.includes(index)) {
|
||||
lineNumber.classList.add(HIDDEN_CLASS_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
// Create gap line between visible ranges. For example we would like to display 10-16 and 20-29 lines.
|
||||
// We should display separator, gap between those two scopes.
|
||||
// TODO: we could replace div it with the component, and allow to expand code block after click.
|
||||
for (const [index, element] of appendGapBefore.entries()) {
|
||||
if (index === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const separator = this.document.createElement('div');
|
||||
separator.textContent = `...`;
|
||||
separator.classList.add(CODE_LINE_CLASS_NAME);
|
||||
separator.classList.add(GAP_CODE_LINE_CLASS_NAME);
|
||||
element.parentNode?.insertBefore(separator, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ts_library(
|
||||
name = "constants",
|
||||
srcs = [
|
||||
"index.ts",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":lib",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "lib",
|
||||
srcs = glob(
|
||||
[
|
||||
"*.ts",
|
||||
],
|
||||
exclude = [
|
||||
"index.ts",
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
/*!
|
||||
* @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
|
||||
*/
|
||||
|
||||
// Used for both the table of contents and the home animation
|
||||
export const RESIZE_EVENT_DELAY = 500;
|
||||
|
||||
// Used for the home animation
|
||||
export const WEBGL_LOADED_DELAY = 250;
|
||||
@@ -0,0 +1,9 @@
|
||||
/*!
|
||||
* @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
|
||||
*/
|
||||
|
||||
export * from './delay';
|
||||
@@ -0,0 +1,18 @@
|
||||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ts_library(
|
||||
name = "directives",
|
||||
srcs = [
|
||||
"index.ts",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//adev/shared-docs/directives/click-outside",
|
||||
"//adev/shared-docs/directives/external-link",
|
||||
"//adev/shared-docs/directives/search-item",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "click-outside",
|
||||
srcs = [
|
||||
"click-outside.directive.ts",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/directives:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
":click-outside",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
"//packages/platform-browser",
|
||||
"//packages/router",
|
||||
"//packages/router/testing",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
/*!
|
||||
* @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} from '@angular/core';
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {ClickOutside} from './click-outside.directive';
|
||||
import {By} from '@angular/platform-browser';
|
||||
|
||||
describe('ClickOutside', () => {
|
||||
let component: ExampleComponent;
|
||||
let fixture: ComponentFixture<ExampleComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ExampleComponent, RouterTestingModule],
|
||||
});
|
||||
fixture = TestBed.createComponent(ExampleComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should docsClickOutside be emitted when user click outside `content` element', () => {
|
||||
const clickedOutsideSpy = spyOn(component, 'clickedOutside');
|
||||
const button = fixture.debugElement.query(By.css('button[id="exampleButton"]'));
|
||||
|
||||
button.nativeElement.click();
|
||||
|
||||
expect(clickedOutsideSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not docsClickOutside be emitted when user click inside `content` element', () => {
|
||||
const clickedOutsideSpy = spyOn(component, 'clickedOutside');
|
||||
const content = fixture.debugElement.query(By.css('div[id="content"]'));
|
||||
|
||||
content.nativeElement.click();
|
||||
|
||||
expect(clickedOutsideSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not docsClickOutside be emitted when user click inside `content` element', () => {
|
||||
const clickedOutsideSpy = spyOn(component, 'clickedOutside');
|
||||
const button = fixture.debugElement.query(By.css('button[id="ignoreThisButton"]'));
|
||||
|
||||
button.nativeElement.click();
|
||||
|
||||
expect(clickedOutsideSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div class="container">
|
||||
<button type="button" id="exampleButton">Click me</button>
|
||||
<button type="button" id="ignoreThisButton">Click me</button>
|
||||
<div
|
||||
id="content"
|
||||
(docsClickOutside)="clickedOutside()"
|
||||
[docsClickOutsideIgnore]="docsClickOutsideIgnore"
|
||||
>
|
||||
Content
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
imports: [ClickOutside],
|
||||
standalone: true,
|
||||
})
|
||||
class ExampleComponent {
|
||||
docsClickOutsideIgnore = ['ignoreThisButton'];
|
||||
|
||||
clickedOutside(): void {}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*!
|
||||
* @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 {DOCUMENT} from '@angular/common';
|
||||
import {Directive, ElementRef, EventEmitter, Input, Output, inject} from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[docsClickOutside]',
|
||||
standalone: true,
|
||||
host: {
|
||||
'(document:click)': 'onClick($event)',
|
||||
},
|
||||
})
|
||||
export class ClickOutside {
|
||||
@Input('docsClickOutsideIgnore') public ignoredElementsIds: string[] = [];
|
||||
@Output('docsClickOutside') public clickOutside = new EventEmitter<void>();
|
||||
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly elementRef = inject(ElementRef<HTMLElement>);
|
||||
|
||||
onClick($event: PointerEvent): void {
|
||||
if (
|
||||
!this.elementRef.nativeElement.contains($event.target) &&
|
||||
!this.wasClickedOnIgnoredElement($event)
|
||||
) {
|
||||
this.clickOutside.emit();
|
||||
}
|
||||
}
|
||||
|
||||
private wasClickedOnIgnoredElement($event: PointerEvent): boolean {
|
||||
if (this.ignoredElementsIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.ignoredElementsIds.some((elementId) => {
|
||||
const element = this.document.getElementById(elementId);
|
||||
const target = $event.target as Node;
|
||||
const contains = element?.contains(target);
|
||||
return contains;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "external-link",
|
||||
srcs = [
|
||||
"external-link.directive.ts",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/directives:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//adev/shared-docs/providers",
|
||||
"//adev/shared-docs/utils",
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"//packages/router",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
":external-link",
|
||||
"//adev/shared-docs/providers",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
"//packages/platform-browser",
|
||||
"//packages/router",
|
||||
"//packages/router/testing",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
/*!
|
||||
* @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} from '@angular/core';
|
||||
import {ExternalLink} from './external-link.directive';
|
||||
import {RouterLink} from '@angular/router';
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {WINDOW} from '../../providers';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
|
||||
describe('ExternalLink', () => {
|
||||
let fixture: ComponentFixture<ExampleComponentWithLinks>;
|
||||
const fakeWindow = {
|
||||
location: {
|
||||
origin: window.origin,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ExampleComponentWithLinks, RouterTestingModule],
|
||||
providers: [
|
||||
{
|
||||
provide: WINDOW,
|
||||
useValue: fakeWindow,
|
||||
},
|
||||
],
|
||||
});
|
||||
fixture = TestBed.createComponent(ExampleComponentWithLinks);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should external link have target=_blank attribute', () => {
|
||||
const externalLink = fixture.debugElement.query(
|
||||
By.css('a[href="https://stackoverflow.com/questions/tagged/angular"]'),
|
||||
);
|
||||
expect(externalLink.attributes['target']).toEqual('_blank');
|
||||
});
|
||||
|
||||
it('should not internal link have target=_blank attribute', () => {
|
||||
const internalLink = fixture.debugElement.query(By.css('a[href="/roadmap"]'));
|
||||
expect(internalLink.attributes['target']).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not set target=_blank attribute external link when anchor has got noBlankForExternalLink attribute', () => {
|
||||
const externalLink = fixture.debugElement.query(
|
||||
By.css('a[href="https://github.com/angular/angular/issues"]'),
|
||||
);
|
||||
expect(externalLink.attributes['target']).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<a
|
||||
href="https://stackoverflow.com/questions/tagged/angular"
|
||||
title="Stack Overflow: where the community answers your technical Angular questions."
|
||||
>
|
||||
Stack Overflow
|
||||
</a>
|
||||
<a routerLink="/roadmap" title="Roadmap">Roadmap</a>
|
||||
<a
|
||||
href="https://github.com/angular/angular/issues"
|
||||
title="Post issues and suggestions on github"
|
||||
noBlankForExternalLink
|
||||
></a>
|
||||
`,
|
||||
imports: [ExternalLink, RouterLink],
|
||||
standalone: true,
|
||||
})
|
||||
class ExampleComponentWithLinks {}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*!
|
||||
* @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 {isPlatformBrowser} from '@angular/common';
|
||||
import {Directive, ElementRef, OnInit, PLATFORM_ID, inject} from '@angular/core';
|
||||
import {isExternalLink} from '../../utils/index';
|
||||
import {WINDOW} from '../../providers/index';
|
||||
|
||||
/**
|
||||
* The directive will set target of anchor elements to '_blank' for the external links.
|
||||
* We can opt-out this behavior by adding `noBlankForExternalLink` attribute to anchor element.
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'a[href]:not([noBlankForExternalLink])',
|
||||
host: {
|
||||
'[attr.target]': 'target',
|
||||
},
|
||||
standalone: true,
|
||||
})
|
||||
export class ExternalLink implements OnInit {
|
||||
private readonly anchor: ElementRef<HTMLAnchorElement> = inject(ElementRef);
|
||||
private readonly platformId = inject(PLATFORM_ID);
|
||||
private readonly window = inject(WINDOW);
|
||||
|
||||
target?: '_blank' | '_self' | '_parent' | '_top' | '';
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setAnchorTarget();
|
||||
}
|
||||
|
||||
private setAnchorTarget(): void {
|
||||
if (!isPlatformBrowser(this.platformId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isExternalLink(this.anchor.nativeElement.href, this.window.location.origin)) {
|
||||
this.target = '_blank';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/*!
|
||||
* @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
|
||||
*/
|
||||
|
||||
export * from './click-outside/click-outside.directive';
|
||||
export * from './external-link/external-link.directive';
|
||||
@@ -0,0 +1,18 @@
|
||||
load("//tools:defaults.bzl", "ng_module")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "search-item",
|
||||
srcs = [
|
||||
"search-item.directive.ts",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/directives:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//adev/shared-docs/interfaces",
|
||||
"//packages/core",
|
||||
"@npm//@angular/cdk",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
/*!
|
||||
* @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 {Directive, ElementRef, Input, inject, signal} from '@angular/core';
|
||||
import {Highlightable} from '@angular/cdk/a11y';
|
||||
import {SearchResult} from '../../interfaces/search-results';
|
||||
|
||||
@Directive({
|
||||
selector: '[docsSearchItem]',
|
||||
standalone: true,
|
||||
host: {
|
||||
'[class.active]': 'isActive',
|
||||
},
|
||||
})
|
||||
export class SearchItem implements Highlightable {
|
||||
@Input() item?: SearchResult;
|
||||
@Input() disabled = false;
|
||||
|
||||
private readonly elementRef = inject(ElementRef<HTMLLIElement>);
|
||||
|
||||
private _isActive = signal(false);
|
||||
|
||||
protected get isActive() {
|
||||
return this._isActive();
|
||||
}
|
||||
|
||||
setActiveStyles(): void {
|
||||
this._isActive.set(true);
|
||||
}
|
||||
|
||||
setInactiveStyles(): void {
|
||||
this._isActive.set(false);
|
||||
}
|
||||
|
||||
getLabel(): string {
|
||||
if (!this.item?.hierarchy) {
|
||||
return '';
|
||||
}
|
||||
const {hierarchy} = this.item;
|
||||
return `${hierarchy.lvl0}${hierarchy.lvl1}${hierarchy.lvl2}`;
|
||||
}
|
||||
|
||||
scrollIntoView(): void {
|
||||
this.elementRef?.nativeElement.scrollIntoView({block: 'nearest'});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
filegroup(
|
||||
name = "icons",
|
||||
srcs = glob([
|
||||
"*.svg",
|
||||
]),
|
||||
visibility = [
|
||||
"//adev/shared-docs:__pkg__",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="10" height="7" viewBox="0 0 10 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M-5.09966e-08 1.83341L1.16667 0.666748L5 4.50008L8.83333 0.666748L10 1.83341L5 6.83341L-5.09966e-08 1.83341Z" fill="#746E7C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 239 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.59948 19.0428C7.59948 18.8069 7.59118 18.182 7.58656 17.3526C4.89071 17.9526 4.32164 16.0201 4.32164 16.0201C3.88087 14.8718 3.24533 14.5663 3.24533 14.5663C2.36518 13.9492 3.31179 13.9621 3.31179 13.9621C4.28471 14.0323 4.79656 14.9868 4.79656 14.9868C5.66102 16.5052 7.06456 16.0672 7.61748 15.8125C7.70564 15.17 7.95579 14.732 8.23271 14.4837C6.08056 14.2331 3.81764 13.3801 3.81764 9.57199C3.81764 8.48737 4.19564 7.6003 4.81548 6.90522C4.71625 6.65414 4.38302 5.64384 4.91056 4.27537C4.91056 4.27537 5.72471 4.00814 7.57594 5.29399C8.34856 5.07384 9.17795 4.96307 10.0027 4.95937C10.8256 4.96307 11.6546 5.07384 12.429 5.29399C14.2793 4.00814 15.0921 4.27537 15.0921 4.27537C15.621 5.64337 15.2883 6.65368 15.1881 6.90522C15.8093 7.6003 16.1841 8.48737 16.1841 9.57199C16.1841 13.3898 13.9179 14.2298 11.7589 14.4758C12.1073 14.7828 12.4166 15.3892 12.4166 16.3165C12.4166 17.6452 12.4041 18.7174 12.4041 19.0428C12.4041 19.3091 12.579 19.6178 13.071 19.5205C16.9193 18.2041 19.6936 14.4814 19.6936 10.0926C19.6936 4.60353 15.3538 0.154297 10.0009 0.154297C4.64887 0.154297 0.309021 4.60353 0.309021 10.0926C0.309483 14.4828 3.08656 18.2078 6.9381 19.5218C7.42225 19.6128 7.59948 19.3058 7.59948 19.0428Z" fill="#A39FA9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.04145 0.04432l6.56351 8.77603L0 15.95564h1.48651l5.78263-6.24705 4.6722 6.24705h5.05865l-6.9328-9.26967L16.21504.04432h-1.48651l-5.32552 5.75341L5.1001.04432H.04145Zm2.18602 1.09497h2.32396l10.26221 13.72122h-2.32396L2.22747 1.13928Z" fill="#A39FA9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 367 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="15" viewBox="0 0 20 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.7556 2.94783C18.5803 1.98018 17.745 1.27549 16.7756 1.05549C15.325 0.747832 12.6403 0.527832 9.73563 0.527832C6.83266 0.527832 4.105 0.747832 2.65266 1.05549C1.685 1.27549 0.847969 1.93549 0.672656 2.94783C0.495625 4.04783 0.320312 5.58783 0.320312 7.56783C0.320312 9.54783 0.495625 11.0878 0.715625 12.1878C0.892656 13.1555 1.72797 13.8602 2.69563 14.0802C4.23563 14.3878 6.87563 14.6078 9.78031 14.6078C12.685 14.6078 15.325 14.3878 16.865 14.0802C17.8327 13.8602 18.668 13.2002 18.845 12.1878C19.0203 11.0878 19.2403 9.50314 19.285 7.56783C19.1956 5.58783 18.9756 4.04783 18.7556 2.94783ZM7.36031 10.6478V4.48783L12.728 7.56783L7.36031 10.6478Z" fill="#A39FA9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 782 B |
@@ -0,0 +1,9 @@
|
||||
load("//adev/shared-docs/pipeline:_guides.bzl", _generate_guides = "generate_guides")
|
||||
load("//adev/shared-docs/pipeline:_stackblitz.bzl", _generate_stackblitz = "generate_stackblitz")
|
||||
load("//adev/shared-docs/pipeline:_playground.bzl", _generate_playground = "generate_playground")
|
||||
load("//adev/shared-docs/pipeline:_tutorial.bzl", _generate_tutorial = "generate_tutorial")
|
||||
|
||||
generate_guides = _generate_guides
|
||||
generate_stackblitz = _generate_stackblitz
|
||||
generate_playground = _generate_playground
|
||||
generate_tutorial = _generate_tutorial
|
||||
@@ -0,0 +1,20 @@
|
||||
/*!
|
||||
* @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
|
||||
*/
|
||||
|
||||
/*
|
||||
* Public API Surface of shared
|
||||
*/
|
||||
|
||||
export * from './directives/index';
|
||||
export * from './components/index';
|
||||
export * from './constants/index';
|
||||
export * from './interfaces/index';
|
||||
export * from './providers/index';
|
||||
export * from './services/index';
|
||||
export * from './utils/index';
|
||||
export * from './testing/index';
|
||||
@@ -0,0 +1,32 @@
|
||||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ts_library(
|
||||
name = "interfaces",
|
||||
srcs = [
|
||||
"index.ts",
|
||||
],
|
||||
visibility = ["//adev/shared-docs:__subpackages__"],
|
||||
deps = [
|
||||
":lib",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "lib",
|
||||
srcs = glob(
|
||||
[
|
||||
"**/*.ts",
|
||||
],
|
||||
exclude = [
|
||||
"index.ts",
|
||||
"**/*.spec.ts",
|
||||
],
|
||||
),
|
||||
deps = [
|
||||
"//packages/core",
|
||||
"@npm//@types/node",
|
||||
"@npm//@webcontainer/api",
|
||||
],
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user