diff --git a/build.js b/build.js new file mode 100644 index 0000000000000000000000000000000000000000..562471085c64b6015c81f80f3af64cf0e75486a5 --- /dev/null +++ b/build.js @@ -0,0 +1,133 @@ +const fs = require('fs'); +const inquirer = require('inquirer'); +const chalk = require('chalk'); +const { spawn } = require("child_process"); +const packageFilepath = "projects/common-form-elements/package.json"; +const filesToRemoveLines = [ + { + filepath: "projects/common-form-elements/src/lib/dynamic-field/dynamic-field.directive.ts", + keyword: "DynamicRichtextComponent" + }, + { + filepath: "projects/common-form-elements/src/lib/common-form-elements.module.ts", + keyword: "DynamicRichtextComponent" + } +]; + +const run = async () => { + try { + const answers = await askLibraryDetails(); + const { environment } = answers; + updateFiles(environment); + updatePackageFile(packageFilepath, answers); + console.log(chalk.bgBlue(' =========== Building Angular Package ===========')); + const child = spawn('ng', ['build', 'common-form-elements', '--prod']); + child.stdout.on('data', (data) => { + console.log(chalk.green(`${data}`)); + }); + child.stderr.on('data', (data) => { + console.error(`${data}`); + }); + child.on('close', (code) => { + if (code === 0) { + console.log(chalk.green('Successfully Done!!!')); + } else { + console.log(`child process exited with code ${code}`); + } + }); + } catch (error) { + console.log(chalk.red(error)); + process.exit(1); + } +}; + +const askLibraryDetails = () => { + const questions = [ + { + type: 'list', + name: 'environment', + message: 'What do you want to build for ?', + choices: ['mobile', 'web'], + default: 'mobile', + filter(val) { + return val.toLowerCase(); + } + }, + { + type: 'input', + name: 'name', + message: "Please enter library name", + validate(value) { + if (value) { + return true; + } + + return 'Please enter a valid package name'; + }, + }, + { + type: 'input', + name: 'version', + message: "Please enter library version", + validate(value) { + const pass = value.match( + /^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?$/ + ); + if (pass) { + return true; + } + + return 'Please enter a valid library version'; + }, + } + ]; + return inquirer.prompt(questions); +} + +const updateFiles = (env) => { + for (let i = 0; i < filesToRemoveLines.length; i++) { + const fileData = fs.readFileSync(filesToRemoveLines[i].filepath, { encoding: 'utf-8' }); + let dataArray = fileData.split('\n'); + for (let index = 0; index < dataArray.length; index++) { + if (dataArray[index].includes(filesToRemoveLines[i].keyword)) { + if (env === 'mobile') { + dataArray[index] = dataArray[index].replace('// MOBILE', ''); + dataArray[index] = `// MOBILE ${dataArray[index]}`; + } + if (env === 'web') { + dataArray[index] = dataArray[index].replace('// MOBILE', '').trimStart(); + } + } + } + const updatedData = dataArray.join('\n'); + fs.writeFileSync(filesToRemoveLines[i].filepath, updatedData); + console.log(chalk.yellow( + 'Successfully updated the filepath ---> ' + + chalk.green.underline.bold(filesToRemoveLines[i].filepath) + )); + } +}; + +const updatePackageFile = (filepath, { environment, name, version }) => { + const jsonString = fs.readFileSync(filepath); + let packageData = JSON.parse(jsonString); + + if(version) { + packageData.version = version; + } + + if (name) { + packageData.name = name; + } else if (environment === 'mobile') { + packageData.name = 'common-form-elements-v9'; + } else if (environment === 'web') { + packageData.name = 'common-form-elements-full-v9'; + } + fs.writeFileSync(filepath, JSON.stringify(packageData, null, 4)); + console.log(chalk.yellow( + 'Package name updated successfully ---> ' + + chalk.green.underline.bold(packageData.name) + )); +}; + +run(); \ No newline at end of file diff --git a/package.json b/package.json index ba7f409c47ac90e2bc18f2c3604aafb451d75334..b21d94fd2f6e1e8d6de93dc38eda98f3b2d31580 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "ng": "ng", "start": "ng serve", "build": "ng build", - "build-lib": "ng build common-form-elements --prod", + "build-lib": "node build.js", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" @@ -19,6 +19,9 @@ "@angular/platform-browser": "^9.1.13", "@angular/platform-browser-dynamic": "^9.1.13", "@angular/router": "^9.1.13", + "chalk": "^4.1.2", + "inquirer": "^8.1.2", + "@project-sunbird/ckeditor-build-classic": "4.1.3", "common-consumption-styles": "0.0.17", "core-js": "^2.5.4", "jquery": "^3.6.0", diff --git a/projects/common-form-elements/ng-package.json b/projects/common-form-elements/ng-package.json index 4d7d88f115c5fc4014e9061f212634fb860d34b1..f1fe2b04987cc46523692e214ab76c8ba2c8e321 100644 --- a/projects/common-form-elements/ng-package.json +++ b/projects/common-form-elements/ng-package.json @@ -7,6 +7,7 @@ "whitelistedNonPeerDependencies": [ "immutable", "ngx-chips", - "moment" + "moment", + "@project-sunbird/ckeditor-build-classic" ] } diff --git a/projects/common-form-elements/package.json b/projects/common-form-elements/package.json index 56255d20c0d18cb251252fd5f34fa24407d2fef9..223bfc39416ec072af5f7a9a767000a89afab9f9 100644 --- a/projects/common-form-elements/package.json +++ b/projects/common-form-elements/package.json @@ -1,15 +1,20 @@ { - "name": "common-form-elements-v9", - "version": "4.3.1", - "peerDependencies": { - "@angular/common": "^9.1.13", - "@angular/core": "^9.1.13", - "@angular/forms": "^9.1.13", - "@project-sunbird/client-services": "3.6.x", - "lodash-es": "^4.17.15" - }, - "dependencies": { - "immutable": "^4.0.0-rc.12", - "moment": "^2.29.1" - } -} + "name": "common-form-elements-v9", + "version": "4.3.1", + "peerDependencies": { + "@angular/common": "^9.1.13", + "@angular/core": "^9.1.13", + "@angular/forms": "^9.1.13", + "@project-sunbird/client-services": "3.6.x", + "lodash-es": "^4.17.15" + }, + "peerDependenciesMeta": { + "@project-sunbird/ckeditor-build-classic": { + "optional": true + } + }, + "dependencies": { + "immutable": "^4.0.0-rc.12", + "moment": "^2.29.1" + } +} \ No newline at end of file diff --git a/projects/common-form-elements/src/lib/common-form-config.ts b/projects/common-form-elements/src/lib/common-form-config.ts index 26e08e313bb8de58aba645e646232e2466a75179..fa4fda35b785af26bac91719b1be1b8c47b6d5d1 100644 --- a/projects/common-form-elements/src/lib/common-form-config.ts +++ b/projects/common-form-elements/src/lib/common-form-config.ts @@ -145,4 +145,5 @@ export interface CustomFormControl extends FormControl { customEventHandler$?: Subject<any>; shouldListenToCustomEvent?: Boolean; isVisible?: any; + richTextCharacterCount?: any; } diff --git a/projects/common-form-elements/src/lib/common-form-elements.module.ts b/projects/common-form-elements/src/lib/common-form-elements.module.ts index 24bf14fc791f61f4083a52c2cd9207326b71f185..1b72826cebdc79309e223cea0b4bf5dd97b035d7 100644 --- a/projects/common-form-elements/src/lib/common-form-elements.module.ts +++ b/projects/common-form-elements/src/lib/common-form-elements.module.ts @@ -37,6 +37,7 @@ import { DynamicRadioComponent } from './dynamic-radio/dynamic-radio.component'; import { DynamicDialcodeComponent } from './dynamic-dialcode/dynamic-dialcode.component'; import { DynamicFrameworkCategoryNestedSelectComponent } from './dynamic-framework-category-nested-select/dynamic-framework-category-nested-select.component'; import { DynamicDateComponent } from './dynamic-date/dynamic-date.component'; +import { DynamicRichtextComponent } from './dynamic-richtext/dynamic-richtext.component'; @NgModule({ declarations: [ @@ -72,7 +73,8 @@ import { DynamicDateComponent } from './dynamic-date/dynamic-date.component'; DynamicRadioComponent, DynamicDialcodeComponent, DynamicFrameworkComponent, - DynamicDateComponent + DynamicDateComponent, +DynamicRichtextComponent ], imports: [ CommonModule, @@ -113,7 +115,8 @@ import { DynamicDateComponent } from './dynamic-date/dynamic-date.component'; DynamicRadioComponent, DynamicDialcodeComponent, DynamicFrameworkComponent, - DynamicDateComponent + DynamicDateComponent, +DynamicRichtextComponent ], entryComponents: [ DynamicFormComponent, @@ -133,7 +136,8 @@ import { DynamicDateComponent } from './dynamic-date/dynamic-date.component'; DynamicRadioComponent, DynamicDialcodeComponent, DynamicFrameworkComponent, - DynamicDateComponent + DynamicDateComponent, +DynamicRichtextComponent ] }) export class CommonFormElementsModule { } diff --git a/projects/common-form-elements/src/lib/dynamic-field/dynamic-field.directive.ts b/projects/common-form-elements/src/lib/dynamic-field/dynamic-field.directive.ts index 6a58aa8502b6de32d6b9fefc311bb45b94369119..f3519e33df0b1f397d1de3dfd199176469f2c85b 100644 --- a/projects/common-form-elements/src/lib/dynamic-field/dynamic-field.directive.ts +++ b/projects/common-form-elements/src/lib/dynamic-field/dynamic-field.directive.ts @@ -24,6 +24,7 @@ import { DynamicRadioComponent } from '../dynamic-radio/dynamic-radio.component' import { DynamicDialcodeComponent } from '../dynamic-dialcode/dynamic-dialcode.component'; import { DynamicFrameworkCategoryNestedSelectComponent } from '../dynamic-framework-category-nested-select/dynamic-framework-category-nested-select.component'; import { DynamicDateComponent } from '../dynamic-date/dynamic-date.component'; +import { DynamicRichtextComponent } from '../dynamic-richtext/dynamic-richtext.component'; const componentMapper = { textarea: DynamicTextareaComponent, @@ -43,7 +44,8 @@ const componentMapper = { frameworkCategorySelect: DynamicFrameworkCategorySelectComponent, radio: DynamicRadioComponent, dialcode: DynamicDialcodeComponent, - date:DynamicDateComponent + date: DynamicDateComponent, +richtext: DynamicRichtextComponent }; @Directive({ diff --git a/projects/common-form-elements/src/lib/dynamic-form/dynamic-form.component.ts b/projects/common-form-elements/src/lib/dynamic-form/dynamic-form.component.ts index 17ef3fd9e44a126e92caf11c715ca4cc0755bb9d..aecb58ffb33555c2a7cdef59f3faa7fe61ede6ff 100644 --- a/projects/common-form-elements/src/lib/dynamic-form/dynamic-form.component.ts +++ b/projects/common-form-elements/src/lib/dynamic-form/dynamic-form.component.ts @@ -267,10 +267,18 @@ export class DynamicFormComponent implements OnInit, OnChanges, OnDestroy { validationList.push(Validators.pattern(element.validations[i].value as string)); break; case 'minLength': - validationList.push(Validators.minLength(element.validations[i].value as number)); + if (element.inputType === 'richtext') { + validationList.push(this.validateRichTextLength.bind(this, 'minLength' , '<', element.validations[i].value )); + } else { + validationList.push(Validators.minLength(element.validations[i].value as number)); + } break; case 'maxLength': - validationList.push(Validators.maxLength(element.validations[i].value as number)); + if (element.inputType === 'richtext') { + validationList.push(this.validateRichTextLength.bind(this, 'maxLength' , '>', element.validations[i].value )); + } else { + validationList.push(Validators.maxLength(element.validations[i].value as number)); + } break; case 'min': validationList.push(Validators.min(element.validations[i].value as number)); @@ -380,4 +388,17 @@ export class DynamicFormComponent implements OnInit, OnChanges, OnDestroy { } return null; } + + validateRichTextLength(validationType, keyOperator, validationValue, control: AbstractControl): ValidationErrors | null { + let comp; + if (control.touched) { + comp = FieldComparator.operators[keyOperator](control['richTextCharacterCount'], validationValue); + } else { + comp = false; + } + if (comp && (control.touched || control.dirty)) { + return { [_.toLower(validationType)]: true }; + } + return null; + } } diff --git a/projects/common-form-elements/src/lib/dynamic-richtext/dynamic-richtext.component.css b/projects/common-form-elements/src/lib/dynamic-richtext/dynamic-richtext.component.css new file mode 100644 index 0000000000000000000000000000000000000000..441e765751fbf18bf4e8074e5f58871f6022bafc --- /dev/null +++ b/projects/common-form-elements/src/lib/dynamic-richtext/dynamic-richtext.component.css @@ -0,0 +1,68 @@ +label { + display: block; + font-size: 1rem; + margin: 0; + } + + .sb-textbox { + width: 100%; + padding: 8px 16px; + border: 0.5px solid #333333; + box-sizing: border-box; + } + + ::placeholder { + padding: 0.25rem; + opacity: 0.99; + color: #999999; + font-family: "Noto Sans"; + font-size: 12px; + font-weight: bold; + } + + .sb-input { + margin: 1rem 0; + } + + .cf-error{ + color: red; + font-family: "Noto Sans"; + font-size: 12px; + } + + .async-container{ + text-align: center; + } + + .async-btn{ + padding: 12px 16px; + background-color: #008840; + color: white; + border-radius: 20px !important; + } + + .async-text{ + display: flex; + align-items: center; + border: 0.5px solid #333333; + } + + .async-text > input{ + border: none + } + + .normal-text > .async-icons > sb-red-exclamation, .normal-text > .async-icons > sb-green-tick, .normal-text > .async-icons > sb-empty-circle{ + display: none; + } + + .prefix{ + white-space: nowrap; + padding: 0 4px; + } + + .async-icons{ + margin: auto; + padding: 0 4px; + } + + .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_se, .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sw{transform:inherit !important} \ No newline at end of file diff --git a/projects/common-form-elements/src/lib/dynamic-richtext/dynamic-richtext.component.html b/projects/common-form-elements/src/lib/dynamic-richtext/dynamic-richtext.component.html new file mode 100644 index 0000000000000000000000000000000000000000..8969f24be5c571991e36ff7878c6f6d7945ba1cf --- /dev/null +++ b/projects/common-form-elements/src/lib/dynamic-richtext/dynamic-richtext.component.html @@ -0,0 +1,24 @@ +<div class="sb-input" *ngIf="visible"> + <label *ngIf="label" [attr.data-title]="field.description ? field.description : null">{{label}}</label> + <div + [ngClass]="{'async-text': (asyncValidation && asyncValidation?.trigger), '': (!asyncValidation || !asyncValidation?.trigger)}"> + <div class="prefix" *ngIf="prefix"> + <span>{{prefix}}</span> + </div> + <textarea *ngIf="showEditor" id="richTextCount" #EDITOR [formControl]="formControlRef" [class.valid]="formControlRef.valid && + (formControlRef.dirty || formControlRef.touched)" [class.invalid]="formControlRef.invalid && + (formControlRef.dirty || formControlRef.touched)" class="sb-textbox {{disabled}} ck ck-reset ck-editor ck-rounded-corners + ck ck-reset ck-editor ck-rounded-corners " placeholder={{placeholder}} type="text" + [attr.disabled]="disabled ? true : null"></textarea> + <div class="async-icons" *ngIf="asyncValidation && asyncValidation?.trigger"> + </div> + </div> + + <ng-container *ngFor="let validation of validations"> + <div class="cf-error" + *ngIf="(validation.type && (validation.type).toLowerCase() && validation.message && formControlRef.errors && formControlRef.errors[(validation.type).toLowerCase()] && (formControlRef.dirty || formControlRef.touched))"> + {{ validation.message }} + </div> + </ng-container> + + </div> \ No newline at end of file diff --git a/projects/common-form-elements/src/lib/dynamic-richtext/dynamic-richtext.component.spec.ts b/projects/common-form-elements/src/lib/dynamic-richtext/dynamic-richtext.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..24db9229c7a086b1016111254d60d070e7132c6c --- /dev/null +++ b/projects/common-form-elements/src/lib/dynamic-richtext/dynamic-richtext.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DynamicRichtextComponent } from './dynamic-richtext.component'; + +describe('DynamicRichtextComponent', () => { + let component: DynamicRichtextComponent; + let fixture: ComponentFixture<DynamicRichtextComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DynamicRichtextComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DynamicRichtextComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/projects/common-form-elements/src/lib/dynamic-richtext/dynamic-richtext.component.ts b/projects/common-form-elements/src/lib/dynamic-richtext/dynamic-richtext.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..c88854915c4c2edfda679e45df27e1ae4f3ce1b4 --- /dev/null +++ b/projects/common-form-elements/src/lib/dynamic-richtext/dynamic-richtext.component.ts @@ -0,0 +1,99 @@ +import { Component, Input, OnInit, AfterViewInit, ViewChild, ElementRef, ViewEncapsulation } from '@angular/core'; +import { FieldConfigAsyncValidation, CustomFormControl } from '../common-form-config'; +import ClassicEditor from '@project-sunbird/ckeditor-build-classic'; +import * as _ from 'lodash-es'; +@Component({ + selector: 'sb-dynamic-richtext', + templateUrl: './dynamic-richtext.component.html', + styleUrls: ['./dynamic-richtext.component.css'], + encapsulation: ViewEncapsulation.None, +}) + +export class DynamicRichtextComponent implements OnInit, AfterViewInit { + @ViewChild('EDITOR', { static: false }) public editorRef: ElementRef; + @Input() asyncValidation?: FieldConfigAsyncValidation; + @Input() label: String; + @Input() labelHtml: any; + @Input() field: any; + @Input() placeholder: String; + @Input() validations?: any; + @Input() formControlRef?: CustomFormControl; + @Input() prefix?: String; + @Input() default: String; + @Input() disabled: Boolean; + @Input() visible: Boolean; + @ViewChild('validationTrigger', { static: false }) validationTrigger: ElementRef; + showEditor = false; + editorConfig: any; + editorInstance: any; + characterCount: any; + constructor() { } + + ngOnInit() { + this.editorConfig = { + toolbar: ['bold', '|', 'italic', '|', 'underline', '|', 'insertTable', + '|', 'numberedList', '|', 'BulletedList', '|', 'fontSize', '|', + ], + fontSize: { + options: [ + 9, + 11, + 13, + 15, + 17, + 19, + 21, + 23, + 25 + ] + }, + isReadOnly: this.disabled, + removePlugins: ['ImageCaption', 'mathtype', 'ChemType'] + }; + this.showEditor = true; + } + ngAfterViewInit() { + if (this.visible) { + this.initializeEditors(); + } + if (this.asyncValidation && this.asyncValidation.asyncValidatorFactory && this.formControlRef) { + if (this.formControlRef.asyncValidator) { + return; + } + this.formControlRef.setAsyncValidators(this.asyncValidation.asyncValidatorFactory( + this.asyncValidation.marker, + this.validationTrigger.nativeElement + )); + } + } + + initializeEditors() { + ClassicEditor.create(this.editorRef.nativeElement, { + extraPlugins: ['Font', 'Table'], + toolbar: this.editorConfig.toolbar, + fontSize: this.editorConfig.fontSize, + isReadOnly: this.editorConfig.isReadOnly, + removePlugins: this.editorConfig.removePlugins, + wordCount: { + onUpdate: stats => { + this.characterCount = stats.characters; + }, + } + }) + .then(editor => { + this.editorInstance = editor; + editor.isReadOnly = this.disabled; + this.onChangeEditor(this.editorInstance); + }) + .catch(error => { + console.error(error.stack); + }); + } + onChangeEditor(editor) { + editor.model.document.on('change', (eventInfo, batch) => { + this.formControlRef.markAsTouched(); + this.formControlRef.richTextCharacterCount = this.characterCount; + this.formControlRef.patchValue(editor.getData()); + }); + } +} diff --git a/src/app/formConfig.ts b/src/app/formConfig.ts index 91d2e9e71627bcbd1ba81d2b5cf231c7bef98421..b9344591a6ff840feda2fc33073548f5efcfcc52 100644 --- a/src/app/formConfig.ts +++ b/src/app/formConfig.ts @@ -1,4 +1,33 @@ export const timer = [ + { + 'name': 'Rich Text Section', + 'fields': [{ + 'code': 'instructions', + 'dataType': 'text', + 'description': 'Name of the Instruction', + 'editable': true, + 'inputType': 'richtext', + 'label': 'Instructions', + 'name': 'Instruction', + 'placeholder': 'Enter instructions', + 'renderingHints': { + 'class': 'sb-g-col-lg-2 required' + }, + 'validations': [ + { + 'type': 'maxLength', + 'value': '100', + 'message': 'Input is Exceeded' + }, + { + 'type': 'required', + 'message': 'Instruction is required' + } + ], + 'required': true, + 'visible': true, + }] + }, { 'name': 'First Section', 'fields': [