@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.
- package/dist/components/index.d.ts +3 -1
- package/dist/components/ui3n-editable/types.d.ts +38 -0
- package/dist/components/ui3n-editable/ui3n-editable.vue.d.ts +20 -0
- package/dist/components/ui3n-editable/useContentEditable.d.ts +12 -0
- package/dist/directives/ui3n-title.d.ts +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/style.css +1 -1
- package/dist/ui3n-components.d.ts +2 -0
- package/dist/ui3n-components.js +8123 -7928
- package/dist/ui3n-plugins.js +2962 -6634
- package/dist/ui3n-utils.js +2019 -5465
- package/dist/utils/common.d.ts +6 -5
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/selection.d.ts +3 -0
- package/dist/variables.css +18 -0
- package/package.json +7 -6
- package/src/components/index.ts +5 -0
- package/src/components/ui3n-button/ui3n-button.vue +1 -1
- package/src/components/ui3n-editable/types.ts +42 -0
- package/src/components/ui3n-editable/ui3n-content-editable.vue +73 -0
- package/src/components/ui3n-editable/ui3n-editable.vue +296 -0
- package/src/components/ui3n-editable/useContentEditable.ts +153 -0
- package/src/components/ui3n-emoji/ui3n-emoji.vue +55 -55
- package/src/components/ui3n-table/ui3n-table.vue +12 -4
- package/src/components/ui3n-tooltip/ui3n-tooltip.vue +1 -1
package/dist/utils/common.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/variables.css
CHANGED
|
@@ -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.
|
|
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/
|
|
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.
|
|
59
|
-
"tslib": "2.8.
|
|
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.
|
|
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.
|
|
87
|
+
"pinia": "2.3.1",
|
|
87
88
|
"postcss-html": "1.7.0",
|
|
88
89
|
"prettier": "^3.3.3",
|
|
89
90
|
"sass": "1.80.3",
|
package/src/components/index.ts
CHANGED
|
@@ -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 '
|
|
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   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   and html tags from string
|
|
100
|
+
const value = (ev.target as HTMLElement)?.innerHTML
|
|
101
|
+
// @ts-ignore
|
|
102
|
+
.replaceAll(/<[^>]*(>|$)| |‌|»|«|>/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
|
+
}
|