36 | HTML Output: {{ htmlContent1 }} 37 |
38 | 42 |43 | Form Value: {{ form.value.signature }} 44 |
45 |46 | Form Status: {{ form.status }} 47 |
48 |Angular cli, npm and webpack.
45 |
46 | ## Fetch the source code
47 |
48 | The code for `angular-editor` is :
49 |
50 | * [AngularEditor](https://github.com/kolkov/angular-editor) (`@kolkov/angular-editor` on npm)
51 |
52 | Clone repository.
53 |
54 | ```
55 | mkdir angular-editor
56 | cd angular-editor
57 | git clone https://github.com/kolkov/angular-editor.git
58 | ```
59 |
60 | ## Install dependencies
61 |
62 | Use `npm` to install the development dependencies for the repository.
63 |
64 | ```
65 | cd angular-editor
66 | npm install
67 | ```
68 |
69 | After executing these steps, your local copy of `@kolkov/angular-editor-app` will be built using your local copy of `@kolkov/angular-editor`
70 | instead of the prebuilt version specified in `package.json`.
71 |
72 | ## Develop
73 |
74 | * `npm run build-watch:lib`: Continuously builds the `@kolkov/angular-editor` library when sources change.
75 | * `npm start`: Runs the demo app (requires library to be built first or running in watch mode)
76 |
77 | **Recommended development workflow:**
78 | ```bash
79 | # Terminal 1: Watch and rebuild library on changes
80 | npm run build-watch:lib
81 |
82 | # Terminal 2: Run demo app
83 | npm start
84 | ```
85 |
86 | This setup ensures the demo app automatically picks up library changes.
87 |
--------------------------------------------------------------------------------
/.github/workflows/README.md:
--------------------------------------------------------------------------------
1 | # GitHub Actions Workflows
2 |
3 | ## npm-publish.yml
4 |
5 | Автоматическая публикация библиотеки в npm при создании git tag.
6 |
7 | ### Как работает:
8 |
9 | 1. **Триггер**: Срабатывает при push тега вида `v*` (например, `v3.0.0`, `v3.1.0`)
10 | 2. **Сборка**: Запускает тесты и production build библиотеки
11 | 3. **Публикация**: Определяет npm tag автоматически:
12 | - Stable версии (3.0.0, 3.1.0) → публикуются с тегом `latest`
13 | - Pre-release (3.0.0-beta.1, 3.1.0-rc.1) → публикуются с тегом `next`
14 | 4. **GitHub Release**: Автоматически создаёт GitHub Release с CHANGELOG
15 |
16 | ### Настройка (один раз):
17 |
18 | #### 🔐 NPM Trusted Publishing (Рекомендуется)
19 |
20 | **Современный способ без токенов! (с 15 ноября 2025 классические токены будут отключены)**
21 |
22 | 1. Зайти на [npmjs.com](https://www.npmjs.com/)
23 | 2. Перейти к пакету: `@kolkov/angular-editor`
24 | 3. **Settings** → **Publishing Access**
25 | 4. **Add Trusted Publisher**
26 | 5. Заполнить форму:
27 | - **Provider**: GitHub
28 | - **Organization/User**: `kolkov`
29 | - **Repository**: `angular-editor`
30 | - **Workflow filename**: `npm-publish.yml`
31 | - **Environment**: оставить пустым (или указать `production` если используете)
32 |
33 | 6. **Save** → Готово! 🎉
34 |
35 | **Преимущества:**
36 | - ✅ Не нужны токены (NPM_TOKEN)
37 | - ✅ Автоматическая ротация ключей
38 | - ✅ Provenance (криптографическое подтверждение источника)
39 | - ✅ Повышенная безопасность через OIDC
40 |
41 | #### 🔑 Альтернатива: Granular Access Token (устаревший способ)
42 |
43 | **Используйте только если Trusted Publishing недоступен**
44 |
45 | 1. Зайти на [npmjs.com](https://www.npmjs.com/)
46 | 2. Settings → Access Tokens → Generate New Token
47 | 3. Выбрать тип: **Granular Access Token**
48 | 4. Настроить:
49 | - **Expiration**: максимум 90 дней
50 | - **Packages**: выбрать `@kolkov/angular-editor`
51 | - **Permissions**: Read and write
52 | 5. Скопировать токен
53 |
54 | 6. Открыть репозиторий на GitHub
55 | 7. Settings → Secrets and variables → Actions
56 | 8. New repository secret:
57 | - Name: `NPM_TOKEN`
58 | - Secret: вставить токен из npm
59 |
60 | 9. Обновить workflow: раскомментировать `NODE_AUTH_TOKEN` в шаге publish
61 |
62 | **Недостатки:**
63 | - ⚠️ Токен истекает через 7-90 дней
64 | - ⚠️ Нужно вручную обновлять в GitHub Secrets
65 | - ⚠️ Классические токены будут отключены 15.11.2025
66 |
67 | #### ✅ Готово!
68 |
69 | Теперь при каждом push тега библиотека будет автоматически опубликована.
70 |
71 | ### Как использовать:
72 |
73 | ```bash
74 | # 1. Обновить версию в package.json (уже сделано)
75 |
76 | # 2. Закоммитить изменения
77 | git add .
78 | git commit -m "chore(release): 3.0.0"
79 |
80 | # 3. Создать и запушить тег
81 | git tag v3.0.0
82 | git push origin v3.0.0
83 |
84 | # 4. GitHub Actions автоматически:
85 | # - Запустит тесты
86 | # - Соберёт production build
87 | # - Опубликует в npm
88 | # - Создаст GitHub Release
89 | ```
90 |
91 | ### Особенности:
92 |
93 | - ✅ **Автоматическое определение тега**: beta/alpha/rc → `next`, stable → `latest`
94 | - ✅ **Provenance**: Публикация с npm provenance для безопасности
95 | - ✅ **Тесты**: Всегда запускаются перед публикацией
96 | - ✅ **GitHub Release**: Автоматически создаётся с CHANGELOG
97 | - ✅ **Безопасность**: NPM токен хранится в GitHub Secrets
98 |
99 | ### Проверка workflow:
100 |
101 | После push тега можно проверить статус:
102 | - GitHub → Actions tab
103 | - Там будет видно выполнение workflow
104 |
105 | ### Откат публикации:
106 |
107 | Если что-то пошло не так:
108 |
109 | ```bash
110 | # Удалить версию из npm (в течение 72 часов)
111 | npm unpublish @kolkov/angular-editor@3.0.0
112 |
113 | # Удалить тег с GitHub
114 | git push origin --delete v3.0.0
115 |
116 | # Удалить локальный тег
117 | git tag -d v3.0.0
118 | ```
119 |
120 | **Важно**: npm unpublish работает только в течение 72 часов после публикации!
121 |
--------------------------------------------------------------------------------
/projects/angular-editor/src/lib/ae-toolbar/ae-toolbar.component.scss:
--------------------------------------------------------------------------------
1 | @import "../styles";
2 |
3 | .angular-editor-toolbar {
4 | font: 100 14px/15px Roboto, Arial, sans-serif;
5 | background-color: var(--ae-toolbar-bg-color, #f5f5f5);
6 | font-size: 0.8rem;
7 | padding: var(--ae-toolbar-padding,0.2rem);
8 | border: var(--ae-toolbar-border-radius, 1px solid #ddd);
9 | display: flex;
10 | flex-wrap: wrap;
11 | gap: 4px;
12 | }
13 |
14 | .select-heading {
15 | display: inline-block;
16 | width: 90px;
17 | @supports not( -moz-appearance:none ) {
18 | optgroup {
19 | font-size: 12px;
20 | background-color: #f4f4f4;
21 | padding: 5px;
22 | }
23 | option {
24 | border: 1px solid;
25 | background-color: white;
26 | }
27 | }
28 |
29 | &:disabled {
30 | background-color: #f5f5f5;
31 | pointer-events: none;
32 | cursor: not-allowed;
33 | }
34 |
35 | &:hover {
36 | cursor: pointer;
37 | background-color: #f1f1f1;
38 | transition: 0.2s ease;
39 | }
40 | }
41 |
42 | .select-font {
43 | display: inline-block;
44 | width: 90px;
45 | @supports not( -moz-appearance:none ) {
46 | optgroup {
47 | font-size: 12px;
48 | background-color: #f4f4f4;
49 | padding: 5px;
50 | }
51 | option {
52 | border: 1px solid;
53 | background-color: white;
54 | }
55 | }
56 |
57 | &:disabled {
58 | background-color: #f5f5f5;
59 | pointer-events: none;
60 | cursor: not-allowed;
61 | }
62 |
63 | &:hover {
64 | cursor: pointer;
65 | background-color: #f1f1f1;
66 | transition: 0.2s ease;
67 | }
68 | }
69 |
70 | .select-font-size {
71 | display: inline-block;
72 | width: 50px;
73 | @supports not( -moz-appearance:none ) {
74 | optgroup {
75 | font-size: 12px;
76 | background-color: #f4f4f4;
77 | padding: 5px;
78 | }
79 | option {
80 | border: 1px solid;
81 | background-color: white;
82 | }
83 | .size1 {
84 | font-size: 10px;
85 | }
86 | .size2 {
87 | font-size: 12px;
88 | }
89 | .size3 {
90 | font-size: 14px;
91 | }
92 | .size4 {
93 | font-size: 16px;
94 | }
95 | .size5 {
96 | font-size: 18px;
97 | }
98 | .size6 {
99 | font-size: 20px;
100 | }
101 | .size7 {
102 | font-size: 22px;
103 | }
104 | }
105 |
106 | &:disabled {
107 | background-color: #f5f5f5;
108 | pointer-events: none;
109 | cursor: not-allowed;
110 | }
111 |
112 | &:hover {
113 | cursor: pointer;
114 | background-color: #f1f1f1;
115 | transition: 0.2s ease;
116 | }
117 | }
118 |
119 | .select-custom-style {
120 | display: inline-block;
121 | width: 90px;
122 | @supports not( -moz-appearance:none ) {
123 | optgroup {
124 | font-size: 12px;
125 | background-color: #f4f4f4;
126 | padding: 5px;
127 | }
128 | option {
129 | border: 1px solid;
130 | background-color: white;
131 | }
132 | }
133 |
134 | &:disabled {
135 | background-color: #f5f5f5;
136 | pointer-events: none;
137 | cursor: not-allowed;
138 | }
139 |
140 | &:hover {
141 | cursor: pointer;
142 | background-color: #f1f1f1;
143 | transition: 0.2s ease;
144 | }
145 | }
146 |
147 | .color-label {
148 | position: relative;
149 | cursor: pointer;
150 | }
151 |
152 | .background {
153 | font-size: smaller;
154 | background: #1b1b1b;
155 | color: white;
156 | padding: 3px;
157 | }
158 |
159 | .foreground {
160 | :after {
161 | position: absolute;
162 | content: "";
163 | left: -1px;
164 | top: auto;
165 | bottom: -3px;
166 | right: auto;
167 | width: 15px;
168 | height: 2px;
169 | z-index: 0;
170 | background: #1b1b1b;
171 | }
172 | }
173 |
174 | .default {
175 | font-size: 16px;
176 | }
177 | h1 {
178 | font-size: 24px;
179 | }
180 | h2 {
181 | font-size: 20px;
182 | }
183 | h3 {
184 | font-size: 16px;
185 | }
186 | h4 {
187 | font-size: 15px;
188 | }
189 | h5 {
190 | font-size: 14px;
191 | }
192 | h6 {
193 | font-size: 13px;
194 | }
195 | div {
196 | font-size: 12px;
197 | }
198 | pre {
199 | font-size: 12px;
200 | }
201 |
--------------------------------------------------------------------------------
/projects/angular-editor/src/lib/ae-select/ae-select.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | ElementRef,
4 | EventEmitter,
5 | forwardRef,
6 | HostBinding,
7 | HostListener,
8 | Input,
9 | OnInit,
10 | Output,
11 | Renderer2,
12 | ViewChild,
13 | ViewEncapsulation
14 | } from '@angular/core';
15 | import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
16 | import {isDefined} from '../utils';
17 |
18 | export interface SelectOption {
19 | label: string;
20 | value: string;
21 | }
22 |
23 | @Component({
24 | selector: 'ae-select',
25 | templateUrl: './ae-select.component.html',
26 | styleUrls: ['./ae-select.component.scss'],
27 | //encapsulation: ViewEncapsulation.None,
28 | providers: [
29 | {
30 | provide: NG_VALUE_ACCESSOR,
31 | useExisting: forwardRef(() => AeSelectComponent),
32 | multi: true,
33 | }
34 | ],
35 | standalone: false
36 | })
37 | export class AeSelectComponent implements OnInit, ControlValueAccessor {
38 | @Input() options: SelectOption[] = [];
39 | // eslint-disable-next-line @angular-eslint/no-input-rename
40 | @Input('hidden') isHidden: boolean;
41 |
42 | selectedOption: SelectOption;
43 | disabled = false;
44 | optionId = 0;
45 |
46 | get label(): string {
47 | return this.selectedOption && this.selectedOption.hasOwnProperty('label') ? this.selectedOption.label : 'Select';
48 | }
49 |
50 | opened = false;
51 |
52 | get value(): string {
53 | return this.selectedOption.value;
54 | }
55 |
56 | @HostBinding('style.display') hidden = 'inline-block';
57 |
58 | // eslint-disable-next-line @angular-eslint/no-output-native, @angular-eslint/no-output-rename
59 | @Output('change') changeEvent = new EventEmitter();
60 |
61 | @ViewChild('labelButton', {static: true}) labelButton: ElementRef;
62 |
63 | constructor(private elRef: ElementRef,
64 | private r: Renderer2,
65 | ) {
66 | }
67 |
68 | ngOnInit() {
69 | this.selectedOption = this.options[0];
70 | if (isDefined(this.isHidden) && this.isHidden) {
71 | this.hide();
72 | }
73 | }
74 |
75 | hide() {
76 | this.hidden = 'none';
77 | }
78 |
79 | optionSelect(option: SelectOption, event: MouseEvent) {
80 | //console.log(event.button, event.buttons);
81 | if (event.buttons !== 1) {
82 | return;
83 | }
84 | event.preventDefault();
85 | event.stopPropagation();
86 | this.setValue(option.value);
87 | this.onChange(this.selectedOption.value);
88 | this.changeEvent.emit(this.selectedOption.value);
89 | this.onTouched();
90 | this.opened = false;
91 | }
92 |
93 | toggleOpen(event: MouseEvent) {
94 | // event.stopPropagation();
95 | if (this.disabled) {
96 | return;
97 | }
98 | this.opened = !this.opened;
99 | }
100 |
101 | @HostListener('document:click', ['$event'])
102 | onClick($event: MouseEvent) {
103 | if (!this.elRef.nativeElement.contains($event.target)) {
104 | this.close();
105 | }
106 | }
107 |
108 | close() {
109 | this.opened = false;
110 | }
111 |
112 | get isOpen(): boolean {
113 | return this.opened;
114 | }
115 |
116 | writeValue(value) {
117 | if (!value || typeof value !== 'string') {
118 | return;
119 | }
120 | this.setValue(value);
121 | }
122 |
123 | setValue(value) {
124 | let index = 0;
125 | const selectedEl = this.options.find((el, i) => {
126 | index = i;
127 | return el.value === value;
128 | });
129 | if (selectedEl) {
130 | this.selectedOption = selectedEl;
131 | this.optionId = index;
132 | }
133 | }
134 |
135 | onChange: any = () => {
136 | }
137 | onTouched: any = () => {
138 | }
139 |
140 | registerOnChange(fn) {
141 | this.onChange = fn;
142 | }
143 |
144 | registerOnTouched(fn) {
145 | this.onTouched = fn;
146 | }
147 |
148 | setDisabledState(isDisabled: boolean): void {
149 | this.labelButton.nativeElement.disabled = isDisabled;
150 | const div = this.labelButton.nativeElement;
151 | const action = isDisabled ? 'addClass' : 'removeClass';
152 | this.r[action](div, 'disabled');
153 | this.disabled = isDisabled;
154 | }
155 |
156 | @HostListener('keydown', ['$event'])
157 | handleKeyDown($event: KeyboardEvent) {
158 | if (!this.opened) {
159 | return;
160 | }
161 | // console.log($event.key);
162 | // if (KeyCode[$event.key]) {
163 | switch ($event.key) {
164 | case 'ArrowDown':
165 | this._handleArrowDown($event);
166 | break;
167 | case 'ArrowUp':
168 | this._handleArrowUp($event);
169 | break;
170 | case 'Space':
171 | this._handleSpace($event);
172 | break;
173 | case 'Enter':
174 | this._handleEnter($event);
175 | break;
176 | case 'Tab':
177 | this._handleTab($event);
178 | break;
179 | case 'Escape':
180 | this.close();
181 | $event.preventDefault();
182 | break;
183 | case 'Backspace':
184 | this._handleBackspace();
185 | break;
186 | }
187 | // } else if ($event.key && $event.key.length === 1) {
188 | // this._keyPress$.next($event.key.toLocaleLowerCase());
189 | // }
190 | }
191 |
192 | _handleArrowDown($event) {
193 | if (this.optionId < this.options.length - 1) {
194 | this.optionId++;
195 | }
196 | }
197 |
198 | _handleArrowUp($event) {
199 | if (this.optionId >= 1) {
200 | this.optionId--;
201 | }
202 | }
203 |
204 | _handleSpace($event) {
205 |
206 | }
207 |
208 | _handleEnter($event) {
209 | this.optionSelect(this.options[this.optionId], $event);
210 | }
211 |
212 | _handleTab($event) {
213 |
214 | }
215 |
216 | _handleBackspace() {
217 |
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | ## [3.0.3](https://github.com/kolkov/angular-editor/compare/v3.0.2...v3.0.3) (2025-01-22) - Security Hotfix
3 |
4 | ### Security
5 | * **CRITICAL:** Fixed XSS vulnerability in `refreshView()` method ([#580](https://github.com/kolkov/angular-editor/issues/580)) ([774a97d](https://github.com/kolkov/angular-editor/commit/774a97d))
6 | - XSS could bypass sanitizer when setting editor value via ngModel/formControl
7 | - Sanitization now properly applied to all innerHTML assignments
8 | - Thanks to @MarioTesoro for responsible disclosure with PoC
9 |
10 | ### Bug Fixes
11 | * **links:** Preserve relative URLs when editing existing links ([#359](https://github.com/kolkov/angular-editor/issues/359)) ([c691d30](https://github.com/kolkov/angular-editor/commit/c691d30))
12 | - Use `getAttribute('href')` instead of `.href` property
13 | - Prevents adding hostname to relative paths
14 | * **debug:** Remove debug `console.log` statement from focus() method ([#324](https://github.com/kolkov/angular-editor/issues/324)) ([c691d30](https://github.com/kolkov/angular-editor/commit/c691d30))
15 |
16 | ### Upgrade Recommendation
17 | **IMMEDIATE UPGRADE RECOMMENDED** for all users. This release fixes a critical security vulnerability.
18 |
19 | ---
20 |
21 |
22 | ## [3.0.2](https://github.com/kolkov/angular-editor/compare/v3.0.1...v3.0.2) (2025-01-22)
23 |
24 | ### Bug Fixes
25 | * **toolbar:** toolbarHiddenButtons option now works without Bootstrap ([#544](https://github.com/kolkov/angular-editor/issues/544)) ([3563552](https://github.com/kolkov/angular-editor/commit/3563552))
26 | * **image:** allow re-uploading same image after deletion ([#543](https://github.com/kolkov/angular-editor/issues/543), [#568](https://github.com/kolkov/angular-editor/issues/568), [#503](https://github.com/kolkov/angular-editor/issues/503)) ([7d21718](https://github.com/kolkov/angular-editor/commit/7d21718))
27 | * **video:** support YouTube short URLs (youtu.be format) ([#557](https://github.com/kolkov/angular-editor/issues/557), [#554](https://github.com/kolkov/angular-editor/issues/554)) ([4aa8397](https://github.com/kolkov/angular-editor/commit/4aa8397))
28 |
29 | ### Maintenance
30 | * **issues:** Systematic triage completed - 61 issues closed, 249 remain open
31 | * **documentation:** Added issue triage session record
32 |
33 | ---
34 |
35 |
36 | ## [3.0.1](https://github.com/kolkov/angular-editor/compare/v3.0.0...v3.0.1) (2025-11-22)
37 |
38 | ### Bug Fixes
39 | * **Icons:** Fixed list icons (unordered/ordered) display consistency in toolbar
40 |
41 | ### CI/CD
42 | * **GitHub Actions:** Added automated npm publishing workflow
43 | * **npm Publishing:** Configured Granular Access Token authentication
44 | * **GitHub Releases:** Automated release creation with changelog
45 |
46 | ---
47 |
48 |
49 | ## [3.0.0](https://github.com/kolkov/angular-editor/compare/v2.0.0...v3.0.0) (2025-11-22) Major Angular 20 Upgrade
50 |
51 | 🎉 **Stable Release** - Production Ready!
52 |
53 | ### Breaking Changes
54 | * **Angular Version:** Minimum required version is now Angular 20.0.0
55 | * **RxJS:** Requires RxJS 7.8.0 or higher (upgraded from 6.5.5)
56 | * **TypeScript:** Requires TypeScript 5.4 or higher
57 | * **zone.js:** Updated to 0.15.1
58 |
59 | ### Features
60 | * **Angular 20 Support:** Full compatibility with Angular 20.3.13 (v20-lts)
61 | * **Angular 21 Ready:** Forward compatible with Angular 21.x
62 | * **Modern Build System:** Updated to latest ng-packagr 20.3.2
63 | * **Enhanced Type Safety:** Improved TypeScript strict mode compliance
64 | * **Font Awesome Removed:** No external icon dependencies - using pure SVG icons (27 icons)
65 | * **Zero External Icon Dependencies:** Fully self-contained icon system
66 |
67 | ### Migration Path
68 | * Migrated through: Angular 13 → 18 → 19 → 20
69 | * All Angular CLI migrations applied successfully
70 | * Updated DOCUMENT import from @angular/core (Angular 20 requirement)
71 | * Modernized test infrastructure (waitForAsync)
72 |
73 | ### Developer Experience
74 | * **ESLint:** Updated to @angular-eslint 20.x
75 | * **Linting:** All files pass linting (0 errors)
76 | * **Build:** Both development and production builds verified
77 | * **Tests:** 13/13 tests passing (100% success rate)
78 |
79 | ### Bug Fixes
80 | * **Tests:** Fixed AeSelectComponent tests for mousedown event handling
81 | * **Demo:** Updated demo app for Angular 20 compatibility
82 |
83 | ### Technical Details
84 | * Removed deprecated `async` test helper (use `waitForAsync`)
85 | * Fixed TypeScript strict type checking for event handlers
86 | * Disabled new strict rules for backward compatibility (prefer-standalone, prefer-inject)
87 | * Updated moduleResolution to 'bundler' (Angular 20 standard)
88 |
89 | ### Peer Dependencies
90 | ```json
91 | {
92 | "@angular/common": "^20.0.0 || ^21.0.0",
93 | "@angular/core": "^20.0.0 || ^21.0.0",
94 | "@angular/forms": "^20.0.0 || ^21.0.0",
95 | "rxjs": "^7.8.0"
96 | }
97 | ```
98 |
99 |
100 | ## [3.0.0-beta.2](https://github.com/kolkov/angular-editor/compare/v3.0.0-beta.1...v3.0.0-beta.2) (2025-01-10)
101 | * Refactor ae-select component (button → span)
102 |
103 |
104 | ## [2.0.0](https://github.com/kolkov/angular-editor/compare/v1.2.0...v2.0.0) (2022-01-06) Major release
105 | * Update to Angular v.13 and new Ivy compatible package format
106 |
107 |
108 | ## [1.0.2](https://github.com/kolkov/angular-editor/compare/v1.0.1...v1.0.2) (2019-11-28) Technical release
109 | * Readme update for npmjs.com
110 |
111 |
112 | ## [1.0.1](https://github.com/kolkov/angular-editor/compare/v1.0.0...v1.0.1) (2019-11-27) Technical release
113 | * Fix logo at npmjs.com readme
114 |
115 |
116 | ## [1.0.0](https://github.com/kolkov/angular-editor/compare/v1.0.0-rc.2...v1.0.0) (2019-11-27) Initial release
117 |
118 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "angular-editor": {
7 | "projectType": "library",
8 | "root": "projects/angular-editor",
9 | "sourceRoot": "projects/angular-editor/src",
10 | "prefix": "lib",
11 | "schematics": {
12 | "@schematics/angular:component": {
13 | "style": "scss"
14 | }
15 | },
16 | "architect": {
17 | "build": {
18 | "builder": "@angular-devkit/build-angular:ng-packagr",
19 | "options": {
20 | "tsConfig": "projects/angular-editor/tsconfig.lib.json",
21 | "project": "projects/angular-editor/ng-package.json"
22 | },
23 | "configurations": {
24 | "production": {
25 | "tsConfig": "projects/angular-editor/tsconfig.lib.prod.json"
26 | }
27 | }
28 | },
29 | "test": {
30 | "builder": "@angular-devkit/build-angular:karma",
31 | "options": {
32 | "main": "projects/angular-editor/src/test.ts",
33 | "tsConfig": "projects/angular-editor/tsconfig.spec.json",
34 | "karmaConfig": "projects/angular-editor/karma.conf.js"
35 | }
36 | },
37 | "lint": {
38 | "builder": "@angular-eslint/builder:lint",
39 | "options": {
40 | "lintFilePatterns": [
41 | "projects/angular-editor/**/*.ts",
42 | "projects/angular-editor/**/*.html"
43 | ]
44 | }
45 | }
46 | }
47 | },
48 | "angular-editor-app": {
49 | "projectType": "application",
50 | "schematics": {
51 | "@schematics/angular:component": {
52 | "style": "scss"
53 | }
54 | },
55 | "root": "projects/angular-editor-app",
56 | "sourceRoot": "projects/angular-editor-app/src",
57 | "prefix": "app",
58 | "architect": {
59 | "build": {
60 | "builder": "@angular-devkit/build-angular:browser",
61 | "options": {
62 | "outputPath": "dist/angular-editor-app",
63 | "index": "projects/angular-editor-app/src/index.html",
64 | "main": "projects/angular-editor-app/src/main.ts",
65 | "polyfills": "projects/angular-editor-app/src/polyfills.ts",
66 | "tsConfig": "projects/angular-editor-app/tsconfig.app.json",
67 | "assets": [
68 | "projects/angular-editor-app/src/favicon.ico",
69 | "projects/angular-editor-app/src/assets",
70 | {
71 | "glob": "**/*",
72 | "input": "projects/angular-editor/assets/icons/",
73 | "output": "assets/ae-icons/"
74 | }
75 | ],
76 | "styles": [
77 | "projects/angular-editor-app/src/styles.scss",
78 | "projects/angular-editor/themes/default.scss"
79 | ],
80 | "scripts": [],
81 | "vendorChunk": true,
82 | "extractLicenses": false,
83 | "buildOptimizer": false,
84 | "sourceMap": true,
85 | "optimization": false,
86 | "namedChunks": true
87 | },
88 | "configurations": {
89 | "production": {
90 | "fileReplacements": [
91 | {
92 | "replace": "projects/angular-editor-app/src/environments/environment.ts",
93 | "with": "projects/angular-editor-app/src/environments/environment.prod.ts"
94 | }
95 | ],
96 | "optimization": true,
97 | "outputHashing": "all",
98 | "sourceMap": false,
99 | "namedChunks": false,
100 | "extractLicenses": true,
101 | "vendorChunk": false,
102 | "buildOptimizer": true,
103 | "budgets": [
104 | {
105 | "type": "initial",
106 | "maximumWarning": "2mb",
107 | "maximumError": "5mb"
108 | },
109 | {
110 | "type": "anyComponentStyle",
111 | "maximumWarning": "6kb"
112 | }
113 | ]
114 | }
115 | }
116 | },
117 | "serve": {
118 | "builder": "@angular-devkit/build-angular:dev-server",
119 | "options": {
120 | "buildTarget": "angular-editor-app:build"
121 | },
122 | "configurations": {
123 | "production": {
124 | "buildTarget": "angular-editor-app:build:production"
125 | }
126 | }
127 | },
128 | "extract-i18n": {
129 | "builder": "@angular-devkit/build-angular:extract-i18n",
130 | "options": {
131 | "buildTarget": "angular-editor-app:build"
132 | }
133 | },
134 | "test": {
135 | "builder": "@angular-devkit/build-angular:karma",
136 | "options": {
137 | "main": "projects/angular-editor-app/src/test.ts",
138 | "polyfills": "projects/angular-editor-app/src/polyfills.ts",
139 | "tsConfig": "projects/angular-editor-app/tsconfig.spec.json",
140 | "karmaConfig": "projects/angular-editor-app/karma.conf.js",
141 | "assets": [
142 | "projects/angular-editor-app/src/favicon.ico",
143 | "projects/angular-editor-app/src/assets"
144 | ],
145 | "styles": [
146 | "projects/angular-editor-app/src/styles.css"
147 | ],
148 | "scripts": []
149 | }
150 | },
151 | "lint": {
152 | "builder": "@angular-eslint/builder:lint",
153 | "options": {
154 | "lintFilePatterns": [
155 | "projects/angular-editor-app/**/*.ts",
156 | "projects/angular-editor-app/**/*.html"
157 | ]
158 | }
159 | },
160 | "e2e": {
161 | "builder": "@angular-devkit/build-angular:protractor",
162 | "options": {
163 | "protractorConfig": "projects/angular-editor-app/e2e/protractor.conf.js",
164 | "devServerTarget": "angular-editor-app:serve"
165 | },
166 | "configurations": {
167 | "production": {
168 | "devServerTarget": "angular-editor-app:serve:production"
169 | }
170 | }
171 | }
172 | }
173 | }
174 | },
175 | "cli": {
176 | "analytics": "fbddda2f-258b-4004-8062-d701809d0a1c",
177 | "schematicCollections": [
178 | "@angular-eslint/schematics"
179 | ]
180 | },
181 | "schematics": {
182 | "@schematics/angular:component": {
183 | "type": "component"
184 | },
185 | "@schematics/angular:directive": {
186 | "type": "directive"
187 | },
188 | "@schematics/angular:service": {
189 | "type": "service"
190 | },
191 | "@schematics/angular:guard": {
192 | "typeSeparator": "."
193 | },
194 | "@schematics/angular:interceptor": {
195 | "typeSeparator": "."
196 | },
197 | "@schematics/angular:module": {
198 | "typeSeparator": "."
199 | },
200 | "@schematics/angular:pipe": {
201 | "typeSeparator": "."
202 | },
203 | "@schematics/angular:resolver": {
204 | "typeSeparator": "."
205 | }
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/projects/angular-editor/src/lib/ae-toolbar/ae-toolbar.component.html:
--------------------------------------------------------------------------------
1 |
160 |
--------------------------------------------------------------------------------
/projects/angular-editor/src/lib/angular-editor.service.ts:
--------------------------------------------------------------------------------
1 | import {Inject, Injectable, DOCUMENT} from '@angular/core';
2 | import {HttpClient, HttpEvent} from '@angular/common/http';
3 | import {Observable} from 'rxjs';
4 |
5 | import {CustomClass} from './config';
6 |
7 | export interface UploadResponse {
8 | imageUrl: string;
9 | }
10 |
11 | @Injectable()
12 | export class AngularEditorService {
13 |
14 | savedSelection: Range | null;
15 | selectedText: string;
16 | uploadUrl: string;
17 | uploadWithCredentials: boolean;
18 |
19 | constructor(
20 | private http: HttpClient,
21 | @Inject(DOCUMENT) private doc: any
22 | ) { }
23 |
24 | /**
25 | * Executed command from editor header buttons exclude toggleEditorMode
26 | * @param command string from triggerCommand
27 | * @param value
28 | */
29 | executeCommand(command: string, value?: string) {
30 | const commands = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre'];
31 | if (commands.includes(command)) {
32 | this.doc.execCommand('formatBlock', false, command);
33 | return;
34 | }
35 | this.doc.execCommand(command, false, value);
36 | }
37 |
38 | /**
39 | * Create URL link
40 | * @param url string from UI prompt
41 | */
42 | createLink(url: string) {
43 | if (!url.includes('http')) {
44 | this.doc.execCommand('createlink', false, url);
45 | } else {
46 | const newUrl = '' + this.selectedText + '';
47 | this.insertHtml(newUrl);
48 | }
49 | }
50 |
51 | /**
52 | * insert color either font or background
53 | *
54 | * @param color color to be inserted
55 | * @param where where the color has to be inserted either text/background
56 | */
57 | insertColor(color: string, where: string): void {
58 | const restored = this.restoreSelection();
59 | if (restored) {
60 | if (where === 'textColor') {
61 | this.doc.execCommand('foreColor', false, color);
62 | } else {
63 | this.doc.execCommand('hiliteColor', false, color);
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Set font name
70 | * @param fontName string
71 | */
72 | setFontName(fontName: string) {
73 | this.doc.execCommand('fontName', false, fontName);
74 | }
75 |
76 | /**
77 | * Set font size
78 | * @param fontSize string
79 | */
80 | setFontSize(fontSize: string) {
81 | this.doc.execCommand('fontSize', false, fontSize);
82 | }
83 |
84 | /**
85 | * Create raw HTML
86 | * @param html HTML string
87 | */
88 | insertHtml(html: string): void {
89 |
90 | const isHTMLInserted = this.doc.execCommand('insertHTML', false, html);
91 |
92 | if (!isHTMLInserted) {
93 | throw new Error('Unable to perform the operation');
94 | }
95 | }
96 |
97 | /**
98 | * save selection when the editor is focussed out
99 | */
100 | public saveSelection = (): void => {
101 | if (this.doc.getSelection) {
102 | const sel = this.doc.getSelection();
103 | if (sel.getRangeAt && sel.rangeCount) {
104 | this.savedSelection = sel.getRangeAt(0);
105 | this.selectedText = sel.toString();
106 | }
107 | } else if (this.doc.getSelection && this.doc.createRange) {
108 | this.savedSelection = document.createRange();
109 | } else {
110 | this.savedSelection = null;
111 | }
112 | }
113 |
114 | /**
115 | * restore selection when the editor is focused in
116 | *
117 | * saved selection when the editor is focused out
118 | */
119 | restoreSelection(): boolean {
120 | if (this.savedSelection) {
121 | if (this.doc.getSelection) {
122 | const sel = this.doc.getSelection();
123 | sel.removeAllRanges();
124 | sel.addRange(this.savedSelection);
125 | return true;
126 | } else if (this.doc.getSelection /*&& this.savedSelection.select*/) {
127 | // this.savedSelection.select();
128 | return true;
129 | }
130 | } else {
131 | return false;
132 | }
133 | }
134 |
135 | /**
136 | * setTimeout used for execute 'saveSelection' method in next event loop iteration
137 | */
138 | public executeInNextQueueIteration(callbackFn: (...args: any[]) => any, timeout = 1e2): void {
139 | setTimeout(callbackFn, timeout);
140 | }
141 |
142 | /** check any selection is made or not */
143 | private checkSelection(): any {
144 |
145 | const selectedText = this.savedSelection.toString();
146 |
147 | if (selectedText.length === 0) {
148 | throw new Error('No Selection Made');
149 | }
150 | return true;
151 | }
152 |
153 | /**
154 | * Upload file to uploadUrl
155 | * @param file The file
156 | */
157 | uploadImage(file: File): Observable
2 |
3 |