@cdmx/wappler_ac_selectv2
Version:
App Connect extension for Select2
330 lines (302 loc) • 11.6 kB
JavaScript
dmx.Component("select2", {
extends: "select",
initialData: {
selectedIndex: -1,
selectedValue: "",
selectedText: "",
selectedOptions: [],
valueSelected: false
},
attributes: {
options: { type: [Array, Object, Number], default: null },
optionText: { type: String, default: "$value" },
optionValue: { type: String, default: "$value" },
field_theme: { type: String, default: "bootstrap-5" },
field_width: { type: String, default: "resolve" },
field_placeholder: { type: String, default: "Select a Option." },
allow_clear: { type: Boolean, default: false },
enable_rtl: { type: Boolean, default: false },
enable_tags: { type: Boolean, default: false },
select_on_close: { type: Boolean, default: false },
close_on_select: { type: Boolean, default: true },
css_class: { type: String, default: "select2--large" },
selection_css: { type: String, default: null },
dropdown_css: { type: String, default: null },
container_css: { type: String, default: null },
multiple: { type: Boolean, default: false },
minimum_results_for_search: { type: Number, default: 0 }
},
methods: {
setSelectedIndex: function (t) {
(this.$node.selectedIndex = t),
this._updateValue()
},
updatedSelectedData: function () {
this._updateValue()
},
setValue: function (t, e) {
if (this.$node.multiple) {
dmx.nextTick(function () {
if (!t) return
const selectedValues = Array.isArray(t) ? t : t.split(',');
this.set('selectedOptions', selectedValues)
$("#" + this.$node.id).val(selectedValues).trigger('change');
this._updateValue();
}, this);
}
else {
dmx.nextTick(function () {
$("#" + this.$node.id).val(t).trigger('change');
this._updateValue();
}, this);
}
},
reset: function () {
this.set('selectedOptions', [])
$("#" + this.$node.id).val(null).trigger("select2:close");
}
},
events: {
selected: Event,
opened: Event,
closed: Event,
changed: Event
},
renderSelect: function () {
let dropdown_parent = null;
// Check if the parent of the element is a modal
if (!this.$node) return
if ($("#" + this.$node.id).closest(".modal").length > 0) {
dropdown_parent = $(this.$node).parent().parent();
}
const selectionClass = `${this.props.css_class} ${this.props.selection_css}`.trim();
const dropdownClass = `${this.props.css_class} ${this.props.dropdown_css}`.trim();
const containerClass = `${this.props.css_class} ${this.props.container_css}`.trim();
$("#" + this.$node.id).select2({
theme: this.props.field_theme,
width: this.props.field_width,
tags: this.props.enable_tags,
dir: (this.props.enable_rtl ? "rtl" : "ltr"),
allowClear: this.props.allow_clear,
selectOnClose: this.props.select_on_close,
closeOnSelect: this.props.close_on_select,
placeholder: this.props.field_placeholder,
dropdownParent: dropdown_parent,
selectionCssClass: selectionClass,
dropdownCssClass: dropdownClass,
containerCssClass: containerClass,
multiple: this.props.multiple,
minimumResultsForSearch: this.props.minimum_results_for_search
});
let tabPressed = false;
// Track if TAB was pressed
$(document).on('keydown', function (e) {
if (e.key === 'Tab') tabPressed = true;
});
$(document).on('mousedown', function () {
tabPressed = false;
});
// Only target your specific select
const $select = $(this.$node);
const $selectContainer = $select.next('.select2-container');
const $selection = $selectContainer.find('.select2-selection--single, .select2-selection--multiple');
$selection.on('focusin', function () {
if (!tabPressed) return;
// Make sure select2 instance exists and is not already open
if (!$select.length || !$select.data('select2') || $select.data('select2').isOpen()) {
return;
}
// Open dropdown on next tick to avoid recursive focus loop
setTimeout(() => {
$select.select2('open');
tabPressed = false; // Reset tabPressed after opening
}, 0);
});
// Reset tabPressed when selection happens
$select.on('select2:select', function () {
tabPressed = false;
});
// Ensure TAB works normally after closing dropdown
$select.on('select2:closing', function (e) {
const $selection = $(e.target).data('select2').$selection;
// Stop focusin from bubbling to prevent stealing TAB focus
$selection.one('focusin', function (ev) {
ev.stopPropagation();
});
});
(this.props.field_placeholder && this.props.value == "")
? $("#" + this.$node.id).val('').trigger('change') : null
},
init: function (e) {
if (!this.$node) return
this._options = [], this.props.value || (this.props.value = this.$node.value, this._updateValue()), this._mutationObserver = new MutationObserver((() => {
this._updatingOptions || this._updateValue()
})), this._mutationObserver.observe(this.$node, {
subtree: !0,
childList: !0,
attributes: !0
}), dmx.Component("form-element").prototype.init.call(this, e)
// Add event listener for browser back button
window.addEventListener('popstate', (event) => {
if (this.$node && this.$node.id) {
const select2Instance = $("#" + this.$node.id).data('select2');
if (select2Instance && select2Instance.isOpen()) {
$("#" + this.$node.id).select2('close');
}
}
});
//Below is select2 events
$(this.$node).on('select2:close', (e) => {
dmx.nextTick(function () {
this.dispatchEvent('closed');
}, this);
});
$(this.$node).on('select2:open', (e) => {
dmx.nextTick(function () {
this.dispatchEvent('opened');
}, this);
});
$(this.$node).on('select2:select', (e) => {
this._updateValue();
dmx.nextTick(function () {
this.dispatchEvent('selected');
}, this);
});
$(this.$node).on('select2:unselect', (e) => {
// Remove empty values from selection
this.initialData.selectedOptions = this.initialData.selectedOptions.filter(n => n != e.params.data.id);
this._updateValue();
});
// Auto-focus search input when dropdown opens for keyboard accessibility
$(this.$node).on('select2:open', () => {
// Use setTimeout to ensure the dropdown is fully rendered
setTimeout(() => {
const searchField = document.querySelector('.select2-container--open .select2-search__field');
if (searchField) {
searchField.focus();
}
}, 0);
});
dmx.nextTick(function () {
this.renderSelect();
if (!this.$node) return
if ($("#" + this.$node.id).closest(".modal").length > 0) {
let modalID = $("#" + this.$node.id).closest(".modal").attr("id");
$("#" + modalID).on("shown.bs.modal", () => {
if (this.$node.multiple) {
let selectedData = Array.from(this.$node.selectedOptions)
.filter(option => option.value !== "")
.map(option => option.value);
$("#" + this.$node.id).val(selectedData.length === 0 ? this.props.value : selectedData).trigger("change");
} else {
$("#" + this.$node.id).val(this.get("selectedValue")).trigger('change');
}
});
}
}, this);
this._updateValue();
this.$watch('selectedValue', value => {
if (value !== null && value !== "" && this.$node) {
$("#" + this.$node.id).val(value).trigger("change")
this.dispatchEvent('changed');
this.dispatchEvent('updated');
}
})
},
_renderOptions() {
this._options.forEach((e => e.remove())),
this._options = [],
this.props.options && (this._updatingOptions = !0,
dmx.repeatItems(this.props.options).forEach((e => {
const t = document.createElement("option");
t.value = dmx.parse(this.props.optionvalue, dmx.DataScope(e, this)),
t.textContent = dmx.parse(this.props.optiontext, dmx.DataScope(e, this)),
t.value == this.props.value && (t.selected = !0),
this.$node.append(t),
this._options.push(t)
})), this._updatingOptions = !1),
this._updateValue()
},
_setValue(e, t) {
if (this.$node.multiple) {
e = Array.isArray(e)
? e
: (e || "").split(",");
e = e.filter(n => n)
null == e && (e = ""), Array.isArray(e) || (e = [e]), e = e.map((e => e.toString())), Array.from(this.$node.options).forEach((s => {
const n = e.includes(s.value);
t ? (s.toggleAttribute("selected", n), s.defaultSelected = s.selected) : s.selected = n
})), this._updateValue(), dmx.nextTick((() => this.dispatchEvent("updated")))
} else {
if (null == e && (e = ""), e = e.toString(), t) Array.from(this.$node.options).forEach((t => {
t.toggleAttribute("selected", t.value == e), t.defaultSelected = t.selected
}));
else {
const t = Array.from(this.$node.options).findIndex((t => t.value == e));
this.$node.selectedIndex = t
}
this._updateValue(), dmx.nextTick((() => this.dispatchEvent("updated")))
}
},
_updateValue() {
if (this.$node.multiple) {
let selectedValues = $(this.$node).val();
// Normalize to array and filter empty values
this.set('selectedOptions', Array.isArray(selectedValues) ? selectedValues.filter(Boolean) : [selectedValues].filter(Boolean));
let e = this._getValue();
e = e.filter(n => n);
if (this.$node && this.$node.id) {
$("#" + this.$node.id).val(e).trigger("change");
}
} else {
const e = this.$node.selectedIndex,
t = this.$node.options[e] || {
value: "",
text: ""
};
this.set({
selectedIndex: e,
selectedValue: t.value,
selectedText: t.text,
value: t.value
})
}
},
_getValue() {
return Array.from(this.$node.selectedOptions).map((e => e.value))
},
performUpdate(e) {
if (this.$node.multiple) {
dmx.Component("select").prototype.performUpdate.call(this, e), e.has("value") && this._setValue(this.props.value, !0)
} else {
// Call the base performUpdate method
dmx.Component("form-element").prototype.performUpdate.call(this, e);
// Re-render options if relevant properties have changed
if (e.has("options") || e.has("optiontext") || e.has("optionvalue")) {
dmx.nextTick(this._renderOptions, this);
}
}
// Refresh select UI and update data
this.renderSelect();
this._updateValue();
},
_inputHandler(e) { },
_changeHandler(e) {
if (!this.$node) return
if (this.$node.dirty) {
this._validate();
}
if (this.$node.multiple) {
dmx.nextTick((() => {
if (!this.$node) return
this.data.selectedIndex === this.$node.selectedIndex && dmx.equal(this.data.value, this._getValue()) || (this._updateValue(), this.dispatchEvent("changed"), dmx.nextTick((() => this.dispatchEvent("updated"))))
}))
} else {
dmx.nextTick((() => {
if (!this.$node) return
this.data.selectedIndex !== this.$node.selectedIndex && (this._updateValue(), this.dispatchEvent("changed"), dmx.nextTick((() => this.dispatchEvent("updated"))))
}))
}
},
});
//Created and Maintained by Roney Dsilva v0.6.9