goodteditor-ui 1.0.91 → 1.0.93

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goodteditor-ui",
3
- "version": "1.0.91",
3
+ "version": "1.0.93",
4
4
  "main": "index.js",
5
5
  "homepage": "https://goodt-ui.netlify.app/",
6
6
  "scripts": {
@@ -1,6 +1,40 @@
1
+ Simple example
2
+
3
+ ```vue
4
+ <template>
5
+ <div class="pad-l5">
6
+ <label>
7
+ <input id="editable-text" class="checkbox" type="checkbox" v-model="options.readonly">
8
+ <span class="mar-left-3 v-mid">readonly</span>
9
+ </label>
10
+ <p>model: {{ model }}</p>
11
+ <ui-editable-text v-model="model" v-bind="options"></ui-editable-text>
12
+ </div>
13
+ </template>
14
+ <script>
15
+ import UiEditableText from './EditableText.vue';
16
+
17
+ export default {
18
+ components: { UiEditableText },
19
+ data: () => ({
20
+ model: 'hello world',
21
+ options: {
22
+ readonly: false
23
+ }
24
+ }),
25
+ };
26
+ </script>
27
+ ```
28
+
29
+ Advanced example. Edit on double-click, large size & custom validation
30
+
1
31
  ```vue
2
32
  <template>
3
33
  <div class="pad-l5">
34
+ <label>
35
+ <input id="editable-text-2" class="checkbox" type="checkbox" v-model="options.readonly">
36
+ <span class="mar-left-3 v-mid">readonly</span>
37
+ </label>
4
38
  <p>model: {{ model }}</p>
5
39
  <ui-editable-text v-model="model" v-bind="options"></ui-editable-text>
6
40
  </div>
@@ -13,7 +47,10 @@ export default {
13
47
  data: () => ({
14
48
  model: 'hello world',
15
49
  options: {
16
- size: 'large'
50
+ size: 'large',
51
+ validate: (value) => value !== '',
52
+ event: 'dblclick',
53
+ readonly: false
17
54
  }
18
55
  }),
19
56
  };
@@ -1,29 +1,32 @@
1
1
  <template>
2
- <div class="ui-editable-text" @click="onRootClick">
3
- <div class="ui-editable-text-label cursor-pointer text-truncate" :class="labelClass">
4
- <!--
5
- @slot
6
- @binding {string} value current value
7
- -->
8
- <slot v-bind="{ value }">{{ value }}</slot>
9
- </div>
10
- <input
11
- v-if="isEditable"
12
- :value="value"
13
- :class="inputClass"
14
- class="ui-editable-text-input input h-100"
15
- type="text"
16
- ref="input"
17
- @change="onValueChange" />
2
+ <div
3
+ class="ui-editable-text"
4
+ :contenteditable="isEditable ? 'plaintext-only' : null"
5
+ :class="cssClass"
6
+ tabindex="0"
7
+ @keydown.stop="onKeydown"
8
+ @keyup.stop="onKeyup"
9
+ @focus="onFocus"
10
+ @focusin.stop="onFocus"
11
+ @blur="onBlur"
12
+ @focusout.stop="onBlur"
13
+ @click.stop
14
+ v-on="{
15
+ [event]: onActivate
16
+ }">
17
+ <!--
18
+ @slot
19
+ @binding {string} value current value
20
+ -->
21
+ <slot v-bind="{ value }">{{ value }}</slot>
18
22
  </div>
19
23
  </template>
20
24
  <script>
21
- import { domEvent } from './utils/Helpers';
22
25
 
23
26
  const Size = {
24
- SMALL: { name: 'small', labelClass: ['text-small'], inputClass: ['input-small'] },
25
- NORMAL: { name: 'normal', labelClass: [], inputClass: [] },
26
- LARGE: { name: 'large', labelClass: ['text-large'], inputClass: ['input-large'] },
27
+ SMALL: { name: 'small', class: ['text-small'] },
28
+ NORMAL: { name: 'normal', class: [] },
29
+ LARGE: { name: 'large', class: ['text-large'] }
27
30
  };
28
31
 
29
32
  export default {
@@ -33,9 +36,16 @@ export default {
33
36
  * @example lorem ipsum sit dolor
34
37
  */
35
38
  value: {
36
- type: String,
39
+ type: [String, Number],
37
40
  default: ''
38
41
  },
42
+ /**
43
+ * supports v-sync
44
+ */
45
+ edit: {
46
+ type: Boolean,
47
+ default: false
48
+ },
39
49
  /**
40
50
  * @default normal
41
51
  * @values small,normal,large
@@ -47,89 +57,185 @@ export default {
47
57
  Object.values(Size)
48
58
  .map(({ name }) => name)
49
59
  .includes(val)
60
+ },
61
+ /**
62
+ * @default click
63
+ * @example click, dblclick
64
+ */
65
+ event: {
66
+ type: String,
67
+ default: 'click'
68
+ },
69
+ /**
70
+ * whether the text is readonly
71
+ */
72
+ readonly: {
73
+ type: Boolean,
74
+ default: false
75
+ },
76
+ /**
77
+ * validation callback
78
+ */
79
+ validate: {
80
+ type: Function,
81
+ default: () => true
50
82
  }
51
83
  },
52
84
  data: () => ({
53
- isEditable: false,
54
- onDocClickDisposable: null
85
+ isEdited: false,
86
+ valueLast: '',
87
+ valueDirty: ''
55
88
  }),
56
89
  computed: {
57
- labelClass() {
58
- const { isEditable, size } = this;
90
+ isEditable() {
91
+ return this.readonly === false && this.isEdited;
92
+ },
93
+ cssClass() {
94
+ const { isEdited, size, readonly } = this;
59
95
  const classes = [];
60
96
  const sizeDef = Object.values(Size).find(({ name }) => name === size);
61
97
 
62
- classes.push(...sizeDef.labelClass);
98
+ classes.push(...sizeDef.class);
99
+
100
+ if (readonly) {
101
+ classes.push('readonly');
102
+ }
63
103
 
64
- if (isEditable) {
65
- classes.push('invisible');
104
+ if (isEdited) {
105
+ classes.push('is-edited');
66
106
  }
67
107
 
68
108
  return classes;
69
109
  },
70
- inputClass() {
71
- const sizeDef = Object.values(Size).find(({ name }) => name === this.size);
72
- return sizeDef.inputClass;
110
+ isValid() {
111
+ return this.validate(this.valueDirty);
73
112
  }
74
113
  },
75
- beforeDestroy() {
76
- const { onDocClickDisposable } = this;
77
- if (onDocClickDisposable != null) {
78
- onDocClickDisposable();
114
+ watch: {
115
+ value: {
116
+ handler(value) {
117
+ this.valueLast = this.valueDirty = value;
118
+ },
119
+ immediate: true
120
+ },
121
+ edit(edit) {
122
+ if (edit) {
123
+ this.onActivate();
124
+ } else {
125
+ this.onBlur();
126
+ }
127
+ },
128
+ isValid(isValid) {
129
+ this.$emit('valid', isValid);
79
130
  }
80
131
  },
81
132
  methods: {
82
- onRootClick() {
83
- if (this.isEditable) {
133
+ /**
134
+ * @param {Event} event
135
+ */
136
+ onActivate(event) {
137
+ event?.stopImmediatePropagation();
138
+
139
+ if (this.isEdited || this.readonly) {
84
140
  return;
85
141
  }
86
- this.isEditable = true;
87
- this.$nextTick(() => {
88
- const { input } = this.$refs;
89
- input.select();
90
- setTimeout(() => {
91
- this.onDocClickDisposable = domEvent(document, 'click', this.onDocClick);
92
- });
93
- });
142
+ this.setIsEdited(true);
94
143
  },
95
- onValueChange({ target }) {
96
- const { value: valueNew } = target;
97
- if (this.value !== valueNew) {
98
- this.$emit('input', valueNew);
99
- this.$emit('change', valueNew);
144
+ /**
145
+ * @param {boolean} isEdited
146
+ */
147
+ setIsEdited(isEdited) {
148
+ this.isEdited = isEdited;
149
+ // autofocus
150
+ if (isEdited) {
151
+ this.$nextTick(() => {
152
+ const range = document.createRange();
153
+ range.selectNodeContents(this.$el);
154
+ const selection = window.getSelection();
155
+ selection.removeAllRanges();
156
+ selection.addRange(range);
157
+ this.$el.focus();
158
+ });
159
+ } else {
160
+ this.$el.blur();
100
161
  }
101
- this.isEditable = false;
102
- this.onDocClickDisposable();
162
+
163
+ this.$emit('update:edit', isEdited);
103
164
  },
104
- onDocClick({ target }) {
105
- if (this.$el.contains(target)) {
106
- return;
165
+ /**
166
+ * @param {string} value
167
+ */
168
+ setValue(value) {
169
+ this.$el.textContent = value;
170
+ this.valueLast = value;
171
+ this.$emit('input', value);
172
+ this.$emit('change', value);
173
+ },
174
+ commitValue() {
175
+ this.setValue(this.isValid ? this.valueDirty : this.valueLast);
176
+ this.setIsEdited(false);
177
+ },
178
+ onFocus() {
179
+ this.$emit('focus');
180
+ },
181
+ onBlur() {
182
+ this.$emit('blur');
183
+ this.commitValue();
184
+ },
185
+ /**
186
+ * @param {KeyboardEvent} event
187
+ */
188
+ onKeydown(event) {
189
+ if (event.code.toLocaleLowerCase() === 'enter') {
190
+ event.preventDefault();
191
+ this.setIsEdited(!this.isEdited);
107
192
  }
108
- this.isEditable = false;
109
- this.onDocClickDisposable();
193
+ },
194
+ /**
195
+ * @param {KeyboardEvent} event
196
+ */
197
+ onKeyup({ target }) {
198
+ this.valueDirty = target.textContent.trim();
110
199
  }
111
200
  }
112
201
  };
113
202
  </script>
114
- <style lang="less" scoped>
203
+ <style scoped>
115
204
  .ui-editable-text {
116
205
  position: relative;
117
206
  display: inline-flex;
207
+ min-width: 1em;
208
+ cursor: pointer;
209
+
210
+ &:focus {
211
+ outline: var(--focus-outline-width) solid var(--color-focus);
212
+ outline-offset: 1px;
213
+ border-radius: var(--border-radius);
214
+ }
215
+
216
+ &:empty {
217
+ border-bottom: 2px dotted var(--color-primary);
218
+ &:before {
219
+ content: ' '
220
+ }
221
+ }
222
+
223
+ &:hover:not(.readonly, :focus) {
224
+ color: var(--color-primary);
225
+ }
118
226
 
119
- &-label {
227
+ &.readonly {
228
+ cursor: default;
120
229
  &:hover {
121
- color: var(--color-primary);
230
+ color: initial;
231
+ }
232
+ &:focus {
233
+ outline: none;
122
234
  }
123
235
  }
124
- &-input {
125
- position: absolute;
126
- top: 0;
127
- left: -0.5rem;
128
- width: calc(100% + 1rem);
129
- border: none;
130
- padding-top: 0;
131
- padding-bottom: 0;
132
- min-height: auto;
236
+
237
+ &.is-edited {
238
+ white-space: pre-line;
133
239
  }
134
240
  }
135
241
  </style>
@@ -37,12 +37,12 @@
37
37
  </ui-badge>
38
38
  </slot>
39
39
  </template>
40
- <div class="ui-select-placeholder events-none" v-else>
40
+ <div class="ui-select-placeholder" v-else>
41
41
  <!--
42
42
  @slot Placeholder slot
43
43
  -->
44
44
  <slot name="placeholder">
45
- <input class="ui-select-placeholder-input input w-100" :placeholder="placeholder" />
45
+ <input class="ui-select-placeholder-input events-none input w-100" :placeholder="placeholder" />
46
46
  </slot>
47
47
  </div>
48
48
  </template>
@@ -65,12 +65,12 @@
65
65
  v-if="optionsSelected.length">
66
66
  {{ getOptionLabel(optionsSelected[0]) }}
67
67
  </slot>
68
- <div class="ui-select-placeholder events-none" v-else>
68
+ <div class="ui-select-placeholder" v-else>
69
69
  <!--
70
70
  @slot Placeholder slot
71
71
  -->
72
72
  <slot name="placeholder">
73
- <input class="ui-select-placeholder-input input w-100" :placeholder="placeholder" />
73
+ <input class="ui-select-placeholder-input events-none input w-100" :placeholder="placeholder" />
74
74
  </slot>
75
75
  </div>
76
76
  </template>
@@ -77,18 +77,6 @@ const generateGetBoundingClientRect = (x = 0, y = 0) => () => ({
77
77
  left: x,
78
78
  });
79
79
 
80
- /**
81
- * @param {EventTarget} target
82
- * @param {string} type
83
- * @param {EventListenerOrEventListenerObject} listener
84
- * @param {AddEventListenerOptions} options
85
- * @returns
86
- */
87
- const domEvent = (target, type, listener, options = undefined) => {
88
- target.addEventListener(type, listener, options);
89
- return () => target.removeEventListener(type, listener, options);
90
- };
91
-
92
80
  export {
93
81
  scrollIntoView,
94
82
  isDateValid,
@@ -97,7 +85,6 @@ export {
97
85
  Position,
98
86
  TriggerOn,
99
87
  debounce,
100
- domEvent,
101
88
  useIntersectionObserver,
102
89
  generateGetBoundingClientRect,
103
90
  };