diff --git a/apps/playground/src/app/components/text-editor/text-editor-series/text-editor-series.component.html b/apps/playground/src/app/components/text-editor/text-editor-series/text-editor-series.component.html
new file mode 100644
index 0000000000..b07d65d2cb
--- /dev/null
+++ b/apps/playground/src/app/components/text-editor/text-editor-series/text-editor-series.component.html
@@ -0,0 +1,25 @@
+
diff --git a/apps/playground/src/app/components/text-editor/text-editor-series/text-editor-series.component.scss b/apps/playground/src/app/components/text-editor/text-editor-series/text-editor-series.component.scss
new file mode 100644
index 0000000000..9e470184fb
--- /dev/null
+++ b/apps/playground/src/app/components/text-editor/text-editor-series/text-editor-series.component.scss
@@ -0,0 +1,5 @@
+.content-display {
+ background-color: white;
+ height: 300px;
+ overflow-y: auto;
+}
diff --git a/apps/playground/src/app/components/text-editor/text-editor-series/text-editor-series.component.ts b/apps/playground/src/app/components/text-editor/text-editor-series/text-editor-series.component.ts
new file mode 100644
index 0000000000..d81338fd3f
--- /dev/null
+++ b/apps/playground/src/app/components/text-editor/text-editor-series/text-editor-series.component.ts
@@ -0,0 +1,64 @@
+import { Component, DestroyRef, OnInit, inject } from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import {
+ FormBuilder,
+ FormControl,
+ FormGroup,
+ ReactiveFormsModule,
+ Validators,
+} from '@angular/forms';
+import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
+import { SkyVerticalTabsetModule } from '@skyux/tabs';
+import { SkyTextEditorModule } from '@skyux/text-editor';
+
+import { startWith } from 'rxjs';
+
+@Component({
+ selector: 'app-text-editor-series',
+ templateUrl: './text-editor-series.component.html',
+ styleUrls: ['./text-editor-series.component.scss'],
+ standalone: true,
+ imports: [SkyTextEditorModule, SkyVerticalTabsetModule, ReactiveFormsModule],
+})
+export class TextEditorSeriesComponent implements OnInit {
+ public displayValue: Record = {};
+
+ public myForm: FormGroup;
+
+ public editors = ['textEditor1', 'textEditor2', 'textEditor3', 'textEditor4'];
+
+ readonly #destroyRef = inject(DestroyRef);
+ readonly #formBuilder = inject(FormBuilder);
+ readonly #sanitizer = inject(DomSanitizer);
+
+ public ngOnInit(): void {
+ this.myForm = this.#formBuilder.group({
+ textEditor1: new FormControl(
+ 'Super styled text 1',
+ [Validators.required],
+ ),
+ textEditor2: new FormControl(
+ 'Super styled text 2',
+ [Validators.required],
+ ),
+ textEditor3: new FormControl(
+ 'Super styled text 3',
+ [Validators.required],
+ ),
+ textEditor4: new FormControl(
+ 'Super styled text 4',
+ [Validators.required],
+ ),
+ });
+
+ this.myForm.valueChanges
+ .pipe(takeUntilDestroyed(this.#destroyRef), startWith(this.myForm.value))
+ .subscribe((value) => {
+ for (const key in value) {
+ this.displayValue[key] = this.#sanitizer.bypassSecurityTrustHtml(
+ value[key],
+ );
+ }
+ });
+ }
+}
diff --git a/apps/playground/src/app/components/text-editor/text-editor.module.ts b/apps/playground/src/app/components/text-editor/text-editor.module.ts
index 45188be792..2f60c1ab5a 100644
--- a/apps/playground/src/app/components/text-editor/text-editor.module.ts
+++ b/apps/playground/src/app/components/text-editor/text-editor.module.ts
@@ -16,6 +16,18 @@ const routes: Routes = [
(m) => m.TextEditorModule,
),
},
+ {
+ path: 'text-editor-series',
+ loadComponent: () =>
+ import('./text-editor-series/text-editor-series.component').then(
+ (m) => m.TextEditorSeriesComponent,
+ ),
+ data: {
+ name: 'Text editor looped',
+ icon: 'pencil-square-o',
+ library: 'text-editor',
+ },
+ },
];
@NgModule({
diff --git a/apps/playground/src/app/components/text-editor/text-editor/text-editor.component.html b/apps/playground/src/app/components/text-editor/text-editor/text-editor.component.html
index e77f199e0a..fcd9894aa7 100644
--- a/apps/playground/src/app/components/text-editor/text-editor/text-editor.component.html
+++ b/apps/playground/src/app/components/text-editor/text-editor/text-editor.component.html
@@ -18,7 +18,7 @@ Text editor
helpPopoverContent="This is some helpful content."
helpPopoverTitle="Did you know?"
hintText="Hint text"
- name="text-editor"
+ stacked
[menus]="menus"
[mergeFields]="mergeFields"
[labelText]="labelText"
diff --git a/apps/playground/src/app/home/home.component.ts b/apps/playground/src/app/home/home.component.ts
index 71334d73e6..97b68cbc87 100644
--- a/apps/playground/src/app/home/home.component.ts
+++ b/apps/playground/src/app/home/home.component.ts
@@ -135,7 +135,6 @@ export class HomeComponent implements AfterViewInit {
routes: ComponentRouteInfo[],
parentPath: string,
): Promise {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
const promises: Promise[] = [];
for (const route of routes) {
diff --git a/libs/components/text-editor/src/lib/modules/text-editor/services/text-editor-adapter.service.spec.ts b/libs/components/text-editor/src/lib/modules/text-editor/services/text-editor-adapter.service.spec.ts
new file mode 100644
index 0000000000..07ffb51d8d
--- /dev/null
+++ b/libs/components/text-editor/src/lib/modules/text-editor/services/text-editor-adapter.service.spec.ts
@@ -0,0 +1,107 @@
+import { TestBed } from '@angular/core/testing';
+import { SkyTextEditorStyleState } from '@skyux/text-editor';
+
+import { STYLE_STATE_DEFAULTS } from '../defaults/style-state-defaults';
+
+import { SkyTextEditorAdapterService } from './text-editor-adapter.service';
+import { SkyTextEditorSelectionService } from './text-editor-selection.service';
+import { SkyTextEditorService } from './text-editor.service';
+
+import SpyObj = jasmine.SpyObj;
+
+describe('SkyTextEditorAdapterService', () => {
+ let styleState: SkyTextEditorStyleState;
+ let doc: SpyObj;
+ let win: SpyObj;
+
+ beforeEach(() => {
+ styleState = Object.assign({}, STYLE_STATE_DEFAULTS);
+ doc = jasmine.createSpyObj(
+ 'Document',
+ [
+ 'addEventListener',
+ 'close',
+ 'createElement',
+ 'open',
+ 'removeEventListener',
+ 'querySelector',
+ ],
+ {
+ body: jasmine.createSpyObj([
+ 'addEventListener',
+ 'setAttribute',
+ ]),
+ head: jasmine.createSpyObj(['appendChild']),
+ },
+ );
+ doc.createElement.and.callFake((tagName: string) => {
+ return document.createElement(tagName);
+ });
+ win = jasmine.createSpyObj(
+ 'Window',
+ ['addEventListener', 'removeEventListener'],
+ {
+ document: doc,
+ },
+ );
+ TestBed.configureTestingModule({
+ providers: [
+ SkyTextEditorAdapterService,
+ SkyTextEditorSelectionService,
+ SkyTextEditorService,
+ ],
+ });
+ });
+
+ it('should initialize the editor', () => {
+ const service = TestBed.inject(SkyTextEditorAdapterService);
+ const iframe = jasmine.createSpyObj(
+ 'HTMLIFrameElement',
+ ['addEventListener', 'removeEventListener'],
+ {
+ contentWindow: win,
+ contentDocument: doc,
+ },
+ );
+ service.initEditor('test', iframe, styleState);
+ expect(doc.body.setAttribute).toHaveBeenCalledWith(
+ 'contenteditable',
+ 'true',
+ );
+ });
+
+ it('should initialize the editor using contentDocument', () => {
+ const service = TestBed.inject(SkyTextEditorAdapterService);
+ const iframe = jasmine.createSpyObj(
+ 'HTMLIFrameElement',
+ ['addEventListener', 'removeEventListener'],
+ {
+ contentWindow: undefined,
+ contentDocument: doc,
+ },
+ );
+ service.initEditor('test', iframe, styleState);
+ expect(doc.body.setAttribute).toHaveBeenCalledWith(
+ 'contenteditable',
+ 'true',
+ );
+ });
+
+ it('should stop initializing the editor when document is null', () => {
+ const service = TestBed.inject(SkyTextEditorAdapterService);
+ const iframe = jasmine.createSpyObj(
+ 'HTMLIFrameElement',
+ ['addEventListener', 'removeEventListener'],
+ {
+ contentWindow: undefined,
+ contentDocument: undefined,
+ },
+ );
+ service.initEditor('test', iframe, styleState);
+ expect(doc.body.setAttribute).not.toHaveBeenCalled();
+ expect(service.editorSelected()).toBeFalse();
+ expect(service.saveSelection()).toBeUndefined();
+ expect(service.getCurrentSelection()).toBeFalsy();
+ expect(service.getSelectedAnchorTag()).toBeFalsy();
+ });
+});
diff --git a/libs/components/text-editor/src/lib/modules/text-editor/services/text-editor-adapter.service.ts b/libs/components/text-editor/src/lib/modules/text-editor/services/text-editor-adapter.service.ts
index 295d64e268..15671fad80 100644
--- a/libs/components/text-editor/src/lib/modules/text-editor/services/text-editor-adapter.service.ts
+++ b/libs/components/text-editor/src/lib/modules/text-editor/services/text-editor-adapter.service.ts
@@ -47,6 +47,9 @@ export class SkyTextEditorAdapterService {
this.#textEditorService.editor = this.#createObservers(iframeElement);
const documentEl = this.#getIframeDocumentEl();
+ if (!documentEl) {
+ return;
+ }
const styleEl = documentEl.createElement('style');
styleEl.innerHTML = `.editor:empty:before {
@@ -109,7 +112,7 @@ export class SkyTextEditorAdapterService {
await navigator.clipboard.readText().then((clipText) => {
/* istanbul ignore else */
if (this.editorSelected()) {
- documentEl.execCommand('insertHTML', false, clipText);
+ documentEl?.execCommand('insertHTML', false, clipText);
this.focusEditor();
this.#textEditorService.editor.commandChangeObservable.next();
@@ -119,7 +122,7 @@ export class SkyTextEditorAdapterService {
} else {
/* istanbul ignore else */
if (this.editorSelected()) {
- documentEl.execCommand(
+ documentEl?.execCommand(
editorCommand.command,
false,
editorCommand.value,
@@ -133,9 +136,11 @@ export class SkyTextEditorAdapterService {
}
public getCurrentSelection(): Selection | null {
- return this.#selectionService.getCurrentSelection(
- this.#getIframeDocumentEl(),
- );
+ const documentEl = this.#getIframeDocumentEl();
+ if (documentEl) {
+ return this.#selectionService.getCurrentSelection(documentEl);
+ }
+ return null;
}
/**
@@ -178,7 +183,7 @@ export class SkyTextEditorAdapterService {
const documentEl = this.#getIframeDocumentEl();
/* istanbul ignore else */
- if (this.editorSelected()) {
+ if (documentEl && this.editorSelected()) {
return {
backColor: this.#getColor(documentEl, 'BackColor'),
fontColor: this.#getColor(documentEl, 'ForeColor'),
@@ -205,9 +210,9 @@ export class SkyTextEditorAdapterService {
public setEditorInnerHtml(value: string): void {
const documentEl = this.#getIframeDocumentEl();
- const editorContent = documentEl.body;
+ const editorContent = documentEl?.body;
/* istanbul ignore else */
- if (editorContent.innerHTML !== value) {
+ if (editorContent && editorContent.innerHTML !== value) {
editorContent.innerHTML = value;
}
}
@@ -217,35 +222,38 @@ export class SkyTextEditorAdapterService {
if (this.#textEditorService.editor) {
const windowEl = this.#getContentWindowEl();
const iframeDocumentEl = this.#getIframeDocumentEl();
- const currentSelection = this.#selectionService.getCurrentSelectionRange(
- iframeDocumentEl,
- windowEl,
- );
- const cursorIsNotActiveAndHasReset =
- currentSelection &&
- currentSelection.startOffset === 0 &&
- currentSelection.endOffset === 0;
-
- if (!this.editorSelected() || cursorIsNotActiveAndHasReset) {
- // focus the end of the editor
- const documentEl = this.#windowRef.nativeWindow.document;
- const editor = iframeDocumentEl.body as HTMLBodyElement;
- const range = documentEl.createRange();
-
- this.#textEditorService.editor.iframeElementRef.focus();
- editor.focus();
-
- if (windowEl.getSelection && documentEl.createRange) {
- range.selectNodeContents(editor);
- range.collapse(false);
- const sel = windowEl.getSelection();
- sel?.removeAllRanges();
- sel?.addRange(range);
+ if (windowEl && iframeDocumentEl) {
+ const currentSelection =
+ this.#selectionService.getCurrentSelectionRange(
+ iframeDocumentEl,
+ windowEl,
+ );
+ const cursorIsNotActiveAndHasReset =
+ currentSelection &&
+ currentSelection.startOffset === 0 &&
+ currentSelection.endOffset === 0;
+
+ if (!this.editorSelected() || cursorIsNotActiveAndHasReset) {
+ // focus the end of the editor
+ const documentEl = this.#windowRef.nativeWindow.document;
+ const editor = iframeDocumentEl.body as HTMLBodyElement;
+ const range = documentEl.createRange();
+
+ this.#textEditorService.editor.iframeElementRef.focus();
+ editor.focus();
+
+ if (windowEl.getSelection && documentEl.createRange) {
+ range.selectNodeContents(editor);
+ range.collapse(false);
+ const sel = windowEl.getSelection();
+ sel?.removeAllRanges();
+ sel?.addRange(range);
+ }
+ } else {
+ // Cursor may not be active, restore it
+ this.#textEditorService.editor.iframeElementRef.focus();
+ iframeDocumentEl.body.focus();
}
- } else {
- // Cursor may not be active, restore it
- this.#textEditorService.editor.iframeElementRef.focus();
- iframeDocumentEl.body.focus();
}
}
}
@@ -273,23 +281,27 @@ export class SkyTextEditorAdapterService {
return this.#getParent(selectedEl, 'a') as HTMLAnchorElement;
}
- /* istanbul ignore next */
return undefined;
}
public saveSelection(): Range | undefined {
- return this.#selectionService.getCurrentSelectionRange(
- this.#getIframeDocumentEl(),
- this.#getContentWindowEl(),
- );
+ const documentEl = this.#getIframeDocumentEl();
+ const windowEl = this.#getContentWindowEl();
+ if (documentEl && windowEl) {
+ return this.#selectionService.getCurrentSelectionRange(
+ documentEl,
+ windowEl,
+ );
+ }
+ return undefined;
}
public selectElement(element: HTMLElement): void {
- this.#selectionService.selectElement(
- this.#getIframeDocumentEl(),
- this.#getContentWindowEl(),
- element,
- );
+ const documentEl = this.#getIframeDocumentEl();
+ const windowEl = this.#getContentWindowEl();
+ if (documentEl && windowEl) {
+ this.#selectionService.selectElement(documentEl, windowEl, element);
+ }
}
public setErrorAttributes(
@@ -297,42 +309,44 @@ export class SkyTextEditorAdapterService {
errors: ValidationErrors | null,
): void {
const documentEl = this.#getIframeDocumentEl();
- documentEl.body.setAttribute('aria-invalid', (!!errors).toString());
+ documentEl?.body.setAttribute('aria-invalid', (!!errors).toString());
if (errors && errorId) {
- documentEl.body.setAttribute('aria-errormessage', errorId);
+ documentEl?.body.setAttribute('aria-errormessage', errorId);
} else {
- documentEl.body.removeAttribute('aria-errormessage');
+ documentEl?.body.removeAttribute('aria-errormessage');
}
}
public setLabelAttribute(label: string | undefined): void {
if (label) {
const documentEl = this.#getIframeDocumentEl();
- documentEl.body.setAttribute('aria-label', label);
+ documentEl?.body.setAttribute('aria-label', label);
}
}
public setPlaceholder(placeholder?: string): void {
const documentEl = this.#getIframeDocumentEl();
- documentEl.body.setAttribute('data-placeholder', placeholder || '');
+ documentEl?.body.setAttribute('data-placeholder', placeholder || '');
}
public setRequiredAttribute(required: boolean): void {
const documentEl = this.#getIframeDocumentEl();
- documentEl.body.setAttribute('aria-required', required.toString());
+ documentEl?.body.setAttribute('aria-required', required.toString());
}
public async setFontSize(fontSize: number): Promise {
const doc = this.#getIframeDocumentEl();
await this.execCommand({ command: 'fontSize', value: '1' });
- const fontElements: HTMLElement[] = Array.from(
- doc.querySelectorAll('font[size="1"]'),
- );
- for (const element of fontElements) {
- element.removeAttribute('size');
- element.style.fontSize = fontSize + 'px';
+ if (doc) {
+ const fontElements: HTMLElement[] = Array.from(
+ doc.querySelectorAll('font[size="1"]'),
+ );
+ for (const element of fontElements) {
+ element.removeAttribute('size');
+ element.style.fontSize = fontSize + 'px';
+ }
+ this.#cleanUpBlankStyleTags(doc);
}
- this.#cleanUpBlankStyleTags(doc);
this.focusEditor();
this.#textEditorService.editor.commandChangeObservable.next();
@@ -357,7 +371,7 @@ export class SkyTextEditorAdapterService {
}
}
- #getContentWindowEl(): Window {
+ #getContentWindowEl(): Window | undefined {
return this.#textEditorService.editor.iframeElementRef
.contentWindow as Window;
}
@@ -414,7 +428,7 @@ export class SkyTextEditorAdapterService {
});
}
- #getIframeDocumentEl(): Document {
+ #getIframeDocumentEl(): Document | undefined {
const contentWindowEl = this.#getContentWindowEl();
/* istanbul ignore else */
if (contentWindowEl) {
@@ -452,11 +466,11 @@ export class SkyTextEditorAdapterService {
/* istanbul ignore next */
const documentEl = element.contentWindow
? element.contentWindow.document
- : (element.contentDocument as Document);
+ : element.contentDocument;
// Firefox bug where we need to open/close to cancel load so it doesn't overwrite attrs
- documentEl.open();
- documentEl.close();
+ documentEl?.open();
+ documentEl?.close();
const selectionObservable = new Subject();
const selectionListener = (): void => selectionObservable.next();
@@ -470,12 +484,12 @@ export class SkyTextEditorAdapterService {
const inputObservable = new Subject();
const inputListener = (): void => inputObservable.next();
- documentEl.addEventListener('selectionchange', selectionListener);
- documentEl.addEventListener('input', inputListener);
- documentEl.addEventListener('mousedown', clickListener);
- documentEl.body.addEventListener('paste', pasteListener);
- documentEl.body.addEventListener('blur', blurListener);
- documentEl.body.addEventListener('focusin', focusListener);
+ documentEl?.addEventListener('selectionchange', selectionListener);
+ documentEl?.addEventListener('input', inputListener);
+ documentEl?.addEventListener('mousedown', clickListener);
+ documentEl?.body.addEventListener('paste', pasteListener);
+ documentEl?.body.addEventListener('blur', blurListener);
+ documentEl?.body.addEventListener('focusin', focusListener);
return {
blurObservable,
clickObservable,
@@ -509,9 +523,13 @@ export class SkyTextEditorAdapterService {
}
#getCurrentSelectionParentElement(): Element | null | undefined {
- return this.#selectionService.getCurrentSelectionParentElement(
- this.#getIframeDocumentEl(),
- );
+ const documentEl = this.#getIframeDocumentEl();
+ if (documentEl) {
+ return this.#selectionService.getCurrentSelectionParentElement(
+ documentEl,
+ );
+ }
+ return undefined;
}
#getColor(documentEl: Document, selector: string): string {
@@ -560,10 +578,13 @@ export class SkyTextEditorAdapterService {
public editorSelected(): boolean {
const documentEl = this.#getIframeDocumentEl();
- return this.#selectionService.isElementSelected(
- documentEl,
- documentEl.body,
- );
+ if (documentEl) {
+ return this.#selectionService.isElementSelected(
+ documentEl,
+ documentEl.body,
+ );
+ }
+ return false;
}
#cleanUpBlankStyleTags(doc: Document): void {
@@ -619,7 +640,7 @@ export class SkyTextEditorAdapterService {
aFocusableChild.tabIndex = disabled ? -1 : 0;
});
}
- this.#getIframeDocumentEl().body.setAttribute(
+ this.#getIframeDocumentEl()?.body.setAttribute(
'contenteditable',
disabled ? 'false' : 'true',
);