@v1nt1248/3nclient-lib 0.1.53 → 0.1.55

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.
@@ -1,11 +1,12 @@
1
1
  export declare function getRandomId(numOfChars: number): string;
2
2
  export declare function getDeliveryErrors(progress: web3n.asmail.DeliveryProgress): Record<string, string>;
3
3
  export declare function transformFileToWeb3NFile(file: File): Promise<web3n.files.File | undefined>;
4
+ export declare function markSearch(str: string, search: string): string;
4
5
  /**
5
- * функция округления с заданной точностью
6
- * @param num {number} - округляемое число
7
- * @param precision {number} - точность округления (количество знаков после запятой
8
- * указывается со знаком "-")
9
- * @return {number} - скорректированная округленная десятичная дробь
6
+ * Rounding with a given accuracy
7
+ * @param num {number}
8
+ * @param precision {number} - rounding accuracy (number of digits after the decimal point
9
+ * indicated with the "-" sign)
10
+ * @return {number}
10
11
  */
11
12
  export declare function round(num: number, precision: number): number;
@@ -5,5 +5,6 @@ export * from './processes/named-procs';
5
5
  export * from './textarea-max-rows';
6
6
  export * from './common';
7
7
  export * from './ui';
8
+ export * from './selection';
8
9
  export { SQLiteOn3NStorage } from './sqlite-on-3nstorage';
9
10
  export type { BindParams, Database, QueryExecResult } from './sqlite-on-3nstorage';
@@ -0,0 +1,3 @@
1
+ export declare function selectAll(el: HTMLElement): void;
2
+ export declare function selectStart(el: HTMLElement): void;
3
+ export declare function selectEnd(el: HTMLElement): void;
@@ -822,3 +822,21 @@ div {
822
822
  animation: ripple var(--ui3n-ripple-duration) linear;
823
823
  background-color: var(--ui3n-ripple-bg-color);
824
824
  }
825
+
826
+ .ui3n-title {
827
+ position: fixed;
828
+ width: max-content;
829
+ top: 0;
830
+ left: 0;
831
+ padding: var(--spacing-xs) var(--spacing-s);
832
+ border-radius: var(--spacing-xs);
833
+ font-size: var(--font-11);
834
+ line-height: var(--font-12);
835
+ font-weight: 400;
836
+ background-color: var(--color-bg-control-secondary-default);
837
+ color: var(--color-text-control-primary-default);
838
+ box-shadow:
839
+ 0 2px 1px -1px rgba(0, 0, 0, 0.2),
840
+ 0 1px 1px 0 rgba(0, 0, 0, 0.14),
841
+ 0 1px 3px 0 rgba(0, 0, 0, 0.12);
842
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@v1nt1248/3nclient-lib",
3
3
  "license": "AGPL-3.0-or-later",
4
4
  "author": "v1nt1248",
5
- "version": "0.1.53",
5
+ "version": "0.1.55",
6
6
  "description": "Library for 3NWeb clients",
7
7
  "type": "module",
8
8
  "files": [
@@ -51,16 +51,17 @@
51
51
  "docs:preview": "vitepress preview docs"
52
52
  },
53
53
  "dependencies": {
54
- "@floating-ui/vue": "1.1.5",
54
+ "@floating-ui/dom": "^1.6.13",
55
+ "@floating-ui/vue": "1.1.6",
55
56
  "dayjs": "1.11.13",
56
57
  "lodash": "4.17.21",
57
58
  "mitt": "3.0.1",
58
- "sanitize-html": "2.13.1",
59
- "tslib": "2.8.0",
59
+ "sanitize-html": "2.14.0",
60
+ "tslib": "2.8.1",
60
61
  "vuedraggable": "4.1.0"
61
62
  },
62
63
  "peerDependencies": {
63
- "pinia": "2.3.0",
64
+ "pinia": "2.3.1",
64
65
  "vue": "3.5.13"
65
66
  },
66
67
  "devDependencies": {
@@ -83,7 +84,7 @@
83
84
  "eslint-plugin-vue": "9.27.0",
84
85
  "jackspeak": "4.0.1",
85
86
  "path": "0.12.7",
86
- "pinia": "2.3.0",
87
+ "pinia": "2.3.1",
87
88
  "postcss-html": "1.7.0",
88
89
  "prettier": "^3.3.3",
89
90
  "sass": "1.80.3",
@@ -81,6 +81,8 @@ import type {
81
81
  Ui3nRadioGroupSlots,
82
82
  Ui3nRadioValue,
83
83
  } from './ui3n-radio-group/types';
84
+ import Ui3nEditable from './ui3n-editable/ui3n-editable.vue';
85
+ import type { Ui3nEditableProps, Ui3nEditableEmits } from './ui3n-editable/types';
84
86
 
85
87
  export {
86
88
  Ui3nIcon,
@@ -183,4 +185,7 @@ export {
183
185
  Ui3nRadioGroupEmits,
184
186
  Ui3nRadioGroupProps,
185
187
  Ui3nRadioGroupSlots,
188
+ Ui3nEditable,
189
+ Ui3nEditableProps,
190
+ Ui3nEditableEmits,
186
191
  };
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" setup>
2
2
  import { computed, onMounted, ref, watch, useCssModule } from 'vue';
3
3
  import Ui3nIcon from '../ui3n-icon/ui3n-icon.vue';
4
- import Ui3nRipple from '../..//directives/ui3n-ripple';
4
+ import Ui3nRipple from '../../directives/ui3n-ripple';
5
5
  import type { Ui3nButtonEmits, Ui3nButtonProps, Ui3nButtonEventName } from './types';
6
6
 
7
7
  const vUi3nRipple = Ui3nRipple;
@@ -0,0 +1,42 @@
1
+ import type { Nullable } from '@/components/types';
2
+
3
+ export interface Ui3nContentEditableProps {
4
+ modelValue: string;
5
+ placeholder?: string;
6
+ maxLength?: number;
7
+ disabled?: boolean;
8
+ selectAllOnFocus?: boolean;
9
+ isMultiline?: boolean;
10
+ disallowEmptyValue?: boolean;
11
+ debounceDelay?: number;
12
+ }
13
+
14
+ export interface Ui3nContentEditableEmits {
15
+ (event: 'init', value: HTMLElement): void;
16
+ (event: 'update:modelValue', value: Nullable<string>): void;
17
+ (event: 'input', value: Nullable<string>): void;
18
+ (event: 'change', value: Nullable<string>): void;
19
+ (event: 'click', value: MouseEvent): void;
20
+ (event: 'keydown', value: KeyboardEvent): void;
21
+ (event: 'focusin', value: FocusEvent): void;
22
+ (event: 'focusout', value: FocusEvent): void;
23
+ }
24
+
25
+ export interface Ui3nEditableProps {
26
+ modelValue: string;
27
+ placeholder?: string;
28
+ maxLength?: number;
29
+ maxWidth?: string | number;
30
+ tooltipWidth?: number;
31
+ disabled?: boolean;
32
+ selectAllOnFocus?: boolean;
33
+ disallowEmptyValue?: boolean;
34
+ }
35
+
36
+ export interface Ui3nEditableEmits {
37
+ (event: 'update:modelValue', value: Nullable<string>): void;
38
+ (event: 'done'): void;
39
+ (event: 'cancel'): void;
40
+ (event: 'focusin', value: FocusEvent): void;
41
+ (event: 'focusout'): void;
42
+ }
@@ -0,0 +1,73 @@
1
+ <script lang="ts" setup>
2
+ import type { Ui3nContentEditableProps, Ui3nContentEditableEmits } from './types';
3
+ import { useContentEditable } from './useContentEditable';
4
+
5
+ const props = withDefaults(defineProps<Ui3nContentEditableProps>(), {
6
+ debounceDelay: 100,
7
+ });
8
+ const emits = defineEmits<Ui3nContentEditableEmits>();
9
+
10
+ const {
11
+ el,
12
+ isFocused,
13
+ onFocusIn,
14
+ onFocusOut,
15
+ onClick,
16
+ onPaste,
17
+ onKeyDown,
18
+ onInput,
19
+ } = useContentEditable(props, emits);
20
+ </script>
21
+
22
+ <template>
23
+ <span
24
+ ref="el"
25
+ :class="[
26
+ $style.ui3nContentEditable,
27
+ !modelValue && !!placeholder && $style.placeholder,
28
+ isFocused && $style.isFocused,
29
+ disabled && $style.isDisabled
30
+ ]"
31
+ :contenteditable="!disabled"
32
+ @focusin="onFocusIn"
33
+ @focusout="onFocusOut"
34
+ @click="onClick"
35
+ @paste="onPaste"
36
+ @keydown="onKeyDown"
37
+ @input="onInput"
38
+ />
39
+ </template>
40
+
41
+ <style lang="scss" module>
42
+ .ui3nContentEditable {
43
+ --ui3n-content-editable-color: var(--color-text-control-primary-default);
44
+ --ui3n-content-editable-placeholder-color: var(--color-text-control-secondary-default);
45
+ --ui3n-content-editable-border-color-focused: var(--color-border-table-accent-default);
46
+
47
+ position: relative;
48
+ display: inline-block;
49
+ width: fit-content;
50
+ font-size: var(--font-12);
51
+ font-weight: 400;
52
+ line-height: var(--font-16);
53
+ overflow-wrap: break-word;
54
+ color: var(--ui3n-content-editable-color);
55
+ padding: var(--spacing-xs) var(--spacing-s);
56
+ background-color: transparent;
57
+ border-radius: var(--spacing-xs);
58
+ }
59
+
60
+ .placeholder {
61
+ color: var(--ui3n-content-editable-placeholder-color);
62
+ }
63
+
64
+ .isFocused {
65
+ border: 1px solid var(--ui3n-content-editable-border-color-focused);
66
+ }
67
+
68
+ .isDisabled {
69
+ pointer-events: none;
70
+ cursor: default;
71
+ opacity: 0.5;
72
+ }
73
+ </style>
@@ -0,0 +1,296 @@
1
+ <script lang="ts" setup>
2
+ import { computed, nextTick, ref, watch } from 'vue';
3
+ import Ui3nIcon from '@/components/ui3n-icon/ui3n-icon.vue';
4
+ import Ui3nRipple from '@/directives/ui3n-ripple';
5
+ import Ui3nClickOutside from '@/directives/ui3n-click-outside';
6
+ import type { Ui3nEditableProps, Ui3nEditableEmits } from './types';
7
+ import type { Nullable } from '@/components/types';
8
+
9
+ const vUi3nRipple = Ui3nRipple;
10
+ const vUi3nClickOutside = Ui3nClickOutside;
11
+
12
+ const props = defineProps<Ui3nEditableProps>();
13
+ const emits = defineEmits<Ui3nEditableEmits>();
14
+
15
+ const minWidth = 60;
16
+
17
+ const inputEl = ref<Nullable<HTMLInputElement>>(null);
18
+ const hiddenEl = ref<Nullable<HTMLSpanElement>>(null);
19
+ const inputElWidth = ref('auto');
20
+ const contentValueEl = ref<Nullable<HTMLSpanElement>>(null);
21
+ const value = ref<string>(props.modelValue);
22
+ const initialValue = ref<string>(props.modelValue);
23
+
24
+ const inEdit = ref(false);
25
+
26
+ const cssMinWidth = computed(() => `${minWidth}px`);
27
+
28
+ const cssMaxWidth = computed(() => {
29
+ if (!props.maxWidth) {
30
+ return '100%';
31
+ }
32
+
33
+ const maxWidthAsNumber = Number(props.maxWidth);
34
+ if (Number.isNaN(maxWidthAsNumber)) {
35
+ return props.maxWidth as string;
36
+ }
37
+
38
+ return `${maxWidthAsNumber}px`;
39
+ });
40
+
41
+ const isContentTruncated = computed(() => {
42
+ if (!contentValueEl.value) {
43
+ return false;
44
+ }
45
+
46
+ return contentValueEl.value.scrollWidth > contentValueEl.value.clientWidth;
47
+ });
48
+
49
+ const isValid = computed(() => {
50
+ if (!props.disallowEmptyValue) {
51
+ return true;
52
+ }
53
+
54
+ return !!value.value;
55
+ });
56
+
57
+ function setInputWidth() {
58
+ hiddenEl.value!.textContent = value.value || props.placeholder || '...';
59
+ const newWidth = Math.max(hiddenEl.value!.scrollWidth, minWidth);
60
+ inputElWidth.value = `${newWidth}px`;
61
+ }
62
+
63
+ function turnOnEditMode() {
64
+ inEdit.value = true;
65
+ initialValue.value = value.value;
66
+
67
+ nextTick(() => {
68
+ if (inputEl.value) {
69
+ setInputWidth();
70
+ inputEl.value.focus();
71
+ props.selectAllOnFocus && inputEl.value.select();
72
+ }
73
+ });
74
+ }
75
+
76
+ function onInput(ev: Event) {
77
+ // @ts-ignore
78
+ value.value = ev.target!.value || '';
79
+ setInputWidth();
80
+ }
81
+
82
+ function done() {
83
+ if (props.disallowEmptyValue && !isValid.value) {
84
+ return;
85
+ }
86
+
87
+ if (value.value !== initialValue.value) {
88
+ initialValue.value = value.value;
89
+ emits('update:modelValue', value.value);
90
+ }
91
+ emits('done');
92
+ inEdit.value = false;
93
+ }
94
+
95
+ function cancel() {
96
+ if (value.value !== initialValue.value) {
97
+ value.value = initialValue.value;
98
+ emits('update:modelValue', value.value);
99
+ }
100
+ emits('cancel');
101
+ inEdit.value = false;
102
+ }
103
+
104
+ function onClickOutside() {
105
+ if (inEdit.value) {
106
+ emits('focusout');
107
+ done();
108
+ }
109
+ }
110
+
111
+ watch(
112
+ () => props.modelValue,
113
+ (val, oVal) => {
114
+ if (val !== oVal) {
115
+ value.value = val;
116
+ initialValue.value = val;
117
+ }
118
+ },
119
+ );
120
+ </script>
121
+
122
+ <template>
123
+ <div
124
+ v-ui3n-click-outside="onClickOutside"
125
+ :class="$style.ui3nEditable"
126
+ @focusin="emits('focusin', $event);"
127
+ >
128
+ <template v-if="inEdit">
129
+ <span ref="hiddenEl" :class="$style.hidden" />
130
+
131
+ <input
132
+ ref="inputEl"
133
+ :class="[$style.input, inEdit && $style.inEdit, inEdit && !isValid && $style.inEditWarning]"
134
+ :style="{ width: inputElWidth }"
135
+ :value="value"
136
+ :placeholder="placeholder"
137
+ :disabled="disabled"
138
+ @input="onInput"
139
+ @keydown.enter="done"
140
+ @keydown.tab="done"
141
+ @keydown.esc="cancel"
142
+ />
143
+
144
+ <div v-ui3n-ripple :class="[$style.btn, $style.doneBtn]" @click="done">
145
+ <ui3n-icon icon="round-done" size="12" color="var(--color-icon-table-accent-default)" />
146
+ </div>
147
+
148
+ <div v-ui3n-ripple :class="[$style.btn, $style.cancelBtn]" @click="cancel">
149
+ <ui3n-icon icon="round-close" size="12" color="var(--color-icon-table-secondary-default)" />
150
+ </div>
151
+ </template>
152
+
153
+ <template v-else>
154
+ <div :class="[$style.content, !value && $style.contentEmpty]" :title="isContentTruncated ? value : undefined">
155
+ <span ref="contentValueEl">{{ value || placeholder || '...' }}</span>
156
+
157
+ <div v-ui3n-ripple :class="[$style.btn, $style.editBtn]" @click.stop.prevent="turnOnEditMode">
158
+ <ui3n-icon icon="round-edit" size="12" color="var(--color-icon-control-accent-unselected)" />
159
+ </div>
160
+ </div>
161
+ </template>
162
+
163
+
164
+ </div>
165
+ </template>
166
+
167
+ <style lang="scss" module>
168
+ @use '../../assets/styles/mixins' as mixins;
169
+
170
+ .ui3nEditable {
171
+ --ui3n-editable-min-width: v-bind(cssMinWidth);
172
+ --ui3n-editable-max-width: v-bind(cssMaxWidth);
173
+ --ui3n-editable-height: 24px;
174
+
175
+ position: relative;
176
+ box-sizing: border-box;
177
+ width: max-content;
178
+ max-width: var(--ui3n-editable-max-width);
179
+ height: var(--ui3n-editable-height);
180
+ border-radius: var(--spacing-xs);
181
+ font-size: var(--font-12);
182
+ font-weight: 400;
183
+ line-height: var(--font-16);
184
+ color: var(--color-text-table-primary-default);
185
+ overflow: hidden;
186
+ }
187
+
188
+ .content {
189
+ position: relative;
190
+ height: var(--ui3n-editable-height);
191
+ min-width: var(--ui3n-editable-min-width);
192
+ max-width: var(--ui3n-editable-max-width);
193
+ padding: var(--spacing-xs) 44px var(--spacing-xs) var(--spacing-xs);
194
+ border-radius: var(--spacing-xs);
195
+ display: flex;
196
+ justify-content: flex-start;
197
+ align-items: center;
198
+ cursor: pointer;
199
+
200
+ &.contentEmpty {
201
+ color: var(--color-text-table-secondary-default);
202
+ }
203
+
204
+ &:hover {
205
+ background-color: var(--color-bg-control-primary-hover);
206
+
207
+ .btn.editBtn {
208
+ opacity: 1;
209
+ }
210
+ }
211
+
212
+ span {
213
+ display: block;
214
+ max-width: 100%;
215
+ @include mixins.text-overflow-ellipsis();
216
+ }
217
+ }
218
+
219
+ .input {
220
+ position: relative;
221
+ min-width: var(--ui3n-editable-min-width);
222
+ max-width: calc(100% - 52px);
223
+ padding: var(--spacing-xs) 44px var(--spacing-xs) var(--spacing-xs);
224
+ font-size: var(--font-12);
225
+ font-weight: 400;
226
+ line-height: var(--font-16);
227
+ border-radius: var(--spacing-xs);
228
+
229
+ &::placeholder {
230
+ color: var(--color-text-table-secondary-default);
231
+ }
232
+
233
+ &.inEdit {
234
+ padding-top: calc(var(--spacing-xs) - 1px);
235
+ padding-bottom: calc(var(--spacing-xs) - 1px);
236
+
237
+ &:not(.inEditWarning) {
238
+ border: 1px solid var(--color-border-table-accent-default);
239
+ }
240
+
241
+ &.inEditWarning {
242
+ border: 1px solid var(--error-content-default);
243
+ }
244
+ }
245
+
246
+ &:focus-visible {
247
+ border: none;
248
+ outline: none;
249
+ }
250
+ }
251
+
252
+ .hidden {
253
+ position: absolute;
254
+ visibility: hidden;
255
+ }
256
+
257
+ .btn {
258
+ position: absolute;
259
+ width: var(--spacing-m);
260
+ min-width: var(--spacing-m);
261
+ height: var(--spacing-m);
262
+ min-height: var(--spacing-m);
263
+ border-radius: 50%;
264
+ background-color: transparent;
265
+ cursor: pointer;
266
+ display: flex;
267
+ justify-content: center;
268
+ align-items: center;
269
+ overflow: hidden;
270
+ transition: opacity 0.2s ease-in-out;
271
+ z-index: 1;
272
+
273
+ &:hover {
274
+ cursor: pointer;
275
+ background-color: var(--color-bg-button-secondary-hover);
276
+ }
277
+
278
+ &.editBtn {
279
+ top: var(--spacing-xs);
280
+ right: var(--spacing-xs);
281
+ opacity: 0;
282
+ }
283
+
284
+ &.cancelBtn {
285
+ top: var(--spacing-xs);
286
+ right: var(--spacing-xs);
287
+ opacity: 1;
288
+ }
289
+
290
+ &.doneBtn {
291
+ top: var(--spacing-xs);
292
+ right: var(--spacing-ml);
293
+ opacity: 1;
294
+ }
295
+ }
296
+ </style>
@@ -0,0 +1,153 @@
1
+ import { onMounted, ref, watch } from 'vue';
2
+ import debounce from 'lodash/debounce';
3
+ import sanitizeHtml from 'sanitize-html';
4
+ import { selectAll } from '@/utils';
5
+ import type { Nullable } from '@/components/types';
6
+ import type { Ui3nContentEditableEmits, Ui3nContentEditableProps } from '@/components/ui3n-editable/types';
7
+
8
+ export function useContentEditable(props: Ui3nContentEditableProps, emits: Ui3nContentEditableEmits) {
9
+ const el = ref<Nullable<HTMLElement>>(null);
10
+ const initialValue = ref('');
11
+ const isFocused = ref(false);
12
+
13
+ function convertHtmlToValue(html: Nullable<string>): Nullable<string> {
14
+ let clearedHtml = (html ?? '').trim();
15
+ if (props.placeholder) {
16
+ clearedHtml = clearedHtml.replace(props.placeholder, '');
17
+ }
18
+
19
+ return clearedHtml === '' ? null : html;
20
+ }
21
+
22
+ function convertValueToHtml(value: string): string {
23
+ return value || (!isFocused.value && props.placeholder) || '';
24
+ }
25
+
26
+ function setHtmlFromValue(value: string): void {
27
+ const sanitizedValue = value ? sanitizeHtml(value) : value;
28
+ el.value!.innerText = convertValueToHtml(sanitizedValue);
29
+ }
30
+
31
+ function getContent(ev: Event): Nullable<string> {
32
+ return (ev.target as HTMLElement)?.textContent;
33
+ }
34
+
35
+ function updateValue(newHtml: Nullable<string>) {
36
+ if (props.disabled) {
37
+ return;
38
+ }
39
+
40
+ const newValue = convertHtmlToValue(newHtml);
41
+ if (props.modelValue !== newValue) {
42
+ emits('update:modelValue', newValue);
43
+ }
44
+ }
45
+
46
+ const updateValueDebounced = debounce(updateValue, props.debounceDelay);
47
+
48
+ function onFocusIn(ev: FocusEvent) {
49
+ if (props.disabled) {
50
+ return;
51
+ }
52
+
53
+ isFocused.value = true;
54
+ if (props.disallowEmptyValue) {
55
+ initialValue.value = props.modelValue;
56
+ }
57
+
58
+ setHtmlFromValue(props.modelValue);
59
+
60
+ if (props.selectAllOnFocus) {
61
+ selectAll(el.value!);
62
+ }
63
+
64
+ emits('focusin', ev);
65
+ }
66
+
67
+ function onFocusOut(ev: FocusEvent) {
68
+ isFocused.value = false;
69
+ const content = getContent(ev);
70
+ const updatedContent = props.disallowEmptyValue && !content ? initialValue.value : content;
71
+
72
+ updateValue(updatedContent);
73
+ setHtmlFromValue(props.modelValue);
74
+ el.value!.scrollLeft = 0;
75
+ emits('focusout', ev);
76
+ emits('change', updatedContent);
77
+ emits('update:modelValue', updatedContent);
78
+ }
79
+
80
+ function onClick(ev: MouseEvent) {
81
+ ev.stopPropagation();
82
+ emits('click', ev);
83
+ }
84
+
85
+ function onPaste(ev: ClipboardEvent) {
86
+ if (props.maxLength) {
87
+ const text = ev.clipboardData?.getData('text/plain');
88
+ // Strip &nbsp and html tags from string
89
+ const value = `${el.value!.textContent}${text}`
90
+ .replace(new RegExp(String.fromCharCode(160), 'g'), ' ')
91
+ .replace(/(<([^>]+)>)/gi, '');
92
+
93
+ if (value.length >= props.maxLength) ev.preventDefault();
94
+ }
95
+ }
96
+
97
+ function onKeyDown(ev: KeyboardEvent) {
98
+ if (props.maxLength) {
99
+ // Strip &nbsp and html tags from string
100
+ const value = (ev.target as HTMLElement)?.innerHTML
101
+ // @ts-ignore
102
+ .replaceAll(/<[^>]*(>|$)|&nbsp;|&zwnj;|&raquo;|&laquo;|&gt;/g, ' ')
103
+ .replaceAll(/(<([^>]+)>)/gi, '');
104
+
105
+ const selection = window.getSelection();
106
+
107
+ if (value.length >= props.maxLength && ev.code !== 'Backspace' && selection?.toString().length === 0)
108
+ ev.preventDefault();
109
+ }
110
+
111
+ emits('keydown', ev);
112
+
113
+ if (ev.code === 'Enter') {
114
+ if (!props.isMultiline) {
115
+ ev.preventDefault();
116
+ (ev.target as HTMLElement).blur();
117
+ window.getSelection()?.removeAllRanges();
118
+ }
119
+ } else if (ev.code === 'Esc') {
120
+ (ev.target as HTMLElement).blur();
121
+ }
122
+ }
123
+
124
+ function onInput(ev: InputEvent) {
125
+ emits('input', (ev.target as HTMLElement).textContent);
126
+ updateValueDebounced((ev.target as HTMLElement).textContent);
127
+ }
128
+
129
+ onMounted(() => {
130
+ el.value && emits('init', el.value);
131
+ });
132
+
133
+ watch(
134
+ () => props.modelValue,
135
+ (val: string) => {
136
+ if (!isFocused.value) {
137
+ setHtmlFromValue(val);
138
+ }
139
+ },
140
+ { immediate: true },
141
+ );
142
+
143
+ return {
144
+ el,
145
+ isFocused,
146
+ onFocusIn,
147
+ onFocusOut,
148
+ onClick,
149
+ onPaste,
150
+ onKeyDown,
151
+ onInput,
152
+ };
153
+ }