js-kompo-utils 1.0.33 → 1.0.35

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.
@@ -56,6 +56,7 @@
56
56
  <input
57
57
  ref="phoneInput"
58
58
  class="phone-input"
59
+ :class="{ 'has-extension': showExtension }"
59
60
  :value="formattedValue"
60
61
  v-bind="$_attributes"
61
62
  v-on="$_events"
@@ -64,6 +65,44 @@
64
65
  @focus="onPhoneFocus"
65
66
  @blur="onPhoneBlur"
66
67
  />
68
+
69
+ <!-- Extension ("poste"), opt-in via ->withExtension(). Collapsed until asked for,
70
+ so the common case stays a single field. -->
71
+ <button
72
+ v-if="showExtension && !extensionOpen"
73
+ type="button"
74
+ class="phone-extension-toggle"
75
+ :title="extensionLabel"
76
+ :disabled="$_attributes.disabled"
77
+ @click="openExtension"
78
+ >
79
+ <span class="phone-extension-plus">+</span>
80
+ <span class="phone-extension-toggle-label">{{ extensionLabel }}</span>
81
+ </button>
82
+
83
+ <div v-else-if="showExtension" class="phone-extension-wrap">
84
+ <span class="phone-extension-prefix">{{ extensionLabel }}</span>
85
+ <input
86
+ ref="extensionInput"
87
+ class="phone-extension-input"
88
+ type="text"
89
+ inputmode="numeric"
90
+ autocomplete="off"
91
+ :value="extensionValue"
92
+ :placeholder="extensionPlaceholder"
93
+ :disabled="$_attributes.disabled"
94
+ :readonly="$_attributes.readonly"
95
+ @input="onExtensionInput"
96
+ @keydown.esc="closeExtension"
97
+ />
98
+ <button
99
+ type="button"
100
+ class="phone-extension-clear"
101
+ :title="extensionClearLabel"
102
+ :disabled="$_attributes.disabled"
103
+ @click="closeExtension"
104
+ >×</button>
105
+ </div>
67
106
  </div>
68
107
  </vl-form-field>
69
108
  </template>
@@ -96,6 +135,11 @@ export default {
96
135
 
97
136
  // Geolocation state
98
137
  isDetectingCountry: false,
138
+
139
+ // Optional extension ("poste"), submitted as `<name>_ext`. Starts open only
140
+ // when there is one to show, so an empty field stays a single box.
141
+ extensionValue: this.normalizeExtension(this.$_config('extensionValue')),
142
+ extensionOpen: this.normalizeExtension(this.$_config('extensionValue')) !== '',
99
143
  }
100
144
  },
101
145
 
@@ -114,6 +158,22 @@ export default {
114
158
  placeholder() {
115
159
  return this.$_config('placeholder') || 'Phone number'
116
160
  },
161
+
162
+ showExtension() {
163
+ return !!this.$_config('withExtension')
164
+ },
165
+
166
+ extensionLabel() {
167
+ return this.$_config('extensionLabel') || 'ext'
168
+ },
169
+
170
+ extensionPlaceholder() {
171
+ return this.$_config('extensionPlaceholder') || '000'
172
+ },
173
+
174
+ extensionClearLabel() {
175
+ return this.$_config('extensionClearLabel') || 'Remove'
176
+ },
117
177
 
118
178
  inputGroupClass() {
119
179
  const baseClass = 'vlInputGroup rounded-lg flex items-stretch'
@@ -212,6 +272,13 @@ export default {
212
272
  }
213
273
  },
214
274
 
275
+ // Kompo swaps `component` in place on a refresh without re-running data(), which is why
276
+ // the number re-syncs above. The extension has to follow or it keeps the stale prefill.
277
+ 'component.config.extensionValue'(newValue) {
278
+ this.extensionValue = this.normalizeExtension(newValue)
279
+ this.extensionOpen = this.extensionOpen || this.extensionValue !== ''
280
+ },
281
+
215
282
  selectedCountryCode(newCode, oldCode) {
216
283
  if (newCode !== oldCode && this.phoneValue) {
217
284
  this.reformatWithCountry(newCode)
@@ -239,6 +306,42 @@ export default {
239
306
  },
240
307
 
241
308
  methods: {
309
+ // The extension is not part of the number, so it rides alongside it as its own
310
+ // key instead of being folded into the value the way E.164 cannot carry it.
311
+ $_fill(jsonFormData) {
312
+ jsonFormData[this.$_name] = (this.$_value === null || this.$_value === undefined) ? '' : this.$_value
313
+
314
+ if (this.showExtension) {
315
+ jsonFormData[this.$_name + '_ext'] = this.extensionValue || ''
316
+ }
317
+ },
318
+
319
+ // `|| ''` would swallow a "0" extension, and the column is a string.
320
+ normalizeExtension(value) {
321
+ return (value === null || value === undefined) ? '' : String(value)
322
+ },
323
+
324
+ onExtensionInput(event) {
325
+ this.extensionValue = event.target.value.replace(/[^0-9#*]/g, '')
326
+
327
+ if (event.target.value !== this.extensionValue) {
328
+ event.target.value = this.extensionValue
329
+ }
330
+ },
331
+
332
+ openExtension() {
333
+ this.extensionOpen = true
334
+
335
+ this.$nextTick(() => this.$refs.extensionInput?.focus())
336
+ },
337
+
338
+ // Collapsing clears the value: the empty companion still posts, so the stored
339
+ // extension is removed rather than silently kept.
340
+ closeExtension() {
341
+ this.extensionValue = ''
342
+ this.extensionOpen = false
343
+ },
344
+
242
345
  // Country selection methods
243
346
  toggleDropdown() {
244
347
  this.isDropdownOpen = !this.isDropdownOpen
@@ -922,4 +1025,100 @@ export default {
922
1025
  color: #6b7280;
923
1026
  cursor: not-allowed;
924
1027
  }
1028
+
1029
+ .phone-input.has-extension {
1030
+ border-radius: 0;
1031
+ }
1032
+
1033
+ /* Collapsed state: a quiet pill closing the group's right edge. */
1034
+ .phone-extension-toggle {
1035
+ display: flex;
1036
+ align-items: center;
1037
+ gap: 0.2rem;
1038
+ flex: none;
1039
+ padding: 0 0.7rem;
1040
+ margin-left: -1px;
1041
+ border: 1px solid #d1d5db;
1042
+ border-radius: 0 0.5rem 0.5rem 0;
1043
+ background-color: transparent;
1044
+ color: #6b7280;
1045
+ font-size: 0.8125rem;
1046
+ line-height: 1;
1047
+ white-space: nowrap;
1048
+ cursor: pointer;
1049
+ transition: color 0.15s ease, background-color 0.15s ease;
1050
+ }
1051
+
1052
+ .phone-extension-toggle:hover:not(:disabled) {
1053
+ color: #3b82f6;
1054
+ background-color: #f3f4f6;
1055
+ }
1056
+
1057
+ .phone-extension-toggle:disabled {
1058
+ cursor: not-allowed;
1059
+ opacity: 0.6;
1060
+ }
1061
+
1062
+ .phone-extension-plus {
1063
+ font-size: 0.95rem;
1064
+ line-height: 1;
1065
+ }
1066
+
1067
+ /* Expanded state: label + field + dismiss, sharing one bordered cell. */
1068
+ .phone-extension-wrap {
1069
+ display: flex;
1070
+ align-items: center;
1071
+ flex: none;
1072
+ margin-left: -1px;
1073
+ padding-left: 0.6rem;
1074
+ border: 1px solid #d1d5db;
1075
+ border-radius: 0 0.5rem 0.5rem 0;
1076
+ }
1077
+
1078
+ .phone-extension-wrap:focus-within {
1079
+ border-color: #3b82f6;
1080
+ box-shadow: 0 0 0 1px #3b82f6;
1081
+ z-index: 1;
1082
+ }
1083
+
1084
+ .phone-extension-prefix {
1085
+ color: #9ca3af;
1086
+ font-size: 0.8125rem;
1087
+ white-space: nowrap;
1088
+ pointer-events: none;
1089
+ }
1090
+
1091
+ .phone-extension-input {
1092
+ width: 3.75rem;
1093
+ min-width: 0;
1094
+ padding: 0.5rem 0.25rem;
1095
+ border: none;
1096
+ outline: none;
1097
+ background-color: transparent;
1098
+ font-size: 1rem;
1099
+ }
1100
+
1101
+ .phone-extension-input:disabled {
1102
+ color: #6b7280;
1103
+ cursor: not-allowed;
1104
+ }
1105
+
1106
+ .phone-extension-clear {
1107
+ padding: 0 0.55rem 0 0.15rem;
1108
+ border: none;
1109
+ background-color: transparent;
1110
+ color: #9ca3af;
1111
+ font-size: 1.05rem;
1112
+ line-height: 1;
1113
+ cursor: pointer;
1114
+ }
1115
+
1116
+ .phone-extension-clear:hover:not(:disabled) {
1117
+ color: #ef4444;
1118
+ }
1119
+
1120
+ .phone-extension-clear:disabled {
1121
+ cursor: not-allowed;
1122
+ opacity: 0.6;
1123
+ }
925
1124
  </style>
@@ -15,6 +15,7 @@
15
15
  @touchend="stopDrawing"
16
16
  ></canvas>
17
17
  <button
18
+ class="signature-pad-button"
18
19
  type="button"
19
20
  :class="clearButtonClass"
20
21
  @click="clearSignature"
@@ -54,17 +55,14 @@ export default {
54
55
  const canvas = this.$refs.canvas
55
56
  this.context = canvas.getContext('2d')
56
57
 
57
- // Fill background
58
58
  this.context.fillStyle = this.backgroundColor
59
59
  this.context.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
60
60
 
61
- // Set drawing style
62
61
  this.context.strokeStyle = this.penColor
63
62
  this.context.lineWidth = this.penWidth
64
63
  this.context.lineCap = 'round'
65
64
  this.context.lineJoin = 'round'
66
65
 
67
- // Load existing signature if any
68
66
  if (this.component.value) {
69
67
  this.loadSignature(this.component.value)
70
68
  }
@@ -74,7 +72,8 @@ export default {
74
72
  const canvas = this.$refs.canvas
75
73
  const rect = canvas.getBoundingClientRect()
76
74
 
77
- // Calculer le ratio entre la taille affichée et la taille réelle du canvas
75
+ // CSS stretches the canvas past its attribute size, so pointer coordinates
76
+ // have to be scaled back into canvas space or the ink lands off-cursor.
78
77
  const scaleX = canvas.width / rect.width
79
78
  const scaleY = canvas.height / rect.height
80
79
 
@@ -94,9 +93,19 @@ export default {
94
93
  startDrawing(event) {
95
94
  event.preventDefault()
96
95
  this.isDrawing = true
96
+
97
97
  const coords = this.getCoordinates(event)
98
98
  this.lastX = coords.x
99
99
  this.lastY = coords.y
100
+
101
+ // A click with no movement is a dot, and a signature is full of them.
102
+ this.context.beginPath()
103
+ this.context.arc(coords.x, coords.y, this.penWidth / 2, 0, Math.PI * 2)
104
+ this.context.fillStyle = this.penColor
105
+ this.context.fill()
106
+ this.context.fillStyle = this.backgroundColor
107
+
108
+ this.isEmpty = false
100
109
  },
101
110
 
102
111
  draw(event) {
@@ -113,12 +122,13 @@ export default {
113
122
  this.lastX = coords.x
114
123
  this.lastY = coords.y
115
124
  this.isEmpty = false
116
-
117
- this.saveSignature()
118
125
  },
119
126
 
120
127
  stopDrawing() {
128
+ if (!this.isDrawing) return
129
+
121
130
  this.isDrawing = false
131
+ this.saveSignature()
122
132
  },
123
133
 
124
134
  clearSignature() {
@@ -130,11 +140,11 @@ export default {
130
140
  },
131
141
 
132
142
  saveSignature() {
133
- if (!this.isEmpty) {
134
- const dataURL = this.$refs.canvas.toDataURL('image/png')
135
- this.component.value = dataURL
136
- this.$emit('input', dataURL)
137
- }
143
+ if (this.isEmpty) return
144
+
145
+ const dataURL = this.$refs.canvas.toDataURL('image/png')
146
+ this.component.value = dataURL
147
+ this.$emit('input', dataURL)
138
148
  },
139
149
 
140
150
  loadSignature(dataURL) {
@@ -154,20 +164,29 @@ export default {
154
164
 
155
165
  <style scoped>
156
166
  .signature-pad-container {
167
+ position: relative;
157
168
  display: flex;
158
169
  flex-direction: column;
159
- gap: 0.75rem;
170
+ gap: 0.5rem;
171
+ border: 1px solid #d1d5db;
172
+ border-radius: 0.5rem;
173
+ padding: 2px;
174
+ }
175
+
176
+ .signature-pad-container:hover {
177
+ border-color: #9ca3af;
160
178
  }
161
179
 
162
180
  .signature-canvas {
163
- border: 1px solid #d1d5db;
164
- border-radius: 0.5rem;
165
181
  cursor: crosshair;
166
182
  touch-action: none;
167
183
  display: block;
184
+ width: 100%;
168
185
  }
169
186
 
170
- .signature-canvas:hover {
171
- border-color: #9ca3af;
187
+ .signature-pad-button {
188
+ position: absolute;
189
+ bottom: 0.5rem;
190
+ right: 0.5rem;
172
191
  }
173
- </style>
192
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-kompo-utils",
3
- "version": "1.0.33",
3
+ "version": "1.0.35",
4
4
  "description": "",
5
5
  "main": "app.js",
6
6
  "scripts": {