apostrophe
Version:
The Apostrophe Content Management System.
344 lines (323 loc) • 9.62 kB
JavaScript
import { klona } from 'klona';
import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
import newInstance from 'apostrophe/modules/@apostrophecms/schema/lib/newInstance.js';
import { getPostprocessedRelationship } from 'Modules/@apostrophecms/piece-type/lib/postprocessRelationships.js';
import {
computePosition, shift, flip
} from '@floating-ui/dom';
export default {
name: 'AposInputRelationship',
mixins: [ AposInputMixin ],
emits: [ 'input' ],
data () {
const next = (this.modelValue && Array.isArray(this.modelValue.data))
? klona(this.modelValue.data)
: (klona(this.field.def) || []);
// Remember relationship subfield values even if a document
// is temporarily deselected, easing the user's pain if they
// inadvertently deselect something for a moment
const subfields = Object.fromEntries(
(next || []).filter(doc => doc._fields)
.map(doc => [ doc._id, doc._fields ])
);
const suggestionFields = this.field.suggestionFields ||
apos.modules[this.field.withType]?.relationshipSuggestionFields;
return {
searchTerm: '',
searchList: [],
searchFocusIndex: null,
searchHint: null,
searchSuggestion: null,
suggestionFields,
next,
subfields,
disabled: false,
searching: false,
searchRequestId: 0,
choosing: false,
relationshipSchema: null
};
},
computed: {
limitReached() {
return this.field.max === this.next.length;
},
pluralLabel() {
return apos.modules[this.field.withType].pluralLabel;
},
// TODO get 'Search' server for better i18n
placeholder() {
return this.field.placeholder || {
key: 'apostrophe:searchDocType',
type: this.$t(this.pluralLabel)
};
},
// TODO get 'Browse' for better i18n
browseLabel() {
return this.modifiers.some(m => [ 'small', 'micro' ].includes(m))
? {
key: 'apostrophe:browse'
}
: {
key: 'apostrophe:browseDocType',
type: this.$t(this.pluralLabel)
};
},
suggestion() {
return {
disabled: true,
tooltip: false,
icon: false,
classes: [ 'suggestion' ],
title: this.$t(this.field.suggestionLabel),
help: this.$t({
key: this.field.suggestionHelp || 'apostrophe:relationshipSuggestionHelp',
type: this.$t(this.pluralLabel)
}),
customFields: [ 'help' ]
};
},
hint() {
return {
disabled: true,
tooltip: false,
icon: 'binoculars-icon',
iconSize: 35,
classes: [ 'hint' ],
title: this.$t('apostrophe:relationshipSuggestionNoResults'),
help: this.$t({
key: this.field.browse
? 'apostrophe:relationshipSuggestionSearchAndBrowse'
: 'apostrophe:relationshipSuggestionSearch',
type: this.$t(this.pluralLabel)
}),
customFields: [ 'help' ]
};
},
chooserComponent() {
return apos.modules[this.field.withType].components.managerModal;
},
disableUnpublished() {
return apos.modules[this.field.withType].localized;
},
buttonModifiers() {
const modifiers = [ 'small' ];
if (this.modifiers.includes('no-search')) {
modifiers.push('block');
}
return modifiers;
},
minSize() {
const [ widgetOptions = {} ] = apos.area.widgetOptions;
return widgetOptions.minSize || [];
},
duplicate() {
return this.modelValue?.duplicate ? 'apos-input--error' : null;
},
widgetOptions() {
return apos.area.widgetOptions[0];
}
},
watch: {
searchList(after, before) {
if (!before.length && after.length) {
this.setDropdownPosition();
}
},
next(after, before) {
for (const doc of before) {
this.subfields[doc._id] = doc._fields;
}
for (const doc of after) {
if (Object.keys(doc._fields || {}).length) {
continue;
}
doc._fields = this.field.schema && (this.subfields[doc._id]
? this.subfields[doc._id]
: this.getDefault());
}
}
},
mounted () {
this.checkLimit();
},
methods: {
validate(value) {
this.checkLimit();
if (this.field.required && !value.length) {
return { message: 'required' };
}
if (this.field.min && this.field.min > value.length) {
return { message: `minimum of ${this.field.min} required` };
}
return false;
},
checkLimit() {
if (this.limitReached) {
this.searchTerm = 'Limit reached!';
} else if (this.searchTerm === 'Limit reached!') {
this.searchTerm = '';
}
this.disabled = !!this.limitReached;
},
async updateSelected(items) {
this.next = await getPostprocessedRelationship(
items,
this.field,
this.widgetOptions
);
},
async search(qs) {
const requestId = ++this.searchRequestId;
const action = apos.modules[this.field.withType].action;
const isPage = apos.modules['@apostrophecms/page'].validPageTypes
.includes(this.field.withType);
if (this.field.suggestionLimit) {
qs.perPage = this.field.suggestionLimit;
}
if (this.field.suggestionSort) {
qs.sort = this.field.suggestionSort;
}
if (this.field.withType === '@apostrophecms/image') {
apos.bus.$emit('piece-relationship-query', qs);
}
if (isPage) {
qs.type = this.field.withType;
}
this.searching = true;
const list = await apos.http.get(action, {
busy: false,
draft: true,
qs
});
if (requestId !== this.searchRequestId) {
return;
}
const removeSelectedItem = item => !this.next.map(i => i._id).includes(item._id);
const formatItems = item => ({
...item,
disabled: this.disableUnpublished && !item.lastPublishedAt
});
const results = (list.results || [])
.filter(removeSelectedItem)
.map(formatItems);
this.searchSuggestion = !qs.autocomplete ? this.suggestion : null;
this.searchHint = (!qs.autocomplete || !results.length) ? this.hint : null;
this.searchList = [ ...results ].filter(Boolean);
this.searching = false;
},
async input () {
const trimmed = this.searchTerm.trim();
const qs = trimmed.length
? {
autocomplete: trimmed
}
: {};
await this.search(qs);
if (this.searchList.length) {
// psuedo focus first element
this.searchFocusIndex = 0;
}
},
handleFocusOut() {
// hide search list when click outside the input
// timeout to execute "@select" method before
setTimeout(() => {
this.searchList = [];
}, 300);
this.searchFocusIndex = null;
},
handleKeydown(event) {
switch (event.key) {
case 'ArrowDown':
if (this.searchFocusIndex + 1 < this.searchList.length) {
this.searchFocusIndex++;
return;
}
if (!this.searchList.length) {
this.input();
}
break;
case 'ArrowUp':
if (this.searchFocusIndex - 1 >= 0) {
return this.searchFocusIndex--;
}
if (!this.searchList.length) {
this.input();
}
break;
case 'Enter':
if (this.searchFocusIndex !== null && this.searchList[this.searchFocusIndex]) {
this.updateSelected([ ...this.next, this.searchList[this.searchFocusIndex] ]);
this.handleFocusOut();
this.input();
}
break;
case 'Escape':
event.stopPropagation();
this.handleFocusOut();
break;
}
},
watchValue () {
this.error = this.modelValue.error;
// Ensure the internal state is an array.
this.next = Array.isArray(this.modelValue.data) ? this.modelValue.data : [];
},
async choose () {
const result = await apos.modal.execute(this.chooserComponent, {
title: this.field.label || this.field.name,
moduleName: this.field.withType,
chosen: this.next,
relationshipField: this.field
});
if (result) {
this.updateSelected(result);
}
},
getDefault() {
return newInstance(this.field.schema);
},
async editRelationship (item) {
const editor = this.field.editor || 'AposRelationshipEditor';
const result = await apos.modal.execute(editor, {
schema: this.field.schema,
item,
title: item.title,
'model-value': item._fields
});
if (!result) {
return;
}
const updatedItems = this.next.map((rel) => {
return rel._id === item._id
? {
...item,
_fields: result
}
: rel;
});
this.next = await getPostprocessedRelationship(
updatedItems,
this.field,
this.widgetOptions
);
},
setDropdownPosition() {
computePosition(
this.$refs.input,
this.$refs.floatingList,
{
placement: 'bottom-start',
middleware: [ flip(), shift() ],
strategy: 'fixed'
}
).then(({ x, y }) => {
Object.assign(this.$refs.floatingList.style, {
left: `${x}px`,
top: `${y}px`
});
});
}
}
};