nexheal-lib 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +17 -0
- package/.vscode/extensions.json +4 -0
- package/.vscode/launch.json +20 -0
- package/.vscode/tasks.json +42 -0
- package/README.md +15 -19
- package/angular.json +36 -0
- package/package.json +47 -23
- package/projects/nexheal-lib/README.md +63 -0
- package/projects/nexheal-lib/ng-package.json +9 -0
- package/projects/nexheal-lib/package.json +12 -0
- package/projects/nexheal-lib/src/directives/clickoutside.directive.ts +34 -0
- package/projects/nexheal-lib/src/lib/controls/autocomplete-control/autocomplete-control.component.html +52 -0
- package/projects/nexheal-lib/src/lib/controls/autocomplete-control/autocomplete-control.component.scss +22 -0
- package/projects/nexheal-lib/src/lib/controls/autocomplete-control/autocomplete-control.component.spec.ts +22 -0
- package/projects/nexheal-lib/src/lib/controls/autocomplete-control/autocomplete-control.component.ts +367 -0
- package/projects/nexheal-lib/src/lib/controls/calendar-control/calendar-control.component.html +152 -0
- package/projects/nexheal-lib/src/lib/controls/calendar-control/calendar-control.component.scss +194 -0
- package/projects/nexheal-lib/src/lib/controls/calendar-control/calendar-control.component.spec.ts +22 -0
- package/projects/nexheal-lib/src/lib/controls/calendar-control/calendar-control.component.ts +759 -0
- package/projects/nexheal-lib/src/lib/controls/checkbox-control/checkbox-control.component.html +4 -0
- package/projects/nexheal-lib/src/lib/controls/checkbox-control/checkbox-control.component.spec.ts +22 -0
- package/projects/nexheal-lib/src/lib/controls/checkbox-control/checkbox-control.component.ts +94 -0
- package/projects/nexheal-lib/src/lib/controls/input-control/input-control.component.html +61 -0
- package/projects/nexheal-lib/src/lib/controls/input-control/input-control.component.scss +132 -0
- package/projects/nexheal-lib/src/lib/controls/input-control/input-control.component.spec.ts +22 -0
- package/projects/nexheal-lib/src/lib/controls/input-control/input-control.component.ts +202 -0
- package/projects/nexheal-lib/src/lib/controls/multiselect-control/multiselect-control.component.html +72 -0
- package/projects/nexheal-lib/src/lib/controls/multiselect-control/multiselect-control.component.scss +90 -0
- package/projects/nexheal-lib/src/lib/controls/multiselect-control/multiselect-control.component.spec.ts +22 -0
- package/projects/nexheal-lib/src/lib/controls/multiselect-control/multiselect-control.component.ts +482 -0
- package/projects/nexheal-lib/src/lib/controls/select-control/select-control.component.html +53 -0
- package/projects/nexheal-lib/src/lib/controls/select-control/select-control.component.scss +19 -0
- package/projects/nexheal-lib/src/lib/controls/select-control/select-control.component.spec.ts +22 -0
- package/projects/nexheal-lib/src/lib/controls/select-control/select-control.component.ts +375 -0
- package/projects/nexheal-lib/src/lib/controls/switch-control/switch-control.component.html +4 -0
- package/projects/nexheal-lib/src/lib/controls/switch-control/switch-control.component.scss +53 -0
- package/projects/nexheal-lib/src/lib/controls/switch-control/switch-control.component.spec.ts +22 -0
- package/projects/nexheal-lib/src/lib/controls/switch-control/switch-control.component.ts +93 -0
- package/projects/nexheal-lib/src/lib/controls/text-editor/text-editor.component.html +88 -0
- package/projects/nexheal-lib/src/lib/controls/text-editor/text-editor.component.scss +122 -0
- package/projects/nexheal-lib/src/lib/controls/text-editor/text-editor.component.spec.ts +22 -0
- package/projects/nexheal-lib/src/lib/controls/text-editor/text-editor.component.ts +314 -0
- package/projects/nexheal-lib/src/lib/controls/textarea-control/textarea-control.component.html +19 -0
- package/projects/nexheal-lib/src/lib/controls/textarea-control/textarea-control.component.scss +15 -0
- package/projects/nexheal-lib/src/lib/controls/textarea-control/textarea-control.component.spec.ts +22 -0
- package/projects/nexheal-lib/src/lib/controls/textarea-control/textarea-control.component.ts +83 -0
- package/projects/nexheal-lib/src/public-api.ts +13 -0
- package/projects/nexheal-lib/src/styles/nexheal.scss +1 -0
- package/projects/nexheal-lib/tsconfig.lib.json +18 -0
- package/projects/nexheal-lib/tsconfig.lib.prod.json +11 -0
- package/projects/nexheal-lib/tsconfig.spec.json +14 -0
- package/tsconfig.json +39 -0
- package/fesm2022/nexheal-lib.mjs +0 -2837
- package/fesm2022/nexheal-lib.mjs.map +0 -1
- package/index.d.ts +0 -498
- package/src/styles/fonts/icomoon.eot +0 -0
- package/src/styles/fonts/icomoon.svg +0 -46
- package/src/styles/fonts/icomoon.ttf +0 -0
- package/src/styles/fonts/icomoon.woff +0 -0
- package/src/styles/icon.css +0 -133
- package/src/styles/nexheal.scss +0 -2
- /package/{src → projects/nexheal-lib/src}/styles/_formcontrols.scss +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
.text-editor {
|
|
2
|
+
border: 1px solid #ccc;
|
|
3
|
+
.toolbar {
|
|
4
|
+
gap: 0.75rem;
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-wrap: wrap;
|
|
7
|
+
background: #f8f8f8;
|
|
8
|
+
padding: 0.75rem 0.75rem;
|
|
9
|
+
border-bottom: 1px solid #cccccc;
|
|
10
|
+
.toolbar-items {
|
|
11
|
+
height: 18px;
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-wrap: wrap;
|
|
14
|
+
align-items: center;
|
|
15
|
+
padding-right: 0.75rem;
|
|
16
|
+
border-right: 1px solid #969090;
|
|
17
|
+
button,
|
|
18
|
+
.input-wrap {
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
min-width: 25px;
|
|
21
|
+
min-height: 20px;
|
|
22
|
+
&:hover {
|
|
23
|
+
i {
|
|
24
|
+
color: #000000;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
button {
|
|
29
|
+
border: 0;
|
|
30
|
+
display: grid;
|
|
31
|
+
place-items: center;
|
|
32
|
+
background: transparent;
|
|
33
|
+
&:hover {
|
|
34
|
+
background: #eeeeee;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
.input-wrap {
|
|
38
|
+
gap: 2px;
|
|
39
|
+
display: flex;
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
align-items: center;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
input[type="color"] {
|
|
45
|
+
width: 80%;
|
|
46
|
+
padding: 0;
|
|
47
|
+
height: 2px;
|
|
48
|
+
border: none;
|
|
49
|
+
cursor: inherit;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
i {
|
|
53
|
+
font-size: 13px;
|
|
54
|
+
color: #3c4148;
|
|
55
|
+
&.he-bold {
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
}
|
|
58
|
+
&.he-underline {
|
|
59
|
+
font-size: 14px;
|
|
60
|
+
}
|
|
61
|
+
&.he-text-drop {
|
|
62
|
+
font-size: 12px;
|
|
63
|
+
}
|
|
64
|
+
&.he-background-drop {
|
|
65
|
+
top: -1px;
|
|
66
|
+
font-size: 12px;
|
|
67
|
+
}
|
|
68
|
+
&.he-heading-1,
|
|
69
|
+
&.he-link,
|
|
70
|
+
&.he-left-align,
|
|
71
|
+
&.he-center-align,
|
|
72
|
+
&.he-right-align,
|
|
73
|
+
&.he-justify {
|
|
74
|
+
font-size: 16px;
|
|
75
|
+
}
|
|
76
|
+
&.he-text-indent-left,
|
|
77
|
+
&.he-text-indent-right {
|
|
78
|
+
font-size: 17px;
|
|
79
|
+
}
|
|
80
|
+
&.he-heading-2 {
|
|
81
|
+
top: 1px;
|
|
82
|
+
font-size: 16px;
|
|
83
|
+
}
|
|
84
|
+
&.he-unordered-list,
|
|
85
|
+
&.he-ordered-list {
|
|
86
|
+
top: -1px;
|
|
87
|
+
font-size: 20px;
|
|
88
|
+
}
|
|
89
|
+
&.he-image {
|
|
90
|
+
font-size: 15px;
|
|
91
|
+
font-weight: 600;
|
|
92
|
+
}
|
|
93
|
+
&.he-redo,
|
|
94
|
+
&.he-undo {
|
|
95
|
+
top: -1px;
|
|
96
|
+
font-size: 14px;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
&.more-gap {
|
|
100
|
+
gap: 5px;
|
|
101
|
+
}
|
|
102
|
+
&:last-child {
|
|
103
|
+
border-right: 0;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
.editor-content {
|
|
108
|
+
outline: none;
|
|
109
|
+
min-height: 200px;
|
|
110
|
+
position: relative;
|
|
111
|
+
background: #ffffff;
|
|
112
|
+
padding: 0.75rem 0.75rem;
|
|
113
|
+
&[data-placeholder]:empty:before {
|
|
114
|
+
content: attr(data-placeholder);
|
|
115
|
+
color: #aaa;
|
|
116
|
+
pointer-events: none;
|
|
117
|
+
position: absolute;
|
|
118
|
+
left: 0.75rem;
|
|
119
|
+
top: 0.75rem;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { TextEditor } from './text-editor.component';
|
|
3
|
+
|
|
4
|
+
describe('TextEditor', () => {
|
|
5
|
+
let component: TextEditor;
|
|
6
|
+
let fixture: ComponentFixture<TextEditor>;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
await TestBed.configureTestingModule({
|
|
10
|
+
imports: [TextEditor]
|
|
11
|
+
})
|
|
12
|
+
.compileComponents();
|
|
13
|
+
|
|
14
|
+
fixture = TestBed.createComponent(TextEditor);
|
|
15
|
+
component = fixture.componentInstance;
|
|
16
|
+
fixture.detectChanges();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should create', () => {
|
|
20
|
+
expect(component).toBeTruthy();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { CommonModule } from '@angular/common';
|
|
2
|
+
import { Component, OnInit, Input, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
|
|
3
|
+
import { ControlValueAccessor } from '@angular/forms';
|
|
4
|
+
|
|
5
|
+
interface HistoryState {
|
|
6
|
+
html: string;
|
|
7
|
+
selection?: SelectionRange;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// To restore selection, we need to store something about it. This is complex.
|
|
11
|
+
// For simplicity, we only store HTML. True undo/redo of selection positions is non-trivial.
|
|
12
|
+
interface SelectionRange {
|
|
13
|
+
startContainerPath: number[];
|
|
14
|
+
startOffset: number;
|
|
15
|
+
endContainerPath: number[];
|
|
16
|
+
endOffset: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Component({
|
|
20
|
+
selector: 'app-text-editor',
|
|
21
|
+
imports:
|
|
22
|
+
[CommonModule,
|
|
23
|
+
|
|
24
|
+
],
|
|
25
|
+
templateUrl: './text-editor.component.html',
|
|
26
|
+
styleUrl: './text-editor.component.scss'
|
|
27
|
+
})
|
|
28
|
+
export class TextEditor implements ControlValueAccessor, OnInit, AfterViewInit {
|
|
29
|
+
|
|
30
|
+
@Input() header: boolean = true;
|
|
31
|
+
@Input() media: boolean = true;
|
|
32
|
+
@Input() link: boolean = true;
|
|
33
|
+
@Input() placeholder: string = 'Type here...';
|
|
34
|
+
|
|
35
|
+
@ViewChild('editor') editorRef!: ElementRef<HTMLDivElement>;
|
|
36
|
+
|
|
37
|
+
private onChange: (value: string) => void = () => { };
|
|
38
|
+
private onTouch: () => void = () => { };
|
|
39
|
+
private undoStack: HistoryState[] = [];
|
|
40
|
+
private redoStack: HistoryState[] = [];
|
|
41
|
+
|
|
42
|
+
ngOnInit() { }
|
|
43
|
+
|
|
44
|
+
ngAfterViewInit() {
|
|
45
|
+
this.saveState();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
writeValue(value: string): void {
|
|
49
|
+
this.editorRef.nativeElement.innerHTML = value || '';
|
|
50
|
+
this.saveState();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
registerOnChange(fn: any): void {
|
|
54
|
+
this.onChange = fn;
|
|
55
|
+
}
|
|
56
|
+
registerOnTouched(fn: any): void {
|
|
57
|
+
this.onTouch = fn;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
onInput() {
|
|
61
|
+
const html = this.editorRef.nativeElement.innerHTML;
|
|
62
|
+
this.onChange(html);
|
|
63
|
+
this.saveState();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// events
|
|
67
|
+
onTouched() {
|
|
68
|
+
this.onTouch();
|
|
69
|
+
}
|
|
70
|
+
setDisabledState(isDisabled: boolean): void {
|
|
71
|
+
this.editorRef.nativeElement.contentEditable = (!isDisabled).toString();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---- Selection & Range Utilities ----
|
|
75
|
+
private getSelectionRange(): Range | null {
|
|
76
|
+
const selection = window.getSelection();
|
|
77
|
+
if (selection && selection.rangeCount > 0) {
|
|
78
|
+
return selection.getRangeAt(0);
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private wrapSelection(htmlTag: string, style?: string) {
|
|
84
|
+
const range = this.getSelectionRange();
|
|
85
|
+
if (!range || range.collapsed) return; // no selection
|
|
86
|
+
const selectedText = range.extractContents();
|
|
87
|
+
|
|
88
|
+
const el = document.createElement(htmlTag);
|
|
89
|
+
if (style) {
|
|
90
|
+
el.setAttribute('style', style);
|
|
91
|
+
}
|
|
92
|
+
el.appendChild(selectedText);
|
|
93
|
+
range.insertNode(el);
|
|
94
|
+
this.mergeAdjacentSimilarElements(el);
|
|
95
|
+
this.onInput();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Merges adjacent spans with the same style to avoid clutter
|
|
99
|
+
private mergeAdjacentSimilarElements(el: HTMLElement) {
|
|
100
|
+
const parent = el.parentElement;
|
|
101
|
+
if (!parent) return;
|
|
102
|
+
|
|
103
|
+
const siblings = Array.from(parent.childNodes);
|
|
104
|
+
for (let i = 0; i < siblings.length - 1; i++) {
|
|
105
|
+
const current = siblings[i] as HTMLElement;
|
|
106
|
+
const next = siblings[i + 1] as HTMLElement;
|
|
107
|
+
if (current.nodeType === 1 && next?.nodeType === 1) {
|
|
108
|
+
if (current.tagName === next.tagName && current.getAttribute('style') === next.getAttribute('style')) {
|
|
109
|
+
// merge them
|
|
110
|
+
while (next.firstChild) {
|
|
111
|
+
current.appendChild(next.firstChild);
|
|
112
|
+
}
|
|
113
|
+
next.remove();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---- Inline Formatting ----
|
|
120
|
+
// applyInlineStyle() toggles inline styles by wrapping selection in a styled span
|
|
121
|
+
applyInlineStyle(styleType: 'bold' | 'italic' | 'underline') {
|
|
122
|
+
let style = '';
|
|
123
|
+
if (styleType === 'bold') style = 'font-weight:bold;';
|
|
124
|
+
else if (styleType === 'italic') style = 'font-style:italic;';
|
|
125
|
+
else if (styleType === 'underline') style = 'text-decoration:underline;';
|
|
126
|
+
|
|
127
|
+
this.wrapSelection('span', style);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
applyColor(event: Event) {
|
|
131
|
+
const input = event.target as HTMLInputElement;
|
|
132
|
+
const color = input.value;
|
|
133
|
+
// Now `color` is a string and `input` is never null
|
|
134
|
+
this.wrapSelection('span', `color:${color};`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
applyHighlight(event: Event) {
|
|
138
|
+
const input = event.target as HTMLInputElement;
|
|
139
|
+
const color = input.value;
|
|
140
|
+
this.wrapSelection('span', `background-color:${color};`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---- Block Formatting (Headings, Paragraph) ----
|
|
144
|
+
// Replaces the parent block element containing selection with a new block type
|
|
145
|
+
applyBlockFormat(blockTag: 'h1' | 'h2' | 'p') {
|
|
146
|
+
const range = this.getSelectionRange();
|
|
147
|
+
if (!range) return;
|
|
148
|
+
|
|
149
|
+
// Find the nearest block element
|
|
150
|
+
let block = this.findBlockAncestor(range.commonAncestorContainer);
|
|
151
|
+
if (!block) {
|
|
152
|
+
// If no block found, wrap the selection in a block
|
|
153
|
+
const wrapper = document.createElement(blockTag);
|
|
154
|
+
wrapper.appendChild(range.extractContents());
|
|
155
|
+
range.insertNode(wrapper);
|
|
156
|
+
} else {
|
|
157
|
+
// Replace the block with a new block type
|
|
158
|
+
const newBlock = document.createElement(blockTag);
|
|
159
|
+
// Move children
|
|
160
|
+
while (block.firstChild) {
|
|
161
|
+
newBlock.appendChild(block.firstChild);
|
|
162
|
+
}
|
|
163
|
+
block.replaceWith(newBlock);
|
|
164
|
+
}
|
|
165
|
+
this.onInput();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private findBlockAncestor(node: Node): HTMLElement | null {
|
|
169
|
+
while (node && node !== this.editorRef.nativeElement) {
|
|
170
|
+
if (this.isBlockElement(node)) {
|
|
171
|
+
return node as HTMLElement;
|
|
172
|
+
}
|
|
173
|
+
node = node.parentNode!;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private isBlockElement(node: Node): boolean {
|
|
179
|
+
if (node.nodeType !== 1) return false;
|
|
180
|
+
const display = window.getComputedStyle(node as HTMLElement).display;
|
|
181
|
+
return display === 'block' || display === 'list-item';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---- Lists ----
|
|
185
|
+
// Convert selected lines into a list
|
|
186
|
+
applyList(listType: 'ul' | 'ol') {
|
|
187
|
+
const range = this.getSelectionRange();
|
|
188
|
+
if (!range) return;
|
|
189
|
+
const commonBlock = this.findBlockAncestor(range.commonAncestorContainer);
|
|
190
|
+
|
|
191
|
+
// Get the text in the selection
|
|
192
|
+
const content = range.extractContents();
|
|
193
|
+
const lines = this.splitContentByLine(content);
|
|
194
|
+
|
|
195
|
+
const listEl = document.createElement(listType);
|
|
196
|
+
for (const line of lines) {
|
|
197
|
+
const li = document.createElement('li');
|
|
198
|
+
li.appendChild(line);
|
|
199
|
+
listEl.appendChild(li);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (commonBlock) {
|
|
203
|
+
// Insert the list right where the block was or at the selection
|
|
204
|
+
range.insertNode(listEl);
|
|
205
|
+
} else {
|
|
206
|
+
// If no common block, just insert at current position
|
|
207
|
+
range.insertNode(listEl);
|
|
208
|
+
}
|
|
209
|
+
this.onInput();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private splitContentByLine(fragment: DocumentFragment): DocumentFragment[] {
|
|
213
|
+
// A very naive approach: split by <br> or block elements.
|
|
214
|
+
// For advanced logic, detect line breaks more thoroughly.
|
|
215
|
+
const lines: DocumentFragment[] = [];
|
|
216
|
+
let currentFrag = document.createDocumentFragment();
|
|
217
|
+
|
|
218
|
+
Array.from(fragment.childNodes).forEach((node) => {
|
|
219
|
+
if (node.nodeName === 'BR' || this.isBlockElement(node)) {
|
|
220
|
+
// This node signifies a new line
|
|
221
|
+
lines.push(currentFrag);
|
|
222
|
+
currentFrag = document.createDocumentFragment();
|
|
223
|
+
if (this.isBlockElement(node)) {
|
|
224
|
+
while (node.firstChild) {
|
|
225
|
+
currentFrag.appendChild(node.firstChild);
|
|
226
|
+
}
|
|
227
|
+
lines.push(currentFrag);
|
|
228
|
+
currentFrag = document.createDocumentFragment();
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
currentFrag.appendChild(node);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (currentFrag.childNodes.length > 0) {
|
|
236
|
+
lines.push(currentFrag);
|
|
237
|
+
}
|
|
238
|
+
return lines;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---- Links ----
|
|
242
|
+
createLink() {
|
|
243
|
+
const url = prompt('Enter URL', 'https://');
|
|
244
|
+
if (!url) return;
|
|
245
|
+
const range = this.getSelectionRange();
|
|
246
|
+
if (!range || range.collapsed) return;
|
|
247
|
+
const selectedContent = range.extractContents();
|
|
248
|
+
const a = document.createElement('a');
|
|
249
|
+
a.href = url;
|
|
250
|
+
a.appendChild(selectedContent);
|
|
251
|
+
range.insertNode(a);
|
|
252
|
+
this.onInput();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---- Images ----
|
|
256
|
+
insertImage() {
|
|
257
|
+
const url = prompt('Enter image URL:');
|
|
258
|
+
if (!url) return;
|
|
259
|
+
const range = this.getSelectionRange();
|
|
260
|
+
if (!range) return;
|
|
261
|
+
const img = document.createElement('img');
|
|
262
|
+
img.src = url;
|
|
263
|
+
range.insertNode(img);
|
|
264
|
+
this.onInput();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---- Alignment ----
|
|
268
|
+
setAlignment(alignment: 'left' | 'center' | 'right' | 'justify') {
|
|
269
|
+
// Find the block and set text-align
|
|
270
|
+
const range = this.getSelectionRange();
|
|
271
|
+
if (!range) return;
|
|
272
|
+
const block = this.findBlockAncestor(range.commonAncestorContainer);
|
|
273
|
+
if (block) {
|
|
274
|
+
block.style.textAlign = alignment;
|
|
275
|
+
this.onInput();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---- Clear Formatting ----
|
|
280
|
+
clearFormatting() {
|
|
281
|
+
const html = this.editorRef.nativeElement.innerText;
|
|
282
|
+
// Convert innerText to a plain <p> block for simplicity
|
|
283
|
+
this.editorRef.nativeElement.innerHTML = `<p>${html}</p>`;
|
|
284
|
+
this.onInput();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---- Undo/Redo ----
|
|
288
|
+
saveState() {
|
|
289
|
+
const currentHtml = this.editorRef.nativeElement.innerHTML;
|
|
290
|
+
if (this.undoStack.length === 0 || this.undoStack[this.undoStack.length - 1].html !== currentHtml) {
|
|
291
|
+
this.undoStack.push({ html: currentHtml });
|
|
292
|
+
this.redoStack = []; // clear redo on new input
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// clear options
|
|
297
|
+
undo() {
|
|
298
|
+
if (this.undoStack.length > 1) {
|
|
299
|
+
const current = this.undoStack.pop()!;
|
|
300
|
+
this.redoStack.push(current);
|
|
301
|
+
const previous = this.undoStack[this.undoStack.length - 1];
|
|
302
|
+
this.editorRef.nativeElement.innerHTML = previous.html;
|
|
303
|
+
this.onChange(previous.html);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
redo() {
|
|
307
|
+
if (this.redoStack.length > 0) {
|
|
308
|
+
const state = this.redoStack.pop()!;
|
|
309
|
+
this.undoStack.push(state);
|
|
310
|
+
this.editorRef.nativeElement.innerHTML = state.html;
|
|
311
|
+
this.onChange(state.html);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
package/projects/nexheal-lib/src/lib/controls/textarea-control/textarea-control.component.html
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<div class="form-group textarea" [ngClass]="customClass">
|
|
2
|
+
@if (title) {
|
|
3
|
+
<label class="inp-label" [ngClass]="{'required' : required}">{{ title }}</label>
|
|
4
|
+
}
|
|
5
|
+
<textarea class="form-control" [placeholder]="placeholder" [formControl]="textareaControl"
|
|
6
|
+
[ngClass]="{ 'is-invalid': error }" (blur)="onBlur()"></textarea>
|
|
7
|
+
|
|
8
|
+
<span class="focus-border"></span>
|
|
9
|
+
|
|
10
|
+
@if (textareaControl.value && clearVal) {
|
|
11
|
+
<label class="clear" (click)="resetTextarea()">
|
|
12
|
+
<i class="he he-close"></i>
|
|
13
|
+
</label>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@if (error) {
|
|
17
|
+
<div class="val-msg">{{ errorMessage }}</div>
|
|
18
|
+
}
|
|
19
|
+
</div>
|
package/projects/nexheal-lib/src/lib/controls/textarea-control/textarea-control.component.spec.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { TextareaControl } from './textarea-control.component';
|
|
3
|
+
|
|
4
|
+
describe('TextareaControl', () => {
|
|
5
|
+
let component: TextareaControl;
|
|
6
|
+
let fixture: ComponentFixture<TextareaControl>;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
await TestBed.configureTestingModule({
|
|
10
|
+
imports: [TextareaControl]
|
|
11
|
+
})
|
|
12
|
+
.compileComponents();
|
|
13
|
+
|
|
14
|
+
fixture = TestBed.createComponent(TextareaControl);
|
|
15
|
+
component = fixture.componentInstance;
|
|
16
|
+
fixture.detectChanges();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should create', () => {
|
|
20
|
+
expect(component).toBeTruthy();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { CommonModule } from '@angular/common';
|
|
2
|
+
import { Component, Input, Output, EventEmitter, forwardRef } from '@angular/core';
|
|
3
|
+
import { NG_VALUE_ACCESSOR, ControlValueAccessor, ReactiveFormsModule, FormControl } from '@angular/forms';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'textarea-control',
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [
|
|
9
|
+
CommonModule,
|
|
10
|
+
ReactiveFormsModule
|
|
11
|
+
],
|
|
12
|
+
providers: [
|
|
13
|
+
{
|
|
14
|
+
provide: NG_VALUE_ACCESSOR,
|
|
15
|
+
useExisting: forwardRef(() => TextareaControl),
|
|
16
|
+
multi: true,
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
templateUrl: './textarea-control.component.html',
|
|
20
|
+
styleUrl: './textarea-control.component.scss'
|
|
21
|
+
})
|
|
22
|
+
export class TextareaControl {
|
|
23
|
+
@Input() title!: string;
|
|
24
|
+
@Input() placeholder: string = '';
|
|
25
|
+
@Input() customClass!: string;
|
|
26
|
+
@Input() clearVal: boolean = true;
|
|
27
|
+
@Input() disabled: boolean = false;
|
|
28
|
+
@Input() required: boolean = false;
|
|
29
|
+
@Input() error: boolean = false; // New input for error state
|
|
30
|
+
@Input() errorMessage: string = ''; // New input for error message
|
|
31
|
+
@Output() textareaChange = new EventEmitter<string>();
|
|
32
|
+
@Output() selectionCleared = new EventEmitter<void>();
|
|
33
|
+
@Output() blurEvent = new EventEmitter<void>();
|
|
34
|
+
|
|
35
|
+
private onChange: any = () => { };
|
|
36
|
+
private onTouched: any = () => { };
|
|
37
|
+
|
|
38
|
+
textareaControl = new FormControl('');
|
|
39
|
+
|
|
40
|
+
constructor() {
|
|
41
|
+
this.textareaControl.valueChanges.subscribe(value => {
|
|
42
|
+
this.onChange(value);
|
|
43
|
+
this.onTouched();
|
|
44
|
+
this.textareaChange.emit(value ?? '');
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
writeValue(value: any): void {
|
|
49
|
+
if (value !== undefined) {
|
|
50
|
+
this.textareaControl.setValue(value, { emitEvent: false });
|
|
51
|
+
}
|
|
52
|
+
if (this.textareaControl.disabled !== this.disabled) {
|
|
53
|
+
this.setDisabledState(this.disabled);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
registerOnChange(fn: any): void {
|
|
58
|
+
this.onChange = fn;
|
|
59
|
+
}
|
|
60
|
+
registerOnTouched(fn: any): void {
|
|
61
|
+
this.onTouched = fn;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// events
|
|
65
|
+
onBlur() {
|
|
66
|
+
this.blurEvent.emit();
|
|
67
|
+
this.onTouched();
|
|
68
|
+
}
|
|
69
|
+
setDisabledState(isDisabled: boolean): void {
|
|
70
|
+
if (this.disabled) {
|
|
71
|
+
this.textareaControl.disable({ emitEvent: false });
|
|
72
|
+
} else {
|
|
73
|
+
this.textareaControl.enable({ emitEvent: false });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// clear
|
|
78
|
+
resetTextarea() {
|
|
79
|
+
this.textareaControl.setValue('');
|
|
80
|
+
this.selectionCleared.emit();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Public API Surface of heal-lib
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { AutocompleteControl } from './lib/controls/autocomplete-control/autocomplete-control.component';
|
|
6
|
+
export { CalendarControl } from './lib/controls/calendar-control/calendar-control.component';
|
|
7
|
+
export { CheckboxControl } from './lib/controls/checkbox-control/checkbox-control.component';
|
|
8
|
+
export { InputControl } from './lib/controls/input-control/input-control.component';
|
|
9
|
+
export { MultiselectControl } from './lib/controls/multiselect-control/multiselect-control.component';
|
|
10
|
+
export { SelectControl } from './lib/controls/select-control/select-control.component';
|
|
11
|
+
export { SwitchControl } from './lib/controls/switch-control/switch-control.component';
|
|
12
|
+
export { TextEditor } from './lib/controls/text-editor/text-editor.component';
|
|
13
|
+
export { TextareaControl } from './lib/controls/textarea-control/textarea-control.component';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@use "formcontrols";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "../../tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "../../out-tsc/lib",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"inlineSources": true,
|
|
10
|
+
"types": []
|
|
11
|
+
},
|
|
12
|
+
"include": [
|
|
13
|
+
"src/**/*.ts"
|
|
14
|
+
],
|
|
15
|
+
"exclude": [
|
|
16
|
+
"**/*.spec.ts"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "./tsconfig.lib.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"declarationMap": false
|
|
7
|
+
},
|
|
8
|
+
"angularCompilerOptions": {
|
|
9
|
+
"compilationMode": "partial"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "../../tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "../../out-tsc/spec",
|
|
7
|
+
"types": [
|
|
8
|
+
"jasmine"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"include": [
|
|
12
|
+
"src/**/*.ts"
|
|
13
|
+
]
|
|
14
|
+
}
|