Local code (#876)

This PR fixes #1 by adding an initial feature to include local code to
our repo, which will then be opened by StackBlitz. We can add other
providers in the future or even change all of our articles to use the
same provider if they'd like
This commit is contained in:
Corbin Crutchley
2023-11-12 20:30:44 -07:00
committed by GitHub
58 changed files with 1929 additions and 10 deletions

View File

@@ -3,3 +3,6 @@ node_modules
package-lock.json
*.md
*.min.js
content/blog/**/*
public/content/blog/**/*

View File

@@ -0,0 +1,100 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"demo": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/demo",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "demo:build"
},
"configurations": {
"production": {
"browserTarget": "demo:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "demo:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [
"styles.css"
],
"scripts": [],
"assets": [
"src/favicon.ico",
"src/assets"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "demo"
}

View File

@@ -0,0 +1,48 @@
{
"name": "angular",
"version": "0.0.0",
"private": true,
"dependencies": {
"@angular/animations": "9.1.0",
"@angular/common": "^9.1.0",
"@angular/compiler": "^9.1.0",
"@angular/core": "^9.1.0",
"@angular/forms": "^9.1.0",
"@angular/platform-browser": "^9.1.0",
"@angular/platform-browser-dynamic": "^9.1.0",
"@angular/router": "^9.1.0",
"core-js": "^3.6.4",
"rxjs": "^6.5.4",
"tslib": "^1.10.0",
"zone.js": "^0.10.3"
},
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.10.0",
"@angular/cli": "~7.0.2",
"@angular/compiler-cli": "~7.0.0",
"@angular/language-service": "~7.0.0",
"@types/node": "~8.9.4",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"codelyzer": "~4.5.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~3.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.1.1"
}
}

View File

@@ -0,0 +1 @@
<app-example-input placeholder="What's your favorite animal?"></app-example-input>

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
name = 'Angular';
}

View File

@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HelloComponent } from './hello.component';
import { ExampleInputComponent } from './example-input/example-input.component';
@NgModule({
imports: [ BrowserModule, FormsModule ],
declarations: [ AppComponent, HelloComponent, ExampleInputComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

View File

@@ -0,0 +1,67 @@
.inputContainer {
display: flex;
flex-direction: column;
position: relative;
}
.inputLabel {
font-size: 1.2rem;
margin-top: 0;
margin-bottom: 0.4rem;
}
.inputInput {
color: #132a58;
padding: 1rem 0.5rem;
flex-grow: 1;
display: block;
border: 1px solid rgba(0, 51, 153, 0.2);
border-radius: 8px;
background: transparent;
}
.hiddenMessage.hideTheMessage {
opacity: 0;
height: 0;
}
.hiddenMessage {
animation: shake 2s ease-in-out;
animation-iteration-count:infinite;
opacity: 1;
transition: opacity 200ms ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.hiddenMessage {
animation: none;
transition: none;
}
}
@keyframes shake {
10%, 90% {
transform: translate3d(-2px, -2px, 0);
}
20%, 80% {
transform: translate3d(2px, 0px, 0);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(4px, 4px, 0);
}
}
.visually-hidden {
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
white-space: nowrap; /* added line */
}

View File

@@ -0,0 +1,38 @@
import { Component, Input } from "@angular/core";
@Component({
selector: "app-example-input",
template: `
<label class="inputContainer">
<span class="inputLabel">{{ placeholder }}</span>
<input
placeholder=""
class="inputInput"
[(ngModel)]="value"
/>
</label>
<p
class="hiddenMessage"
[class.hideTheMessage]="!isSecretValue"
aria-hidden="true"
>
You unlocked the secret unicorn rave!<span>🦄🦄🦄</span>
</p>
<p aria-live="assertive" class="visually-hidden">
{{
isSecretValue
? "You discovered the secret unicorn rave! They're all having a party now that you summoned them by typing their name"
: ""
}}
</p>
`,
styleUrls: ["./example-input.component.css"]
})
export class ExampleInputComponent {
@Input() placeholder: string;
value: any = "";
get isSecretValue() {
return /unicorns/.exec(this.value.toLowerCase());
}
}

View File

@@ -0,0 +1,10 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'hello',
template: `<h1>Hello {{name}}!</h1>`,
styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent {
@Input() name: string;
}

View File

@@ -0,0 +1,8 @@
<html>
<head>
<title>Angular App</title>
</head>
<body>
<my-app>loading</my-app>
</body>
</html>

View File

@@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/my-app'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

View File

@@ -0,0 +1,16 @@
import './polyfills';
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {
// Ensure Angular destroys itself on hot reloads.
if (window['ngRef']) {
window['ngRef'].destroy();
}
window['ngRef'] = ref;
// Otherwise, log the boot error
}).catch(err => console.error(err));

View File

@@ -0,0 +1,71 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/set';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** IE10 and IE11 requires the following to support `@angular/animation`. */
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/** Evergreen browsers require these. **/
// import 'core-js/es6/reflect';
// import 'core-js/es7/reflect';
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/***************************************************************************************************
* Zone JS is required by Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
/**
* Date, currency, decimal and percent pipes.
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
*/
// import 'intl'; // Run `npm install --save intl`.

View File

@@ -0,0 +1 @@
/* Add application styles & imports to this file! */

View File

@@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"types": []
},
"files": [
"main.ts",
"polyfills.ts"
],
"include": [
"**/*.d.ts"
]
}

View File

@@ -0,0 +1,18 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"test.ts",
"polyfills.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@@ -0,0 +1,23 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"lib": [
"es2018",
"dom"
]
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true
}
}

View File

@@ -0,0 +1,100 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"demo": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/demo",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "demo:build"
},
"configurations": {
"production": {
"browserTarget": "demo:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "demo:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [
"styles.css"
],
"scripts": [],
"assets": [
"src/favicon.ico",
"src/assets"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "demo"
}

View File

@@ -0,0 +1,47 @@
{
"name": "angular",
"version": "0.0.0",
"private": true,
"dependencies": {
"@angular/animations": "9.1.9",
"@angular/common": "^9.1.4",
"@angular/compiler": "9.1.9",
"@angular/core": "^9.1.4",
"@angular/forms": "^9.1.4",
"@angular/platform-browser": "^9.1.4",
"@angular/platform-browser-dynamic": "^9.1.9",
"@angular/router": "^9.1.4",
"rxjs": "^6.5.5",
"tslib": "^1.11.1",
"zone.js": "^0.10.3"
},
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.10.0",
"@angular/cli": "9.0.0-rc.7",
"@angular/compiler-cli": "9.0.0-rc.7",
"@angular/language-service": "~7.0.0",
"@types/node": "~8.9.4",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"codelyzer": "~4.5.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~3.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.1.1"
}
}

View File

@@ -0,0 +1,3 @@
<h1>Form Control</h1>
<app-example-input placeholder="What's your favorite animal?" [formControl]="control"></app-example-input>
<p>The value of the input is: {{control.value}}</p>

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
import {FormControl, Validators} from '@angular/forms';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
control = new FormControl('', Validators.required);
}

View File

@@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { ExampleInputComponent } from './example-input/example-input.component';
@NgModule({
imports: [ ReactiveFormsModule, FormsModule, BrowserModule ],
declarations: [ AppComponent, ExampleInputComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

View File

@@ -0,0 +1,75 @@
.inputContainer {
display: flex;
flex-direction: column;
position: relative;
}
.inputLabel {
font-size: 1.2rem;
margin-top: 0;
margin-bottom: 0.4rem;
}
.inputInput {
color: #132a58;
padding: 1rem 0.5rem;
flex-grow: 1;
display: block;
border: 1px solid rgba(0, 51, 153, 0.2);
border-radius: 8px;
background: transparent;
}
.redtext {
color: red;
}
.redoutline {
border-color: red;
}
.hiddenMessage.hideTheMessage {
opacity: 0;
height: 0;
}
.hiddenMessage {
animation: shake 2s ease-in-out;
animation-iteration-count:infinite;
opacity: 1;
transition: opacity 200ms ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.hiddenMessage {
animation: none;
transition: none;
}
}
@keyframes shake {
10%, 90% {
transform: translate3d(-2px, -2px, 0);
}
20%, 80% {
transform: translate3d(2px, 0px, 0);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(4px, 4px, 0);
}
}
.visually-hidden {
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
white-space: nowrap; /* added line */
}

View File

@@ -0,0 +1,20 @@
<label class="inputContainer">
<span class="inputLabel" [class.redtext]="errors"
>{{ placeholder }}</span>
<input
placeholder=""
class="inputInput"
[class.redoutline]="errors"
[disabled]="disabled"
[(ngModel)]="value"
(change)="onChange($event)"
(blur)="onTouched()"
/>
</label>
<p class="hiddenMessage" [class.hideTheMessage]="!isSecretValue" aria-hidden="true">
You unlocked the secret unicorn
rave!<span>🦄🦄🦄</span></p>
<p aria-live="assertive" class="visually-hidden">
{{isSecretValue ? "You discovered the secret unicorn rave! They're all having a party now that you summoned them by typing their name" : ""}}
</p>
<p aria-live="assertive">You have the following errors: {{errors | json}}</p>

View File

@@ -0,0 +1,114 @@
import {
Component,
Input,
ChangeDetectorRef,
Optional,
Self,
OnInit
} from "@angular/core";
import { ControlValueAccessor, NgControl } from "@angular/forms";
function coerceBooleanProperty(value: any): boolean {
return value != null && `${value}` !== "false";
}
@Component({
selector: "app-example-input",
templateUrl: "./example-input.component.html",
styleUrls: ["./example-input.component.css"]
})
export class ExampleInputComponent
implements OnInit, ControlValueAccessor {
constructor(
@Optional() @Self() public ngControl: NgControl,
private _changeDetector: ChangeDetectorRef
) {
if (ngControl != null) {
// Setting the value accessor directly (instead of using
// the providers) to avoid running into a circular import.
ngControl.valueAccessor = this;
}
}
@Input() placeholder: string;
private _value: any = "";
private _disabled: boolean = false;
/** The method to be called in order to update ngModel */
_controlValueAccessorChangeFn: (value: any) => void = () => {};
/**
* onTouch function registered via registerOnTouch (ControlValueAccessor).
*/
onTouched: () => any = () => {};
get isSecretValue() {
return /unicorns/.exec(this._value);
}
ngOnInit() {
const control = this.ngControl && this.ngControl.control;
if (control) {
console.log("ngOnInit", control);
// FormControl should be available here
}
}
get errors() {
const control = this.ngControl && this.ngControl.control;
if (control) {
return control.touched && control.errors;
}
return null;
}
/**
* Getters and setters for internal values
*/
@Input()
get value(): any {
return this._value;
}
set value(newValue: any) {
if (this._value !== newValue) {
// Set this before proceeding to ensure no circular loop occurs with selection.
this._value = newValue;
}
}
@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(value) {
this._disabled = coerceBooleanProperty(value);
}
onChange(event: any) {
this._controlValueAccessorChangeFn(event.target.value);
}
/**
* Methods from the ControlValueAccessor
*/
writeValue(value: any) {
this.value = value;
this._changeDetector.markForCheck();
}
registerOnChange(fn: (value: any) => void) {
this._controlValueAccessorChangeFn = fn;
}
registerOnTouched(fn: any) {
this.onTouched = fn;
}
// Optional
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
this._changeDetector.markForCheck();
}
}

View File

@@ -0,0 +1,8 @@
<html>
<head>
<title>Angular App</title>
</head>
<body>
<my-app>loading</my-app>
</body>
</html>

View File

@@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/my-app'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

View File

@@ -0,0 +1,16 @@
import './polyfills';
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {
// Ensure Angular destroys itself on hot reloads.
if (window['ngRef']) {
window['ngRef'].destroy();
}
window['ngRef'] = ref;
// Otherwise, log the boot error
}).catch(err => console.error(err));

View File

@@ -0,0 +1,71 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/set';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** IE10 and IE11 requires the following to support `@angular/animation`. */
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/** Evergreen browsers require these. **/
// import 'core-js/es6/reflect';
// import 'core-js/es7/reflect';
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/***************************************************************************************************
* Zone JS is required by Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
/**
* Date, currency, decimal and percent pipes.
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
*/
// import 'intl'; // Run `npm install --save intl`.

View File

@@ -0,0 +1 @@
/* Add application styles & imports to this file! */

View File

@@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"types": []
},
"files": [
"main.ts",
"polyfills.ts"
],
"include": [
"**/*.d.ts"
]
}

View File

@@ -0,0 +1,18 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"test.ts",
"polyfills.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@@ -0,0 +1,27 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
]
},
"angularCompilerOptions": {
"enableIvy": true,
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true
}
}

View File

@@ -0,0 +1,100 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"demo": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/demo",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "demo:build"
},
"configurations": {
"production": {
"browserTarget": "demo:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "demo:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [
"styles.css"
],
"scripts": [],
"assets": [
"src/favicon.ico",
"src/assets"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "demo"
}

View File

@@ -0,0 +1,47 @@
{
"name": "angular",
"version": "0.0.0",
"private": true,
"dependencies": {
"@angular/animations": "9.1.9",
"@angular/common": "^9.1.4",
"@angular/compiler": "9.1.9",
"@angular/core": "^9.1.4",
"@angular/forms": "^9.1.4",
"@angular/platform-browser": "^9.1.4",
"@angular/platform-browser-dynamic": "^9.1.9",
"@angular/router": "^9.1.4",
"rxjs": "^6.5.5",
"tslib": "^1.11.1",
"zone.js": "^0.10.3"
},
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.10.0",
"@angular/cli": "9.0.0-rc.7",
"@angular/compiler-cli": "9.0.0-rc.7",
"@angular/language-service": "~7.0.0",
"@types/node": "~8.9.4",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"codelyzer": "~4.5.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~3.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.1.1"
}
}

View File

@@ -0,0 +1,6 @@
<h1>Form Control</h1>
<app-example-input placeholder="What's your favorite animal?" [formControl]="control"></app-example-input>
<p>The value of the input is: {{control.value}}</p>
<h1>ngModel</h1>
<app-example-input placeholder="What's your favorite animal?" [(ngModel)]="modelValue"></app-example-input>
<p>The value of the input is: {{modelValue}}</p>

View File

@@ -0,0 +1,12 @@
import { Component, VERSION } from '@angular/core';
import {FormControl} from '@angular/forms';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
control = new FormControl('');
modelValue = "";
}

View File

@@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { ExampleInputComponent } from './example-input/example-input.component';
@NgModule({
imports: [ ReactiveFormsModule, FormsModule, BrowserModule ],
declarations: [ AppComponent, ExampleInputComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

View File

@@ -0,0 +1,67 @@
.inputContainer {
display: flex;
flex-direction: column;
position: relative;
}
.inputLabel {
font-size: 1.2rem;
margin-top: 0;
margin-bottom: 0.4rem;
}
.inputInput {
color: #132a58;
padding: 1rem 0.5rem;
flex-grow: 1;
display: block;
border: 1px solid rgba(0, 51, 153, 0.2);
border-radius: 8px;
background: transparent;
}
.hiddenMessage.hideTheMessage {
opacity: 0;
height: 0;
}
.hiddenMessage {
animation: shake 2s ease-in-out;
animation-iteration-count:infinite;
opacity: 1;
transition: opacity 200ms ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.hiddenMessage {
animation: none;
transition: none;
}
}
@keyframes shake {
10%, 90% {
transform: translate3d(-2px, -2px, 0);
}
20%, 80% {
transform: translate3d(2px, 0px, 0);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(4px, 4px, 0);
}
}
.visually-hidden {
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
white-space: nowrap; /* added line */
}

View File

@@ -0,0 +1,17 @@
<label class="inputContainer">
<span class="inputLabel">{{ placeholder }}</span>
<input
placeholder=""
class="inputInput"
[disabled]="disabled"
[(ngModel)]="value"
(change)="onChange($event)"
(blur)="onTouched()"
/>
</label>
<p class="hiddenMessage" [class.hideTheMessage]="!isSecretValue" aria-hidden="true">
You unlocked the secret unicorn
rave!<span>🦄🦄🦄</span></p>
<p aria-live="assertive" class="visually-hidden">
{{isSecretValue ? "You discovered the secret unicorn rave! They're all having a party now that you summoned them by typing their name" : ""}}
</p>

View File

@@ -0,0 +1,96 @@
import { Component, forwardRef, Input, ChangeDetectorRef } from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
function coerceBooleanProperty(value: any): boolean {
return value != null && `${value}` !== 'false';
}
/**
* Provider Expression that allows your component to register as a ControlValueAccessor. This
* allows it to support [(ngModel)] and ngControl.
*/
export const EXAMPLE_CONTROL_VALUE_ACCESSOR: any = {
/**
* Used to provide a `ControlValueAccessor` for form controls.
*/
provide: NG_VALUE_ACCESSOR,
/**
* Allows to refer to references which are not yet defined.
* This is because it's needed to `providers` in the component but references
* the component itself. Handles circular dependency issues
*/
useExisting: forwardRef(() => ExampleInputComponent),
multi: true
};
@Component({
selector: 'app-example-input',
templateUrl: './example-input.component.html',
styleUrls: ['./example-input.component.css'],
providers: [EXAMPLE_CONTROL_VALUE_ACCESSOR]
})
export class ExampleInputComponent implements ControlValueAccessor {
@Input() placeholder: string;
private _value: any = "";
private _disabled: boolean = false;
/** The method to be called in order to update ngModel */
_controlValueAccessorChangeFn: (value: any) => void = () => {};
/**
* onTouch function registered via registerOnTouch (ControlValueAccessor).
*/
onTouched: () => any = () => {};
constructor(private _changeDetector: ChangeDetectorRef) { }
get isSecretValue() {
return /unicorns/.exec(this._value);
}
/**
* Getters and setters for internal values
*/
@Input()
get value(): any { return this._value; }
set value(newValue: any) {
if (this._value !== newValue) {
// Set this before proceeding to ensure no circular loop occurs with selection.
this._value = newValue;
}
}
@Input()
get disabled(): boolean { return this._disabled; }
set disabled(value) {
this._disabled = coerceBooleanProperty(value);
}
onChange(event: any) {
this._controlValueAccessorChangeFn(event.target.value);
}
/**
* Methods from the ControlValueAccessor
*/
writeValue(value: any) {
this.value = value;
this._changeDetector.markForCheck();
}
registerOnChange(fn: (value: any) => void) {
this._controlValueAccessorChangeFn = fn;
}
registerOnTouched(fn: any) {
this.onTouched = fn;
}
// Optional
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
this._changeDetector.markForCheck();
}
}

View File

@@ -0,0 +1,8 @@
<html>
<head>
<title>Angular App</title>
</head>
<body>
<my-app>loading</my-app>
</body>
</html>

View File

@@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/my-app'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

View File

@@ -0,0 +1,16 @@
import './polyfills';
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {
// Ensure Angular destroys itself on hot reloads.
if (window['ngRef']) {
window['ngRef'].destroy();
}
window['ngRef'] = ref;
// Otherwise, log the boot error
}).catch(err => console.error(err));

View File

@@ -0,0 +1,71 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/set';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** IE10 and IE11 requires the following to support `@angular/animation`. */
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/** Evergreen browsers require these. **/
// import 'core-js/es6/reflect';
// import 'core-js/es7/reflect';
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/***************************************************************************************************
* Zone JS is required by Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
/**
* Date, currency, decimal and percent pipes.
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
*/
// import 'intl'; // Run `npm install --save intl`.

View File

@@ -0,0 +1 @@
/* Add application styles & imports to this file! */

View File

@@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"types": []
},
"files": [
"main.ts",
"polyfills.ts"
],
"include": [
"**/*.d.ts"
]
}

View File

@@ -0,0 +1,18 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"test.ts",
"polyfills.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@@ -0,0 +1,27 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
]
},
"angularCompilerOptions": {
"enableIvy": true,
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true
}
}

View File

@@ -70,7 +70,7 @@ export class ExampleInputComponent {
With only a bit of CSS, we have a visually appealing, A11Y friendly, and quirky input component. Look, it even wiggles the unicorns!
<iframe src="https://stackblitz.com/edit/angular-unicorns-text-input?embed=1&file=src/app/app.component.ts" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
<iframe data-frame-title="Angular Unicorns Text Input - StackBlitz" src="uu-code:./angular-unicorns-text-input?embed=1&file=src/app/app.component.ts" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Now, this component is far from feature complete. There's no way to `disable` the input, there's no way to extract data out from the typed input, there's not a lot of functionality you'd typically expect to see from an input component. Let's change that.
@@ -303,7 +303,7 @@ Finally, you can pass these options to `ngModel` and `formControl` (or even `for
If done properly, you should see something like this:
<iframe src="https://stackblitz.com/edit/angular-value-accessor-example?embed=1&file=src/app/app.component.ts" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
<iframe data-frame-title="Angular Value Accessor Example - StackBlitz" src="uu-code:./angular-value-accessor-example?embed=1&file=src/app/app.component.ts" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
# Form Control Classes
@@ -407,7 +407,7 @@ export class AppComponent {
}
```
<iframe src="https://stackblitz.com/edit/angular-value-accessor-dep-inject?embed=1&file=src/app/app.component.ts" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
<iframe data-frame-title="Angular Value Accessor Dep Inject - StackBlitz" src="uu-code:./angular-value-accessor-dep-inject?embed=1&file=src/app/app.component.ts" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Not only do you have [a wide range of Angular-built validators at your disposal](https://angular.io/api/forms/Validators), but you're even able to [make your own validator](https://angular.io/api/forms/Validator)!

272
package-lock.json generated
View File

@@ -29,6 +29,7 @@
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/preact": "^3.2.3",
"@testing-library/user-event": "^14.4.3",
"@types/git-branch": "^2.0.5",
"@types/hast": "^3.0.0",
"@types/html-escaper": "^3.0.0",
"@types/jest": "^29.5.2",
@@ -51,6 +52,7 @@
"eslint-plugin-jsx-a11y": "^6.7.1",
"feed": "^4.2.2",
"fuse.js": "^6.6.2",
"git-branch": "^2.0.1",
"gray-matter": "^4.0.3",
"hast-util-from-html": "^2.0.1",
"hast-util-has-property": "^3.0.0",
@@ -7111,6 +7113,12 @@
"@types/estree": "*"
}
},
"node_modules/@types/git-branch": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/git-branch/-/git-branch-2.0.5.tgz",
"integrity": "sha512-gNZyvixgeXQLQnFkMUAO47sBnQBzJ/pPMJNuOZUD4bvh3O6taXjpEbugjz2qBjNz4ZFH2mVQ42WJzImEiio1Qg==",
"dev": true
},
"node_modules/@types/graceful-fs": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz",
@@ -11524,6 +11532,15 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/detect-file": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
"integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/detect-libc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
@@ -12872,6 +12889,18 @@
"node": ">=6"
}
},
"node_modules/expand-tilde": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
"integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==",
"dev": true,
"dependencies": {
"homedir-polyfill": "^1.0.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/expect": {
"version": "29.6.1",
"resolved": "https://registry.npmjs.org/expect/-/expect-29.6.1.tgz",
@@ -13340,6 +13369,161 @@
"pkg-dir": "^4.2.0"
}
},
"node_modules/findup-sync": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz",
"integrity": "sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==",
"dev": true,
"dependencies": {
"detect-file": "^1.0.0",
"is-glob": "^3.1.0",
"micromatch": "^3.0.4",
"resolve-dir": "^1.0.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/findup-sync/node_modules/braces": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
"integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
"dev": true,
"dependencies": {
"arr-flatten": "^1.1.0",
"array-unique": "^0.3.2",
"extend-shallow": "^2.0.1",
"fill-range": "^4.0.0",
"isobject": "^3.0.1",
"repeat-element": "^1.1.2",
"snapdragon": "^0.8.1",
"snapdragon-node": "^2.0.1",
"split-string": "^3.0.2",
"to-regex": "^3.0.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/findup-sync/node_modules/fill-range": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
"integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==",
"dev": true,
"dependencies": {
"extend-shallow": "^2.0.1",
"is-number": "^3.0.0",
"repeat-string": "^1.6.1",
"to-regex-range": "^2.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/findup-sync/node_modules/is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
"dev": true
},
"node_modules/findup-sync/node_modules/is-extendable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
"integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
"dev": true,
"dependencies": {
"is-plain-object": "^2.0.4"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/findup-sync/node_modules/is-glob": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
"integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/findup-sync/node_modules/is-number": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
"integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==",
"dev": true,
"dependencies": {
"kind-of": "^3.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/findup-sync/node_modules/is-number/node_modules/kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
"dev": true,
"dependencies": {
"is-buffer": "^1.1.5"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/findup-sync/node_modules/micromatch": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
"integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
"dev": true,
"dependencies": {
"arr-diff": "^4.0.0",
"array-unique": "^0.3.2",
"braces": "^2.3.1",
"define-property": "^2.0.2",
"extend-shallow": "^3.0.2",
"extglob": "^2.0.4",
"fragment-cache": "^0.2.1",
"kind-of": "^6.0.2",
"nanomatch": "^1.2.9",
"object.pick": "^1.3.0",
"regex-not": "^1.0.0",
"snapdragon": "^0.8.1",
"to-regex": "^3.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/findup-sync/node_modules/micromatch/node_modules/extend-shallow": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
"integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==",
"dev": true,
"dependencies": {
"assign-symbols": "^1.0.0",
"is-extendable": "^1.0.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/findup-sync/node_modules/to-regex-range": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
"integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==",
"dev": true,
"dependencies": {
"is-number": "^3.0.0",
"repeat-string": "^1.6.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/flat-cache": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@@ -13699,6 +13883,18 @@
"node": ">=0.10.0"
}
},
"node_modules/git-branch": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/git-branch/-/git-branch-2.0.1.tgz",
"integrity": "sha512-jMCT1kjXvsUdZKQd2p8E1uZhKsIuR1pnHgcDYQpQiXBtzE9cmYGvOcCSGqqi58x0B9CPS0lUSu/yti866est8g==",
"dev": true,
"dependencies": {
"findup-sync": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@@ -13740,6 +13936,48 @@
"node": ">= 6"
}
},
"node_modules/global-modules": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
"integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
"dev": true,
"dependencies": {
"global-prefix": "^1.0.1",
"is-windows": "^1.0.1",
"resolve-dir": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/global-prefix": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
"integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==",
"dev": true,
"dependencies": {
"expand-tilde": "^2.0.2",
"homedir-polyfill": "^1.0.1",
"ini": "^1.3.4",
"is-windows": "^1.0.1",
"which": "^1.2.14"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/global-prefix/node_modules/which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"which": "bin/which"
}
},
"node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -14413,6 +14651,18 @@
"integrity": "sha512-tWCK4biJ6hcLqTviLXVR9DTRfYGQMXEIUj3gwJ2rZ5wO/at3XtkI4g8mCvFdUF9l1KMBNCfmNAdnahm1cgavQA==",
"dev": true
},
"node_modules/homedir-polyfill": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
"integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
"dev": true,
"dependencies": {
"parse-passwd": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@@ -23802,6 +24052,15 @@
"node": ">=6"
}
},
"node_modules/parse-passwd": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
"integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/parse5": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@@ -25386,6 +25645,19 @@
"node": ">=8"
}
},
"node_modules/resolve-dir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
"integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==",
"dev": true,
"dependencies": {
"expand-tilde": "^2.0.0",
"global-modules": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",

View File

@@ -53,6 +53,7 @@
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/preact": "^3.2.3",
"@testing-library/user-event": "^14.4.3",
"@types/git-branch": "^2.0.5",
"@types/hast": "^3.0.0",
"@types/html-escaper": "^3.0.0",
"@types/jest": "^29.5.2",
@@ -75,6 +76,7 @@
"eslint-plugin-jsx-a11y": "^6.7.1",
"feed": "^4.2.2",
"fuse.js": "^6.6.2",
"git-branch": "^2.0.1",
"gray-matter": "^4.0.3",
"hast-util-from-html": "^2.0.1",
"hast-util-has-property": "^3.0.0",

View File

@@ -1,4 +1,5 @@
import { Root, Element } from "hast";
import { VFile } from "vfile";
import { Plugin } from "unified";
import { visit } from "unist-util-visit";
@@ -13,7 +14,9 @@ import type { GetPictureResult } from "@astrojs/image/dist/lib/get-picture";
import probe from "probe-image-size";
import { IFramePlaceholder } from "./iframe-placeholder";
interface RehypeUnicornIFrameClickToRunProps {}
interface RehypeUnicornIFrameClickToRunProps {
srcReplacements?: Array<(val: string, root: VFile) => string>;
}
// default icon, used if a frame's favicon cannot be resolved
let defaultPageIcon: Promise<GetPictureResult>;
@@ -120,7 +123,7 @@ type PageInfo = {
icon: GetPictureResult;
};
async function fetchPageInfo(src: string): Promise<PageInfo | null> {
export async function fetchPageInfo(src: string): Promise<PageInfo | null> {
// fetch origin url, catch any connection timeout errors
const url = new URL(src);
url.search = ""; // remove any search params
@@ -143,8 +146,8 @@ async function fetchPageInfo(src: string): Promise<PageInfo | null> {
export const rehypeUnicornIFrameClickToRun: Plugin<
[RehypeUnicornIFrameClickToRunProps | never],
Root
> = () => {
return async (tree) => {
> = ({ srcReplacements = [], ...props }) => {
return async (tree, file) => {
const iframeNodes: Element[] = [];
visit(tree, (node: Element) => {
if (node.tagName === "iframe") {
@@ -165,6 +168,10 @@ export const rehypeUnicornIFrameClickToRun: Plugin<
...propsToPreserve
} = iframeNode.properties;
for (const replacement of srcReplacements) {
src = replacement(src as string, file);
}
width = width ?? EMBED_SIZE.w;
height = height ?? EMBED_SIZE.h;
const info: PageInfo = (await fetchPageInfo(

View File

@@ -1,6 +1,9 @@
import { Plugin } from "unified";
import rehypeSlug from "rehype-slug-custom-id";
import rehypeRaw from "rehype-raw";
import { VFile } from "vfile";
import { resolve, relative, dirname } from "path";
import * as branch from "git-branch";
import { rehypeTabs } from "./tabs/rehype-transform";
import { rehypeTooltips } from "./tooltips/rehype-transform";
import { rehypeHints } from "./hints/rehype-transform";
@@ -20,6 +23,7 @@ import { rehypeHeaderText } from "./rehype-header-text";
import { rehypeHeaderClass } from "./rehype-header-class";
import { rehypeFileTree } from "./file-tree/rehype-file-tree";
import { rehypeTwoslashTabindex } from "./twoslash-tabindex/rehype-transform";
import { siteMetadata } from "../../constants/site-config";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type RehypePlugin = Plugin<any[]> | [Plugin<any[]>, any];
@@ -67,7 +71,38 @@ export function createRehypePlugins(config: MarkdownConfig): RehypePlugin[] {
rehypeHints,
rehypeTooltips,
rehypeAstroImageMd,
[
rehypeUnicornIFrameClickToRun,
{
srcReplacements: [
(val: string, file: VFile) => {
const iFrameUrl = new URL(val);
if (!iFrameUrl.protocol.startsWith("uu-code:")) return val;
const contentDir = dirname(file.path);
const fullPath = resolve(contentDir, iFrameUrl.pathname);
const fsRelativePath = relative(process.cwd(), fullPath);
// Windows paths need to be converted to URLs
let urlRelativePath = fsRelativePath.replace(/\\/g, "/");
if (urlRelativePath.startsWith("/")) {
urlRelativePath = urlRelativePath.slice(1);
}
const q = iFrameUrl.search;
const currentBranch =
process.env.VERCEL_GIT_COMMIT_REF ?? branch.sync();
const repoPath = siteMetadata.repoPath;
const provider = `stackblitz.com/github`;
return `
https://${provider}/${repoPath}/tree/${currentBranch}/${urlRelativePath}${q}
`.trim();
},
],
},
] as RehypePlugin,
rehypeUnicornElementMap,
rehypeTwoslashTabindex,
rehypeFileTree,

View File

@@ -28,5 +28,6 @@
"uu-utils": ["./src/utils"],
"assets/*": ["./src/assets/*"]
}
}
},
"exclude": ["./content/blog"]
}