@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.
Files changed (104) hide show
  1. package/README.md +1 -1
  2. package/package.json +4 -4
  3. package/src/components/action/action.scss +2 -2
  4. package/src/components/action/action.vue +5 -5
  5. package/src/components/app/app.scss +3 -3
  6. package/src/components/app/app.type.ts +0 -6
  7. package/src/components/app/app.vue +2 -8
  8. package/src/components/avatar/avatar.scss +4 -3
  9. package/src/components/avatar/avatar.vue +6 -6
  10. package/src/components/button/button.scss +29 -21
  11. package/src/components/button/button.type.ts +2 -4
  12. package/src/components/button/button.vue +7 -7
  13. package/src/components/cable/cable.vue +2 -2
  14. package/src/components/card/card/card.scss +3 -3
  15. package/src/components/card/card/card.vue +5 -5
  16. package/src/components/card/card-body/card-body.scss +3 -3
  17. package/src/components/card/card-body/card-body.vue +2 -2
  18. package/src/components/card/card-footer/card-footer.scss +4 -4
  19. package/src/components/card/card-footer/card-footer.vue +2 -2
  20. package/src/components/card/card-header/card-header.scss +4 -4
  21. package/src/components/card/card-header/card-header.vue +2 -2
  22. package/src/components/checkbox/checkbox.html +1 -1
  23. package/src/components/checkbox/checkbox.scss +19 -14
  24. package/src/components/checkbox/checkbox.type.ts +1 -3
  25. package/src/components/checkbox/checkbox.vue +7 -7
  26. package/src/components/collapse/collapse.html +1 -1
  27. package/src/components/collapse/collapse.scss +4 -7
  28. package/src/components/collapse/collapse.vue +9 -9
  29. package/src/components/container/container.vue +2 -2
  30. package/src/components/decision-modal/decision-modal.scss +11 -11
  31. package/src/components/decision-modal/decision-modal.vue +8 -8
  32. package/src/components/divider/divider.scss +3 -3
  33. package/src/components/divider/divider.vue +4 -4
  34. package/src/components/dropdown/dropdown.scss +4 -4
  35. package/src/components/dropdown/dropdown.type.ts +2 -2
  36. package/src/components/dropdown/dropdown.vue +7 -9
  37. package/src/components/field/field.html +28 -9
  38. package/src/components/field/{outline/outline.scss → field.scss} +29 -125
  39. package/src/components/field/field.type.ts +4 -4
  40. package/src/components/field/field.vue +83 -46
  41. package/src/components/field/index.ts +1 -1
  42. package/src/components/form-group/form-group.vue +2 -2
  43. package/src/components/gesture-indicator/gesture-indicator.scss +2 -2
  44. package/src/components/gesture-indicator/gesture-indicator.vue +4 -4
  45. package/src/components/image/image.vue +12 -5
  46. package/src/components/layout/flex/flex.vue +8 -8
  47. package/src/components/layout/flex-item/flex-item.vue +6 -6
  48. package/src/components/layout/grid/grid.vue +6 -6
  49. package/src/components/layout/grid-item/grid-item.vue +6 -6
  50. package/src/components/modal/modal.scss +2 -2
  51. package/src/components/modal/modal.vue +68 -5
  52. package/src/components/page/page.vue +2 -2
  53. package/src/components/present/present.scss +4 -4
  54. package/src/components/present/present.vue +14 -14
  55. package/src/components/radio/radio/radio.html +1 -1
  56. package/src/components/radio/radio/radio.scss +19 -14
  57. package/src/components/radio/radio/radio.type.ts +1 -3
  58. package/src/components/radio/radio/radio.vue +6 -6
  59. package/src/components/radio/radio-group/radio-group.vue +2 -2
  60. package/src/components/refresher/refresher.html +0 -3
  61. package/src/components/refresher/refresher.scss +1 -25
  62. package/src/components/refresher/refresher.vue +2 -16
  63. package/src/components/route/route-navigator/route-navigator.html +1 -1
  64. package/src/components/route/route-navigator/route-navigator.scss +3 -3
  65. package/src/components/route/route-navigator/route-navigator.vue +11 -14
  66. package/src/components/route/route-wrapper/route-wrapper.composable.ts +5 -15
  67. package/src/components/route/route-wrapper/route-wrapper.type.ts +0 -4
  68. package/src/components/route/route-wrapper/route-wrapper.vue +4 -12
  69. package/src/components/route/route.type.ts +0 -1
  70. package/src/components/segmented-field/segmented-field.html +1 -1
  71. package/src/components/segmented-field/segmented-field.scss +3 -3
  72. package/src/components/segmented-field/segmented-field.vue +8 -8
  73. package/src/components/select/select.html +2 -2
  74. package/src/components/select/select.scss +11 -11
  75. package/src/components/select/select.vue +10 -10
  76. package/src/components/skeleton/skeleton.scss +1 -1
  77. package/src/components/skeleton/skeleton.vue +7 -7
  78. package/src/components/slide-range/slide-range.html +3 -4
  79. package/src/components/slide-range/slide-range.scss +17 -14
  80. package/src/components/slide-range/slide-range.vue +17 -16
  81. package/src/components/switch/switch.html +1 -1
  82. package/src/components/switch/switch.scss +23 -21
  83. package/src/components/switch/switch.type.ts +2 -3
  84. package/src/components/switch/switch.vue +23 -9
  85. package/src/components/tabs/tab/tab.html +1 -1
  86. package/src/components/tabs/tab/tab.scss +13 -0
  87. package/src/components/tabs/tab/tab.vue +4 -2
  88. package/src/components/tabs/tabs/index.ts +1 -0
  89. package/src/components/tabs/tabs/tabs.scss +82 -54
  90. package/src/components/tabs/tabs/tabs.type.ts +4 -1
  91. package/src/components/tabs/tabs/tabs.vue +47 -23
  92. package/src/components/toast/toast/toast.scss +1 -1
  93. package/src/components/toast/toast/toast.vue +2 -2
  94. package/src/components/toast/toast-content/toast-content.scss +4 -4
  95. package/src/components/toast/toast-content/toast-content.vue +5 -5
  96. package/src/components/toolbar/toolbar.scss +4 -4
  97. package/src/components/toolbar/toolbar.vue +5 -5
  98. package/src/factory.ts +105 -51
  99. package/src/type.ts +2 -1
  100. package/src/utils/style/index.ts +9 -9
  101. package/src/utils/style.md +9 -9
  102. package/src/components/field/outline/index.ts +0 -1
  103. package/src/components/field/outline/outline.html +0 -36
  104. 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 CreateToifeOptions } from "./type";
47
- import type { App as VueApp } from "vue";
46
+ import { type SubscribeOptions } from "./type";
47
+ import { DefineComponent, ref, type App as VueApp } from "vue";
48
48
 
49
- export const createToife = (options?: CreateToifeOptions) => {
50
- return {
51
- install: (app: VueApp) => {
52
- const prefix = options?.prefix || "t-";
53
- app.component(prefix + "app", App);
54
- app.component(prefix + "action", Action);
55
- app.component(prefix + "avatar", Avatar);
56
- app.component(prefix + "button", Button);
57
- app.component(prefix + "cable", Cable);
58
- app.component(prefix + "card", Card);
59
- app.component(prefix + "card-body", CardBody);
60
- app.component(prefix + "card-header", CardHeader);
61
- app.component(prefix + "card-footer", CardFooter);
62
- app.component(prefix + "checkbox", Checkbox);
63
- app.component(prefix + "radio", Radio);
64
- app.component(prefix + "radio-group", RadioGroup);
65
- app.component(prefix + "collapse", Collapse);
66
- app.component(prefix + "container", Container);
67
- app.component(prefix + "decision-modal", DecisionModal);
68
- app.component(prefix + "divider", Divider);
69
- app.component(prefix + "dropdown", Dropdown);
70
- app.component(prefix + "field", Field);
71
- app.component(prefix + "flex", Flex);
72
- app.component(prefix + "flex-item", FlexItem);
73
- app.component(prefix + "form-group", FormGroup);
74
- app.component(prefix + "gesture-indicator", GestureIndicator);
75
- app.component(prefix + "grid", Grid);
76
- app.component(prefix + "grid-item", GridItem);
77
- app.component(prefix + "image", Image);
78
- app.component(prefix + "modal", Modal);
79
- app.component(prefix + "page", Page);
80
- app.component(prefix + "present", Present);
81
- app.component(prefix + "refresher", Refresher);
82
- app.component(prefix + "route-navigator", RouteNavigator);
83
- app.component(prefix + "route-wrapper", RouteWrapper);
84
- app.component(prefix + "route-provider", RouteProvider);
85
- app.component(prefix + "route-outlet", RouteOutlet);
86
- app.component(prefix + "segmented-field", SegmentedField);
87
- app.component(prefix + "select", Select);
88
- app.component(prefix + "slide-range", SlideRange);
89
- app.component(prefix + "skeleton", Skeleton);
90
- app.component(prefix + "switch", Switch);
91
- app.component(prefix + "tab", Tab);
92
- app.component(prefix + "tabs", Tabs);
93
- app.component(prefix + "toast", Toast);
94
- app.component(prefix + "toast-content", ToastContent);
95
- app.component(prefix + "toolbar", Toolbar);
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
@@ -1,3 +1,4 @@
1
- export type CreateToifeOptions = {
1
+ export type SubscribeOptions = {
2
2
  prefix?: string;
3
+ name: string;
3
4
  };
@@ -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 getSeparator = () => {
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 getPrefix = () => {
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 withPrefix = (name: string | string[]) => {
28
- const p = getPrefix();
29
- const s = getSeparator();
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 property = (name: string | string[]) => {
49
- return `--${withPrefix(name)}`;
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 variable = (name: string | string[]) => {
56
- return `var(${property(name)})`;
55
+ export const cssVariable = (name: string | string[]) => {
56
+ return `var(${cssProperty(name)})`;
57
57
  };
@@ -1,6 +1,6 @@
1
1
  # Utils: `style`
2
2
 
3
- > Import: `import { utils } from "@toife/vue"` then `utils.withPrefix`, … (or import from `src/utils` in the monorepo).
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
- ### `getPrefix(): string`
13
+ ### `getCssPrefix(): string`
14
14
 
15
15
  Trimmed `--prefix` on `:root` (singleton cache).
16
16
 
17
- ### `getSeparator(): string`
17
+ ### `getCssSeparator(): string`
18
18
 
19
19
  Trimmed `--separator` (often `-` or `_`) used to join name parts.
20
20
 
21
- ### `withPrefix(name: string | string[]): string`
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
- ### `property(name: string | string[]): string`
25
+ ### `cssProperty(name: string | string[]): string`
26
26
 
27
- Returns the CSS custom property name **without** `var()`: `"--" + withPrefix(name)`.
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.withPrefix("button");
39
- utils.property(["field", "line"]);
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 `withPrefix` for BEM classes
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>