Created
June 20, 2021 19:06
-
-
Save aryanshridhar/3afc7a82a72955626813d9f560dab725 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import $ from "jquery"; | |
import _ from "lodash"; | |
import render_dropdown_list from "../templates/settings/dropdown_list.hbs"; | |
import * as blueslip from "./blueslip"; | |
import * as ListWidget from "./list_widget"; | |
export function DropdownListWidget({ | |
widget_name, | |
data, | |
default_text, | |
render_text = (item_name) => item_name, | |
null_value = null, | |
include_current_item = true, | |
value, | |
on_update = () => {}, | |
}) { | |
// Initializing values | |
this.widget_name = widget_name; | |
this.data = data; | |
this.default_text = default_text; | |
this.render_text = render_text; | |
this.null_value = null_value; | |
this.include_current_item = include_current_item; | |
this.initial_value = value; | |
this.on_update = on_update; | |
this.container_id = `${widget_name}_widget`; | |
this.value_id = `id_${widget_name}`; | |
if (value === undefined) { | |
this.initial_value = null_value; | |
blueslip.warn("dropdown-list-widget: Called without a default value; using null value"); | |
} | |
// Setting up dropdown_list_widget | |
this.setup(); | |
} | |
DropdownListWidget.prototype.render_default_text = function (elem) { | |
elem.text(this.default_text); | |
elem.addClass("text-warning"); | |
elem.closest(".input-group").find(".dropdown_list_reset_button:enabled").hide(); | |
}; | |
DropdownListWidget.prototype.render = function (value) { | |
$(`#${CSS.escape(this.container_id)} #${CSS.escape(this.value_id)}`).data("value", value); | |
const elem = $(`#${CSS.escape(this.container_id)} #${CSS.escape(this.widget_name)}_name`); | |
if (!value || value === this.null_value) { | |
this.render_default_text(elem); | |
return; | |
} | |
// Happy path | |
const item = this.data.find((x) => x.value === value.toString()); | |
if (item === undefined) { | |
this.render_default_text(elem); | |
return; | |
} | |
const text = this.render_text(item.name); | |
elem.text(text); | |
elem.removeClass("text-warning"); | |
elem.closest(".input-group").find(".dropdown_list_reset_button:enabled").show(); | |
}; | |
DropdownListWidget.prototype.update = function (value) { | |
this.render(value); | |
this.on_update(value); | |
}; | |
DropdownListWidget.prototype.register_event_handlers = function () { | |
$(`#${CSS.escape(this.container_id)} .dropdown-list-body`).on( | |
"click keypress", | |
".list_item", | |
(e) => { | |
const setting_elem = $(this).closest(`.${CSS.escape(this.widget_name)}_setting`); | |
if (e.type === "keypress") { | |
if (e.key === "Enter") { | |
setting_elem.find(".dropdown-menu").dropdown("toggle"); | |
} else { | |
return; | |
} | |
} | |
const value = $(e.target.closest("li")).attr("data-value"); | |
this.update(value); | |
}, | |
); | |
$(`#${CSS.escape(this.container_id)} .dropdown_list_reset_button`).on("click", (e) => { | |
this.update(this.null_value); | |
e.preventDefault(); | |
}); | |
}; | |
DropdownListWidget.prototype.setup_dropdown_widget = function (data) { | |
const dropdown_list_body = $( | |
`#${CSS.escape(this.container_id)} .dropdown-list-body`, | |
).expectOne(); | |
const search_input = $(`#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`); | |
const get_data = () => { | |
if (this.include_current_item) { | |
return data; | |
} | |
return data.filter((x) => x.value !== this.value.toString()); | |
}; | |
ListWidget.create(dropdown_list_body, get_data(data), { | |
name: `${CSS.escape(this.widget_name)}_list`, | |
modifier(item) { | |
return render_dropdown_list({item}); | |
}, | |
filter: { | |
element: search_input, | |
predicate(item, value) { | |
return item.name.toLowerCase().includes(value); | |
}, | |
}, | |
simplebar_container: $(`#${CSS.escape(this.container_id)} .dropdown-list-wrapper`), | |
}); | |
}; | |
DropdownListWidget.prototype.setup = function () { | |
// populate the dropdown | |
const dropdown_list_body = $( | |
`#${CSS.escape(this.container_id)} .dropdown-list-body`, | |
).expectOne(); | |
const search_input = $(`#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`); | |
const dropdown_toggle = $(`#${CSS.escape(this.container_id)} .dropdown-toggle`); | |
this.setup_dropdown_widget(this.data); | |
$(`#${CSS.escape(this.container_id)} .dropdown-search`).on("click", (e) => { | |
e.stopPropagation(); | |
}); | |
dropdown_toggle.on("click", () => { | |
search_input.val("").trigger("input"); | |
}); | |
dropdown_toggle.on("focus", (e) => { | |
// On opening a Bootstrap Dropdown, the parent element receives focus. | |
// Here, we want our search input to have focus instead. | |
e.preventDefault(); | |
// This function gets called twice when focusing the | |
// dropdown, and only in the second call is the input | |
// field visible in the DOM; so the following visibility | |
// check ensures we wait for the second call to focus. | |
if (dropdown_list_body.is(":visible")) { | |
search_input.trigger("focus"); | |
} | |
}); | |
search_input.on("keydown", (e) => { | |
const {key, keyCode, which} = e; | |
const navigation_keys = ["ArrowUp", "ArrowDown", "Escape"]; | |
if (!navigation_keys.includes(key)) { | |
return; | |
} | |
e.preventDefault(); | |
e.stopPropagation(); | |
// We pass keyCode instead of key here because the outdated | |
// bootstrap library we have at static/third/ still uses the | |
// deprecated keyCode & which properties. | |
const custom_event = new $.Event("keydown.dropdown.data-api", {keyCode, which}); | |
dropdown_toggle.trigger(custom_event); | |
}); | |
this.render(this.initial_value); | |
this.register_event_handlers(); | |
}; | |
// Returns the updated value | |
DropdownListWidget.prototype.value = function () { | |
let val = $(`#${CSS.escape(this.container_id)} #${CSS.escape(this.value_id)}`).data("value"); | |
if (val === null) { | |
val = ""; | |
} | |
return val; | |
}; | |
export function MultiSelectDropdownListWidget({ | |
widget_name, | |
data, | |
default_text, | |
null_value = null, | |
on_update = () => {}, | |
value, | |
limit, | |
}) { | |
// A widget mostly similar to `DropdownListWidget` but | |
// used in cases of multiple dropdown selection. | |
// Initializing values specific to `MultiSelectDropdownListWidget`; | |
this.limit = limit; | |
// Populate the dropdown values selected by user. | |
// Important thing to note is that this needs to be maintained as | |
// a reference type array and not to deep clone it, so that it | |
// can be later referenced in `list_widget` as well. | |
this.data_selected = []; | |
DropdownListWidget.call(this, { | |
widget_name, | |
data, | |
default_text, | |
null_value, | |
on_update, | |
value, | |
}); | |
if (limit === undefined) { | |
this.limit = 2; | |
blueslip.warn( | |
"Multiselect dropdown-list-widget: Called without limit value; using 2 as the limit", | |
); | |
} | |
this.initialize_dropdown_values(); | |
} | |
MultiSelectDropdownListWidget.prototype = Object.create(DropdownListWidget.prototype); | |
MultiSelectDropdownListWidget.prototype.initialize_dropdown_values = function(){ | |
if(!this.initial_value){ | |
return; | |
} | |
const elem = $(`#${CSS.escape(this.container_id)} #${CSS.escape(this.widget_name)}_name`); | |
// Push values from initial valued array to `data_selected` | |
// without assigning it to a new array. | |
this.data_selected.push(...this.initial_value); | |
this.render_button_text(elem,this.limit); | |
} | |
// Set the button text as per the selected data. | |
MultiSelectDropdownListWidget.prototype.render_button_text = function (elem, limit) { | |
const items_selected = this.data_selected.length; | |
let text = ""; | |
if (items_selected === 0) { | |
this.render_default_text(elem); | |
return; | |
} else if (limit >= items_selected) { | |
const data_selected = this.data.filter((data) => | |
this.data_selected.includes(Number.parseInt(data.value, 10)), | |
); | |
text = data_selected.map((data) => data.name).toString(); | |
} else { | |
text = `${items_selected} selected`; | |
} | |
elem.text(text); | |
elem.removeClass("text-warning"); | |
elem.closest(".input-group").find(".dropdown_list_reset_button:enabled").show(); | |
}; | |
// Override the DrodownListWidget `render` function. | |
MultiSelectDropdownListWidget.prototype.render = function (value) { | |
const elem = $(`#${CSS.escape(this.container_id)} #${CSS.escape(this.widget_name)}_name`); | |
if (!value || value === this.null_value) { | |
this.render_default_text(elem); | |
return; | |
} | |
this.render_button_text(elem, this.limit); | |
}; | |
// Cases where a user presses any dropdown item but accidentally closes | |
// the dropdown list | |
MultiSelectDropdownListWidget.prototype.reset_dropdown_items = function(){ | |
const original_values = this.checked_items ? this.checked_items : this.initial_value; | |
const values_added = _.difference(this.data_selected, original_values); | |
// Removing all the items added from dropdown | |
for(const val of values_added){ | |
const index = this.data_selected.indexOf(val); | |
if (index > -1) { | |
this.data_selected.splice(index, 1); | |
} | |
} | |
// Items that are removed in dropdwon but should have been a part of it | |
const values_removed = _.difference(original_values, this.data_selected); | |
this.data_selected.push(...values_removed); | |
} | |
// Override the DrodownListWidget `setup_dropdown_widget` function. | |
MultiSelectDropdownListWidget.prototype.setup_dropdown_widget = function (data) { | |
const dropdown_list_body = $( | |
`#${CSS.escape(this.container_id)} .dropdown-list-body`, | |
).expectOne(); | |
const search_input = $(`#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`); | |
ListWidget.create(dropdown_list_body, data, { | |
name: `${CSS.escape(this.widget_name)}_list`, | |
modifier(item) { | |
return render_dropdown_list({item}); | |
}, | |
multiselect: { | |
selected_items: this.data_selected, | |
}, | |
filter: { | |
element: search_input, | |
predicate(item, value) { | |
return item.name.toLowerCase().includes(value); | |
}, | |
}, | |
simplebar_container: $(`#${CSS.escape(this.container_id)} .dropdown-list-wrapper`), | |
}); | |
}; | |
// Add the check mark to dropdown element passed. | |
MultiSelectDropdownListWidget.prototype.add_check_mark = function (element) { | |
const value = Number.parseInt(element.attr("data-value"), 10); | |
const link_elem = element.find("a").expectOne(); | |
link_elem.prepend('<i class="fa fa-check" aria-hidden="true"></i>'); | |
element.addClass("checked"); | |
this.data_selected.push(value); | |
}; | |
// Remove the check mark from dropdown element. | |
MultiSelectDropdownListWidget.prototype.remove_check_mark = function (element) { | |
const icon = element.find("i").expectOne(); | |
const value = Number.parseInt(element.attr("data-value"), 10); | |
const index = this.data_selected.indexOf(value); | |
if (index > -1) { | |
icon.remove(); | |
element.removeClass("checked"); | |
this.data_selected.splice(index, 1); | |
} | |
}; | |
// Override the `register_event_handlers` function. | |
MultiSelectDropdownListWidget.prototype.register_event_handlers = function () { | |
const dropdown_toggle = $(`#${CSS.escape(this.container_id)} .dropdown-toggle`); | |
const search_input = $(`#${CSS.escape(this.container_id)} .dropdown-search > input[type=text]`); | |
const dropdown_list_body = $( | |
`#${CSS.escape(this.container_id)} .dropdown-list-body`, | |
).expectOne(); | |
dropdown_toggle.on("click", () => { | |
this.reset_dropdown_items() | |
search_input.val("").trigger("input"); | |
}); | |
dropdown_list_body.on("click keypress", ".list_item", (e) => { | |
const element = $(e.target.closest("li")); | |
if (element.hasClass("checked")) { | |
this.remove_check_mark(element); | |
} else { | |
this.add_check_mark(element); | |
} | |
e.stopPropagation(); | |
}); | |
$(`#${CSS.escape(this.container_id)} .dropdown_list_reset_button`).on("click", (e) => { | |
// Default back the value. | |
this.checked_items = undefined; | |
this.update(this.null_value); | |
e.preventDefault(); | |
}); | |
$(`#${CSS.escape(this.container_id)} .multiselect_btn`).on("click", (e) => { | |
e.preventDefault(); | |
// We deep clone the values of `data_selected` to a new | |
// variable. This is so because, arrays are reference types | |
// and modifying the parent array can change the values | |
// within the child array. Here, `checked_items` copies over the | |
// value and not just the reference. | |
this.checked_items = _.cloneDeep(this.data_selected); | |
this.update(this.data_selected); | |
}); | |
}; | |
// Returns array of values selected by user | |
MultiSelectDropdownListWidget.prototype.value = function () { | |
let val = this.checked_items; | |
if (val === undefined) { | |
val = this.initial_value; | |
} | |
return val; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment