@toife/vue 3.1.4 → 3.1.6
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/README.md +1 -1
- package/package.json +4 -4
- package/src/components/action/action.scss +2 -2
- package/src/components/action/action.vue +5 -5
- package/src/components/app/app.scss +3 -3
- package/src/components/app/app.type.ts +0 -6
- package/src/components/app/app.vue +2 -8
- package/src/components/avatar/avatar.scss +4 -3
- package/src/components/avatar/avatar.vue +6 -6
- package/src/components/button/button.scss +29 -21
- package/src/components/button/button.type.ts +2 -4
- package/src/components/button/button.vue +7 -7
- package/src/components/cable/cable.vue +2 -2
- package/src/components/card/card/card.scss +3 -3
- package/src/components/card/card/card.vue +5 -5
- package/src/components/card/card-body/card-body.scss +3 -3
- package/src/components/card/card-body/card-body.vue +2 -2
- package/src/components/card/card-footer/card-footer.scss +4 -4
- package/src/components/card/card-footer/card-footer.vue +2 -2
- package/src/components/card/card-header/card-header.scss +4 -4
- package/src/components/card/card-header/card-header.vue +2 -2
- package/src/components/checkbox/checkbox.html +1 -1
- package/src/components/checkbox/checkbox.scss +19 -14
- package/src/components/checkbox/checkbox.type.ts +1 -3
- package/src/components/checkbox/checkbox.vue +7 -7
- package/src/components/collapse/collapse.html +1 -1
- package/src/components/collapse/collapse.scss +4 -7
- package/src/components/collapse/collapse.vue +9 -9
- package/src/components/container/container.vue +2 -2
- package/src/components/decision-modal/decision-modal.scss +11 -11
- package/src/components/decision-modal/decision-modal.vue +8 -8
- package/src/components/divider/divider.scss +3 -3
- package/src/components/divider/divider.vue +4 -4
- package/src/components/dropdown/dropdown.scss +4 -4
- package/src/components/dropdown/dropdown.type.ts +2 -2
- package/src/components/dropdown/dropdown.vue +7 -9
- package/src/components/field/field.html +28 -9
- package/src/components/field/{outline/outline.scss → field.scss} +29 -125
- package/src/components/field/field.type.ts +4 -4
- package/src/components/field/field.vue +83 -46
- package/src/components/field/index.ts +1 -1
- package/src/components/form-group/form-group.vue +2 -2
- package/src/components/gesture-indicator/gesture-indicator.scss +2 -2
- package/src/components/gesture-indicator/gesture-indicator.vue +4 -4
- package/src/components/image/image.vue +12 -5
- package/src/components/layout/flex/flex.vue +8 -8
- package/src/components/layout/flex-item/flex-item.vue +6 -6
- package/src/components/layout/grid/grid.vue +6 -6
- package/src/components/layout/grid-item/grid-item.vue +6 -6
- package/src/components/modal/modal.scss +2 -2
- package/src/components/modal/modal.vue +68 -5
- package/src/components/page/page.vue +2 -2
- package/src/components/present/present.scss +4 -4
- package/src/components/present/present.vue +14 -14
- package/src/components/radio/radio/radio.html +1 -1
- package/src/components/radio/radio/radio.scss +19 -14
- package/src/components/radio/radio/radio.type.ts +1 -3
- package/src/components/radio/radio/radio.vue +6 -6
- package/src/components/radio/radio-group/radio-group.vue +2 -2
- package/src/components/refresher/refresher.html +0 -3
- package/src/components/refresher/refresher.scss +1 -25
- package/src/components/refresher/refresher.vue +2 -16
- package/src/components/route/route-navigator/route-navigator.html +1 -1
- package/src/components/route/route-navigator/route-navigator.scss +3 -3
- package/src/components/route/route-navigator/route-navigator.vue +11 -14
- package/src/components/route/route-wrapper/route-wrapper.composable.ts +5 -15
- package/src/components/route/route-wrapper/route-wrapper.type.ts +0 -4
- package/src/components/route/route-wrapper/route-wrapper.vue +4 -12
- package/src/components/route/route.type.ts +0 -1
- package/src/components/segmented-field/segmented-field.html +1 -1
- package/src/components/segmented-field/segmented-field.scss +3 -3
- package/src/components/segmented-field/segmented-field.vue +8 -8
- package/src/components/select/select.html +2 -2
- package/src/components/select/select.scss +11 -11
- package/src/components/select/select.vue +10 -10
- package/src/components/skeleton/skeleton.scss +1 -1
- package/src/components/skeleton/skeleton.vue +7 -7
- package/src/components/slide-range/slide-range.html +3 -4
- package/src/components/slide-range/slide-range.scss +17 -14
- package/src/components/slide-range/slide-range.vue +17 -16
- package/src/components/switch/switch.html +1 -1
- package/src/components/switch/switch.scss +23 -21
- package/src/components/switch/switch.type.ts +2 -3
- package/src/components/switch/switch.vue +23 -9
- package/src/components/tabs/tab/tab.html +1 -1
- package/src/components/tabs/tab/tab.scss +13 -0
- package/src/components/tabs/tab/tab.vue +4 -2
- package/src/components/tabs/tabs/index.ts +1 -0
- package/src/components/tabs/tabs/tabs.scss +82 -54
- package/src/components/tabs/tabs/tabs.type.ts +4 -1
- package/src/components/tabs/tabs/tabs.vue +47 -23
- package/src/components/toast/toast/toast.scss +1 -1
- package/src/components/toast/toast/toast.vue +2 -2
- package/src/components/toast/toast-content/toast-content.scss +4 -4
- package/src/components/toast/toast-content/toast-content.vue +5 -5
- package/src/components/toolbar/toolbar.scss +4 -4
- package/src/components/toolbar/toolbar.vue +5 -5
- package/src/factory.ts +105 -51
- package/src/type.ts +2 -1
- package/src/utils/style/index.ts +9 -9
- package/src/utils/style.md +9 -9
- package/src/components/field/outline/index.ts +0 -1
- package/src/components/field/outline/outline.html +0 -36
- package/src/components/field/outline/outline.vue +0 -286
package/src/factory.ts
CHANGED
|
@@ -43,56 +43,110 @@ import {
|
|
|
43
43
|
ToastContent,
|
|
44
44
|
Toolbar,
|
|
45
45
|
} from "./components";
|
|
46
|
-
import { type
|
|
47
|
-
import type
|
|
46
|
+
import { type SubscribeOptions } from "./type";
|
|
47
|
+
import { DefineComponent, ref, type App as VueApp } from "vue";
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
49
|
+
const apps = ref<Record<string, any>>({});
|
|
50
|
+
const defaultAppName = "toife";
|
|
51
|
+
|
|
52
|
+
class Toife {
|
|
53
|
+
/**
|
|
54
|
+
* Subscribe options
|
|
55
|
+
*/
|
|
56
|
+
public options: SubscribeOptions;
|
|
57
|
+
public app: VueApp;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Constructor
|
|
61
|
+
*/
|
|
62
|
+
constructor(app: VueApp, options?: SubscribeOptions) {
|
|
63
|
+
this.options = options || {
|
|
64
|
+
name: defaultAppName,
|
|
65
|
+
prefix: "t-",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
this.app = app;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Subscribe components
|
|
73
|
+
*/
|
|
74
|
+
subscribeComponents() {
|
|
75
|
+
const prefix = this.options.prefix;
|
|
76
|
+
this.app.component(prefix + "app", App);
|
|
77
|
+
this.app.component(prefix + "action", Action);
|
|
78
|
+
this.app.component(prefix + "avatar", Avatar);
|
|
79
|
+
this.app.component(prefix + "button", Button);
|
|
80
|
+
this.app.component(prefix + "cable", Cable);
|
|
81
|
+
this.app.component(prefix + "card", Card);
|
|
82
|
+
this.app.component(prefix + "card-body", CardBody);
|
|
83
|
+
this.app.component(prefix + "card-header", CardHeader);
|
|
84
|
+
this.app.component(prefix + "card-footer", CardFooter);
|
|
85
|
+
this.app.component(prefix + "checkbox", Checkbox);
|
|
86
|
+
this.app.component(prefix + "radio", Radio);
|
|
87
|
+
this.app.component(prefix + "radio-group", RadioGroup);
|
|
88
|
+
this.app.component(prefix + "collapse", Collapse);
|
|
89
|
+
this.app.component(prefix + "container", Container);
|
|
90
|
+
this.app.component(prefix + "decision-modal", DecisionModal);
|
|
91
|
+
this.app.component(prefix + "divider", Divider);
|
|
92
|
+
this.app.component(prefix + "dropdown", Dropdown);
|
|
93
|
+
this.app.component(prefix + "field", Field);
|
|
94
|
+
this.app.component(prefix + "flex", Flex);
|
|
95
|
+
this.app.component(prefix + "flex-item", FlexItem);
|
|
96
|
+
this.app.component(prefix + "form-group", FormGroup);
|
|
97
|
+
this.app.component(prefix + "gesture-indicator", GestureIndicator);
|
|
98
|
+
this.app.component(prefix + "grid", Grid);
|
|
99
|
+
this.app.component(prefix + "grid-item", GridItem);
|
|
100
|
+
this.app.component(prefix + "image", Image);
|
|
101
|
+
this.app.component(prefix + "modal", Modal);
|
|
102
|
+
this.app.component(prefix + "page", Page);
|
|
103
|
+
this.app.component(prefix + "present", Present);
|
|
104
|
+
this.app.component(prefix + "refresher", Refresher);
|
|
105
|
+
this.app.component(prefix + "route-navigator", RouteNavigator);
|
|
106
|
+
this.app.component(prefix + "route-wrapper", RouteWrapper);
|
|
107
|
+
this.app.component(prefix + "route-provider", RouteProvider);
|
|
108
|
+
this.app.component(prefix + "route-outlet", RouteOutlet);
|
|
109
|
+
this.app.component(prefix + "segmented-field", SegmentedField);
|
|
110
|
+
this.app.component(prefix + "select", Select);
|
|
111
|
+
this.app.component(prefix + "slide-range", SlideRange);
|
|
112
|
+
this.app.component(prefix + "skeleton", Skeleton);
|
|
113
|
+
this.app.component(prefix + "switch", Switch);
|
|
114
|
+
this.app.component(prefix + "tab", Tab);
|
|
115
|
+
this.app.component(prefix + "tabs", Tabs);
|
|
116
|
+
this.app.component(prefix + "toast", Toast);
|
|
117
|
+
this.app.component(prefix + "toast-content", ToastContent);
|
|
118
|
+
this.app.component(prefix + "toolbar", Toolbar);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Subscribe a component
|
|
123
|
+
*/
|
|
124
|
+
subscribe(name: string, component: DefineComponent<{}, {}, any>) {
|
|
125
|
+
if (!this.app.component(this.options.prefix + name)) {
|
|
126
|
+
this.app.component(this.options.prefix + name, component);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Subscribe components
|
|
133
|
+
*/
|
|
134
|
+
export const createToife = (app: VueApp, options?: SubscribeOptions) => {
|
|
135
|
+
const instance = new Toife(app, options);
|
|
136
|
+
apps.value[instance.options.name] = instance;
|
|
137
|
+
return instance;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get component name
|
|
142
|
+
*/
|
|
143
|
+
export const useApp = (name: string = defaultAppName) => {
|
|
144
|
+
return apps.value[name] || null;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get all apps
|
|
149
|
+
*/
|
|
150
|
+
export const getApps = () => {
|
|
151
|
+
return apps.value;
|
|
98
152
|
};
|
package/src/type.ts
CHANGED
package/src/utils/style/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ let separator: string | null = null;
|
|
|
4
4
|
/**
|
|
5
5
|
* Get the separator from the document element
|
|
6
6
|
*/
|
|
7
|
-
export const
|
|
7
|
+
export const getCssSeparator = () => {
|
|
8
8
|
if (!separator)
|
|
9
9
|
separator = getComputedStyle(document.documentElement).getPropertyValue("--separator").trim();
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ export const getSeparator = () => {
|
|
|
14
14
|
/**
|
|
15
15
|
* Get the prefix from the document element
|
|
16
16
|
*/
|
|
17
|
-
export const
|
|
17
|
+
export const getCssPrefix = () => {
|
|
18
18
|
if (!prefix)
|
|
19
19
|
prefix = getComputedStyle(document.documentElement).getPropertyValue("--prefix").trim();
|
|
20
20
|
|
|
@@ -24,9 +24,9 @@ export const getPrefix = () => {
|
|
|
24
24
|
/**
|
|
25
25
|
* Generate the prefixed name
|
|
26
26
|
*/
|
|
27
|
-
export const
|
|
28
|
-
const p =
|
|
29
|
-
const s =
|
|
27
|
+
export const cssPrefix = (name: string | string[]) => {
|
|
28
|
+
const p = getCssPrefix();
|
|
29
|
+
const s = getCssSeparator();
|
|
30
30
|
let names = [];
|
|
31
31
|
|
|
32
32
|
if (typeof name === "string") {
|
|
@@ -45,13 +45,13 @@ export const withPrefix = (name: string | string[]) => {
|
|
|
45
45
|
/**
|
|
46
46
|
* Generate the property name
|
|
47
47
|
*/
|
|
48
|
-
export const
|
|
49
|
-
return `--${
|
|
48
|
+
export const cssProperty = (name: string | string[]) => {
|
|
49
|
+
return `--${cssPrefix(name)}`;
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Generate the name with var() syntax
|
|
54
54
|
*/
|
|
55
|
-
export const
|
|
56
|
-
return `var(${
|
|
55
|
+
export const cssVariable = (name: string | string[]) => {
|
|
56
|
+
return `var(${cssProperty(name)})`;
|
|
57
57
|
};
|
package/src/utils/style.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Utils: `style`
|
|
2
2
|
|
|
3
|
-
> Import: `import { utils } from "@toife/vue"` then `utils.
|
|
3
|
+
> Import: `import { utils } from "@toife/vue"` then `utils.cssPrefix`, … (or import from `src/utils` in the monorepo).
|
|
4
4
|
|
|
5
5
|
**Source:** `src/utils/style/index.ts`
|
|
6
6
|
|
|
@@ -10,21 +10,21 @@ Reads `--prefix` and `--separator` from `document.documentElement` (cached), the
|
|
|
10
10
|
|
|
11
11
|
## API
|
|
12
12
|
|
|
13
|
-
### `
|
|
13
|
+
### `getCssPrefix(): string`
|
|
14
14
|
|
|
15
15
|
Trimmed `--prefix` on `:root` (singleton cache).
|
|
16
16
|
|
|
17
|
-
### `
|
|
17
|
+
### `getCssSeparator(): string`
|
|
18
18
|
|
|
19
19
|
Trimmed `--separator` (often `-` or `_`) used to join name parts.
|
|
20
20
|
|
|
21
|
-
### `
|
|
21
|
+
### `cssPrefix(name: string | string[]): string`
|
|
22
22
|
|
|
23
23
|
If prefix exists: `[prefix, ...parts].join(separator)`. `name` can be a string or parts array (e.g. `['layer','surface']`).
|
|
24
24
|
|
|
25
|
-
### `
|
|
25
|
+
### `cssProperty(name: string | string[]): string`
|
|
26
26
|
|
|
27
|
-
Returns the CSS custom property name **without** `var()`: `"--" +
|
|
27
|
+
Returns the CSS custom property name **without** `var()`: `"--" + cssPrefix(name)`.
|
|
28
28
|
|
|
29
29
|
### `variable(name: string | string[]): string`
|
|
30
30
|
|
|
@@ -35,8 +35,8 @@ Returns `var(--<prefixed…>)`.
|
|
|
35
35
|
```ts
|
|
36
36
|
import { utils } from "@toife/vue";
|
|
37
37
|
|
|
38
|
-
utils.
|
|
39
|
-
utils.
|
|
38
|
+
utils.cssPrefix("button");
|
|
39
|
+
utils.cssProperty(["field", "line"]);
|
|
40
40
|
utils.variable(["field", "line"]);
|
|
41
41
|
```
|
|
42
42
|
|
|
@@ -46,4 +46,4 @@ Requires `document` and `getComputedStyle` — client-only after `:root` defines
|
|
|
46
46
|
|
|
47
47
|
## See also
|
|
48
48
|
|
|
49
|
-
- Components using `
|
|
49
|
+
- Components using `cssPrefix` for BEM classes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default as OutlineField } from "./outline.vue";
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
<div v-bind="fieldAttrs">
|
|
2
|
-
<div v-bind="fieldContentAttrs">
|
|
3
|
-
<slot name="start-input"></slot>
|
|
4
|
-
<slot name="input">
|
|
5
|
-
<div v-bind="fieldInputShellAttrs" v-if="type !== 'password'">
|
|
6
|
-
<div
|
|
7
|
-
v-bind="fieldEditableAttrs"
|
|
8
|
-
@input="onInput"
|
|
9
|
-
@compositionstart="onCompositionStart"
|
|
10
|
-
@compositionend="onCompositionEnd"
|
|
11
|
-
@focus="onFocus"
|
|
12
|
-
@blur="onBlur"
|
|
13
|
-
@beforeinput="onBeforeinput"
|
|
14
|
-
ref="contentRef"
|
|
15
|
-
></div>
|
|
16
|
-
</div>
|
|
17
|
-
<input
|
|
18
|
-
v-bind="fieldPasswordInputAttrs"
|
|
19
|
-
@input="onInput"
|
|
20
|
-
@compositionstart="onCompositionStart"
|
|
21
|
-
@compositionend="onCompositionEnd"
|
|
22
|
-
@focus="onFocus"
|
|
23
|
-
@blur="onBlur"
|
|
24
|
-
@beforeinput="onBeforeinput"
|
|
25
|
-
ref="contentRef"
|
|
26
|
-
:value="content"
|
|
27
|
-
type="password"
|
|
28
|
-
v-else
|
|
29
|
-
/>
|
|
30
|
-
</slot>
|
|
31
|
-
<slot name="end-input"></slot>
|
|
32
|
-
</div>
|
|
33
|
-
<div v-bind="fieldMessageAttrs" v-if="message">{{ message }}</div>
|
|
34
|
-
<div v-bind="fieldHelpAttrs" v-if="help">{{ help }}</div>
|
|
35
|
-
<slot></slot>
|
|
36
|
-
</div>
|
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
<style lang="scss" src="./outline.scss" scoped></style>
|
|
2
|
-
<template src="./outline.html"></template>
|
|
3
|
-
<script lang="ts" setup>
|
|
4
|
-
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
|
5
|
-
import type { FieldProps, FieldEmit } from "../field.type";
|
|
6
|
-
import { property, withPrefix } from "../../../utils";
|
|
7
|
-
|
|
8
|
-
// Component setup (props, emits, injects)
|
|
9
|
-
// ----------------------------------------------------------------------------
|
|
10
|
-
const props = withDefaults(defineProps<FieldProps>(), {
|
|
11
|
-
modelValue: "",
|
|
12
|
-
type: "text",
|
|
13
|
-
size: "standard",
|
|
14
|
-
disabled: false,
|
|
15
|
-
readonly: false,
|
|
16
|
-
message: "",
|
|
17
|
-
help: "",
|
|
18
|
-
variant: "fill",
|
|
19
|
-
placeholder: "",
|
|
20
|
-
shadow: undefined,
|
|
21
|
-
direction: undefined,
|
|
22
|
-
});
|
|
23
|
-
const emit = defineEmits<FieldEmit>();
|
|
24
|
-
|
|
25
|
-
// Reactive state
|
|
26
|
-
// ----------------------------------------------------------------------------
|
|
27
|
-
const isFocus = ref(false);
|
|
28
|
-
const contentRef = ref<HTMLElement>();
|
|
29
|
-
const caret = ref(0);
|
|
30
|
-
const isComposing = ref(false);
|
|
31
|
-
|
|
32
|
-
// Computed properties
|
|
33
|
-
// ----------------------------------------------------------------------------
|
|
34
|
-
const content = computed(() => {
|
|
35
|
-
return props.value || props.modelValue;
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const fieldAttrs = computed(() => {
|
|
39
|
-
return {
|
|
40
|
-
class: [
|
|
41
|
-
withPrefix(["layer", "field"]),
|
|
42
|
-
withPrefix(["role", props.role || ""]),
|
|
43
|
-
withPrefix(["shape", props.shape || ""]),
|
|
44
|
-
withPrefix("field"),
|
|
45
|
-
withPrefix(["size", props.size]),
|
|
46
|
-
withPrefix(["direction", props.direction || "left"]),
|
|
47
|
-
props.variant,
|
|
48
|
-
props.type,
|
|
49
|
-
{
|
|
50
|
-
disabled: props.disabled,
|
|
51
|
-
focus: isFocus.value,
|
|
52
|
-
shadow: props.shadow,
|
|
53
|
-
empty: !content.value,
|
|
54
|
-
readonly: props.readonly,
|
|
55
|
-
typing: isComposing.value,
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
style: {
|
|
59
|
-
[property(["field", "line"])]: props.line,
|
|
60
|
-
[property(["field", "max-line"])]: props.maxLine || props.line,
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const fieldContentAttrs = computed(() => ({
|
|
66
|
-
class: [withPrefix("field-content")],
|
|
67
|
-
}));
|
|
68
|
-
|
|
69
|
-
/** Outer shell: layout, placeholder pseudo-element (must stay off the contenteditable node on iOS). */
|
|
70
|
-
const fieldInputShellAttrs = computed(() => ({
|
|
71
|
-
class: [withPrefix("field-input")],
|
|
72
|
-
placeholder: props.placeholder,
|
|
73
|
-
}));
|
|
74
|
-
|
|
75
|
-
/** contenteditable as the literal string "true"|"false" improves mobile WebKit behavior vs boolean. */
|
|
76
|
-
const fieldEditableAttrs = computed(() => ({
|
|
77
|
-
class: [withPrefix("field-input-editable")],
|
|
78
|
-
name: props.name,
|
|
79
|
-
id: props.id,
|
|
80
|
-
contenteditable:
|
|
81
|
-
props.type === "password" ? undefined : props.disabled || props.readonly ? "false" : "true",
|
|
82
|
-
autocomplete: props.autocomplete,
|
|
83
|
-
tabindex: props.readonly ? 0 : props.disabled ? -1 : props.tabindex,
|
|
84
|
-
}));
|
|
85
|
-
|
|
86
|
-
const fieldPasswordInputAttrs = computed(() => ({
|
|
87
|
-
class: [withPrefix("field-input")],
|
|
88
|
-
name: props.name,
|
|
89
|
-
id: props.id,
|
|
90
|
-
placeholder: props.placeholder,
|
|
91
|
-
autocomplete: props.autocomplete,
|
|
92
|
-
tabindex: props.readonly ? 0 : props.disabled ? -1 : props.tabindex,
|
|
93
|
-
readonly: props.readonly,
|
|
94
|
-
disabled: props.disabled,
|
|
95
|
-
}));
|
|
96
|
-
|
|
97
|
-
const fieldMessageAttrs = computed(() => ({
|
|
98
|
-
class: [withPrefix("field-message")],
|
|
99
|
-
}));
|
|
100
|
-
|
|
101
|
-
const fieldHelpAttrs = computed(() => ({
|
|
102
|
-
class: [withPrefix("field-help")],
|
|
103
|
-
}));
|
|
104
|
-
|
|
105
|
-
// Methods
|
|
106
|
-
// ----------------------------------------------------------------------------
|
|
107
|
-
const isBr = (node: Node): node is HTMLBRElement =>
|
|
108
|
-
node.nodeType === 1 && (node as HTMLElement).tagName === "BR";
|
|
109
|
-
|
|
110
|
-
const isBlockLineBreak = (node: Node, parent: Node | null): node is HTMLElement =>
|
|
111
|
-
parent != null && node.nodeType === 1 && ["DIV", "P"].includes((node as HTMLElement).tagName);
|
|
112
|
-
|
|
113
|
-
const getCaretOffset = (el: HTMLElement) => {
|
|
114
|
-
const selection = window.getSelection();
|
|
115
|
-
if (!selection || selection.rangeCount === 0) return 0;
|
|
116
|
-
|
|
117
|
-
const range = selection.getRangeAt(0);
|
|
118
|
-
const endContainer = range.endContainer;
|
|
119
|
-
const endOffset = range.endOffset;
|
|
120
|
-
let offset = 0;
|
|
121
|
-
|
|
122
|
-
function walk(node: Node, parent: Node | null): boolean {
|
|
123
|
-
if (node === endContainer) {
|
|
124
|
-
if (node.nodeType === 3) {
|
|
125
|
-
offset += endOffset;
|
|
126
|
-
} else if (isBr(node)) {
|
|
127
|
-
offset += endOffset > 0 ? 1 : 0;
|
|
128
|
-
} else if (isBlockLineBreak(node, parent)) {
|
|
129
|
-
offset += 1;
|
|
130
|
-
} else if (node === el) {
|
|
131
|
-
for (let i = 0; i < endOffset; i++) {
|
|
132
|
-
const child = node.childNodes[i];
|
|
133
|
-
if (!child) break;
|
|
134
|
-
if (child.nodeType === 3) offset += child.textContent?.length ?? 0;
|
|
135
|
-
else if (isBr(child)) offset += 1;
|
|
136
|
-
else if (isBlockLineBreak(child, el)) offset += 1;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
if (node.nodeType === 3) {
|
|
142
|
-
offset += node.textContent?.length ?? 0;
|
|
143
|
-
} else if (isBr(node)) {
|
|
144
|
-
offset += 1;
|
|
145
|
-
} else if (isBlockLineBreak(node, parent)) {
|
|
146
|
-
offset += 1;
|
|
147
|
-
}
|
|
148
|
-
for (const child of Array.from(node.childNodes)) {
|
|
149
|
-
if (walk(child, node)) return true;
|
|
150
|
-
}
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
walk(el, null);
|
|
154
|
-
return offset;
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const setCaretOffset = (el: HTMLElement, offset: number) => {
|
|
158
|
-
const sel = window.getSelection();
|
|
159
|
-
if (!sel) return;
|
|
160
|
-
let current = 0;
|
|
161
|
-
const range = document.createRange();
|
|
162
|
-
|
|
163
|
-
function walk(node: Node): boolean {
|
|
164
|
-
if (node.nodeType === 3) {
|
|
165
|
-
const len = node.textContent?.length ?? 0;
|
|
166
|
-
const next = current + len;
|
|
167
|
-
if (offset <= next) {
|
|
168
|
-
range.setStart(node, Math.min(offset - current, len));
|
|
169
|
-
range.collapse(true);
|
|
170
|
-
sel!.removeAllRanges();
|
|
171
|
-
sel!.addRange(range);
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
current = next;
|
|
175
|
-
} else if (isBr(node)) {
|
|
176
|
-
if (offset <= current) {
|
|
177
|
-
range.setStartBefore(node);
|
|
178
|
-
range.collapse(true);
|
|
179
|
-
sel!.removeAllRanges();
|
|
180
|
-
sel!.addRange(range);
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
183
|
-
if (offset <= current + 1) {
|
|
184
|
-
range.setStartAfter(node);
|
|
185
|
-
range.collapse(true);
|
|
186
|
-
sel!.removeAllRanges();
|
|
187
|
-
sel!.addRange(range);
|
|
188
|
-
return true;
|
|
189
|
-
}
|
|
190
|
-
current += 1;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
for (const child of Array.from(node.childNodes)) {
|
|
194
|
-
if (walk(child)) return true;
|
|
195
|
-
}
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
walk(el);
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const onInput = (ev: Event) => {
|
|
203
|
-
if (props.disabled || props.readonly || isComposing.value) return;
|
|
204
|
-
|
|
205
|
-
caret.value = getCaretOffset(ev.target as HTMLElement);
|
|
206
|
-
emit("input", ev);
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
const onCompositionStart = () => {
|
|
210
|
-
isComposing.value = true;
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const onCompositionEnd = (ev: CompositionEvent) => {
|
|
214
|
-
const el = ev.target as HTMLElement;
|
|
215
|
-
isComposing.value = false;
|
|
216
|
-
nextTick(() => {
|
|
217
|
-
caret.value = getCaretOffset(el);
|
|
218
|
-
const inputEv = new InputEvent("input", { bubbles: true });
|
|
219
|
-
el.dispatchEvent(inputEv);
|
|
220
|
-
emit("input", inputEv);
|
|
221
|
-
});
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
const normalizeText = (s: string) => s.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
225
|
-
|
|
226
|
-
const ensureEditableCanReceiveCaret = (el: HTMLElement) => {
|
|
227
|
-
// iOS Safari often refuses to show caret / accept input in an empty contenteditable.
|
|
228
|
-
if (!el.textContent?.length && el.childNodes.length === 0) {
|
|
229
|
-
el.appendChild(document.createElement("br"));
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
const syncContentFromModel = async () => {
|
|
234
|
-
const el = contentRef.value;
|
|
235
|
-
if (!el || props.type === "password") return;
|
|
236
|
-
if (isComposing.value) return;
|
|
237
|
-
|
|
238
|
-
const next = normalizeText(content.value ?? "");
|
|
239
|
-
const current = normalizeText(el.innerText);
|
|
240
|
-
if (current === next) return;
|
|
241
|
-
|
|
242
|
-
if (isFocus.value) {
|
|
243
|
-
const saved = caret.value;
|
|
244
|
-
el.innerText = next;
|
|
245
|
-
await nextTick();
|
|
246
|
-
el.focus();
|
|
247
|
-
setCaretOffset(el, Math.min(saved, next.length));
|
|
248
|
-
} else {
|
|
249
|
-
el.innerText = next;
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const onFocus = (ev: FocusEvent) => {
|
|
254
|
-
if (props.disabled) return;
|
|
255
|
-
isFocus.value = true;
|
|
256
|
-
const el = contentRef.value;
|
|
257
|
-
if (el && props.type !== "password") {
|
|
258
|
-
ensureEditableCanReceiveCaret(el);
|
|
259
|
-
}
|
|
260
|
-
emit("focus", ev);
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const onBlur = (ev: FocusEvent) => {
|
|
264
|
-
if (props.disabled) return;
|
|
265
|
-
isFocus.value = false;
|
|
266
|
-
emit("blur", ev);
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
const onBeforeinput = (ev: Event) => {
|
|
270
|
-
if (props.disabled || props.readonly) {
|
|
271
|
-
ev.preventDefault();
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
emit("beforeinput", ev);
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
// Lifecycle
|
|
278
|
-
// ----------------------------------------------------------------------------
|
|
279
|
-
watch(
|
|
280
|
-
() => content.value,
|
|
281
|
-
() => void syncContentFromModel(),
|
|
282
|
-
{ flush: "post" }
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
onMounted(() => void syncContentFromModel());
|
|
286
|
-
</script>
|