diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 23c59401a..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,4 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. See -[standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. diff --git a/apps/demo-integrations/cypress/support/helpers/demo-paths.ts b/apps/demo-integrations/cypress/support/helpers/demo-paths.ts index dcfbd3f9f..afb6fb226 100644 --- a/apps/demo-integrations/cypress/support/helpers/demo-paths.ts +++ b/apps/demo-integrations/cypress/support/helpers/demo-paths.ts @@ -2,7 +2,7 @@ import type {TuiDocPage, TuiDocPages} from '@taiga-ui/addon-doc'; import {DEMO_PAGES} from '../../../../demo/src/app/app.pages'; -const EXCLUSION_SECTIONS: string[] = [`Other`]; +const EXCLUSION_SECTIONS: string[] = [`Documentation`]; const EXCLUSION_ROUTES: string[] = [`Starter Kit`]; export const DEMO_PATHS = flatPages(DEMO_PAGES) diff --git a/apps/demo/src/app/app.component.html b/apps/demo/src/app/app.component.html index 5e8a6067c..03163f521 100644 --- a/apps/demo/src/app/app.component.html +++ b/apps/demo/src/app/app.component.html @@ -24,6 +24,16 @@ target="_blank" class="link" > + +

+ (await import(`./pages/stackblitz/starter/stackblitz-starter.module`)) + .StackblitzStarterModule, + data: { + title: `Stackblitz Starter`, + }, + }, { path: TuiDemoPath.Installation, loadChildren: async () => diff --git a/apps/demo/src/app/constants/demo-path.ts b/apps/demo/src/app/constants/demo-path.ts index cd7f7ed97..afa2cacf4 100644 --- a/apps/demo/src/app/constants/demo-path.ts +++ b/apps/demo/src/app/constants/demo-path.ts @@ -1,5 +1,7 @@ // eslint-disable-next-line no-restricted-syntax export const enum TuiDemoPath { + Stackblitz = `stackblitz`, + Changelog = `changelog`, StarterKit = `starter-kit`, Installation = `installation`, ColorPicker = `color-picker`, @@ -22,5 +24,4 @@ export const enum TuiDemoPath { EmbedYoutube = `embed/youtube`, EmbedIframe = `embed/iframe`, EmbedHtml5 = `embed/html5`, - Changelog = `changelog`, } diff --git a/apps/demo/src/app/pages/changelog/editor-changelog.component.ts b/apps/demo/src/app/pages/changelog/editor-changelog.component.ts index 21d422482..735972db2 100644 --- a/apps/demo/src/app/pages/changelog/editor-changelog.component.ts +++ b/apps/demo/src/app/pages/changelog/editor-changelog.component.ts @@ -1,7 +1,8 @@ import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; import {tuiRawLoad} from '@taiga-ui/addon-doc'; +import MarkdownIt from 'markdown-it'; import {of} from 'rxjs'; -import {switchMap} from 'rxjs/operators'; +import {map, switchMap} from 'rxjs/operators'; @Component({ selector: 'changelog', @@ -11,7 +12,22 @@ import {switchMap} from 'rxjs/operators'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class TuiChangelogComponent { - readonly changelog$ = of(import('../../../../../../CHANGELOG.md?raw')).pipe( + readonly changelog$ = of( + import('../../../../../../libs/tui-editor/CHANGELOG.md?raw'), + ).pipe( switchMap(tuiRawLoad), + map((value: string) => + new MarkdownIt() + .render( + value + .replaceAll( + 'All notable changes to this project will be documented in this file. See\n[Conventional Commits](https://conventionalcommits.org) for commit guidelines.', + '', + ) + .replace(/# Change Log\n/g, '') + .trim(), + ) + .replace(/h1|h2|h3|h4|h5|h6/g, 'h2'), + ), ); } diff --git a/apps/demo/src/app/pages/changelog/editor-changelog.style.less b/apps/demo/src/app/pages/changelog/editor-changelog.style.less index f85ee7262..7a9cbee16 100644 --- a/apps/demo/src/app/pages/changelog/editor-changelog.style.less +++ b/apps/demo/src/app/pages/changelog/editor-changelog.style.less @@ -1,60 +1,35 @@ -markdown { +.changelog { max-width: 58.25rem; - & > :nth-child(1), - & > :nth-child(2) { - display: none; - } - - > * { - margin-left: 1.25rem; - } - - & h2 { - font-size: 2em; - padding-bottom: 0.5em; - margin-left: 0; - border-bottom: 1px solid var(--tui-base-03); - } - - & h3 { - text-transform: uppercase; - font-weight: normal; - font-size: 1.5rem; - margin: 1rem 0; - } - - & h3:not([id^='feat']):not([id^='bug']):not([id^='deprecations']) { - font-size: 1.75rem; - padding-bottom: 0.5em; - margin: 2rem 0 0 0; - border-bottom: 1px solid var(--tui-base-03); - } - - & h3[id^='breaking'] { + > *:not(h2) { margin-left: 1.25rem; - color: var(--tui-error-fill); } - & code { - color: #d45d8c; - } - - & h3[id^='feat']:before { - content: '🚀'; - } + a { + text-decoration: none; + color: var(--tui-link); - & h3[id^='bug']:before { - content: '🐞'; + &:hover, + &:active { + color: var(--tui-link-hover); + } } - & h3[id^='deprecations']:before { - content: '⚠️'; + li { + position: relative; + padding-left: 1.5rem; + word-wrap: break-word; + margin-top: 0.75rem; } - & h3[id^='feat']:before, - & h3[id^='bug']:before, - & h3[id^='deprecations']:before { - margin-right: 0.5rem; + li:before { + content: ''; + position: absolute; + left: 0; + top: 0.5rem; + width: 0.5rem; + height: 0.5rem; + border-radius: 100%; + background-color: var(--tui-primary); } } diff --git a/apps/demo/src/app/pages/changelog/editor-changelog.template.html b/apps/demo/src/app/pages/changelog/editor-changelog.template.html index b0e3d74cd..10d1dcd5e 100644 --- a/apps/demo/src/app/pages/changelog/editor-changelog.template.html +++ b/apps/demo/src/app/pages/changelog/editor-changelog.template.html @@ -1,3 +1,6 @@ - +

diff --git a/apps/demo/src/app/pages/stackblitz/classes/index.ts b/apps/demo/src/app/pages/stackblitz/classes/index.ts new file mode 100644 index 000000000..582f1ddbd --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/classes/index.ts @@ -0,0 +1,3 @@ +export * from './ts-file.parser'; +export * from './ts-file-component.parser'; +export * from './ts-file-module.parser'; diff --git a/apps/demo/src/app/pages/stackblitz/classes/ts-file-component.parser.ts b/apps/demo/src/app/pages/stackblitz/classes/ts-file-component.parser.ts new file mode 100644 index 000000000..033288183 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/classes/ts-file-component.parser.ts @@ -0,0 +1,24 @@ +import {TsFileParser} from './ts-file.parser'; + +export class TsFileComponentParser extends TsFileParser { + set selector(newSelector: string) { + this.rawFileContent = this.rawFileContent.replace( + /(selector:\s['"`])(.*)(['"`])/gi, + `$1${newSelector}$3`, + ); + } + + set templateUrl(newUrl: string) { + this.rawFileContent = this.rawFileContent.replace( + /(templateUrl:\s['"`])(.*)(['"`])/gi, + `$1${newUrl}$3`, + ); + } + + set styleUrls(newUrls: string[] | readonly string[]) { + this.rawFileContent = this.rawFileContent.replace( + /(styleUrls:\s)(\[.*\])/gi, + `$1${JSON.stringify(newUrls)}`, + ); + } +} diff --git a/apps/demo/src/app/pages/stackblitz/classes/ts-file-module.parser.ts b/apps/demo/src/app/pages/stackblitz/classes/ts-file-module.parser.ts new file mode 100644 index 000000000..dbc9e870a --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/classes/ts-file-module.parser.ts @@ -0,0 +1,24 @@ +import {TsFileParser} from './ts-file.parser'; + +export class TsFileModuleParser extends TsFileParser { + addDeclaration(entity: string): void { + this.rawFileContent = this.rawFileContent.replace( + `declarations: [`, + `declarations: [${entity}, `, + ); + } + + addModuleImport(entity: string): void { + this.rawFileContent = this.rawFileContent.replace( + `imports: [`, + `imports: [\n${entity}, `, + ); + } + + hasDeclarationEntity(entity: string): boolean { + const [, declarations = ``] = + this.rawFileContent.match(/(?:declarations:\s\[)(.*)(?:\])/i) || []; + + return declarations.includes(entity); + } +} diff --git a/apps/demo/src/app/pages/stackblitz/classes/ts-file.parser.ts b/apps/demo/src/app/pages/stackblitz/classes/ts-file.parser.ts new file mode 100644 index 000000000..1b8f5eef0 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/classes/ts-file.parser.ts @@ -0,0 +1,73 @@ +import {TuiTsParserException} from '@taiga-ui/cdk'; + +export class TsFileParser { + get className(): string { + const [, className] = this.rawFileContent.match(/(?:export class\s)(\w*)/i) || []; + + return className || ``; + } + + set className(newClassName: string) { + this.rawFileContent = this.rawFileContent.replace( + /(export class\s)(\w*)/i, + `$1${newClassName}`, + ); + } + + get hasNgModule(): boolean { + return this.rawFileContent.includes(`@NgModule`); + } + + get hasNgComponent(): boolean { + return this.rawFileContent.includes(`@Component`); + } + + constructor(protected rawFileContent: string) { + const classesInside = rawFileContent.match(/export class/gi) || []; + + if (classesInside.length > 1) { + throw new TuiTsParserException(); + } + + this.replaceMetaAssets(); + } + + addImport(entity: string, packageOrPath: string): void { + const fromName = packageOrPath.replace(`.ts`, ``); + + this.rawFileContent = this.rawFileContent.includes(fromName) + ? this.addIntoExistingImport(entity, fromName) + : `import {${entity}} from '${fromName}';\n${this.rawFileContent};`; + } + + toString(): string { + return this.rawFileContent; + } + + private addIntoExistingImport(entity: string, packageName: string): string { + const packageImportsRegex = new RegExp( + `(?:import\\s?\\{\\r?\\n?)(?:(?:.*),\\r?\\n?)*?(?:.*?)\\r?\\n?} from (?:'|")${packageName}(?:'|");`, + `gm`, + ); + + return this.rawFileContent.replace(packageImportsRegex, parsed => + parsed.includes(entity) ? parsed : parsed.replace(`{`, `{${entity}, `), + ); + } + + /** + * @description: + * The 'import.meta' doesn't support on Stackblitz + */ + private replaceMetaAssets(): void { + this.rawFileContent = this.rawFileContent.replace( + `import {assets} from '@demo/utils';\n`, + ``, + ); + + this.rawFileContent = this.rawFileContent.replace( + `assets\``, + `\`https://taiga-ui.dev/assets`, + ); + } +} diff --git a/apps/demo/src/app/pages/stackblitz/project-files/configs.md b/apps/demo/src/app/pages/stackblitz/project-files/configs.md new file mode 100644 index 000000000..fc4e65bc7 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/project-files/configs.md @@ -0,0 +1,82 @@ +# angular.json + +```json +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "demo": { + "root": "", + "sourceRoot": "", + "projectType": "application", + "prefix": "my-app", + "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.less"] + }, + "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"} + } + } + } + }, + "defaultProject": "demo" +} +``` + +# tsconfig.json + +```json +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "sourceMap": false, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "module": "es2020", + "moduleResolution": "node", + "importHelpers": true, + "target": "es5", + "typeRoots": ["node_modules/@types"], + "lib": ["esnext", "dom"] + }, + "angularCompilerOptions": { + "enableIvy": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true + } +} +``` diff --git a/apps/demo/src/app/pages/stackblitz/project-files/src/app/app.module.ts.md b/apps/demo/src/app/pages/stackblitz/project-files/src/app/app.module.ts.md new file mode 100644 index 000000000..91ff8ac4a --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/project-files/src/app/app.module.ts.md @@ -0,0 +1,27 @@ +```ts +import {NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {TuiRootModule} from '@taiga-ui/core'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {tuiSvgOptionsProvider, TUI_SANITIZER} from '@taiga-ui/core'; +import {NgDompurifySanitizer} from '@tinkoff/ng-dompurify'; +import {TuiEditorModule, TuiEditorSocketModule} from '@tinkoff/tui-editor'; + +import {AppComponent} from './app.component'; + +@NgModule({ + declarations: [AppComponent], + bootstrap: [AppComponent], + imports: [BrowserModule, TuiRootModule, TuiEditorModule, TuiEditorSocketModule, FormsModule, ReactiveFormsModule], + providers: [ + tuiSvgOptionsProvider({ + path: 'https://taiga-ui.dev/assets/taiga-ui/icons', + }), + { + provide: TUI_SANITIZER, + useClass: NgDompurifySanitizer, + }, + ], +}) +export class AppModule {} +``` diff --git a/apps/demo/src/app/pages/stackblitz/project-files/src/index.html.md b/apps/demo/src/app/pages/stackblitz/project-files/src/index.html.md new file mode 100644 index 000000000..f9c8b9462 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/project-files/src/index.html.md @@ -0,0 +1,14 @@ +```html + + + Taiga UI + + + + + + +``` diff --git a/apps/demo/src/app/pages/stackblitz/project-files/src/main.ts.md b/apps/demo/src/app/pages/stackblitz/project-files/src/main.ts.md new file mode 100644 index 000000000..6bd8ad5ed --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/project-files/src/main.ts.md @@ -0,0 +1,9 @@ +```ts +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; + +import {AppModule} from './app/app.module'; + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch(err => console.error(err)); +``` diff --git a/apps/demo/src/app/pages/stackblitz/project-files/src/polyfills.ts.md b/apps/demo/src/app/pages/stackblitz/project-files/src/polyfills.ts.md new file mode 100644 index 000000000..8c13d51c6 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/project-files/src/polyfills.ts.md @@ -0,0 +1,3 @@ +```ts +import 'zone.js'; +``` diff --git a/apps/demo/src/app/pages/stackblitz/project-files/src/styles.less.md b/apps/demo/src/app/pages/stackblitz/project-files/src/styles.less.md new file mode 100644 index 000000000..cf8c99ec6 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/project-files/src/styles.less.md @@ -0,0 +1,13 @@ +```less +@import '@taiga-ui/core/styles/taiga-ui-theme.less'; +@import '@taiga-ui/core/styles/taiga-ui-fonts.less'; +@import '@taiga-ui/styles/taiga-ui-global.less'; + +/* Add application styles & imports to this file! */ +my-app { + display: block; + padding: 1.5625rem; + height: 100%; + box-sizing: border-box; +} +``` diff --git a/apps/demo/src/app/pages/stackblitz/stackblitz-deps.service.ts b/apps/demo/src/app/pages/stackblitz/stackblitz-deps.service.ts new file mode 100644 index 000000000..3dbf76cc1 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/stackblitz-deps.service.ts @@ -0,0 +1,59 @@ +import {Injectable} from '@angular/core'; + +@Injectable({ + providedIn: `root`, +}) +export class StackblitzDepsService { + readonly angular = { + '@angular/cdk': `12.x.x`, + '@angular/core': `12.x.x`, + '@angular/common': `12.x.x`, + '@angular/compiler': `12.x.x`, + '@angular/forms': `12.x.x`, + '@angular/localize': `12.x.x`, + '@angular/platform-browser': `12.x.x`, + '@angular/platform-browser-dynamic': `12.x.x`, + '@angular/animations': `12.x.x`, + '@angular/router': `12.x.x`, + }; + + readonly taiga = { + '@taiga-ui/cdk': `3.x.x`, + '@taiga-ui/i18n': `3.x.x`, + '@taiga-ui/core': `3.x.x`, + '@taiga-ui/kit': `3.x.x`, + '@taiga-ui/icons': `3.x.x`, + '@taiga-ui/styles': `3.x.x`, + }; + + readonly tinkoff = { + '@tinkoff/tui-editor': `1.x.x`, + '@tinkoff/ng-polymorpheus': `4.x.x`, + '@tinkoff/ng-dompurify': `4.x.x`, + '@tinkoff/ng-event-plugins': `3.x.x`, + }; + + readonly webApis = { + '@ng-web-apis/common': `3.x.x`, + '@ng-web-apis/intersection-observer': `3.x.x`, + '@ng-web-apis/resize-observer': `3.x.x`, + '@ng-web-apis/mutation-observer': `3.x.x`, + }; + + readonly common = { + 'zone.js': `0.11.8`, + dompurify: `2.4.5`, + typescript: `4.3.5`, + rxjs: `6.6.7`, + }; + + get(): Record { + return { + ...this.angular, + ...this.taiga, + ...this.webApis, + ...this.common, + ...this.tinkoff, + }; + } +} diff --git a/apps/demo/src/app/pages/stackblitz/stackblitz-resources-loader.ts b/apps/demo/src/app/pages/stackblitz/stackblitz-resources-loader.ts new file mode 100644 index 000000000..358c78b26 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/stackblitz-resources-loader.ts @@ -0,0 +1,42 @@ +import {tuiRawLoad, tuiTryParseMarkdownCodeBlock} from '@taiga-ui/addon-doc'; + +interface TuiProjectFiles { + angularJson: string; + tsconfig: string; + mainTs: string; + indexHtml: string; + polyfills: string; + appModuleTs: string; + styles: string; +} + +export abstract class AbstractTuiStackblitzResourcesLoader { + static async getProjectFiles(): Promise { + const [ + configsContent, + mainTsContent, + indexHtmlContent, + polyfillsContent, + stylesContent, + appModuleTsContent, + ]: string[] = await Promise.all( + [ + import(`./project-files/configs.md?raw`), + import(`./project-files/src/main.ts.md?raw`), + import(`./project-files/src/index.html.md?raw`), + import(`./project-files/src/polyfills.ts.md?raw`), + import(`./project-files/src/styles.less.md?raw`), + import(`./project-files/src/app/app.module.ts.md?raw`), + ].map(tuiRawLoad), + ); + + const [angularJson, tsconfig] = tuiTryParseMarkdownCodeBlock(configsContent); + const [mainTs] = tuiTryParseMarkdownCodeBlock(mainTsContent); + const [indexHtml] = tuiTryParseMarkdownCodeBlock(indexHtmlContent); + const [polyfills] = tuiTryParseMarkdownCodeBlock(polyfillsContent); + const [styles] = tuiTryParseMarkdownCodeBlock(stylesContent); + const [appModuleTs] = tuiTryParseMarkdownCodeBlock(appModuleTsContent); + + return {angularJson, tsconfig, mainTs, indexHtml, polyfills, appModuleTs, styles}; + } +} diff --git a/apps/demo/src/app/pages/stackblitz/stackblitz.service.ts b/apps/demo/src/app/pages/stackblitz/stackblitz.service.ts new file mode 100644 index 000000000..d469641c9 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/stackblitz.service.ts @@ -0,0 +1,131 @@ +import {Inject, Injectable} from '@angular/core'; +import stackblitz, {OpenOptions, Project} from '@stackblitz/sdk'; +import {TuiCodeEditor} from '@taiga-ui/addon-doc'; +import {PolymorpheusComponent} from '@tinkoff/ng-polymorpheus'; + +import {TsFileComponentParser, TsFileModuleParser} from './classes'; +import {StackblitzDepsService} from './stackblitz-deps.service'; +import {AbstractTuiStackblitzResourcesLoader} from './stackblitz-resources-loader'; +import {StackblitzEditButtonComponent} from './starter/stackblitz-edit-button.component'; +import { + appPrefix, + getComponentsClassNames, + getSupportFiles, + getSupportModules, + prepareLess, + prepareSupportFiles, +} from './utils'; + +const APP_COMP_META = { + SELECTOR: `my-app`, + TEMPLATE_URL: `./app.component.html`, + STYLE_URLS: [`./app.component.less`], + CLASS_NAME: `AppComponent`, +} as const; + +@Injectable() +export class TuiStackblitzService implements TuiCodeEditor { + readonly name = `Stackblitz`; + readonly content = new PolymorpheusComponent(StackblitzEditButtonComponent); + + constructor( + @Inject(StackblitzDepsService) + private readonly deps: StackblitzDepsService, + ) {} + + async edit( + component: string, + sampleId: string, + content: Record, + ): Promise { + if (!content.HTML || !content.TypeScript) { + return; + } + + const {appModuleTs} = + await AbstractTuiStackblitzResourcesLoader.getProjectFiles(); + + const appModule = new TsFileModuleParser(appModuleTs); + const appCompTs = new TsFileComponentParser(content.TypeScript); + const supportFilesTuples = getSupportFiles(content); + const supportModulesTuples = getSupportModules(supportFilesTuples); + const supportCompClassNames = getComponentsClassNames(supportFilesTuples); + const modifiedSupportFiles = prepareSupportFiles(supportFilesTuples); + + supportCompClassNames.forEach(([fileName, className]) => { + const insideAnotherModule = supportModulesTuples.some(([_, module]) => + module.hasDeclarationEntity(className), + ); + + if (insideAnotherModule) { + return; + } + + appModule.addImport(className, `./${fileName}`); + appModule.addDeclaration(className); + }); + + appCompTs.selector = APP_COMP_META.SELECTOR; + appCompTs.templateUrl = APP_COMP_META.TEMPLATE_URL; + appCompTs.styleUrls = APP_COMP_META.STYLE_URLS; + appCompTs.className = APP_COMP_META.CLASS_NAME; + + return stackblitz.openProject({ + ...this.getStackblitzProjectConfig(), + title: `${component}-${sampleId}`, + description: `TUI Editor example`, + files: { + ...(await this.getBaseAngularProjectFiles()), + ...modifiedSupportFiles, + [appPrefix`app.module.ts`]: appModule.toString(), + [appPrefix`app.component.ts`]: appCompTs.toString(), + [appPrefix`app.component.html`]: `\n\n${content.HTML}\n`, + [appPrefix`app.component.less`]: prepareLess(content.LESS || ``), + }, + }); + } + + async openStarter( + {title, description, files}: Pick, + openOptions?: OpenOptions, + ): Promise { + return stackblitz.openProject( + { + ...this.getStackblitzProjectConfig(), + title, + description, + files: { + ...(await this.getBaseAngularProjectFiles()), + ...files, + }, + }, + openOptions, + ); + } + + private async getBaseAngularProjectFiles(): Promise { + const {tsconfig, angularJson, mainTs, polyfills, indexHtml, styles, appModuleTs} = + await AbstractTuiStackblitzResourcesLoader.getProjectFiles(); + + return { + 'tsconfig.json': tsconfig, + 'angular.json': angularJson, + 'src/main.ts': mainTs, + 'src/polyfills.ts': polyfills, + 'src/index.html': indexHtml, + 'src/styles.less': styles, + [appPrefix`app.module.ts`]: appModuleTs.toString(), + }; + } + + private getStackblitzProjectConfig(): Pick< + Project, + 'dependencies' | 'tags' | 'template' + > { + return { + template: `angular-cli`, + dependencies: this.deps.get(), + tags: [`Angular`, `Taiga UI`, `Angular components`, `UI Kit`], + }; + } +} diff --git a/apps/demo/src/app/pages/stackblitz/starter/files/app.component.html.md b/apps/demo/src/app/pages/stackblitz/starter/files/app.component.html.md new file mode 100644 index 000000000..38321e122 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/starter/files/app.component.html.md @@ -0,0 +1,19 @@ +```html + + + Start typing + + +

HTML:

+ + +

Text:

+

{{ control.value }}

+
+``` diff --git a/apps/demo/src/app/pages/stackblitz/starter/files/app.component.ts.md b/apps/demo/src/app/pages/stackblitz/starter/files/app.component.ts.md new file mode 100644 index 000000000..21ffdd993 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/starter/files/app.component.ts.md @@ -0,0 +1,23 @@ +```ts +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {FormControl} from '@angular/forms'; +import {defaultEditorExtensions, defaultEditorTools, TUI_EDITOR_EXTENSIONS, TuiEditorTool} from '@tinkoff/tui-editor'; + +@Component({ + selector: `my-app`, + templateUrl: `./app.component.html`, + styleUrls: [`./app.component.less`], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: TUI_EDITOR_EXTENSIONS, + useValue: defaultEditorExtensions, + }, + ], +}) +export class AppComponent { + readonly control = new FormControl(); + + readonly tools: TuiEditorTool[] = defaultEditorTools; +} +``` diff --git a/apps/demo/src/app/pages/stackblitz/starter/files/index.html.md b/apps/demo/src/app/pages/stackblitz/starter/files/index.html.md new file mode 100644 index 000000000..c8aeb6aed --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/starter/files/index.html.md @@ -0,0 +1,18 @@ +```html + + + +
+ logo + +

TUI Editor

+
+ +``` diff --git a/apps/demo/src/app/pages/stackblitz/starter/files/styles.less.md b/apps/demo/src/app/pages/stackblitz/starter/files/styles.less.md new file mode 100644 index 000000000..caf0320e9 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/starter/files/styles.less.md @@ -0,0 +1,18 @@ +```less +@import '@taiga-ui/core/styles/taiga-ui-theme.less'; +@import '@taiga-ui/core/styles/taiga-ui-fonts.less'; +@import '@taiga-ui/styles/taiga-ui-global.less'; + +.header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid var(--tui-base-04); + padding: 5px 20px; +} + +tui-root { + margin: 20px; + font-size: 16px; +} +``` diff --git a/apps/demo/src/app/pages/stackblitz/starter/stackblitz-edit-button.component.ts b/apps/demo/src/app/pages/stackblitz/starter/stackblitz-edit-button.component.ts new file mode 100644 index 000000000..0918049a7 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/starter/stackblitz-edit-button.component.ts @@ -0,0 +1,18 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; + +@Component({ + selector: 'stackblitz-edit-button', + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class StackblitzEditButtonComponent {} diff --git a/apps/demo/src/app/pages/stackblitz/starter/stackblitz-starter.component.html b/apps/demo/src/app/pages/stackblitz/starter/stackblitz-starter.component.html new file mode 100644 index 000000000..12effeef9 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/starter/stackblitz-starter.component.html @@ -0,0 +1,6 @@ + diff --git a/apps/demo/src/app/pages/stackblitz/starter/stackblitz-starter.component.ts b/apps/demo/src/app/pages/stackblitz/starter/stackblitz-starter.component.ts new file mode 100644 index 000000000..8acec4f55 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/starter/stackblitz-starter.component.ts @@ -0,0 +1,55 @@ +import {ChangeDetectionStrategy, Component, Inject, OnInit} from '@angular/core'; +import {tuiRawLoad, tuiTryParseMarkdownCodeBlock} from '@taiga-ui/addon-doc'; + +import {TuiStackblitzService} from '../stackblitz.service'; +import {appPrefix} from '../utils'; + +@Component({ + selector: 'demo-stackblitz-starter', + templateUrl: './stackblitz-starter.component.html', + styleUrls: ['./stackblitz-starter.style.less'], + providers: [TuiStackblitzService], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class StackblitzStarterComponent implements OnInit { + constructor( + @Inject(TuiStackblitzService) private readonly stackblitz: TuiStackblitzService, + ) {} + + async ngOnInit(): Promise { + await this.openStackblitz(); + } + + async openStackblitz(): Promise { + const [appTemplate, appComponent, indexHtml, stylesLess] = await Promise.all( + [ + import('./files/app.component.html.md?raw'), + import('./files/app.component.ts.md?raw'), + import('./files/index.html.md?raw'), + import('./files/styles.less.md?raw'), + ].map(tuiRawLoad), + ).then(markdowns => markdowns.map(md => tuiTryParseMarkdownCodeBlock(md)[0])); + + return this.stackblitz.openStarter( + { + title: 'TUI Editor Starter', + description: + 'A starter for TUI Editor\nDocumentation: https://tinkoff.github.io/tui-editor', + files: { + 'src/index.html': indexHtml, + 'src/styles.less': stylesLess, + [appPrefix`app.component.html`]: appTemplate, + [appPrefix`app.component.ts`]: appComponent, + [appPrefix`app.component.less`]: + // eslint-disable-next-line @typescript-eslint/quotes + "@import '@taiga-ui/core/styles/taiga-ui-local.less';", + }, + }, + { + newWindow: false, + openFile: appPrefix`app.component.html`, + hideExplorer: true, + }, + ); + } +} diff --git a/apps/demo/src/app/pages/stackblitz/starter/stackblitz-starter.module.ts b/apps/demo/src/app/pages/stackblitz/starter/stackblitz-starter.module.ts new file mode 100644 index 000000000..39b4fdb86 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/starter/stackblitz-starter.module.ts @@ -0,0 +1,17 @@ +import {NgModule} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {TuiButtonModule, TuiLoaderModule} from '@taiga-ui/core'; + +import {StackblitzEditButtonComponent} from './stackblitz-edit-button.component'; +import {StackblitzStarterComponent} from './stackblitz-starter.component'; + +@NgModule({ + imports: [ + TuiLoaderModule, + TuiButtonModule, + RouterModule.forChild([{path: ``, component: StackblitzStarterComponent}]), + ], + declarations: [StackblitzStarterComponent, StackblitzEditButtonComponent], + exports: [StackblitzStarterComponent], +}) +export class StackblitzStarterModule {} diff --git a/apps/demo/src/app/pages/stackblitz/starter/stackblitz-starter.style.less b/apps/demo/src/app/pages/stackblitz/starter/stackblitz-starter.style.less new file mode 100644 index 000000000..06409ea35 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/starter/stackblitz-starter.style.less @@ -0,0 +1,7 @@ +@import '@taiga-ui/core/styles/taiga-ui-local.less'; + +.loader { + .fullsize(fixed); + background: var(--tui-base-01); + z-index: 1; +} diff --git a/apps/demo/src/app/pages/stackblitz/utils.ts b/apps/demo/src/app/pages/stackblitz/utils.ts new file mode 100644 index 000000000..85a6d2da1 --- /dev/null +++ b/apps/demo/src/app/pages/stackblitz/utils.ts @@ -0,0 +1,106 @@ +import type {Project} from '@stackblitz/sdk'; +import {TUI_EXAMPLE_PRIMARY_FILE_NAME} from '@taiga-ui/addon-doc'; + +import {TsFileComponentParser, TsFileModuleParser, TsFileParser} from './classes'; + +export function processTs(fileContent: string): string { + const tsFileContent = new TsFileParser(fileContent); + + if (tsFileContent.hasNgComponent) { + tsFileContent.addImport(`ChangeDetectionStrategy`, `@angular/core`); + } + + return tsFileContent + .toString() + .replace(/import {encapsulation} from '.*';\n/gm, ``) + .replace(/import {changeDetection} from '.*';\n/gm, ``) + .replace(/\n +encapsulation,/gm, ``) + .replace( + /changeDetection,/gm, + `changeDetection: ChangeDetectionStrategy.OnPush,`, + ); +} + +export function processLess(fileContent: string): string { + return fileContent.replace( + `@import 'taiga-ui-local';`, + `@import '@taiga-ui/core/styles/taiga-ui-local.less';`, + ); +} + +export function isTS(fileName: string): boolean { + return fileName === TUI_EXAMPLE_PRIMARY_FILE_NAME.TS || fileName.endsWith(`.ts`); +} + +export function isLess(fileName: string): boolean { + return ( + fileName === TUI_EXAMPLE_PRIMARY_FILE_NAME.LESS || `${fileName}`.endsWith(`.less`) + ); +} + +export function isPrimaryComponentFile(fileName: string): boolean { + return Object.values(TUI_EXAMPLE_PRIMARY_FILE_NAME).includes(fileName); +} + +export const prepareLess = (content: string): string => { + return content.replace( + /@import.+taiga-ui-local(.less)?';/g, + `@import '@taiga-ui/core/styles/taiga-ui-local.less';`, + ); +}; + +export const appPrefix = (stringsPart: TemplateStringsArray, path: string = ``): string => + `src/app/${stringsPart.join(``)}${path}`; + +type FileName = string; + +type FileContent = string; + +export const getSupportFiles = >( + files: T, +): Array<[FileName, FileContent]> => { + return Object.entries(files).filter( + ([fileName, content]) => content && !isPrimaryComponentFile(fileName), + ); +}; + +export const prepareSupportFiles = ( + files: Array<[FileName, FileContent]>, +): Project['files'] => { + const processedContent: Project['files'] = {}; + + for (const [fileName, fileContent] of files) { + const prefixedFileName = appPrefix`${fileName}`; + + processedContent[prefixedFileName] = isLess(fileName) + ? prepareLess(fileContent) + : fileContent; + } + + return processedContent; +}; + +export const getComponentsClassNames = ( + files: Array<[FileName, FileContent]>, +): Array<[FileName, FileContent]> => { + return files + .filter( + ([fileName, fileContent]) => + isTS(fileName) && new TsFileParser(fileContent).hasNgComponent, + ) + .map(([fileName, fileContent]) => [ + fileName, + new TsFileComponentParser(fileContent).className, + ]); +}; + +export const getSupportModules = ( + files: Array<[FileName, FileContent]>, +): Array<[FileName, TsFileModuleParser]> => { + return files + .filter(([name, content]) => isTS(name) && new TsFileParser(content).hasNgModule) + .map(([fileName, fileContent]) => [ + fileName, + new TsFileModuleParser(fileContent), + ]); +}; diff --git a/tsconfig.json b/tsconfig.json index ea2133725..74b920e9b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,7 @@ "outDir": "./dist", "target": "es2015", "module": "es2020", - "lib": ["es2017", "es2018.asynciterable", "dom"], + "lib": ["es2021", "dom"], "typeRoots": ["node_modules/@types", "libs/tui-editor/types"], "types": ["ng-dev-mode"], "skipLibCheck": true,