From 953f812c4502221150cd6a1e1618ccb9396311af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 30 Jun 2022 17:24:56 +0200 Subject: [PATCH] Add prism editor for webpage --- client/package.json | 3 +- .../instance/webpage/components/index.ts | 4 +- .../webpage-form-content.component.html | 22 +++++ .../webpage-form-content.component.scss | 56 +++++++++++++ .../webpage-form-content.component.ts | 82 +++++++++++++++++++ .../components/webpage-form.component.html | 6 +- .../webpage/services/prism.service.ts | 28 +++++++ .../admin/instance/webpage/webpage.module.ts | 8 +- .../metamodel/selectors/webpage.selector.ts | 8 +- client/src/styles.scss | 32 ++++++++ client/yarn.lock | 18 ++-- 11 files changed, 239 insertions(+), 28 deletions(-) create mode 100644 client/src/app/admin/instance/webpage/components/webpage-form-content.component.html create mode 100644 client/src/app/admin/instance/webpage/components/webpage-form-content.component.scss create mode 100644 client/src/app/admin/instance/webpage/components/webpage-form-content.component.ts create mode 100644 client/src/app/admin/instance/webpage/services/prism.service.ts diff --git a/client/package.json b/client/package.json index 7cb731f8..48959757 100644 --- a/client/package.json +++ b/client/package.json @@ -24,7 +24,6 @@ "@ngrx/router-store": "13.0.2", "@ngrx/store": "13.0.2", "@ngrx/store-devtools": "13.0.2", - "@tinymce/tinymce-angular": "^6.0.1", "bootstrap": "4.6.1", "d3": "^5.15.1", "file-saver": "^2.0.5", @@ -34,8 +33,8 @@ "ngx-dynamic-hooks": "^2.0.3", "ngx-json-viewer": "^3.0.2", "ngx-toastr": "^14.2.1", + "prismjs": "^1.28.0", "rxjs": "~7.5.0", - "tinymce": "^6.0.3", "tslib": "^2.3.0", "zone.js": "~0.11.4" }, diff --git a/client/src/app/admin/instance/webpage/components/index.ts b/client/src/app/admin/instance/webpage/components/index.ts index 401f94b1..4d87cc8d 100644 --- a/client/src/app/admin/instance/webpage/components/index.ts +++ b/client/src/app/admin/instance/webpage/components/index.ts @@ -13,6 +13,7 @@ import { EditWebpageFamilyComponent } from './edit-webpage-family.component'; import { WebpageFamilyFormComponent } from './webpage-family-form.component'; import { WebpageCardComponent } from './webpage-card.component'; import { WebpageFormComponent } from './webpage-form.component'; +import { WebpageFormContentComponent } from './webpage-form-content.component'; export const dummiesComponents = [ WebpageButtonsComponent, @@ -20,5 +21,6 @@ export const dummiesComponents = [ EditWebpageFamilyComponent, WebpageFamilyFormComponent, WebpageCardComponent, - WebpageFormComponent + WebpageFormComponent, + WebpageFormContentComponent ]; diff --git a/client/src/app/admin/instance/webpage/components/webpage-form-content.component.html b/client/src/app/admin/instance/webpage/components/webpage-form-content.component.html new file mode 100644 index 00000000..15c6c4d7 --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/webpage-form-content.component.html @@ -0,0 +1,22 @@ +<form [formGroup]="form" novalidate> + <div class="form-group"> + <label for="{{ controlName }}">{{ controlLabel }}</label> + <div class="code-container line-numbers"> + <textarea + #textArea + placeholder="Add your code (default html)" + class="text-area-code-editor" + id="{{ controlName }}" + name="{{ controlName }}" + formControlName="{{ controlName }}" + spellcheck="false" + ></textarea> + <pre + aria-hidden="true" + class="pre line-numbers" + #pre + ><code [ngClass]="['code', 'language-' + codeType]" #codeContent></code> + </pre> + </div> + </div> +</form> diff --git a/client/src/app/admin/instance/webpage/components/webpage-form-content.component.scss b/client/src/app/admin/instance/webpage/components/webpage-form-content.component.scss new file mode 100644 index 00000000..f72059d2 --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/webpage-form-content.component.scss @@ -0,0 +1,56 @@ +.code-container { + position: relative; + overflow: hidden; + min-height: 150px; +} + +.text-area-code-editor { + &, + &:focus { + box-shadow: none; + outline: 0; + border: 0; + } +} + +.text-area-code-editor, +.pre, +.code { + tab-size: 4; + margin: 0; + padding: 0; + font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; + font-size: 1em; + line-height: 1.5; +} + +.pre { + height: 100%; + min-height: inherit; + z-index: 1; + overflow: auto; + padding: 15px 0 0 70px; + + .code { + min-height: inherit; + overflow: hidden; + height: 100%; + } +} + +.text-area-code-editor { + caret-color: rgb(255, 255, 255); + white-space: nowrap; + overflow: auto; + text-align: left; + display: block; + resize: none; + position: absolute; + top: 0; + bottom: 0; + width: 100%; + color: transparent; + background: transparent; + z-index: 2; + padding: 15px 0 0 70px; +} diff --git a/client/src/app/admin/instance/webpage/components/webpage-form-content.component.ts b/client/src/app/admin/instance/webpage/components/webpage-form-content.component.ts new file mode 100644 index 00000000..72bad32d --- /dev/null +++ b/client/src/app/admin/instance/webpage/components/webpage-form-content.component.ts @@ -0,0 +1,82 @@ +import { AfterViewChecked, AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { fromEvent, Subscription } from 'rxjs'; + +import { PrismService } from '../services/prism.service'; + +@Component({ + selector: 'app-webpage-form-content', + templateUrl: 'webpage-form-content.component.html', + styleUrls: ['webpage-form-content.component.scss'] +}) +export class WebpageFormContentComponent implements OnInit, AfterViewChecked, AfterViewInit, OnDestroy { + @Input() form: FormGroup; + @Input() controlName: string; + @Input() controlLabel: string; + + @ViewChild('textArea', { static: true }) + textArea!: ElementRef; + @ViewChild('codeContent', { static: true }) + codeContent!: ElementRef; + @ViewChild('pre', { static: true }) + pre!: ElementRef; + + sub!: Subscription; + highlighted = false; + codeType = 'html'; + + constructor( + private prismService: PrismService, + private renderer: Renderer2 + ) {} + + ngOnInit(): void { + this.listenForm() + this.synchronizeScroll(); + if (this.form.value[this.controlName]) { + this.highlight(this.form.value[this.controlName]); + } + } + + ngAfterViewInit() { + this.prismService.highlightAll(); + } + + ngAfterViewChecked() { + if (this.highlighted) { + this.prismService.highlightAll(); + this.highlighted = false; + } + } + + highlight(content: string) { + const modifiedContent = this.prismService.convertHtmlIntoString(content); + + this.renderer.setProperty(this.codeContent.nativeElement, 'innerHTML', modifiedContent); + + this.highlighted = true; + } + + ngOnDestroy() { + this.sub?.unsubscribe(); + } + + private listenForm() { + this.sub = this.form.valueChanges.subscribe(val => { + this.highlight(val[this.controlName]); + }); + } + + private synchronizeScroll() { + const localSub = fromEvent(this.textArea.nativeElement, 'scroll').subscribe(() => { + const toTop = this.textArea.nativeElement.scrollTop; + const toLeft = this.textArea.nativeElement.scrollLeft; + + this.renderer.setProperty(this.pre.nativeElement, 'scrollTop', toTop); + this.renderer.setProperty(this.pre.nativeElement, 'scrollLeft', toLeft + 0.2); + }); + + this.sub.add(localSub); + } +} diff --git a/client/src/app/admin/instance/webpage/components/webpage-form.component.html b/client/src/app/admin/instance/webpage/components/webpage-form.component.html index a8c4a0ad..90cd8c62 100644 --- a/client/src/app/admin/instance/webpage/components/webpage-form.component.html +++ b/client/src/app/admin/instance/webpage/components/webpage-form.component.html @@ -22,11 +22,7 @@ <option *ngFor="let family of webpageFamilyList" [ngValue]="family.id">{{ family.label }}</option> </select> </div> - <div class="form-group"> - <label for="content">Content</label> - <textarea class="form-control" id="content" rows="20" formControlName="content"> - </textarea> - </div> + <app-webpage-form-content [form]="form" [controlName]="'content'" [controlLabel]="'Content'"></app-webpage-form-content> <div class="form-group pt-4"> <ng-content></ng-content> </div> diff --git a/client/src/app/admin/instance/webpage/services/prism.service.ts b/client/src/app/admin/instance/webpage/services/prism.service.ts new file mode 100644 index 00000000..a99215c0 --- /dev/null +++ b/client/src/app/admin/instance/webpage/services/prism.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; + +import 'prismjs'; +import 'prismjs/plugins/line-numbers/prism-line-numbers'; +import 'prismjs/components/prism-css'; +import 'prismjs/components/prism-javascript'; +import 'prismjs/components/prism-java'; +import 'prismjs/components/prism-markup'; +import 'prismjs/components/prism-typescript'; +import 'prismjs/components/prism-sass'; +import 'prismjs/components/prism-scss'; + +declare var Prism: any; + +@Injectable() +export class PrismService { + constructor() {} + + highlightAll() { + Prism.highlightAll(); + } + + convertHtmlIntoString(text: string) { + return text + .replace(new RegExp('&', 'g'), '&') + .replace(new RegExp('<', 'g'), '<'); + } +} diff --git a/client/src/app/admin/instance/webpage/webpage.module.ts b/client/src/app/admin/instance/webpage/webpage.module.ts index fa6df84f..ce08a41b 100644 --- a/client/src/app/admin/instance/webpage/webpage.module.ts +++ b/client/src/app/admin/instance/webpage/webpage.module.ts @@ -9,12 +9,11 @@ import { NgModule } from '@angular/core'; -import { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular'; - import { SharedModule } from 'src/app/shared/shared.module'; import { WebpageRoutingModule, routedComponents } from './webpage-routing.module'; import { dummiesComponents } from './components'; import { AdminSharedModule } from '../../admin-shared/admin-shared.module'; +import { PrismService } from './services/prism.service'; /** * @class @@ -24,15 +23,14 @@ import { AdminSharedModule } from '../../admin-shared/admin-shared.module'; imports: [ SharedModule, WebpageRoutingModule, - AdminSharedModule, - EditorModule + AdminSharedModule ], declarations: [ routedComponents, dummiesComponents ], providers: [ - { provide: TINYMCE_SCRIPT_SRC, useValue: 'tinymce/tinymce.min.js' } + PrismService ] }) export class WebpageModule { } diff --git a/client/src/app/metamodel/selectors/webpage.selector.ts b/client/src/app/metamodel/selectors/webpage.selector.ts index 20a5be4d..ad73a895 100644 --- a/client/src/app/metamodel/selectors/webpage.selector.ts +++ b/client/src/app/metamodel/selectors/webpage.selector.ts @@ -58,7 +58,11 @@ export const selectFirstWebpage = createSelector( webpageFamilySelector.selectAllWebpageFamilies, selectAllWebpages, (webpageFamilyList, webpageList) => { - const firstWebpageFamily = webpageFamilyList[0]; - return webpageList.filter(webpage => webpage.id_webpage_family === firstWebpageFamily.id)[0]; + if (webpageFamilyList && webpageList) { + const firstWebpageFamily = webpageFamilyList[0]; + return webpageList.filter(webpage => webpage.id_webpage_family === firstWebpageFamily.id)[0]; + } else { + return null; + } } ); \ No newline at end of file diff --git a/client/src/styles.scss b/client/src/styles.scss index d706a62f..5c065f8b 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -39,6 +39,38 @@ /* Import datepicker ngx bootstrap styled design */ @import '~ngx-bootstrap/datepicker/bs-datepicker.css'; +/* Import prismjs styled design */ +@import "~prismjs/themes/prism-okaidia"; +// @import "~prismjs/themes/prism-tomorrow"; +// @import "~prismjs/themes/prism-solarizedlight"; +// @import "~prismjs/themes/prism"; + +// Prism.js line numbers plugin +.line-numbers .line-numbers-rows { + position: absolute; + pointer-events: none; + top: 15px; + font-size: 100%; + left: 0; + width: 3em; + letter-spacing: -1px; + border-right: 1px solid rgb(179, 179, 179); + user-select: none; +} + +.line-numbers-rows > span { + display: block; + counter-increment: linenumber; +} + +.line-numbers-rows > span:before { +content: counter(linenumber); + color: rgb(219, 219, 219); + display: block; + padding-right: 0.8em; + text-align: right; +} + /* Global styles */ main { margin-top: 100px; diff --git a/client/yarn.lock b/client/yarn.lock index 49623490..165f2201 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1928,14 +1928,6 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@tinymce/tinymce-angular@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@tinymce/tinymce-angular/-/tinymce-angular-6.0.1.tgz#8573bef54be8533c29e938c8c96cb2057e08b7a6" - integrity sha512-USuwQwcBmvl1fN9n1FsUqM8ZOQjJLe2VleQRENU9R46ZgrB6Ic5thyUV2RPHUrNgN99QJ+4HyE465qzgv+M7Mw== - dependencies: - tinymce "^6.0.0 || ^5.5.0" - tslib "^2.3.0" - "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -7267,6 +7259,11 @@ pretty-format@^27.5.1: ansi-styles "^5.0.0" react-is "^17.0.1" +prismjs@^1.28.0: + version "1.28.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.28.0.tgz#0d8f561fa0f7cf6ebca901747828b149147044b6" + integrity sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -8197,11 +8194,6 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -"tinymce@^6.0.0 || ^5.5.0", tinymce@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-6.0.3.tgz#993db09afa473a764ad8b594cdaf744b2c7e2e74" - integrity sha512-4cu80kWF7nRGhviE10poZtjTkl3jNL+lycilCMfdm3KU5V7FtiQQrKbEo6GInXT05RY78Ha/NFP0gOBELcSpfg== - tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" -- GitLab