apostrophe
Version:
The Apostrophe Content Management System.
213 lines (207 loc) • 6.12 kB
JavaScript
import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin.js';
import AposFieldDirectionMixin from 'Modules/@apostrophecms/schema/mixins/AposFieldDirection.js';
export default {
name: 'AposInputString',
mixins: [ AposInputMixin, AposFieldDirectionMixin ],
emits: [ 'return' ],
props: {
rows: {
type: Number,
default: 5
}
},
data() {
return {
step: undefined,
wasPopulated: false
};
},
computed: {
tabindex() {
return this.field.disableFocus ? '-1' : '0';
},
type() {
if (this.field.type) {
if (this.field.type === 'float' || this.field.type === 'integer') {
return 'number';
}
if (this.field.type === 'string' && this.field.textarea) {
return 'textarea';
}
if (this.field.type === 'string' || this.field.type === 'slug') {
return 'text';
}
return this.field.type;
} else {
return 'text';
}
},
classes() {
return [
'apos-input',
`apos-input--${this.type}`,
this.directionClass,
this.modelValue?.duplicate && 'apos-input--error'
].filter(Boolean);
},
icon() {
if (this.error) {
return 'circle-medium-icon';
} else if (this.field.icon) {
return this.field.icon;
} else {
return null;
}
}
},
watch: {
followingValues: {
// We may be following multiple fields, like firstName and lastName,
// or none at all, depending
deep: true,
handler(newValue, oldValue) {
if (this.field.followingIgnore === true) {
return;
}
let ov = oldValue;
let nv = newValue;
if (Array.isArray(this.field.followingIgnore)) {
ov = Object.fromEntries(
Object.entries(oldValue).filter(([ key ]) => {
return !this.field.followingIgnore.includes(key);
})
);
nv = Object.fromEntries(
Object.entries(newValue).filter(([ key ]) => {
return !this.field.followingIgnore.includes(key);
})
);
}
// Follow the value of the other field(s), but only if our
// previous value matched the previous value of the other field(s)
oldValue = Object.values(ov).join(' ').trim();
newValue = Object.values(nv).join(' ').trim();
if (
(!this.wasPopulated &&
((this.next == null) || (!this.next.length))) || (this.next === oldValue)
) {
this.next = newValue;
}
}
},
next() {
if (this.next && this.next.length) {
this.wasPopulated = true;
}
}
},
mounted() {
this.defineStep();
this.wasPopulated = this.next && this.next.length;
},
methods: {
enterEmit() {
if (this.field.enterSubmittable) {
// Include the validated results in cases where an Enter keydown should
// act as submitting a form.
this.$emit('return', {
data: this.next,
error: this.validate(this.next)
});
} else {
this.$emit('return');
}
},
validate(value = '') {
const isString = typeof value === 'string';
if (isString && !value.length) {
// Also correct for float and integer because Vue coerces
// number fields to either a number or the empty string
return this.field.required ? 'required' : false;
}
const minMaxFields = [
'integer',
'float',
'string',
'date',
'password'
];
if (typeof value === 'string' && this.field.pattern) {
const regex = new RegExp(this.field.pattern);
if (!regex.test(value)) {
return 'invalid';
}
}
if (this.field.min && minMaxFields.includes(this.field.type)) {
if (
(!isString || value.length) &&
(this.minMaxComparable(value) < this.field.min)
) {
return 'min';
}
}
if (this.field.max && minMaxFields.includes(this.field.type)) {
if (
(!isString || value.length) &&
(this.minMaxComparable(value) > this.field.max)
) {
return 'max';
}
}
if (this.field.type === 'email' && value) {
// regex source: https://emailregex.com/
const matches = value.match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/);
if (!matches) {
return 'invalid';
}
}
return false;
},
defineStep() {
if (this.type === 'number') {
this.step = this.field.type === 'float' ? 'any' : 1;
}
},
convert(s) {
if (this.field.type === 'integer') {
if ((s == null) || (s === '')) {
return s;
} else {
return parseInt(s);
}
} else if (this.field.type === 'float') {
if ((s == null) || (s === '')) {
return s;
} else {
// The native parse float converts 3.0 to 3 and makes
// next to become integer. In theory we don't need parseFloat
// as the value is natively guarded by the browser 'number' type.
// However we need a float value sent to the backend
// and we force that when focus is lost.
if (this.focus && `${s}`.match(/\.[0]*$/)) {
return s;
}
return parseFloat(s);
}
} else {
if (s == null) {
return '';
} else {
return s.toString();
}
}
},
minMaxComparable(s) {
const converted = this.convert(s);
if (
[ 'integer', 'float', 'date', 'range', 'time' ].includes(this.field.type)
) {
// Compare the actual values for these types
return converted;
} else {
// Compare the length for other types, like string or password or url
return converted.length;
}
}
}
};