mat-contenteditable
Version:
Angular contenteditable directive for Angular forms and Material Design
619 lines (608 loc) • 22.1 kB
JavaScript
import { __decorate, __metadata, __param } from 'tslib';
import { ElementRef, Renderer2, Optional, Self, Input, HostBinding, HostListener, Directive, Host, ViewContainerRef, NgModule } from '@angular/core';
import { MatFormFieldControl } from '@angular/material/form-field';
import { NgControl, NgForm, FormGroupDirective } from '@angular/forms';
import { mixinErrorState, ErrorStateMatcher } from '@angular/material/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Subject } from 'rxjs';
import { CKEditorComponent } from '@ckeditor/ckeditor5-angular';
var MatContenteditableDirective_1;
// Boilerplate for applying mixins to MatInput.
/** @docs-private */
class MatInputBase {
constructor(_defaultErrorStateMatcher, _parentForm, _parentFormGroup,
/** @docs-private */
ngControl) {
this._defaultErrorStateMatcher = _defaultErrorStateMatcher;
this._parentForm = _parentForm;
this._parentFormGroup = _parentFormGroup;
this.ngControl = ngControl;
}
}
const _MatInputMixinBase = mixinErrorState(MatInputBase);
let MatContenteditableDirective = MatContenteditableDirective_1 = class MatContenteditableDirective extends _MatInputMixinBase {
constructor(elementRef, renderer, ngControl, _parentForm, _parentFormGroup, _defaultErrorStateMatcher) {
super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl);
this.elementRef = elementRef;
this.renderer = renderer;
this.ngControl = ngControl;
this.stateChanges = new Subject();
this.id = `mat-input-${MatContenteditableDirective_1.nextId++}`;
this.focused = false;
this.contentEmpty = ['<br>', '<div><br></div>'];
this._required = false;
this._disabled = false;
this.controlType = 'mat-input';
this.describedBy = '';
this.propValueAccessor = 'innerHTML';
// Setting the value accessor directly (instead of using
// the providers) to avoid running into a circular import.
if (this.ngControl != null) {
this.ngControl.valueAccessor = this;
}
}
get value() { return this.elementRef.nativeElement[this.propValueAccessor]; }
set value(value) {
if (value !== this.value) {
this.elementRef.nativeElement[this.propValueAccessor] = value;
this.stateChanges.next();
}
}
get placeholder() {
return this._placeholder;
}
set placeholder(plh) {
this._placeholder = plh;
this.stateChanges.next();
}
get empty() {
return !this.value || this.contentEmpty.includes(this.value);
}
get shouldLabelFloat() { return this.focused || !this.empty; }
get required() {
return this._required;
}
set required(req) {
this._required = coerceBooleanProperty(req);
this.stateChanges.next();
}
get disabled() {
return this._disabled;
}
set disabled(dis) {
this._disabled = coerceBooleanProperty(dis);
this.stateChanges.next();
}
ngDoCheck() {
if (this.ngControl) {
// We need to re-evaluate this on every change detection cycle, because there are some
// error triggers that we can't subscribe to (e.g. parent form submissions). This means
// that whatever logic is in here has to be super lean or we risk destroying the performance.
this.updateErrorState();
}
}
callOnChange() {
if (typeof this.onChange === 'function') {
this.onChange(this.elementRef.nativeElement[this.propValueAccessor]);
}
}
callOnFocused() {
if (this.focused !== true) {
this.focused = true;
this.stateChanges.next();
}
}
callOnTouched() {
if (typeof this.onTouched === 'function') {
this.onTouched();
}
if (this.focused !== false) {
this.focused = false;
this.stateChanges.next();
}
}
setDescribedByIds(ids) {
this.describedBy = ids.join(' ');
}
onContainerClick() { this.elementRef.nativeElement.focus(); }
/**
* Writes a new value to the element.
* This method will be called by the forms API to write
* to the view when programmatic (model -> view) changes are requested.
*
* See: [ControlValueAccessor](https://angular.io/api/forms/ControlValueAccessor#members)
*/
writeValue(value) {
const normalizedValue = value == null ? '' : value;
this.renderer.setProperty(this.elementRef.nativeElement, this.propValueAccessor, normalizedValue);
}
/**
* Registers a callback function that should be called when
* the control's value changes in the UI.
*
* This is called by the forms API on initialization so it can update
* the form model when values propagate from the view (view -> model).
*/
registerOnChange(fn) {
this.onChange = fn;
}
/**
* Registers a callback function that should be called when the control receives a blur event.
* This is called by the forms API on initialization so it can update the form model on blur.
*/
registerOnTouched(fn) {
this.onTouched = fn;
}
/**
* This function is called by the forms API when the control status changes to or from "DISABLED".
* Depending on the value, it should enable or disable the appropriate DOM element.
*/
setDisabledState(isDisabled) {
if (isDisabled) {
this.renderer.setAttribute(this.elementRef.nativeElement, 'disabled', 'true');
this.removeDisabledState = this.renderer.listen(this.elementRef.nativeElement, 'keydown', this.listenerDisabledState);
}
else {
if (this.removeDisabledState) {
this.renderer.removeAttribute(this.elementRef.nativeElement, 'disabled');
this.removeDisabledState();
}
}
}
listenerDisabledState(e) {
e.preventDefault();
}
};
/**
* Implemented as part of MatFormFieldControl.
* See https://material.angular.io/guide/creating-a-custom-form-field-control
*/
MatContenteditableDirective.nextId = 0;
MatContenteditableDirective.ctorParameters = () => [
{ type: ElementRef },
{ type: Renderer2 },
{ type: NgControl, decorators: [{ type: Optional }, { type: Self }] },
{ type: NgForm, decorators: [{ type: Optional }] },
{ type: FormGroupDirective, decorators: [{ type: Optional }] },
{ type: ErrorStateMatcher }
];
__decorate([
Input(),
__metadata("design:type", String),
__metadata("design:paramtypes", [String])
], MatContenteditableDirective.prototype, "value", null);
__decorate([
HostBinding(),
__metadata("design:type", Object)
], MatContenteditableDirective.prototype, "id", void 0);
__decorate([
Input(),
__metadata("design:type", Object),
__metadata("design:paramtypes", [Object])
], MatContenteditableDirective.prototype, "placeholder", null);
__decorate([
Input(),
__metadata("design:type", Array)
], MatContenteditableDirective.prototype, "contentEmpty", void 0);
__decorate([
Input(),
__metadata("design:type", Object),
__metadata("design:paramtypes", [Object])
], MatContenteditableDirective.prototype, "required", null);
__decorate([
Input(),
__metadata("design:type", Object),
__metadata("design:paramtypes", [Object])
], MatContenteditableDirective.prototype, "disabled", null);
__decorate([
HostBinding('attr.aria-invalid'),
__metadata("design:type", Boolean)
], MatContenteditableDirective.prototype, "errorState", void 0);
__decorate([
Input(),
__metadata("design:type", ErrorStateMatcher)
], MatContenteditableDirective.prototype, "errorStateMatcher", void 0);
__decorate([
HostBinding('attr.aria-describedby'),
__metadata("design:type", Object)
], MatContenteditableDirective.prototype, "describedBy", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], MatContenteditableDirective.prototype, "propValueAccessor", void 0);
__decorate([
HostListener('input'),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], MatContenteditableDirective.prototype, "callOnChange", null);
__decorate([
HostListener('focus'),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], MatContenteditableDirective.prototype, "callOnFocused", null);
__decorate([
HostListener('blur'),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], MatContenteditableDirective.prototype, "callOnTouched", null);
MatContenteditableDirective = MatContenteditableDirective_1 = __decorate([
Directive({
selector: '[contenteditable]',
providers: [
{ provide: MatFormFieldControl, useExisting: MatContenteditableDirective_1 },
]
}),
__param(2, Optional()), __param(2, Self()),
__param(3, Optional()),
__param(4, Optional()),
__metadata("design:paramtypes", [ElementRef,
Renderer2,
NgControl,
NgForm,
FormGroupDirective,
ErrorStateMatcher])
], MatContenteditableDirective);
var MatCkeditorDirective_1;
let MatCkeditorDirective = MatCkeditorDirective_1 = class MatCkeditorDirective extends _MatInputMixinBase {
constructor(editor, viewRef, ngControl, _parentForm, _parentFormGroup, _defaultErrorStateMatcher) {
super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl);
this.editor = editor;
this.viewRef = viewRef;
this.ngControl = ngControl;
this.stateChanges = new Subject();
this.id = `mat-input-${MatCkeditorDirective_1.nextId++}`;
// Need support from Ckeditor
this.placeholder = '';
this.contentEmpty = ['<br>', '<p> </p>'];
this.focused = false;
this.required = false;
this.controlType = 'mat-input';
this.describedBy = '';
}
get value() {
return !!this.editor.editorInstance && this.editor.editorInstance.getData();
}
set value(value) {
if (value !== this.value) {
this.editor.data = value;
this.stateChanges.next();
}
}
get empty() {
return !this.value || this.contentEmpty.includes(this.value);
}
get shouldLabelFloat() { return this.focused || !this.empty; }
set disabled(isDisabled) {
this.editor.setDisabledState(isDisabled);
this.stateChanges.next();
}
get disabled() {
return this.editor.disabled;
}
ngOnInit() {
this.editor.blur.subscribe(() => {
this.focused = false;
this.stateChanges.next();
});
this.editor.focus.subscribe(() => {
this.focused = true;
this.stateChanges.next();
});
}
ngDoCheck() {
if (this.ngControl) {
// We need to re-evaluate this on every change detection cycle, because there are some
// error triggers that we can't subscribe to (e.g. parent form submissions). This means
// that whatever logic is in here has to be super lean or we risk destroying the performance.
this.updateErrorState();
}
}
setDescribedByIds(ids) {
this.describedBy = ids.join(' ');
}
onContainerClick() {
if (this.editor.editorInstance) {
this.editor.editorInstance.editing.view.focus();
this.stateChanges.next();
}
}
};
/**
* Implemented as part of MatFormFieldControl.
* See https://material.angular.io/guide/creating-a-custom-form-field-control
*/
MatCkeditorDirective.nextId = 0;
MatCkeditorDirective.ctorParameters = () => [
{ type: CKEditorComponent, decorators: [{ type: Host }, { type: Self }, { type: Optional }] },
{ type: ViewContainerRef },
{ type: NgControl, decorators: [{ type: Optional }, { type: Self }] },
{ type: NgForm, decorators: [{ type: Optional }] },
{ type: FormGroupDirective, decorators: [{ type: Optional }] },
{ type: ErrorStateMatcher }
];
__decorate([
Input(),
__metadata("design:type", String),
__metadata("design:paramtypes", [String])
], MatCkeditorDirective.prototype, "value", null);
__decorate([
HostBinding(),
__metadata("design:type", Object)
], MatCkeditorDirective.prototype, "id", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], MatCkeditorDirective.prototype, "placeholder", void 0);
__decorate([
Input(),
__metadata("design:type", Array)
], MatCkeditorDirective.prototype, "contentEmpty", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], MatCkeditorDirective.prototype, "required", void 0);
__decorate([
Input(),
__metadata("design:type", Boolean),
__metadata("design:paramtypes", [Boolean])
], MatCkeditorDirective.prototype, "disabled", null);
__decorate([
HostBinding('attr.aria-invalid'),
__metadata("design:type", Boolean)
], MatCkeditorDirective.prototype, "errorState", void 0);
__decorate([
Input(),
__metadata("design:type", ErrorStateMatcher)
], MatCkeditorDirective.prototype, "errorStateMatcher", void 0);
__decorate([
HostBinding('attr.aria-describedby'),
__metadata("design:type", Object)
], MatCkeditorDirective.prototype, "describedBy", void 0);
MatCkeditorDirective = MatCkeditorDirective_1 = __decorate([
Directive({
selector: '[matCkeditor]',
providers: [
{ provide: MatFormFieldControl, useExisting: MatCkeditorDirective_1 },
]
}),
__param(0, Host()), __param(0, Self()), __param(0, Optional()),
__param(2, Optional()), __param(2, Self()),
__param(3, Optional()),
__param(4, Optional()),
__metadata("design:paramtypes", [CKEditorComponent,
ViewContainerRef,
NgControl,
NgForm,
FormGroupDirective,
ErrorStateMatcher])
], MatCkeditorDirective);
var MatCkeditorBalloonDirective_1;
let MatCkeditorBalloonDirective = MatCkeditorBalloonDirective_1 = class MatCkeditorBalloonDirective extends MatCkeditorDirective {
set toolbar(show) {
if (this.editor && show !== this.toolbarOpen) {
const balloon = this.editor.editorInstance.plugins.get('BalloonToolbar');
if (show) {
this.showToolbar(balloon);
}
else {
balloon.hide();
this.toolbarOpen = false;
}
}
}
ngOnInit() {
super.ngOnInit();
this.editor.ready.subscribe(editor => {
const balloon = editor.plugins.get('BalloonToolbar');
balloon.stopListening(editor.model.document.selection, 'change:range');
balloon.stopListening(balloon, '_selectionChangeDebounced');
});
this.editor.focus.subscribe(() => {
if (this.toolbarOpen) {
const balloon = this.editor.editorInstance.plugins.get('BalloonToolbar');
this.showToolbar(balloon);
}
});
}
showToolbar(balloon) {
if (!balloon._balloon.hasView(balloon.toolbarView)) {
balloon.listenTo(this.editor.editorInstance.ui, 'update', () => {
balloon._balloon.updatePosition(balloon._getBalloonPositionData());
});
balloon._balloon.add({
view: balloon.toolbarView,
position: balloon._getBalloonPositionData(),
balloonClassName: 'ck-toolbar-container'
});
this.toolbarOpen = true;
}
}
};
__decorate([
Input(),
__metadata("design:type", Boolean),
__metadata("design:paramtypes", [Boolean])
], MatCkeditorBalloonDirective.prototype, "toolbar", null);
MatCkeditorBalloonDirective = MatCkeditorBalloonDirective_1 = __decorate([
Directive({
selector: '[matCkeditorBalloon]',
providers: [
{ provide: MatFormFieldControl, useExisting: MatCkeditorBalloonDirective_1 },
]
})
], MatCkeditorBalloonDirective);
let FormFieldSizerDirective = class FormFieldSizerDirective {
constructor(renderer, elementRef) {
this.renderer = renderer;
this.elementRef = elementRef;
}
ngAfterContentInit() {
this.updateSize();
}
updateSize() {
const infix = this.getElement('mat-form-field-infix');
this.renderer.removeStyle(infix, 'min-height');
setTimeout(() => {
const wrapper = this.getElement('mat-form-field-wrapper');
const offset = this.elementRef.nativeElement.offsetHeight -
wrapper.offsetHeight -
parseFloat(getComputedStyle(wrapper).marginTop) -
parseFloat(getComputedStyle(wrapper).marginBottom) +
parseFloat(getComputedStyle(infix).height);
this.renderer.setStyle(infix, 'min-height', `${offset}px`);
});
}
getElement(name) {
return this.elementRef.nativeElement.getElementsByClassName(name).item(0);
}
};
FormFieldSizerDirective.ctorParameters = () => [
{ type: Renderer2 },
{ type: ElementRef }
];
FormFieldSizerDirective = __decorate([
Directive({
selector: '[formFieldSizer]'
}),
__metadata("design:paramtypes", [Renderer2,
ElementRef])
], FormFieldSizerDirective);
let MatContenteditableModule = class MatContenteditableModule {
};
MatContenteditableModule = __decorate([
NgModule({
imports: [],
declarations: [MatContenteditableDirective, FormFieldSizerDirective],
exports: [MatContenteditableDirective, FormFieldSizerDirective],
})
], MatContenteditableModule);
let MatCkeditorModule = class MatCkeditorModule {
};
MatCkeditorModule = __decorate([
NgModule({
imports: [],
declarations: [MatCkeditorDirective, MatCkeditorBalloonDirective],
exports: [MatCkeditorDirective, MatCkeditorBalloonDirective],
})
], MatCkeditorModule);
function getText(html) {
if (html) {
const element = document.createElement('span');
element.innerHTML = html;
return element.textContent.replace(/\s/g, '');
}
return '';
}
/**
* @description
* Provides a set of built-in validators that can be used by form controls.
*
* A validator is a function that processes a `FormControl` or collection of
* controls and returns an error map or null. A null map means that validation has passed.
*
* @see [Form Validation](/guide/form-validation)
*
* @publicApi
*/
class HtmlValidators {
/**
* @description
* Validator that requires the control have a non-empty value.
*
* @usageNotes
*
* ### Validate that the field is non-empty
*
* ```typescript
* const control = new FormControl('', Validators.required);
*
* console.log(control.errors); // {required: true}
* ```
*
* @returns An error map with the `required` property
* if the validation check fails, otherwise `null`.
*
*/
static required(control) {
const text = getText(control.value);
return text ? null : { 'required': true };
}
/**
* @description
* Validator that requires the length of the control's value to be greater than or equal
* to the provided minimum length. This validator is also provided by default if you use the
* the HTML5 `minlength` attribute.
*
* @usageNotes
*
* ### Validate that the field has a minimum of 3 characters
*
* ```typescript
* const control = new FormControl('ng', Validators.minLength(3));
*
* console.log(control.errors); // {minlength: {requiredLength: 3, actualLength: 2}}
* ```
*
* ```html
* <input minlength="5">
* ```
*
* @returns A validator function that returns an error map with the
* `minlength` if the validation check fails, otherwise `null`.
*/
static minLength(minLength) {
const fn = (control) => {
const text = getText(control.value);
if (text) {
return text.length < minLength ?
{ 'minlength': { 'requiredLength': minLength, 'actualLength': text.length } } :
null;
}
return null; // don't validate empty values to allow optional controls
};
return fn;
}
/**
* @description
* Validator that requires the length of the control's value to be less than or equal
* to the provided maximum length. This validator is also provided by default if you use the
* the HTML5 `maxlength` attribute.
*
* @usageNotes
*
* ### Validate that the field has maximum of 5 characters
*
* ```typescript
* const control = new FormControl('Angular', Validators.maxLength(5));
*
* console.log(control.errors); // {maxlength: {requiredLength: 5, actualLength: 7}}
* ```
*
* ```html
* <input maxlength="5">
* ```
*
* @returns A validator function that returns an error map with the
* `maxlength` property if the validation check fails, otherwise `null`.
*/
static maxLength(maxLength) {
const fn = (control) => {
const text = getText(control.value);
return text.length > maxLength ?
{ 'maxlength': { 'requiredLength': maxLength, 'actualLength': text.length } } :
null;
};
return fn;
}
}
/*
* Public API Surface of mat-contenteditable
*/
/**
* Generated bundle index. Do not edit.
*/
export { FormFieldSizerDirective, HtmlValidators, MatCkeditorBalloonDirective, MatCkeditorDirective, MatCkeditorModule, MatContenteditableDirective, MatContenteditableModule, _MatInputMixinBase };
//# sourceMappingURL=mat-contenteditable.js.map