@thinkpixellab-public/px-vue 3.0.93 → 3.0.94

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,34 +1,28 @@
1
- <!--
2
- Describe this component...
3
- -->
4
1
  <template>
5
- <input
6
- v-if="!multiline"
7
- :class="bem(variantsMap)"
8
- v-on="$listeners"
9
- :value="value"
10
- @input="inputUpdate"
11
- @change="changeUpdate"
12
- />
13
- <textarea
14
- v-else
15
- :class="bem({ resize: showResize, ...variantsMap })"
16
- v-on="$listeners"
17
- :value="value"
18
- @input="inputUpdate"
19
- @change="changeUpdate"
20
- />
2
+ <div
3
+ :class="
4
+ bem({
5
+ placeholder: placeholderVisible,
6
+ multiline: multiline,
7
+ ...variantsMap,
8
+ })
9
+ "
10
+ ref="input"
11
+ @input="onInput"
12
+ @blur="onBlur"
13
+ @keydown="onKeydown"
14
+ :contenteditable="enabled ? 'plaintext-only' : false"
15
+ :placeholder="placeholder"
16
+ :disabled="disabled"
17
+ ></div>
21
18
  </template>
22
19
 
23
20
  <script>
24
21
  import PxBase from './PxBase.vue';
25
22
  import PxBaseVariants from './PxBaseVariants.vue';
23
+ import { getCaretPosition, setCaretPosition } from '../utils/caretPosition.js';
26
24
 
27
25
  export default {
28
- model: {
29
- prop: 'value',
30
- event: 'update:value',
31
- },
32
26
  name: 'px-textbox',
33
27
  mixins: [PxBase, PxBaseVariants],
34
28
  props: {
@@ -38,28 +32,117 @@ export default {
38
32
  // the value of the textbox
39
33
  value: { type: String, default: null },
40
34
 
41
- // whether to show the resize element in the corner of multiline textbox
42
- showResize: { type: Boolean, default: false },
43
-
44
35
  // whether to fire an update / v-model event on every character change ('input') or when the textbox loses focus ('change')
45
36
  updateMode: {
46
37
  type: String,
47
38
  default: 'input',
48
39
  validator: val => ['input', 'change'].includes(val),
49
40
  },
41
+
42
+ disabled: { type: Boolean, default: false },
43
+
44
+ placeholder: { type: String, default: '' },
45
+
46
+ lineBreakReplacement: { type: String, default: ' ' }, // the character to replace line breaks with when multiline is false
47
+ },
48
+ data() {
49
+ return {
50
+ minHeight: 300,
51
+ placeholderVisible: false,
52
+ };
53
+ },
54
+ computed: {
55
+ enabled() {
56
+ return !this.disabled;
57
+ },
50
58
  },
51
- computed: {},
59
+
60
+ watch: {
61
+ value(nv) {
62
+ this.setValue(nv);
63
+ },
64
+ },
65
+ mounted() {
66
+ this.setValue(this.value);
67
+ },
68
+
52
69
  methods: {
53
- inputUpdate(e) {
70
+ setValue(val) {
71
+ const nbsp = '\u202f';
72
+ const startPos = getCaretPosition(this.$refs.input);
73
+ const prevValue = this.$refs.input.textContent;
74
+
75
+ // if the first character is nbps, we remove it (assuming we had added it to begin with)
76
+ if (val.startsWith(nbsp)) {
77
+ val = val.slice(1);
78
+ }
79
+
80
+ // remove line breaks if we are not in multiline mode
81
+ if (!this.multiline) {
82
+ val = val.replace(/(\r\n|\n|\r)/gm, this.lineBreakReplacement);
83
+ }
84
+
85
+ // remove leading and trailing whitespace / new lines
86
+ const trimmed = val.trim().replace(/^\s*\n|\n\s*$/g, '');
87
+
88
+ // if the trimmed value is empty, we set it to a single non-breaking space (this eliminates weird UI behaviors when the textbox is empty)
89
+ if (trimmed.length == 0) {
90
+ this.placeholderVisible = true;
91
+ val = nbsp;
92
+ } else {
93
+ this.placeholderVisible = false;
94
+ }
95
+
96
+ // set the text content
97
+ this.$refs.input.textContent = val;
98
+
99
+ // raise an update event
100
+ this.$emit('update:value', val);
101
+
102
+ // update the caret position
103
+ const newPos = startPos + (val.length - prevValue.length);
104
+
105
+ if (newPos > 0) {
106
+ setCaretPosition(this.$refs.input, newPos);
107
+ }
108
+ },
109
+ onInput(e) {
110
+ const value = e.target.textContent;
54
111
  if (this.updateMode == 'input') {
55
- this.$emit('update:value', e.target.value);
112
+ this.setValue(value);
56
113
  }
114
+ this.$emit('input', value);
57
115
  },
58
- changeUpdate(e) {
116
+
117
+ onBlur(e) {
118
+ const value = e.target.textContent;
59
119
  if (this.updateMode == 'change') {
60
- this.$emit('update:value', e.target.value);
120
+ this.setValue(value);
121
+ }
122
+ this.$emit('change', value);
123
+ this.$emit('blue');
124
+ },
125
+ onKeydown(e) {
126
+ if (!this.multiline) {
127
+ if (e.key == 'Enter') {
128
+ e.preventDefault();
129
+ }
130
+ }
131
+ this.$emit('keydown', e);
132
+ },
133
+
134
+ focus(cursorPosition = 'end') {
135
+ if (this.$refs?.input) {
136
+ this.$refs.input.focus();
137
+
138
+ if (cursorPosition == 'end') {
139
+ setCaretPosition(this.$refs.input, this.$refs.input.textContent.length);
140
+ } else {
141
+ setCaretPosition(this.$refs.input, 0);
142
+ }
61
143
  }
62
144
  },
145
+
63
146
  selectAll() {
64
147
  this.$el.select();
65
148
  },
@@ -69,22 +152,39 @@ export default {
69
152
 
70
153
  <style lang="scss">
71
154
  @use '../styles/px.scss' as *;
155
+ @use '@thinkpixellab-public/px-styles/src/modules/controls' as *;
72
156
 
73
- // variant: default
157
+ .px-textbox {
158
+ @include control-reset();
74
159
 
75
- @include block(px-textbox, default) {
76
- @include textbox();
77
- }
160
+ overflow: hidden;
161
+ white-space: nowrap;
162
+ text-align: left;
163
+ display: inline-flex;
78
164
 
79
- textarea.px-textbox--default {
80
- padding-top: 0.5em;
81
- }
165
+ &--placeholder {
166
+ &:after {
167
+ content: attr(placeholder);
168
+ font-size: inherit;
169
+ font-family: inherit;
170
+ opacity: 0.5;
171
+ white-space: nowrap;
172
+ }
173
+ }
82
174
 
83
- textarea.px-textbox {
84
- resize: none;
85
- }
175
+ &--default {
176
+ @include textbox(
177
+ (
178
+ display: inline-flex,
179
+ text-align: left,
180
+ align-items: center,
181
+ )
182
+ );
183
+ }
86
184
 
87
- textarea.px-textbox--resize {
88
- resize: initial;
185
+ &--multiline {
186
+ white-space: normal;
187
+ overflow-y: auto;
188
+ }
89
189
  }
90
190
  </style>
@@ -10,6 +10,7 @@
10
10
 
11
11
  <script>
12
12
  import gsap from 'gsap';
13
+ import utils from '../utils/utils.js';
13
14
 
14
15
  export default {
15
16
  // components: { Component },
@@ -63,10 +64,6 @@ export default {
63
64
  type: String,
64
65
  default: null,
65
66
  },
66
- fixedLeave: {
67
- type: Boolean,
68
- default: false,
69
- },
70
67
  },
71
68
  // data() { return { b: 0 }; },f
72
69
  computed: {
@@ -185,28 +182,7 @@ export default {
185
182
  ...params.to,
186
183
  duration: this.hideDur,
187
184
  ease: this.hideEase,
188
- onStart: () => {
189
- if (this.fixedLeave && element) {
190
- const rect = element.getBoundingClientRect();
191
- element.style.width = `${rect.width}px`;
192
- element.style.height = `${rect.height}px`;
193
- element.style.top = `${rect.top}px`;
194
- element.style.left = `${rect.left}px`;
195
- element.style.position = 'fixed';
196
- }
197
- },
198
- onComplete: () => {
199
- if (this.fixedLeave && element) {
200
- if (element) {
201
- element.style.width = '';
202
- element.style.height = '';
203
- element.style.position = '';
204
- element.style.top = '';
205
- element.style.left = '';
206
- }
207
- }
208
- done();
209
- },
185
+ onComplete: done,
210
186
  });
211
187
  }
212
188
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkpixellab-public/px-vue",
3
- "version": "3.0.93",
3
+ "version": "3.0.94",
4
4
  "description": "General purpose Vue components and helpers that can be used across projects.",
5
5
  "author": "Pixel Lab",
6
6
  "license": "MIT",
package/utils/bem.js CHANGED
@@ -32,7 +32,10 @@ function hyphenate(str, shouldHyphenate = true) {
32
32
  // Get block name
33
33
  function getBlockName(component) {
34
34
  if (!component.$options._blockName) {
35
- let block = 'bemBlockName' in component && component.bemBlockName();
35
+ let block =
36
+ 'bemBlockName' in component && typeof component?.bemBlockName === 'function'
37
+ ? component.bemBlockName()
38
+ : null;
36
39
  block = block || component.$options.name || component.$options._componentTag;
37
40
  if (block && block.indexOf('/') > -1) {
38
41
  block = block.split('/').pop().split('.')[0];
@@ -43,7 +46,7 @@ function getBlockName(component) {
43
46
  return component.$options._blockName;
44
47
  }
45
48
 
46
- // Calcualte the bem classes for a given block, element and set of modifiers
49
+ // Calculate the bem classes for a given block, element and set of modifiers
47
50
  function getBemClasses(blockName, elementName, modifiers, options) {
48
51
  const block = hyphenate(blockName, options.hyphenate.block);
49
52
  const element = elementName
@@ -0,0 +1,75 @@
1
+ function createRange(node, endPos) {
2
+ let range = document.createRange();
3
+ range.selectNode(node);
4
+ range.setStart(node, 1);
5
+
6
+ let pos = 0;
7
+ const stack = [node];
8
+ while (stack.length > 0) {
9
+ const current = stack.pop();
10
+
11
+ if (current.nodeType === Node.TEXT_NODE) {
12
+ const len = current.textContent.length;
13
+ if (pos + len >= endPos) {
14
+ range.setEnd(current, endPos - pos);
15
+ return range;
16
+ }
17
+ pos += len;
18
+ } else if (current.childNodes && current.childNodes.length > 0) {
19
+ for (let i = current.childNodes.length - 1; i >= 0; i--) {
20
+ stack.push(current.childNodes[i]);
21
+ }
22
+ }
23
+ }
24
+
25
+ // The target position is greater than the
26
+ // length of the contenteditable element.
27
+ range.setEnd(node, node.childNodes.length);
28
+ return range;
29
+ }
30
+
31
+ function canProceed(element) {
32
+ if (
33
+ !element ||
34
+ !window ||
35
+ typeof window == undefined ||
36
+ !document ||
37
+ typeof document == undefined ||
38
+ !element.contains(document.activeElement)
39
+ ) {
40
+ return false;
41
+ }
42
+
43
+ return true;
44
+ }
45
+
46
+ function getCaretPosition(element) {
47
+ if (!canProceed(element)) {
48
+ return null;
49
+ }
50
+
51
+ const selection = window.getSelection();
52
+
53
+ if (selection.rangeCount === 0) {
54
+ return 0;
55
+ }
56
+
57
+ const range = selection.getRangeAt(0);
58
+ const clonedRange = range.cloneRange();
59
+ clonedRange.selectNodeContents(element);
60
+ clonedRange.setEnd(range.startContainer, range.startOffset);
61
+ return clonedRange.toString().length;
62
+ }
63
+
64
+ function setCaretPosition(element, position) {
65
+ if (!canProceed(element)) {
66
+ return;
67
+ }
68
+
69
+ const range = createRange(element, position);
70
+ const selection = window.getSelection();
71
+ selection.removeAllRanges();
72
+ selection.addRange(range);
73
+ }
74
+
75
+ export { getCaretPosition, setCaretPosition };