├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── browserslist ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── core │ │ ├── core.module.ts │ │ ├── decorator │ │ │ ├── debounce.decorator.ts │ │ │ └── throttle.decorator.ts │ │ ├── directive │ │ │ ├── aftercontentinit.ts │ │ │ ├── autofous.directive.ts │ │ │ └── ondestroy.ts │ │ ├── model │ │ │ ├── cell-range.class.ts │ │ │ ├── cell-style.class.ts │ │ │ ├── cell.class.ts │ │ │ ├── float-element.ts │ │ │ ├── index.ts │ │ │ ├── key-code.enmu.ts │ │ │ ├── logic-position.enmu.ts │ │ │ ├── operate-state.enum.ts │ │ │ ├── row.class.ts │ │ │ ├── status-state.enum.ts │ │ │ ├── style.const.ts │ │ │ └── value-type.enmu.ts │ │ └── utils │ │ │ ├── function.ts │ │ │ └── index.ts │ └── editor │ │ ├── editor-panel │ │ ├── cell-edit │ │ │ ├── cell-edit.component.html │ │ │ ├── cell-edit.component.scss │ │ │ └── cell-edit.component.ts │ │ ├── editor-panel.component.html │ │ ├── editor-panel.component.scss │ │ ├── editor-panel.component.ts │ │ └── panel.ts │ │ ├── editor-routing.module.ts │ │ ├── editor.component.html │ │ ├── editor.component.scss │ │ ├── editor.component.ts │ │ └── editor.module.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── style │ ├── iconfont.css │ ├── iconfont.eot │ ├── iconfont.svg │ ├── iconfont.ttf │ ├── iconfont.woff │ └── iconfont.woff2 ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReportEditor 2 | 最终目标:实现一个Excel报表编辑器 3 | 目前正在处理前端Excel实现,抽空编码,进度缓慢 4 | [Demo](https://yuhuali.github.io/report-editor) 5 | 6 | ## Preview 7 | ![img](https://github.com/YuhuaLi/report-editor/blob/gh-pages/%E6%8D%95%E8%8E%B7.PNG?raw=true) 8 | 9 | ## Completed 10 | 11 | 页面绘制 12 | 页面滚动 13 | 单元格编辑 14 | 设置单元格样式 15 | 合并单元格 16 | 拖拽调整行列宽高 17 | 常用按键事件(Ctrl+a,Ctrl+c,Ctrl+v,PageUp,PageDown,Del,Home,Tab,Enter,Shift+Enter,Shift+Tab,Esc,Top/Left/Right/Down,Shift+Top/Left/Right/Down等) 18 | 从Excel文件中粘贴单元格(包含Excel单元的合并及背景色) 19 | 复制Excel文件中图片及显示 20 | 缩放 21 | 加载图片文件 22 | 图片拖拽移动和缩放 23 | 图片拖拽缩放移动边界处理优化 24 | 单元格内容部分样式实现,工具栏输入框同步修改内容并保持样式 25 | 单元格内容左中右对齐优化 26 | 27 | ## Doing 28 | 单元格内容换行实现 29 | 30 | ## To Do 31 | 图片旋转 32 | 公式 33 | 34 | 35 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "report-editor": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/report-editor", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "aot": true, 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | "src/styles.scss" 32 | ], 33 | "scripts": [] 34 | }, 35 | "configurations": { 36 | "production": { 37 | "fileReplacements": [ 38 | { 39 | "replace": "src/environments/environment.ts", 40 | "with": "src/environments/environment.prod.ts" 41 | } 42 | ], 43 | "optimization": true, 44 | "outputHashing": "all", 45 | "sourceMap": false, 46 | "extractCss": true, 47 | "namedChunks": false, 48 | "extractLicenses": true, 49 | "vendorChunk": false, 50 | "buildOptimizer": true, 51 | "budgets": [ 52 | { 53 | "type": "initial", 54 | "maximumWarning": "2mb", 55 | "maximumError": "5mb" 56 | }, 57 | { 58 | "type": "anyComponentStyle", 59 | "maximumWarning": "6kb", 60 | "maximumError": "10kb" 61 | } 62 | ] 63 | } 64 | } 65 | }, 66 | "serve": { 67 | "builder": "@angular-devkit/build-angular:dev-server", 68 | "options": { 69 | "browserTarget": "report-editor:build" 70 | }, 71 | "configurations": { 72 | "production": { 73 | "browserTarget": "report-editor:build:production" 74 | } 75 | } 76 | }, 77 | "extract-i18n": { 78 | "builder": "@angular-devkit/build-angular:extract-i18n", 79 | "options": { 80 | "browserTarget": "report-editor:build" 81 | } 82 | }, 83 | "test": { 84 | "builder": "@angular-devkit/build-angular:karma", 85 | "options": { 86 | "main": "src/test.ts", 87 | "polyfills": "src/polyfills.ts", 88 | "tsConfig": "tsconfig.spec.json", 89 | "karmaConfig": "karma.conf.js", 90 | "assets": [ 91 | "src/favicon.ico", 92 | "src/assets" 93 | ], 94 | "styles": [ 95 | "src/styles.scss" 96 | ], 97 | "scripts": [] 98 | } 99 | }, 100 | "lint": { 101 | "builder": "@angular-devkit/build-angular:tslint", 102 | "options": { 103 | "tsConfig": [ 104 | "tsconfig.app.json", 105 | "tsconfig.spec.json", 106 | "e2e/tsconfig.json" 107 | ], 108 | "exclude": [ 109 | "**/node_modules/**" 110 | ] 111 | } 112 | }, 113 | "e2e": { 114 | "builder": "@angular-devkit/build-angular:protractor", 115 | "options": { 116 | "protractorConfig": "e2e/protractor.conf.js", 117 | "devServerTarget": "report-editor:serve" 118 | }, 119 | "configurations": { 120 | "production": { 121 | "devServerTarget": "report-editor:serve:production" 122 | } 123 | } 124 | } 125 | } 126 | }}, 127 | "defaultProject": "report-editor" 128 | } 129 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('report-editor app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/report-editor'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "report-editor", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "myself": "ng serve --host 0.0.0.0 --port 4201" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "~9.1.1", 16 | "@angular/common": "~9.1.1", 17 | "@angular/compiler": "~9.1.1", 18 | "@angular/core": "~9.1.1", 19 | "@angular/forms": "~9.1.1", 20 | "@angular/platform-browser": "~9.1.1", 21 | "@angular/platform-browser-dynamic": "~9.1.1", 22 | "@angular/router": "~9.1.1", 23 | "rxjs": "~6.5.4", 24 | "tslib": "^1.10.0", 25 | "zone.js": "~0.10.2" 26 | }, 27 | "devDependencies": { 28 | "@angular-devkit/build-angular": "~0.901.1", 29 | "@angular/cli": "~9.1.1", 30 | "@angular/compiler-cli": "~9.1.1", 31 | "@angular/language-service": "~9.1.1", 32 | "@types/jasmine": "~3.5.0", 33 | "@types/jasminewd2": "~2.0.3", 34 | "@types/node": "^12.11.1", 35 | "codelyzer": "^5.1.2", 36 | "jasmine-core": "~3.5.0", 37 | "jasmine-spec-reporter": "~4.2.1", 38 | "karma": "~4.4.1", 39 | "karma-chrome-launcher": "~3.1.0", 40 | "karma-coverage-istanbul-reporter": "~2.1.0", 41 | "karma-jasmine": "~3.0.1", 42 | "karma-jasmine-html-reporter": "^1.4.2", 43 | "lodash": "^4.17.21", 44 | "protractor": "~5.4.3", 45 | "ts-node": "~8.3.0", 46 | "tslint": "~6.1.0", 47 | "typescript": "~3.8.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { AppComponent } from './app.component'; 2 | import { NgModule } from '@angular/core'; 3 | import { Routes, RouterModule } from '@angular/router'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: AppComponent, 9 | children: [ 10 | { 11 | path: 'editor', 12 | loadChildren: () => 13 | import('./editor/editor.module').then((m) => m.EditorModule), 14 | }, 15 | { 16 | path: '', 17 | pathMatch: 'full', 18 | redirectTo: 'editor', 19 | }, 20 | ], 21 | }, 22 | ]; 23 | 24 | @NgModule({ 25 | imports: [RouterModule.forRoot(routes)], 26 | exports: [RouterModule], 27 | }) 28 | export class AppRoutingModule {} 29 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuhuaLi/report-editor/802da80e24332a84fc07daeb47feae709bf9b742/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | })); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'report-editor'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('report-editor'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement; 33 | expect(compiled.querySelector('.content span').textContent).toContain('report-editor app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'report-editor'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | 7 | @NgModule({ 8 | declarations: [AppComponent], 9 | imports: [ 10 | BrowserModule, 11 | AppRoutingModule, 12 | ], 13 | providers: [], 14 | bootstrap: [AppComponent], 15 | }) 16 | export class AppModule {} 17 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { AutoFocusDirective } from './directive/autofous.directive'; 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { AfterContentInitDirective } from './directive/aftercontentinit'; 5 | import { OnDestroyDirective } from './directive/ondestroy'; 6 | 7 | @NgModule({ 8 | declarations: [AutoFocusDirective, AfterContentInitDirective, OnDestroyDirective], 9 | imports: [CommonModule], 10 | exports: [AutoFocusDirective, AfterContentInitDirective, OnDestroyDirective], 11 | }) 12 | export class CoreModule {} 13 | -------------------------------------------------------------------------------- /src/app/core/decorator/debounce.decorator.ts: -------------------------------------------------------------------------------- 1 | export function debounce(delay: number = 300): MethodDecorator { 2 | return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { 3 | const timeoutKey = Symbol(`__timeout__${propertyKey}`); 4 | const original = descriptor.value; 5 | descriptor.value = (...args: any[]) => { 6 | clearTimeout(this[timeoutKey]); 7 | this[timeoutKey] = setTimeout(() => original.apply(this, args), delay); 8 | }; 9 | return descriptor; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/core/decorator/throttle.decorator.ts: -------------------------------------------------------------------------------- 1 | export function throttle(threshhold: number = 10): MethodDecorator { 2 | return function ( 3 | target: any, 4 | propertyKey: string, 5 | descriptor: PropertyDescriptor 6 | ) { 7 | const timeoutKey = Symbol(`__timeout__${propertyKey}`); 8 | const original = descriptor.value; 9 | descriptor.value = function (...args: any[]) { 10 | if (!this[timeoutKey]) { 11 | this[timeoutKey] = setTimeout(() => { 12 | original.apply(this, args); 13 | clearTimeout(this[timeoutKey]); 14 | this[timeoutKey] = null; 15 | }, threshhold); 16 | } 17 | }; 18 | return descriptor; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/core/directive/aftercontentinit.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterContentInit, 3 | Directive, 4 | ElementRef, 5 | EventEmitter, 6 | Output, 7 | } from '@angular/core'; 8 | 9 | @Directive({ selector: '[appAfterContentInit]' }) 10 | export class AfterContentInitDirective implements AfterContentInit { 11 | @Output('appAfterContentInit') 12 | public after: EventEmitter = new EventEmitter(); 13 | 14 | constructor(private elementRef: ElementRef) {} 15 | 16 | public ngAfterContentInit(): void { 17 | setTimeout(() => this.after.next(this.elementRef.nativeElement)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/core/directive/autofous.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, OnInit, ElementRef, Input, OnChanges, SimpleChanges } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appAutoFocus]', 5 | }) 6 | export class AutoFocusDirective implements OnInit, OnChanges { 7 | @Input() appAutoFocus: boolean; 8 | constructor(private elementRef: ElementRef) {} 9 | 10 | ngOnInit(): void { 11 | if (this.appAutoFocus) { 12 | this.elementRef.nativeElement.focus(); 13 | } 14 | } 15 | ngOnChanges(changes: SimpleChanges) { 16 | // if (this.appAutoFocus) { 17 | // this.elementRef.nativeElement.focus(); 18 | // } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/core/directive/ondestroy.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | ElementRef, 4 | EventEmitter, 5 | OnDestroy, 6 | Output, 7 | } from '@angular/core'; 8 | 9 | @Directive({ selector: '[appOnDestroy]' }) 10 | export class OnDestroyDirective implements OnDestroy { 11 | @Output('appOnDestroy') 12 | public destory: EventEmitter = new EventEmitter(); 13 | 14 | constructor(private elementRef: ElementRef) {} 15 | 16 | public ngOnDestroy(): void { 17 | setTimeout(() => {this.destory.next(this.elementRef.nativeElement); 18 | console.log('destroy')}); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/core/model/cell-range.class.ts: -------------------------------------------------------------------------------- 1 | export class CellRange { 2 | rowStart: number; 3 | rowEnd: number; 4 | columnStart: number; 5 | columnEnd: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/core/model/cell-style.class.ts: -------------------------------------------------------------------------------- 1 | export class CellStyle { 2 | background?: string; 3 | color?: string; 4 | fontWeight?: string; 5 | fontStyle?: string; 6 | fontSize?: number; 7 | fontFamily?: string; 8 | textAlign?: CanvasTextAlign; 9 | textBaseline?: CanvasTextBaseline; 10 | borderWidth?: number; 11 | borderColor?: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/core/model/cell.class.ts: -------------------------------------------------------------------------------- 1 | import { CellStyle } from './cell-style.class'; 2 | 3 | export class Cell { 4 | // cells: Cell[][]; 5 | // offsetLeft: number; 6 | // offsetTop: number; 7 | columns?: { x?: number; width?: number }[]; 8 | rows?: { y?: number; height?: number }[]; 9 | position: { row: number; column: number }; 10 | index: string; 11 | x: number; 12 | y: number; 13 | width: number; 14 | height: number; 15 | content: any; 16 | style: CellStyle; 17 | type: string; 18 | rowSpan: number; 19 | colSpan: number; 20 | isCombined?: boolean; 21 | combineCell?: Cell; 22 | hidden?: boolean; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/core/model/float-element.ts: -------------------------------------------------------------------------------- 1 | export class FloatElement { 2 | constructor(x, y, width, height, content) { 3 | this.x = x; 4 | this.y = y; 5 | this.width = width; 6 | this.height = height; 7 | this.content = content; 8 | this.isActive = true; 9 | } 10 | 11 | x: number; 12 | y: number; 13 | width: number; 14 | height: number; 15 | content: any; 16 | isActive: boolean; 17 | moveOrigin: { x: number; y: number }; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/core/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cell-range.class'; 2 | export * from './cell-style.class'; 3 | export * from './cell.class'; 4 | export * from './key-code.enmu'; 5 | export * from './style.const'; 6 | export * from './value-type.enmu'; -------------------------------------------------------------------------------- /src/app/core/model/key-code.enmu.ts: -------------------------------------------------------------------------------- 1 | export enum KeyboardCode { 2 | Tab = 'Tab', 3 | Enter = 'Enter', 4 | PageUp = 'PageUp', 5 | PageDown = 'PageDown', 6 | Home = 'Home', 7 | ArrowLeft = 'ArrowLeft', 8 | ArrowUp = 'ArrowUp', 9 | ArrowRight = 'ArrowRight', 10 | ArrowDown = 'ArrowDown', 11 | Escape = 'Escape', 12 | Delete = 'Delete', 13 | Backspace = 'Backspace', 14 | KeyA = 'KeyA', 15 | KeyC = 'KeyC', 16 | KeyV = 'KeyV', 17 | KeyX = 'KeyX', 18 | KeyZ = 'KeyZ' 19 | } 20 | -------------------------------------------------------------------------------- /src/app/core/model/logic-position.enmu.ts: -------------------------------------------------------------------------------- 1 | export enum LogicPosition { 2 | LeftTop = 'leftTop', 3 | Top = 'top', 4 | rightTop = 'rightTop', 5 | Left = 'left', 6 | Right = 'right', 7 | LeftBottom = 'leftBottom', 8 | Bottom = 'bottom', 9 | RightBottom = 'rightBottom', 10 | Other = 'other', 11 | } 12 | -------------------------------------------------------------------------------- /src/app/core/model/operate-state.enum.ts: -------------------------------------------------------------------------------- 1 | export enum OperateState { 2 | None = 0, 3 | EditCell, 4 | SelectCell, 5 | UnSelectCell, 6 | SelectScrollYThumb, 7 | SelectScrollXThumb, 8 | SelectRulerX, 9 | SelectRulerY, 10 | UnSelectRulerX, 11 | UnSelectRulerY, 12 | ResizeColumn, 13 | ResizeRow, 14 | MoveFloat, 15 | ResizeFloat, 16 | } 17 | -------------------------------------------------------------------------------- /src/app/core/model/row.class.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuhuaLi/report-editor/802da80e24332a84fc07daeb47feae709bf9b742/src/app/core/model/row.class.ts -------------------------------------------------------------------------------- /src/app/core/model/status-state.enum.ts: -------------------------------------------------------------------------------- 1 | export enum StatusState { 2 | None = 0, 3 | ScrollXThumbHover, 4 | ScrollYThumbHover, 5 | } 6 | -------------------------------------------------------------------------------- /src/app/core/model/style.const.ts: -------------------------------------------------------------------------------- 1 | export const Style = { 2 | scrollBarBackgroundColor: '#f0f0f0', 3 | scrollBarBorderColor: '#cacaca', 4 | scrollBarThumbColor: '#c0c0c0', 5 | scrollBarThumbActiveColor: '#7b7b7b', 6 | scrollBarThumbMargin: 2, 7 | scrollBarBorderWidth: 0.5, 8 | scrollBarWidth: 18, 9 | scrollBarThumbMinSize: 15, 10 | scrollBarThumbRadius: 4, 11 | cellWidth: 100, 12 | cellHeight: 30, 13 | rulerCellBackgroundColor: '#f0f0f0', 14 | activeRulerCellBacgroundColor: '#c9c9c9', 15 | rulerCellColor: '#000', 16 | rulerCellFontSize: 12, 17 | rulerCellFontWeight: 'bold', 18 | rulerCellFontFamily: 'sans-serif', 19 | rulerCellBorderColor: '#aeaeae', 20 | rulerCellBorderWidth: 1, 21 | rulerResizeGapWidth: 4, 22 | cellBackgroundColor: '#fff', 23 | cellColor: '#000', 24 | cellFontSize: 10, 25 | cellFontWeight: 'normal', 26 | cellFontStyle: 'normal', 27 | cellFontFamily: 'sans-serif', 28 | cellBorderColor: '#aeaeae', 29 | cellBorderWidth: 1, 30 | cellTextAlignCenter: 'center', 31 | cellTextAlignLeft: 'left', 32 | cellTextAlignRight: 'right', 33 | cellTextBaseline: 'alphabetic', 34 | activeCellBorderWidth: 4, 35 | activeCellBorderColor: '#090', 36 | activeCellShadowColor: '#0f0', 37 | activeCellShadowBlur: 1, 38 | selectedCellBackgroundColor: '#0000001f', 39 | unSelectedCellBackgroundColor: '#ffffff80', 40 | activeFloatElementBorderColor: '#aeaeae', 41 | activeFloatElementBorderWidth: 1, 42 | activeFloatElementResizeArcRadius: 6, 43 | allCellTriangleSideChief: 15 44 | }; 45 | -------------------------------------------------------------------------------- /src/app/core/model/value-type.enmu.ts: -------------------------------------------------------------------------------- 1 | export enum ValueType { 2 | String = 'string', 3 | Number = 'number', 4 | } 5 | -------------------------------------------------------------------------------- /src/app/core/utils/function.ts: -------------------------------------------------------------------------------- 1 | export function inRange( 2 | num: number, 3 | start: number, 4 | end: number, 5 | inclusive?: boolean 6 | ) { 7 | return inclusive 8 | ? num >= Math.min(start, end) && num <= Math.max(start, end) 9 | : num > Math.min(start, end) && num < Math.max(start, end); 10 | } 11 | -------------------------------------------------------------------------------- /src/app/core/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './function'; 2 | -------------------------------------------------------------------------------- /src/app/editor/editor-panel/cell-edit/cell-edit.component.html: -------------------------------------------------------------------------------- 1 |
14 |
39 |
40 | -------------------------------------------------------------------------------- /src/app/editor/editor-panel/cell-edit/cell-edit.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuhuaLi/report-editor/802da80e24332a84fc07daeb47feae709bf9b742/src/app/editor/editor-panel/cell-edit/cell-edit.component.scss -------------------------------------------------------------------------------- /src/app/editor/editor-panel/cell-edit/cell-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterContentInit, 3 | AfterViewInit, 4 | ChangeDetectionStrategy, 5 | ChangeDetectorRef, 6 | Component, 7 | ElementRef, 8 | EventEmitter, 9 | Input, 10 | OnDestroy, 11 | OnInit, 12 | Output, 13 | ViewChild, 14 | } from '@angular/core'; 15 | import { DomSanitizer } from '@angular/platform-browser'; 16 | import { Cell } from 'src/app/core/model'; 17 | import { OperateState } from '../../../core/model/operate-state.enum'; 18 | 19 | @Component({ 20 | selector: 'app-cell-edit', 21 | templateUrl: './cell-edit.component.html', 22 | styleUrls: ['./cell-edit.component.scss'], 23 | }) 24 | export class CellEditComponent implements OnInit, AfterViewInit, OnDestroy { 25 | @Input() editingCell: Cell; 26 | @Input() state; 27 | @Input() multiple = 1; 28 | @Input() offsetLeft = 0; 29 | @Input() offsetTop = 0; 30 | @Input() scrollTop = 0; 31 | @Input() scrollLeft = 0; 32 | @Output() onkeydown: EventEmitter = new EventEmitter(); 33 | @Output() afterInit: EventEmitter = new EventEmitter(); 34 | @ViewChild('editArea') editArea: ElementRef; 35 | html: any; 36 | editStatus = OperateState.EditCell; 37 | 38 | constructor( 39 | private sanitizer: DomSanitizer, 40 | private cdr: ChangeDetectorRef 41 | ) {} 42 | 43 | ngOnInit(): void { 44 | this.html = this.safeHtml( 45 | this.editingCell.content.html || this.editingCell.content.value 46 | ); 47 | } 48 | 49 | ngAfterViewInit() { 50 | this.afterInit.emit(this.editArea.nativeElement); 51 | } 52 | 53 | safeHtml(html) { 54 | return (html && this.sanitizer.bypassSecurityTrustHtml(html)) || ''; 55 | } 56 | 57 | onpaste(event) { 58 | console.log('paste', event); 59 | } 60 | 61 | onInput(event) { 62 | if (this.editingCell) { 63 | this.editingCell.content.value = event.target.textContent.replace( 64 | / /g, 65 | ' ' 66 | ); 67 | } 68 | } 69 | 70 | ngOnDestroy() { 71 | // this.editingCell.content.html = this.editArea.nativeElement.innerHTML; 72 | // console.log(this.editArea.nativeElement.childNodes); 73 | // console.log(this.editArea.nativeElement.children); 74 | // console.log(this.editArea.nativeElement.textContent); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/editor/editor-panel/editor-panel.component.html: -------------------------------------------------------------------------------- 1 |
2 | GitHub 8 |
9 |
10 | 11 | 12 | 25 | B 39 | 53 | I 68 | 80 | 88 | 100 | 108 | 120 | 128 | 129 | 135 |
139 | 140 |
141 |
142 | 143 | 149 |
153 | 154 |
155 |
156 | 157 | 158 | 159 | 160 | 169 | 178 |
179 | 186 | 198 |
199 |
200 |
201 | 212 |
213 |
214 | 235 | 236 | 237 | 238 | 239 | 240 |
249 |
250 | 279 | 291 |
292 |
293 |
294 |
295 | 296 | 297 |
298 |
299 |
300 |
305 |
306 |
307 |
312 |
313 |
314 |
319 |
320 |
321 |
326 |
327 |
328 |
333 |
334 |
335 |
340 |
341 |
342 |
347 |
348 |
349 |
354 |
355 |
356 |
361 |
362 |
363 |
364 |
365 | -------------------------------------------------------------------------------- /src/app/editor/editor-panel/editor-panel.component.scss: -------------------------------------------------------------------------------- 1 | :host ::ng-deep { 2 | .panel-wrapper { 3 | position: relative; 4 | overflow: hidden; 5 | height: calc(100% - 100px); 6 | } 7 | .toolbar { 8 | user-select: none; 9 | background-color: rgba(0, 0, 0, 0.12); 10 | .iconfont { 11 | font-size: 1.5rem; 12 | } 13 | a { 14 | display: inline-block; 15 | padding: 4px 10px; 16 | cursor: pointer; 17 | } 18 | } 19 | 20 | .formula-bar { 21 | display: flex; 22 | height: 25px; 23 | line-height: 25px; 24 | min-height: 25px; 25 | z-index: 99; 26 | border: 1px solid #e2e4e8; 27 | input { 28 | border: none; 29 | flex: auto; 30 | padding: 0 4px; 31 | &:focus { 32 | outline: none; 33 | } 34 | } 35 | .bar-label { 36 | display: inline-block; 37 | width: 60px; 38 | height: 100%; 39 | box-sizing: border-box; 40 | border-right: 1px solid #e0e2e4; 41 | text-align: center; 42 | line-height: inherit; 43 | background-color: #fff; 44 | color: #777; 45 | -webkit-touch-callout: none; 46 | -webkit-user-select: none; 47 | -moz-user-select: none; 48 | -ms-user-select: none; 49 | user-select: none; 50 | overflow: hidden; 51 | white-space: nowrap; 52 | text-overflow: ellipsis; 53 | padding: 0 4px; 54 | } 55 | } 56 | 57 | canvas { 58 | position: absolute; 59 | top: 0; 60 | left: 0; 61 | right: 0; 62 | bottom: 0; 63 | &:focus { 64 | outline: none; 65 | } 66 | } 67 | 68 | .action-panel { 69 | background: transparent; 70 | pointer-events: none; 71 | display: block; 72 | width: 100%; 73 | height: 100%; 74 | } 75 | 76 | .mask { 77 | position: absolute; 78 | top: 0; 79 | left: 0; 80 | right: 0; 81 | bottom: 0; 82 | overflow: hidden; 83 | pointer-events: none; 84 | .editCell { 85 | display: table-cell; 86 | padding: 0; 87 | // position: absolute; 88 | box-sizing: border-box; 89 | pointer-events: auto; 90 | // white-space: nowrap; 91 | white-space:pre; 92 | overflow: hidden; 93 | vertical-align: middle; 94 | line-height: 1; 95 | // border: 2px solid #096; 96 | border: none; 97 | &:focus { 98 | outline: none; 99 | } 100 | } 101 | } 102 | 103 | .palette-color-grid { 104 | font-size: 0; 105 | white-space: nowrap; 106 | line-height: 0; 107 | box-sizing: border-box; 108 | padding: 0 12px; 109 | z-index: 1000; 110 | background-color: rgba(0, 0, 0, 0.12); 111 | .color-box { 112 | box-sizing: border-box; 113 | display: inline-block; 114 | position: relative; 115 | width: 20px; 116 | height: 20px; 117 | padding: 1px; 118 | border: 1px solid transparent; 119 | .inner-box { 120 | width: 16px; 121 | height: 16px; 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/app/editor/editor-panel/editor-panel.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | ViewChild, 5 | ElementRef, 6 | AfterViewInit, 7 | HostListener, 8 | ChangeDetectorRef, 9 | ChangeDetectionStrategy, 10 | ViewContainerRef, 11 | } from '@angular/core'; 12 | import { DomSanitizer } from '@angular/platform-browser'; 13 | import { Panel } from './panel'; 14 | 15 | @Component({ 16 | selector: 'app-editor-panel', 17 | templateUrl: './editor-panel.component.html', 18 | styleUrls: ['./editor-panel.component.scss'], 19 | changeDetection: ChangeDetectionStrategy.OnPush, 20 | }) 21 | export class EditorPanelComponent 22 | extends Panel 23 | implements OnInit, AfterViewInit 24 | { 25 | @ViewChild('panel') panel: ElementRef; 26 | @ViewChild('actionPanel') actionPanel: ElementRef; 27 | @ViewChild('animationPanel') animationPanel: ElementRef; 28 | @ViewChild('floatPanel') floatPanel: ElementRef; 29 | @ViewChild('floatActionPanel') floatActionPanel: ElementRef; 30 | @ViewChild('cellEditor') cellEditor: any; 31 | zoomSize: number; 32 | selection = []; 33 | 34 | fontFamilyArr = [ 35 | 'sans-serif', 36 | 'Arial', 37 | 'SimSun', 38 | 'SimHei', 39 | 'Microsoft YaHei', 40 | 'KaiTi', 41 | 'Microsoft JhengHei', 42 | ]; 43 | fontSizeArr = Array.from({ length: 30 }).map((val, idx) => idx + 10); 44 | 45 | showFontColorPanel = false; 46 | showBackColorPanel = false; 47 | 48 | constructor( 49 | private elmentRef: ElementRef, 50 | private cdr: ChangeDetectorRef, 51 | private sanitizer: DomSanitizer 52 | ) { 53 | super(); 54 | } 55 | 56 | @HostListener('window:resize', ['$event']) 57 | onResize(event) { 58 | console.log('resize'); 59 | this.resize(); 60 | this.refreshView(); 61 | } 62 | 63 | ngOnInit(): void {} 64 | 65 | ngAfterViewInit() { 66 | this.canvas = this.panel.nativeElement; 67 | this.actionCanvas = this.actionPanel.nativeElement; 68 | this.animationCanvas = this.animationPanel.nativeElement; 69 | this.floatCanvas = this.floatPanel.nativeElement; 70 | this.floatActionCanvas = this.floatActionPanel.nativeElement; 71 | this.init(); 72 | this.cdr.detectChanges(); 73 | } 74 | 75 | editCellCompelte(change = true) { 76 | if (change) { 77 | this.editingCell.content.html = 78 | this.cellEditor.editArea.nativeElement.innerHTML; 79 | } 80 | super.editCellCompelte(change); 81 | } 82 | 83 | zoomIn() { 84 | if (this.multiple < 1.5) { 85 | this.multiple += 0.1; 86 | this.init(); 87 | this.cdr.detectChanges(); 88 | } 89 | } 90 | zoomOut() { 91 | if (this.multiple > 0.5) { 92 | this.multiple -= 0.1; 93 | this.init(); 94 | this.cdr.detectChanges(); 95 | } 96 | } 97 | 98 | onBarInputMouseup(event) { 99 | // console.log(event.target.selectionStart, event.target.selectionEnd); 100 | const input = event.target; 101 | if (input.selectionStart !== input.selectionEnd) { 102 | this.selection = [input.selectionStart, input.selectionEnd]; 103 | } else { 104 | this.selection = []; 105 | } 106 | } 107 | 108 | onBarInputKeypress(event) { 109 | // console.log(event.target.selectionStart, event.target.selectionEnd); 110 | const input = event.target; 111 | if (input.selectionStart !== input.selectionEnd) { 112 | this.selection = [input.selectionStart, input.selectionEnd]; 113 | } else { 114 | this.selection = []; 115 | } 116 | } 117 | 118 | onBarInputFocus(event) { 119 | // event.preventDefault(); 120 | event.target.focus(); 121 | if (!this.editingCell) { 122 | this.editingCell = this.activeCell; 123 | this.editingCell.content.previousValue = this.editingCell.content.value; 124 | this.editingCell.content.previousHtml = this.editingCell.content.html; 125 | } 126 | // console.log(this.getSelection(), this.getSelection().toString()); 127 | } 128 | 129 | toggleFontColorPanel(event) { 130 | // event.preventDefault(); 131 | this.showFontColorPanel = !this.showFontColorPanel; 132 | // this.state.isCellEdit = true; 133 | } 134 | toggleBackColorPanel() { 135 | this.showBackColorPanel = !this.showBackColorPanel; 136 | // this.state.isCellEdit = true; 137 | } 138 | changeColor(event) { 139 | if (this.showFontColorPanel) { 140 | this.changeStyle({ color: event.target.style.backgroundColor }); 141 | } else if (this.showBackColorPanel) { 142 | this.changeStyle({ background: event.target.style.backgroundColor }); 143 | } 144 | this.cdr.detectChanges(); 145 | } 146 | 147 | insertImage(event) { 148 | for (const file of event.target.files) { 149 | if (/image\/\w*/.test(file.type)) { 150 | createImageBitmap(event.target.files[0]).then((img) => { 151 | this.addImage(img); 152 | event.target.value = null; 153 | }); 154 | break; 155 | } 156 | } 157 | this.canvas.focus(); 158 | } 159 | 160 | getSelection() { 161 | return window.getSelection 162 | ? window.getSelection() 163 | : document.getSelection 164 | ? document.getSelection() 165 | : null; 166 | } 167 | 168 | changeStyle(style) { 169 | const selection = this.getSelection(); 170 | if (selection && selection.toString()) { 171 | document.execCommand('styleWithCss', false); 172 | Object.keys(style).forEach((key) => { 173 | switch (key) { 174 | case 'fontWeight': 175 | document.execCommand('bold', false); 176 | break; 177 | case 'color': 178 | document.execCommand('foreColor', false, style[key]); 179 | break; 180 | case 'fontSize': 181 | document.execCommand('fontSize', false, style[key]); 182 | selection.anchorNode.parentElement.style.fontSize = 183 | style[key] + 'pt'; 184 | break; 185 | case 'fontFamily': 186 | document.execCommand('fontName', false, style[key]); 187 | break; 188 | case 'fontStyle': 189 | document.execCommand('italic', false); 190 | break; 191 | default: 192 | this.changeCellStyle(style); 193 | break; 194 | } 195 | }); 196 | } else { 197 | this.changeCellStyle(style); 198 | } 199 | } 200 | 201 | afterEditCellInit(event) { 202 | const selection = this.getSelection(); 203 | if (selection && selection.anchorOffset === 0) { 204 | const range = document.createRange(); 205 | range.selectNodeContents(event); 206 | range.collapse(false); 207 | selection.removeAllRanges(); 208 | selection.addRange(range); 209 | } 210 | } 211 | 212 | clickWithoutFocus(event) { 213 | const selection = this.getSelection(); 214 | event.preventDefault(); 215 | return false; 216 | } 217 | 218 | onEditCellDestory(event) { 219 | this.editingCell.content.html = this.sanitizer.bypassSecurityTrustHtml( 220 | event.target.innerHTML 221 | ); 222 | } 223 | 224 | deleteCellValue(prev, cur, element, index) { 225 | const textObj = this.parseNode(element); 226 | let del; 227 | if (cur.length === index) { 228 | del = prev.substring(index); 229 | } else { 230 | del = prev.substr(index, prev.length - cur.length); 231 | } 232 | let curIndex = 0; 233 | for (const obj of textObj) { 234 | const textLen = obj.text.length; 235 | // if (curIndex <= index && curIndex + obj.text.length > index) { 236 | // if (del.length < obj.text.length) { 237 | // obj.node.textContent = obj.node.textContent.replace(del, ''); 238 | // } else { 239 | // if (curIndex === index) { 240 | // obj.node.parentNode.removeChild(obj.node); 241 | // } else { 242 | // obj.node.textContent = obj.node.textContent.substring( 243 | // curIndex, 244 | // index 245 | // ); 246 | // } 247 | // del = del.substring(textLen - index + curIndex); 248 | // index += textLen - index + curIndex; 249 | // } 250 | // } 251 | if (curIndex <= index && curIndex + obj.text.length > index) { 252 | if (curIndex === index && obj.text.length <= del.length) { 253 | obj.node.parentNode.removeChild(obj.node); 254 | } else { 255 | obj.node.textContent = 256 | obj.node.textContent.substring(0, index - curIndex) + 257 | obj.node.textContent.substring(index - curIndex + del.length); 258 | } 259 | del = del.substring(textLen - index + curIndex); 260 | index += textLen - index + curIndex; 261 | } 262 | curIndex += textLen; 263 | } 264 | } 265 | 266 | refreshEditCellValue(event, input) { 267 | let prev = this.editingCell.content.value || ''; 268 | const cur = event; 269 | 270 | // const element = this.htmlToElement( 271 | // this.editingCell.content.html || this.editingCell.content.value 272 | // ); 273 | const element = this.cellEditor.editArea.nativeElement; 274 | 275 | if (this.selection.length) { 276 | this.deleteCellValue( 277 | prev, 278 | prev.substring(0, this.selection[0]) + prev.substring(this.selection[1]), 279 | element, 280 | this.selection[0] 281 | ); 282 | // input.setSelectionRange( 283 | // cur.length - prev.length + this.selection[1], 284 | // cur.length - prev.length + this.selection[1] 285 | // ); 286 | prev = prev.replace( 287 | prev.substring(this.selection[0], this.selection[1]), 288 | '' 289 | ); 290 | this.selection = []; 291 | } 292 | const textObj = this.parseNode(element); 293 | if (prev.length === cur.length) { 294 | // replace 295 | let i = 0; 296 | let index = -1; 297 | let diffLen = 0; 298 | while (i < cur.length) { 299 | if (cur[i] !== prev[i] && index === -1) { 300 | index = i; 301 | } 302 | if (cur.substring(i) === prev.substring(i) && index > -1) { 303 | break; 304 | } 305 | if (index > -1) { 306 | diffLen++; 307 | } 308 | i++; 309 | } 310 | let diff = cur.substring(index, index + diffLen); 311 | let curIndex = 0; 312 | for (const obj of textObj) { 313 | if (curIndex <= index && curIndex + obj.text.length > index) { 314 | if (diff.length <= obj.text.length) { 315 | const charArr = obj.node.textContent.split(''); 316 | charArr.splice(index - curIndex, diff.length, ...diff); 317 | obj.node.textContent = charArr.join(''); 318 | break; 319 | } else { 320 | const charArr = obj.node.textContent.split(''); 321 | charArr.splice( 322 | index - curIndex, 323 | obj.text.length, 324 | ...diff.substring(0, obj.text.length - index + curIndex) 325 | ); 326 | obj.node.textContent = charArr.join(''); 327 | diff = diff.substring(obj.text.length - index + curIndex); 328 | index += obj.text.length - index + curIndex; 329 | } 330 | } 331 | curIndex += obj.text.length; 332 | } 333 | // this.cellEditor.cdr.detectChanges(); 334 | // this.cdr.detectChanges(); 335 | } else if (prev.length > cur.length) { 336 | // delete 337 | let index = input.selectionStart; 338 | let del; 339 | if (cur.length === index) { 340 | del = prev.substring(index); 341 | } else { 342 | del = prev.substr(index, prev.length - cur.length); 343 | } 344 | let curIndex = 0; 345 | for (const obj of textObj) { 346 | const textLen = obj.text.length; 347 | if (curIndex <= index && curIndex + obj.text.length > index) { 348 | if (del.length < obj.text.length) { 349 | obj.node.textContent = 350 | obj.node.textContent.substring(0, index - curIndex) + 351 | obj.node.textContent.substring(index - curIndex + del.length); 352 | // obj.node.textContent = obj.node.textContent.replace(del, ''); 353 | } else { 354 | if (curIndex === index) { 355 | obj.node.parentNode.removeChild(obj.node); 356 | } else { 357 | obj.node.textContent = obj.node.textContent.substring( 358 | curIndex, 359 | index 360 | ); 361 | } 362 | del = del.substring(textLen - index + curIndex); 363 | index += textLen - index + curIndex; 364 | } 365 | } 366 | curIndex += textLen; 367 | } 368 | } else { 369 | // add 370 | const index = input.selectionStart - cur.length + prev.length; 371 | const add = cur.substr(index, cur.length - prev.length); 372 | let curIndex = 0; 373 | for (const obj of textObj) { 374 | const textLen = obj.text.length; 375 | if (curIndex <= index && curIndex + obj.text.length > index) { 376 | obj.node.textContent = 377 | obj.node.textContent.substring(0, index - curIndex) + 378 | add + 379 | obj.node.textContent.substring(index - curIndex); 380 | break; 381 | } 382 | curIndex += textLen; 383 | } 384 | if (input.selectionStart === cur.length && textObj.length) { 385 | textObj[textObj.length - 1].node.textContent += add; 386 | } else if (!textObj.length) { 387 | element.textContent = add; 388 | } 389 | } 390 | 391 | // if (!this.editingCell) { 392 | // this.cellEditor.editArea.nativeElement.innerHTML = ''; 393 | // this.cellEditor.editArea.nativeElement.appendChild(element); 394 | // this.editingCell.content.html = this.cellEditor.editArea.nativeElement.innerHTML; 395 | // } 396 | this.editingCell.content.value = event; 397 | 398 | // this.cellEditor.editArea.nativeElement.textContent = event; 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /src/app/editor/editor-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { EditorComponent } from './editor.component'; 2 | import { NgModule } from '@angular/core'; 3 | import { Routes, RouterModule } from '@angular/router'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: EditorComponent, 9 | }, 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule], 15 | }) 16 | export class EditorRoutingModule {} 17 | -------------------------------------------------------------------------------- /src/app/editor/editor.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/editor/editor.component.scss: -------------------------------------------------------------------------------- 1 | :host ::ng-deep { 2 | .panel { 3 | display: block; 4 | width: 100%; 5 | height: 100%; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/editor/editor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-editor', 5 | templateUrl: './editor.component.html', 6 | styleUrls: ['./editor.component.scss'], 7 | }) 8 | export class EditorComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit(): void {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/editor/editor.module.ts: -------------------------------------------------------------------------------- 1 | import { CoreModule } from './../core/core.module'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { NgModule } from '@angular/core'; 4 | import { CommonModule } from '@angular/common'; 5 | 6 | import { EditorRoutingModule } from './editor-routing.module'; 7 | import { EditorComponent } from './editor.component'; 8 | import { EditorPanelComponent } from './editor-panel/editor-panel.component'; 9 | import { CellEditComponent } from './editor-panel/cell-edit/cell-edit.component'; 10 | 11 | @NgModule({ 12 | declarations: [EditorComponent, EditorPanelComponent, CellEditComponent], 13 | imports: [CommonModule, EditorRoutingModule, FormsModule, CoreModule], 14 | }) 15 | export class EditorModule {} 16 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuhuaLi/report-editor/802da80e24332a84fc07daeb47feae709bf9b742/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuhuaLi/report-editor/802da80e24332a84fc07daeb47feae709bf9b742/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ReportEditor 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/style/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face {font-family: "iconfont"; 2 | src: url('iconfont.eot?t=1599545939561'); /* IE9 */ 3 | src: url('iconfont.eot?t=1599545939561#iefix') format('embedded-opentype'), /* IE6-IE8 */ 4 | url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAW0AAsAAAAAC4wAAAVlAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCECgqJAIc0ATYCJAMkCxQABCAFhG0HgQQb5AlRlA/SILIfhU2ZezQSi+FkGSJkkTe5mb/ek8N4P4Ln37XP+/KS/ExmkRWVmByxXVdAxYrQ7vFldKhwSuT7j9q02aZK6kLNqLiTtqeRhVCBHFnOTb9mL3HRk5WWrWtHKfdvkovL1K/jD1bXzh6kVMEXrb2k02PmKQ0oQKV0ThdNKMARxdgAr67xXaPfvhsCRJqIA40rr20FWyxykACyQeC1YFsyY3sIQtD5ljNtP3QARzD7zGNgf/zz8o0+EsDgFLnXrFVlBijMmDe7VFvzY1mmY9m6PSdgnwcKdAOwIEdb3UdA03Q3VMTp37WsASjDGRUm6/FsctadkWxwNuLzmjeT/v+H+iiZJpTpP3mAxSEYFE8gB+KBusDAb8kEyBgfgYWsyoiDphtGBBoCIwaawTCi0IwAAg+f1yAI8H1SGzkAkvCMMoAGQOYCyCNEOy5W63O5v+JQDLbWvsGeUYUFzlmbVrExaPGZRe1s6oKRxN5m60223dq2GW5oW5Q7xg2/Yw0ddZOtt6Wn0Lbt07d6AxVsuXJbuBqWbZuxA6GUTh23fYTzRow9Fe6yZcadHuo4xzjR91/Y0T/rZenbjxiwZtRuM2rsyVTXzd1RIES6HNqecp7eeCP8l1W4w5HZy6XruRanJoMnx2NHzHNsmzsrLMX0074r7LVoR7B3jr39rScJZ4cqEB62UnfKwhNUq60wNzbU5/IJEdTGrcM0w6mhao2Drk+H1t6m0EQp8IWSLnawKTCpFPoCiQvY3DYK1K15GiSfXxgX5rciBELzC1h1nrODL1y48DVxeg2avP/FiH6jkv/2Gdiv38Dkf71Hpo64M2eyTJueuGfTxR+my5Hp6K7OvXZ9bhDU69dVken6mfwK1679Oq4QtyFhvAUhrkNUk0tQgoxhQ11Jw25DIFe3bj3dNz1lbbjc5cCY0qSvxuR/0m3UKmwiRt+vrv0OlwJuN78e8zXx9vXjzwdy/KwV/WvM71CrxhNJbmcfQfGj6ymJr0d/S4RHjmXlpaGl5c1ttbXNzS3Ns7OysvUqzT3jxj0eN/57eYDe2A5Nrg9N8T8Rssg8vXbRqKjAoQNGtQdGDxjKhIk5qqOPLy6zGCqGfTq19n/Mu87Xd/eihft1I/QeWLhRZbPRYv5BEyhEqyBQ9aTqKj+l28VZzxsfQ+8DvhxyT4npdHeL4Yq3TF8+YPUs/RUfqjoWdnRK/zQ2quRTelpkofGkMphtaPie/j1QsFkcYEC7aQHat4cxWwHIr9W3OigWHGvRrddb4W5qDgDoFot/QP6+rtWeRHqq47Qse0gfac2fYi/+9eNPD3DK/RrUP5QVb0aNbWFjfux2OscCAQNLS/4Cfe4agLvpyMSS2IncFy01si5eg5IwGqukAaJOwBffX8a97Worl06CEEoSGww5GkEJNKMtvhs4UvqBJzASIl2pOD+lHBkVYhOgC5MIhBL2gKGIa6CUcAlt8XfBUcVb8JSIQmSBuF0xpVXWUiUy1hJsoHXDaEEviZxnI1RS24x5pUcrF2UxkcWyWYjRSUxiNVeBRSxv4xjzYD6ZEI7mZKmXLqf74Z4eiTbJUhfWE8ZIiCk7IYFreyFGL/UCxVhkMFoIGAM0OoZBI6BHQoQLpxOo2vc3g+FR6EGL3DNySsuCkTEjbJ4mCYbEJdAVanGpkceyvZnB8CRDIHDGfhwyEr3QlMOJ6dG0BI2pfbcuMHoIDMY1kiaySeCluGXVzP61vY/1BiAiL5aIERUrTrzkJEgiEb9KK/IGre1wRTIowiDBdpikJCfYdCnDjZLI2xFBK+qtm9qYFWmooJXshgtEGKYVzdjJiHXCqk5xmLIJFwYAAAA=') format('woff2'), 5 | url('iconfont.woff?t=1599545939561') format('woff'), 6 | url('iconfont.ttf?t=1599545939561') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ 7 | url('iconfont.svg?t=1599545939561#iconfont') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .iconfont { 11 | font-family: "iconfont" !important; 12 | font-size: 16px; 13 | font-style: normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .icon-fangda:before { 19 | content: "\e66b"; 20 | } 21 | 22 | .icon-zuoduiqi:before { 23 | content: "\e674"; 24 | } 25 | 26 | .icon-youduiqi:before { 27 | content: "\e676"; 28 | } 29 | 30 | .icon-juzhong:before { 31 | content: "\e62e"; 32 | } 33 | 34 | .icon-tianchong:before { 35 | content: "\e649"; 36 | } 37 | 38 | .icon-suoxiao:before { 39 | content: "\e67b"; 40 | } 41 | 42 | .icon-zitiyanse:before { 43 | content: "\ec85"; 44 | } 45 | 46 | .icon-hebingdanyuange:before { 47 | content: "\e89f"; 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/style/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuhuaLi/report-editor/802da80e24332a84fc07daeb47feae709bf9b742/src/style/iconfont.eot -------------------------------------------------------------------------------- /src/style/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by iconfont 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/style/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuhuaLi/report-editor/802da80e24332a84fc07daeb47feae709bf9b742/src/style/iconfont.ttf -------------------------------------------------------------------------------- /src/style/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuhuaLi/report-editor/802da80e24332a84fc07daeb47feae709bf9b742/src/style/iconfont.woff -------------------------------------------------------------------------------- /src/style/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuhuaLi/report-editor/802da80e24332a84fc07daeb47feae709bf9b742/src/style/iconfont.woff2 -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | html, 3 | body { 4 | width: 100%; 5 | height: 100%; 6 | margin: 0; 7 | padding: 0; 8 | overflow: hidden; 9 | } 10 | 11 | article, 12 | aside, 13 | blockquote, 14 | body, 15 | button, 16 | code, 17 | dd, 18 | details, 19 | div, 20 | dl, 21 | dt, 22 | fieldset, 23 | figcaption, 24 | figure, 25 | footer, 26 | form, 27 | h1, 28 | h2, 29 | h3, 30 | h4, 31 | h5, 32 | h6, 33 | header, 34 | hgroup, 35 | hr, 36 | input, 37 | legend, 38 | li, 39 | menu, 40 | nav, 41 | ol, 42 | p, 43 | pre, 44 | section, 45 | td, 46 | textarea, 47 | th, 48 | ul { 49 | margin: 0; 50 | padding: 0; 51 | } 52 | @import './style/iconfont.css'; 53 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "lib": [ 15 | "es2018", 16 | "dom" 17 | ] 18 | }, 19 | "angularCompilerOptions": { 20 | "fullTemplateTypeCheck": true, 21 | "strictInjectionParameters": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "align": { 5 | "options": [ 6 | "parameters", 7 | "statements" 8 | ] 9 | }, 10 | "array-type": false, 11 | "arrow-return-shorthand": true, 12 | "curly": true, 13 | "deprecation": { 14 | "severity": "warning" 15 | }, 16 | "component-class-suffix": true, 17 | "contextual-lifecycle": true, 18 | "directive-class-suffix": true, 19 | "directive-selector": [ 20 | true, 21 | "attribute", 22 | "app", 23 | "camelCase" 24 | ], 25 | "component-selector": [ 26 | true, 27 | "element", 28 | "app", 29 | "kebab-case" 30 | ], 31 | "eofline": true, 32 | "import-blacklist": [ 33 | true, 34 | "rxjs/Rx" 35 | ], 36 | "import-spacing": true, 37 | "indent": { 38 | "options": [ 39 | "spaces" 40 | ] 41 | }, 42 | "max-classes-per-file": false, 43 | "max-line-length": [ 44 | true, 45 | 140 46 | ], 47 | "member-ordering": [ 48 | true, 49 | { 50 | "order": [ 51 | "static-field", 52 | "instance-field", 53 | "static-method", 54 | "instance-method" 55 | ] 56 | } 57 | ], 58 | "no-console": [ 59 | true, 60 | "debug", 61 | "info", 62 | "time", 63 | "timeEnd", 64 | "trace" 65 | ], 66 | "no-empty": false, 67 | "no-inferrable-types": [ 68 | true, 69 | "ignore-params" 70 | ], 71 | "no-non-null-assertion": true, 72 | "no-redundant-jsdoc": true, 73 | "no-switch-case-fall-through": true, 74 | "no-var-requires": false, 75 | "object-literal-key-quotes": [ 76 | true, 77 | "as-needed" 78 | ], 79 | "quotemark": [ 80 | true, 81 | "single" 82 | ], 83 | "semicolon": { 84 | "options": [ 85 | "always" 86 | ] 87 | }, 88 | "space-before-function-paren": { 89 | "options": { 90 | "anonymous": "never", 91 | "asyncArrow": "always", 92 | "constructor": "never", 93 | "method": "never", 94 | "named": "never" 95 | } 96 | }, 97 | "typedef-whitespace": { 98 | "options": [ 99 | { 100 | "call-signature": "nospace", 101 | "index-signature": "nospace", 102 | "parameter": "nospace", 103 | "property-declaration": "nospace", 104 | "variable-declaration": "nospace" 105 | }, 106 | { 107 | "call-signature": "onespace", 108 | "index-signature": "onespace", 109 | "parameter": "onespace", 110 | "property-declaration": "onespace", 111 | "variable-declaration": "onespace" 112 | } 113 | ] 114 | }, 115 | "variable-name": { 116 | "options": [ 117 | "ban-keywords", 118 | "check-format", 119 | "allow-pascal-case" 120 | ] 121 | }, 122 | "whitespace": { 123 | "options": [ 124 | "check-branch", 125 | "check-decl", 126 | "check-operator", 127 | "check-separator", 128 | "check-type", 129 | "check-typecast" 130 | ] 131 | }, 132 | "no-conflicting-lifecycle": true, 133 | "no-host-metadata-property": true, 134 | "no-input-rename": true, 135 | "no-inputs-metadata-property": true, 136 | "no-output-native": true, 137 | "no-output-on-prefix": true, 138 | "no-output-rename": true, 139 | "no-outputs-metadata-property": true, 140 | "template-banana-in-box": true, 141 | "template-no-negated-async": true, 142 | "use-lifecycle-interface": true, 143 | "use-pipe-transform-interface": true 144 | }, 145 | "rulesDirectory": [ 146 | "codelyzer" 147 | ] 148 | } --------------------------------------------------------------------------------