4 dojo.require("fieldmapper.OrgUtils");
5 dojo.require("openils.PermaCrud");
6 dojo.require("openils.User");
7 dojo.require("openils.widget.OrgUnitFilteringSelect");
8 dojo.require("dojo.data.ItemFileReadStore");
9 dojo.require("dijit.form.DateTextBox");
10 dojo.require("dijit.form.TimeTextBox");
11 dojo.require("dojo.date.stamp");
12 dojo.requireLocalization("openils.booking", "reservation");
15 * Globals; prototypes and their instances
17 var localeStrings = dojo.i18n.getLocalization("openils.booking", "reservation");
18 var pcrud = new openils.PermaCrud();
21 var pickup_lib_selected;
25 var just_reserved_now = {};
28 function AttrValueTable() { this.t = {}; }
29 AttrValueTable.prototype.set = function(attr, value) { this.t[attr] = value; };
30 AttrValueTable.prototype.update_from_selector = function(selector) {
31 var attr = selector.name.match(/_(\d+)$/)[1];
32 var value = selector.options[selector.selectedIndex].value;
34 attr_value_table.set(attr, value);
36 AttrValueTable.prototype.get_all_values = function() {
38 for (var k in this.t) {
39 if (this.t[k] != undefined && this.t[k] != "")
40 values.push(this.t[k]);
44 var attr_value_table = new AttrValueTable();
46 function TimestampRange() {
47 this.start = new Date();
48 this.end = new Date();
50 this.validity = {"start": false, "end": false};
52 "start": {"date": undefined, "time": undefined},
53 "end": {"date": undefined, "time": undefined}
55 this.saved_style_properties = {};
56 this.invalid_style_properties = {
57 "backgroundColor": "#ffcccc",
59 "borderColor": "#990000",
63 TimestampRange.prototype.get_timestamp = function(when) {
64 return dojo.date.stamp.toISOString(this[when]).
65 replace("T", " ").substr(0, 19);
67 TimestampRange.prototype.get_range = function() {
68 return this.is_backwards() ?
69 [this.get_timestamp("end"), this.get_timestamp("start")] :
70 [this.get_timestamp("start"), this.get_timestamp("end")];
72 TimestampRange.prototype.update_from_widget = function(widget) {
73 var when = widget.name.match(/(start|end)/)[1];
74 var which = widget.name.match(/(date|time)/)[1];
76 if (this.nodes[when][which] == undefined)
77 this.nodes[when][which] = widget.domNode; /* We'll need this later */
80 this.update_timestamp(when, which, widget.attr("value"));
83 this.compute_validity();
84 this.paint_validity();
86 TimestampRange.prototype.compute_validity = function() {
87 if (Math.abs(this.start - this.end) < 1000) {
88 this.validity.end = false;
90 if (this.start < this.current_minimum())
91 this.validity.start = false;
93 this.validity.start = true;
95 if (this.end < this.current_minimum())
96 this.validity.end = false;
98 this.validity.end = true;
101 /* This method provides the minimum timestamp that is considered valid. For
102 * now it's arbitrarily "now + 15 minutes", meaning that all reservations
103 * must be made at least 15 minutes in the future.
105 * For reasons of keeping the middle layer happy, this should always return
106 * a time that is at least somewhat in the future. The ML isn't able to target
107 * any resources for a reservation with a start date that isn't in the future.
109 TimestampRange.prototype.current_minimum = function() {
110 /* XXX This is going to be a problem with local clocks that are off. */
112 n.setTime(n.getTime() + 1000 * 900); /* XXX 15 minutes; stop hardcoding! */
115 TimestampRange.prototype.update_timestamp = function(when, which, value) {
116 if (which == "date") {
117 this[when].setFullYear(value.getFullYear());
118 /* month and date MUST be done together */
119 this[when].setMonth(value.getMonth(), value.getDate());
120 } else { /* "time" */
121 this[when].setHours(value.getHours());
122 this[when].setMinutes(value.getMinutes());
123 this[when].setSeconds(0);
126 TimestampRange.prototype.is_backwards = function() {
127 return (this.start > this.end);
129 TimestampRange.prototype.paint_validity = function() {
130 for (var when in this.validity) {
131 if (this.validity[when]) {
132 this.paint_valid_node(this.nodes[when].date);
133 this.paint_valid_node(this.nodes[when].time);
135 this.paint_invalid_node(this.nodes[when].date);
136 this.paint_invalid_node(this.nodes[when].time);
140 TimestampRange.prototype.paint_invalid_node = function(node) {
142 /* Just toggling the class of something would be better than
143 * manually setting style here, but I haven't been able to get that
144 * to play nicely with dojo's styling of the date/time textboxen.
146 if (this.saved_style_properties.backgroundColor == undefined) {
147 for (var k in this.invalid_style_properties) {
148 this.saved_style_properties[k] = node.style[k];
151 for (var k in this.invalid_style_properties) {
152 node.style[k] = this.invalid_style_properties[k];
156 TimestampRange.prototype.paint_valid_node = function(node) {
158 for (var k in this.saved_style_properties) {
159 node.style[k] = this.saved_style_properties[k];
163 TimestampRange.prototype.is_valid = function() {
164 return (this.validity.start && this.validity.end);
166 var reserve_timestamp_range = new TimestampRange();
168 function SelectorMemory(selector) {
169 this.selector = selector;
172 SelectorMemory.prototype.save = function() {
173 for (var i = 0; i < this.selector.options.length; i++) {
174 if (this.selector.options[i].selected) {
175 this.memory[this.selector.options[i].value] = true;
179 SelectorMemory.prototype.restore = function() {
180 for (var i = 0; i < this.selector.options.length; i++) {
181 if (this.memory[this.selector.options[i].value]) {
182 if (!this.selector.options[i].disabled)
183 this.selector.options[i].selected = true;
189 * These functions communicate with the middle layer.
191 function get_all_noncat_brt() {
192 return pcrud.search("brt",
193 {"id": {"!=": null}, "catalog_item": "f"},
194 {"order_by": {"brt":"name"}}
198 function get_brt_by_id(id) {
199 return pcrud.retrieve("brt", id);
202 function get_brsrc_id_list() {
203 var options = {"type": our_brt.id(), "pickup_lib": pickup_lib_selected};
205 /* This mechanism for avoiding the passing of an empty 'attribute_values'
206 * option is essential because if you pass such an option to the
207 * middle layer API at all, it won't return any IDs for brsrcs that
208 * don't have at least one attribute of some kind.
210 var attribute_values = attr_value_table.get_all_values();
211 if (attribute_values.length > 0)
212 options.attribute_values = attribute_values;
214 options.available = reserve_timestamp_range.get_range();
216 return fieldmapper.standardRequest(
217 ["open-ils.booking", "open-ils.booking.resources.filtered_id_list"],
218 [openils.User.authtoken, options]
222 /* FIXME: We need failure checking after pcrud.retrieve() */
223 function add_brsrc_to_index_if_needed(list, further) {
224 for (var i in list) {
225 if (!brsrc_index[list[i]]) {
226 brsrc_index[list[i]] = pcrud.retrieve("brsrc", list[i]);
229 further(brsrc_index[list[i]]);
233 function sync_brsrc_index_from_ids(available_list, additional_list) {
234 /* Default states for everything in the index. Read the further comments. */
235 for (var i in brsrc_index) {
236 brsrc_index[i].isdeleted(true);
237 brsrc_index[i].ischanged(false);
240 /* Populate the cache with anything that's missing and tag everything
241 * in the "available" list as *not* deleted, and tag everything in the
242 * additional list as "changed." See below. */
243 add_brsrc_to_index_if_needed(
244 available_list, function(o) { o.isdeleted(false); }
246 add_brsrc_to_index_if_needed(
249 if (!(o.id() in just_reserved_now)) o.ischanged(true);
252 /* NOTE: We lightly abuse the isdeleted() and ischanged() magic fieldmapper
253 * attributes of the brsrcs in our cache. Because we're not going to
254 * pass back any brsrcs to the middle layer, it doesn't really matter
255 * what we set this attribute to. What we're using it for is to indicate
256 * in our little brsrc cache how a given brsrc should be displayed in this
257 * UI's current state (based on whether the brsrc matches timestamp range
258 * availability (isdeleted(false)) and whether the brsrc has been forced
259 * into the list because it was selected in a previous interface (like
260 * the catalog) (ischanged(true))).
264 function check_bresv_targeting(results) {
267 for (var i in results) {
268 var targ = results[i].targeting;
269 if (!(targ && targ.current_resource)) {
272 if (targ.error == "NO_COPIES" && targ.conflicts) {
273 for (var k in targ.conflicts) {
274 /* Could potentially get more circ information from
275 * targ.conflicts for display in the future. */
276 due_dates.push(humanize_timestamp_string2(targ.conflicts[k].due_date()));
281 just_reserved_now[results[i].targeting.current_resource] = true;
284 return {"missing": missing, "due_dates": due_dates};
287 function create_bresv(resource_list) {
288 var barcode = document.getElementById("patron_barcode").value;
290 alert(localeStrings.WHERES_THE_BARCODE);
292 } else if (!reserve_timestamp_range.is_valid()) {
293 alert(localeStrings.INVALID_TS_RANGE);
296 var email_notify = document.getElementById("email_notify").checked ? true : false;
299 results = fieldmapper.standardRequest(
300 ["open-ils.booking", "open-ils.booking.reservations.create"],
302 openils.User.authtoken,
304 reserve_timestamp_range.get_range(),
308 attr_value_table.get_all_values(),
313 alert(localeStrings.CREATE_BRESV_LOCAL_ERROR + E);
316 if (is_ils_event(results)) {
317 if (is_ils_actor_card_error(results)) {
318 alert(localeStrings.ACTOR_CARD_NOT_FOUND);
321 localeStrings.CREATE_BRESV_SERVER_ERROR, results
325 var targeting = check_bresv_targeting(results);
326 if (targeting.missing) {
327 if (aous_cache["booking.require_successful_targeting"]) {
329 dojo.string.substitute(
330 localeStrings.CREATE_BRESV_OK_MISSING_TARGET,
331 [results.length, targeting.missing]
333 dojo.string.substitute(
334 localeStrings.CREATE_BRESV_OK_MISSING_TARGET_BLOCKED_BY_CIRC,
335 [targeting.due_dates]
337 localeStrings.CREATE_BRESV_OK_MISSING_TARGET_WILL_CANCEL
341 function(o) { return o.bresv; },
342 true /* skip_update */
347 dojo.string.substitute(
348 localeStrings.CREATE_BRESV_OK_MISSING_TARGET,
349 [results.length, targeting.missing]
351 dojo.string.substitute(
352 localeStrings.CREATE_BRESV_OK_MISSING_TARGET_BLOCKED_BY_CIRC,
353 [targeting.due_dates]
359 dojo.string.substitute(
360 localeStrings.CREATE_BRESV_OK, [results.length]
368 alert(localeStrings.CREATE_BRESV_SERVER_NO_RESPONSE);
372 function flatten_to_dojo_data(obj_list) {
376 "items": obj_list.map(function(o) {
379 "type": o.target_resource_type().name(),
380 "start_time": humanize_timestamp_string(o.start_time()),
381 "end_time": humanize_timestamp_string(o.end_time())
384 if (o.current_resource())
385 new_obj["resource"] = o.current_resource().barcode();
386 else if (o.target_resource())
387 new_obj["resource"] = "* " + o.target_resource().barcode();
389 new_obj["resource"] = "* " + localeStrings.UNTARGETED + " *";
395 function create_bresv_on_brsrc() {
396 var selector = document.getElementById("brsrc_list");
397 var selected_values = [];
398 for (var i in selector.options) {
399 if (selector.options[i] && selector.options[i].selected)
400 selected_values.push(selector.options[i].value);
402 if (selected_values.length > 0)
403 create_bresv(selected_values);
405 alert(localeStrings.SELECT_A_BRSRC_THEN);
408 function create_bresv_on_brt() {
409 if (any_usable_brsrc())
412 alert(localeStrings.NO_USABLE_BRSRC);
415 function get_actor_by_barcode(barcode) {
416 var usr = fieldmapper.standardRequest(
417 ["open-ils.actor", "open-ils.actor.user.fleshed.retrieve_by_barcode"],
418 [openils.User.authtoken, barcode]
421 alert(localeStrings.GET_PATRON_NO_RESULT);
422 } else if (is_ils_event(usr)) {
423 return null; /* XXX inelegant: this function is quiet about errors
424 here because to report them would be redundant with
425 another function that gets called right after this one.
432 function init_bresv_grid(barcode) {
433 var result = fieldmapper.standardRequest(
435 "open-ils.booking.reservations.filtered_id_list"
437 [openils.User.authtoken, {
438 "user_barcode": barcode,
444 }, /* whole_obj */ true]
446 if (result == null) {
447 set_datagrid_empty_store(bresvGrid, flatten_to_dojo_data);
448 alert(localeStrings.GET_BRESV_LIST_NO_RESULT);
449 } else if (is_ils_event(result)) {
450 set_datagrid_empty_store(bresvGrid, flatten_to_dojo_data);
451 if (is_ils_actor_card_error(result)) {
452 alert(localeStrings.ACTOR_CARD_NOT_FOUND);
454 alert(my_ils_error(localeStrings.GET_BRESV_LIST_ERR, result));
457 if (result.length < 1) {
458 document.getElementById("bresv_grid_alt_explanation").innerHTML =
459 localeStrings.NO_EXISTING_BRESV;
460 hide_dom_element(document.getElementById("bresv_grid"));
461 reveal_dom_element(document.getElementById("reserve_under"));
463 document.getElementById("bresv_grid_alt_explanation").innerHTML =
465 reveal_dom_element(document.getElementById("bresv_grid"));
466 reveal_dom_element(document.getElementById("reserve_under"));
468 /* May as well do the following in either case... */
470 new dojo.data.ItemFileReadStore(
471 {"data": flatten_to_dojo_data(result)}
475 for (var i in result) {
476 bresv_index[result[i].id()] = result[i];
481 function cancel_reservations(bresv_id_list, skip_update) {
483 var result = fieldmapper.standardRequest(
484 ["open-ils.booking", "open-ils.booking.reservations.cancel"],
485 [openils.User.authtoken, bresv_id_list]
488 alert(localeStrings.CXL_BRESV_FAILURE2 + E);
491 if (!skip_update) setTimeout(update_bresv_grid, 0);
493 alert(localeStrings.CXL_BRESV_FAILURE);
494 } else if (is_ils_event(result)) {
495 alert(my_ils_error(localeStrings.CXL_BRESV_FAILURE2, result));
498 dojo.string.substitute(
499 localeStrings.CXL_BRESV_SUCCESS, [result.length]
505 function munge_specific_resource(barcode) {
507 var copy_list = pcrud.search(
508 "acp", {"barcode": barcode, "deleted": "f"}
510 if (copy_list && copy_list.length > 0) {
511 var r = fieldmapper.standardRequest(
513 "open-ils.booking.resources.create_from_copies"],
514 [openils.User.authtoken,
515 copy_list.map(function(o) { return o.id(); })]
519 alert(localeStrings.ON_FLY_NO_RESPONSE);
520 } else if (is_ils_event(r)) {
521 alert(my_ils_error(localeStrings.ON_FLY_ERROR, r));
523 if (!(our_brt = get_brt_by_id(r.brt[0][0]))) {
524 alert(localeStrings.COULD_NOT_RETRIEVE_BRT_PASSED_IN);
526 opts.booking_results = r;
527 init_reservation_interface();
531 alert(localeStrings.BRSRC_NOT_FOUND);
534 alert(localeStrings.BRSRC_RETRIEVE_ERROR + E);
539 * These functions deal with interface tricks (populating widgets,
540 * changing the page, etc.).
542 function init_pickup_lib_selector() {
543 var User = new openils.User();
544 User.buildPermOrgSelector(
545 "ADMIN_BOOKING_RESERVATION", pickup_lib_selector, null,
547 pickup_lib_selected = pickup_lib_selector.getValue();
548 dojo.connect(pickup_lib_selector, "onChange",
550 pickup_lib_selected = this.getValue();
558 function provide_brt_selector(targ_div) {
560 alert(localeStrings.NO_TARG_DIV);
562 brt_list = get_all_noncat_brt();
563 if (!brt_list || brt_list.length < 1) {
564 document.getElementById("select_noncat_brt_block").
565 style.display = "none";
567 var selector = document.createElement("select");
568 selector.setAttribute("id", "brt_selector");
569 selector.setAttribute("name", "brt_selector");
570 /* I'm reluctantly hardcoding this "size" attribute as 8
571 * because you can't accomplish this with CSS anyway.
573 selector.setAttribute("size", 8);
574 for (var i in brt_list) {
575 var option = document.createElement("option");
576 option.setAttribute("value", brt_list[i].id());
577 option.appendChild(document.createTextNode(brt_list[i].name()));
578 selector.appendChild(option);
580 targ_div.innerHTML = "";
581 targ_div.appendChild(selector);
586 function init_resv_iface_arb() {
587 init_reservation_interface(document.getElementById("arbitrary_resource"));
590 function init_resv_iface_sel() {
591 init_reservation_interface(document.getElementById("brt_selector"));
594 function init_reservation_interface(widget) {
595 /* Show or hide the email notification checkbox depending on org unit setting. */
596 if (!aous_cache["booking.allow_email_notify"]) {
597 hide_dom_element(document.getElementById("contain_email_notify"));
599 /* Save a global reference to the brt we're going to reserve */
600 if (widget && (widget.selectedIndex != undefined)) {
601 our_brt = brt_list[widget.selectedIndex];
602 } else if (widget != undefined) {
603 if (!munge_specific_resource(widget.value))
607 /* Hide and reveal relevant divs. */
608 var search_block = document.getElementById("brt_search_block");
609 var reserve_block = document.getElementById("brt_reserve_block");
610 hide_dom_element(search_block);
611 reveal_dom_element(reserve_block);
613 /* Get a list of attributes that can apply to that brt. */
614 var bra_list = pcrud.search("bra", {"resource_type": our_brt.id()});
616 alert(localeString.NO_BRA_LIST);
620 /* Get a table of values that can apply to the above attributes. */
621 var brav_by_bra = {};
622 bra_list.map(function(o) {
623 brav_by_bra[o.id()] = pcrud.search("brav", {"attr": o.id()});
626 /* Hide the label over the attributes widgets if we have nothing to show. */
627 var domf = (bra_list.length < 1) ? hide_dom_element : reveal_dom_element;
628 domf(document.getElementById("bra_and_brav_header"));
630 /* Create DOM widgets to represent each attribute/values set. */
631 for (var i in bra_list) {
632 var bra_div = document.createElement("div");
633 bra_div.setAttribute("class", "nice_vertical_padding");
635 var bra_select = document.createElement("select");
636 bra_select.setAttribute("name", "bra_" + bra_list[i].id());
637 bra_select.setAttribute(
639 "attr_value_table.update_from_selector(this); update_brsrc_list();"
642 var bra_opt_any = document.createElement("option");
643 bra_opt_any.appendChild(document.createTextNode(localeStrings.ANY));
644 bra_opt_any.setAttribute("value", "");
646 bra_select.appendChild(bra_opt_any);
648 var bra_label = document.createElement("label");
649 bra_label.setAttribute("class", "bra");
650 bra_label.appendChild(document.createTextNode(bra_list[i].name()));
652 var j = bra_list[i].id();
653 for (var k in brav_by_bra[j]) {
654 var bra_opt = document.createElement("option");
655 bra_opt.setAttribute("value", brav_by_bra[j][k].id());
657 document.createTextNode(brav_by_bra[j][k].valid_value())
659 bra_select.appendChild(bra_opt);
662 bra_div.appendChild(bra_label);
663 bra_div.appendChild(bra_select);
664 document.getElementById("bra_and_brav").appendChild(bra_div);
666 /* Add a prominent label reminding the user what resource type they're
668 document.getElementById("brsrc_list_header").innerHTML = our_brt.name();
669 init_pickup_lib_selector();
673 function update_brsrc_list() {
674 var brsrc_id_list = get_brsrc_id_list();
675 var force_list = (opts.booking_results && opts.booking_results.brsrc) ?
676 opts.booking_results.brsrc.map(function(o) { return o[0]; }) : [];
678 sync_brsrc_index_from_ids(brsrc_id_list, force_list);
680 var target_selector = document.getElementById("brsrc_list");
681 var selector_memory = new SelectorMemory(target_selector);
682 selector_memory.save();
683 target_selector.innerHTML = "";
685 for (var i in brsrc_index) {
686 if (brsrc_index[i].isdeleted() && (!brsrc_index[i].ischanged()))
689 var opt = document.createElement("option");
690 opt.setAttribute("value", brsrc_index[i].id());
691 opt.appendChild(document.createTextNode(brsrc_index[i].barcode()));
693 if (brsrc_index[i].isdeleted() && (brsrc_index[i].ischanged())) {
694 opt.setAttribute("class", "forced_unavailable");
695 opt.setAttribute("disabled", "disabled");
698 target_selector.appendChild(opt);
701 selector_memory.restore();
704 function any_usable_brsrc() {
705 for (var i in brsrc_index) {
706 if (!brsrc_index[i].isdeleted())
712 function update_bresv_grid() {
713 var widg = document.getElementById("patron_barcode");
714 if (widg.value != "") {
715 setTimeout(function() {
716 var target = document.getElementById(
717 "existing_reservation_patron_line"
719 var patron = get_actor_by_barcode(widg.value);
722 localeStrings.HERE_ARE_EXISTING_BRESV + " " +
723 formal_name(patron) + ": "
726 target.innerHTML = "";
729 setTimeout(function() { init_bresv_grid(widg.value); }, 0);
733 function init_timestamp_widgets() {
734 var when = ["start", "end"];
735 for (var i in when) {
736 reserve_timestamp_range.update_from_widget(
737 new dijit.form.TimeTextBox({
738 name: "reserve_time_" + when[i],
741 timePattern: "HH:mm",
742 clickableIncrement: "T00:15:00",
743 visibleIncrement: "T00:15:00",
744 visibleRange: "T01:30:00"
746 onChange: function() {
747 reserve_timestamp_range.update_from_widget(this);
750 }, "reserve_time_" + when[i])
752 reserve_timestamp_range.update_from_widget(
753 new dijit.form.DateTextBox({
754 name: "reserve_date_" + when[i],
756 onChange: function() {
757 reserve_timestamp_range.update_from_widget(this);
760 }, "reserve_date_" + when[i])
765 function cancel_selected_bresv(bresv_dojo_items) {
766 if (bresv_dojo_items && bresv_dojo_items.length > 0 &&
767 (bresv_dojo_items[0].length == undefined ||
768 bresv_dojo_items[0].length > 0)) {
770 bresv_dojo_items.map(function(o) { return o.id[0]; })
772 /* After some delay to allow the cancellations a chance to get
773 * committed, refresh the brsrc list as it might reflect newly
774 * available resources now. */
775 if (our_brt) setTimeout(update_brsrc_list, 2000);
777 alert(localeStrings.CXL_BRESV_SELECT_SOMETHING);
781 /* The following function should return true if the reservation interface
782 * should start normally (show a list of brt to choose from) or false if
783 * it should not (because we've "started" it some other way by setting up
784 * and displaying other widgets).
786 function early_action_passthru() {
787 if (opts.booking_results) {
788 if (opts.booking_results.brt.length != 1) {
789 alert(localeStrings.NEED_EXACTLY_ONE_BRT_PASSED_IN);
791 } else if (!(our_brt = get_brt_by_id(opts.booking_results.brt[0][0]))) {
792 alert(localeStrings.COULD_NOT_RETRIEVE_BRT_PASSED_IN);
796 init_reservation_interface();
800 if (opts.patron_barcode) {
801 document.getElementById("contain_patron_barcode").style.display="none";
802 document.getElementById("patron_barcode").value = opts.patron_barcode;
809 function init_aous_cache() {
810 /* The following method call could be given a longer
811 * list of OU settings to fetch in the future if needed. */
812 var results = fieldmapper.aou.fetchOrgSettingBatch(
813 openils.User.user.ws_ou(), ["booking.require_successful_targeting", "booking.allow_email_notify"]
815 if (results && !is_ils_event(results)) {
816 for (var k in results) {
817 if (results[k] != undefined)
818 aous_cache[k] = results[k].value;
820 } else if (results) {
821 alert(my_ils_error(localeStrings.ERROR_FETCHING_AOUS, results));
823 alert(localeStrings.ERROR_FETCHING_AOUS);
831 hide_dom_element(document.getElementById("brt_reserve_block"));
832 reveal_dom_element(document.getElementById("brt_search_block"));
833 hide_dom_element(document.getElementById("reserve_under"));
834 init_auto_l10n(document.getElementById("auto_l10n_start_here"));
836 init_timestamp_widgets();
840 if (!(opts = xulG.bresv_interface_opts)) opts = {};
841 if (early_action_passthru())
842 provide_brt_selector(document.getElementById("brt_selector_here"));