Archive

Posts Tagged ‘HTML’

koGrid: Bug – Checkboxes column duplication

January 24th, 2017 No comments

    Using the koGrid to bring an edit-in-place grid to a web app, I’ve run into a bug when the predefined first column of checkboxes are being unexpectedly replicated, i.e. being wrapped in the if-binding, the grid has as many checkboxes columns as many times its presence has been changed.

koGrid: Checkbox Column Duplication Bug

I use the following to define the grid

<!-- ko if: shouldGridBeVisible -->
   <div data-bind="koGrid: koGridOptions()"></div>
<!-- /ko -->
// typescript
private customKoGridColumnDefs = [
   { field: 'Name', displayName: 'Name' },
   ...
];

private koGridColumnDefs = ko.pureComputed(() => {
   return this.customKoGridColumnDefs;
});

public koGridOptions = ko.pureComputed(() => {        
   return {
      data: this.items, // observable array of data
      selectWithCheckboxOnly: true, // only want to be able to select with checkboxes
      selectedItems: this.selectedItems, // observable array to store selected items
      columnDefs: this.koGridColumnDefs, // observable columns definitions
      displaySelectionCheckbox : true // show column of checkboxes
      ...
   }
});

The “buggy” place is within the buildColumns function defined in the window.kg.Grid. Among other things the function adds a column of checkboxes to the existing columns definitions, not checking if such column is already there. Below are shortened listings of the buildColumns and related functions, ending with koGrid-binding

// koGrid-2.1.1.debug.js
window.kg.Grid = function (options) {
   ...
   self.config.columnDefs = ko.utils.unwrapObservable(options.columnDefs);
   ...
   self.buildColumns = function () {
      var columnDefs = self.config.columnDefs,
         cols = [];

      if (!columnDefs) {
         self.buildColumnDefsFromData();
         columnDefs = self.config.columnDefs;
      }
      if (self.config.displaySelectionCheckbox && self.config.canSelectRows) {
         // the columns definitions array passed in the options is about to be modified
         columnDefs.splice(0, 0, {
            field: '\u2714',
            width: self.elementDims.rowSelectedCellW,
            sortable: false,
            resizable: false,
            headerCellTemplate: '<input class="kgSelectionHeader" type="checkbox" data-bind="visible: $grid.multiSelect, checked: $grid.allSelected" />',
            cellTemplate: '<div class="kgSelectionCell"><input class="kgSelectionCheckbox" type="checkbox" data-bind="checked: $parent.selected" /></div>'
         });
      }
   ...
   };
   ...
   self.init = function () {
      ...
      self.buildColumns(); // build columns
      ...
   };
   ...	
   self.init(); // initialize grid
};
...
ko.bindingHandlers['koGrid'] = (function () {
   return {
      'init': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
         var options = valueAccessor();
         ...
         var grid = new window.kg.Grid(options); // create and initialize grid
         ...
         // if columndefs are observable watch for changes and rebuild columns.
         if (ko.isObservable(options.columnDefs)) {
            options.columnDefs.subscribe(function (newDefs) {
               grid.columns([]);
               grid.config.columnDefs = newDefs;
               grid.buildColumns(); // rebuild the columns
               grid.configureColumnWidths();
            });
         }
         ...
         return { controlsDescendantBindings: true };
      }
   };
}());

So, whenever if-binding’s conditions are met, the grid becomes visible, and we have the following call sequence:

ko.bindingHandlers[‘koGrid’].init -> window.kg.Grid.init -> window.kg.Grid.buildColumns

which ends up with adding the next-in-turn checkboxes column to the columns definitions and, therefore, to the grid. The same, by the way, may happen also if the columns definitions (options.columnDefs) are observable and being mutated (line 49 in the listing above).

It’s a well-known issue (I was able to find at least 2 describing this bug: #292 and #209) with a simple fix already residing among the pull requests (#213), but not applied yet (and may never be applied as the project have not been updated since 2014 and looks abandoned). The fix is to the window.kg.Grid.buildColumns and shown below:

// koGrid-2.1.1.debug.js
self.buildColumns = function () {
   var columnDefs = self.config.columnDefs,
      cols = [];

   if (!columnDefs) {
      self.buildColumnDefsFromData();
      columnDefs = self.config.columnDefs;
   }
   if (self.config.displaySelectionCheckbox && self.config.canSelectRows) {
      if (columnDefs.length > 0 && columnDefs[0].field != '\u2714') {
         columnDefs.splice(0, 0, {
            field: '\u2714',
            width: self.elementDims.rowSelectedCellW,
            sortable: false,
            resizable: false,
            headerCellTemplate: '<input class="kgSelectionHeader" type="checkbox" data-bind="visible: $grid.multiSelect, checked: $grid.allSelected"/>',
            cellTemplate: '<div class="kgSelectionCell"><input class="kgSelectionCheckbox" type="checkbox" data-bind="checked: $parent.selected" /></div>'
         });
      }
   }
   ...
};

I really don’t like changing the 3rd party libraries’ source code in my projects and always try to find an alternative way. This issue can be resolved with the fix utilizing the knockout.js‘s power and placed outside of the koGrid-2.1.1.debug.js and koGrid-2.1.1.js. So, below is the binding-wrapper to prevent duplication right before the original koGrid-binding gets control:

// javascript
ko.bindingHandlers["koGridFixed"] = {
   init: function (element, valueAccessor, allBindingsAccessor, data, context) {
      var gridOptions = ko.utils.unwrapObservable(valueAccessor());
      if (gridOptions && gridOptions.columnDefs) {
         var columnDefsArr = ko.utils.unwrapObservable(gridOptions.columnDefs);
         if (columnDefsArr && columnDefsArr.length > 0 && columnDefsArr[0].field === '\u2714')
            columnDefsArr.splice(0, 1);
      }

      return ko.bindingHandlers["koGrid"].init(element, valueAccessor, allBindingsAccessor, data, context);		
   }
};

So, just replace the koGrid-binding everywhere in views with the koGridFixed and the problem is solved with no affecting the grid’s source code

<!-- ko if: shouldGridBeVisible -->
   <div data-bind="koGridFixed: koGridOptions()"></div>
<!-- /ko -->
Related posts:

Select2: Custom matcher and highlighting

September 28th, 2015 No comments

    The Select2 uses the “contains”-matcher by default. In case of a big number of options, we’ve run into the time lag whenever the drop-down appears with the filtered out items. Of course, we could increase the minimumInputLength setting to minimize items within the drop-down. We, however, decided to change the algorithm used for filtering. The method below looks for options, any word of which is starting with the pattern typed in.

function wordStartMatcher(term, text, highlighting) {
	var myRe  = new RegExp("(?:^|\\s)" + term, "i");
	var match = myRe.exec(text);

	if (match != null && highlighting) {
		myRe = new RegExp("\\b" + term, "i");
		match = myRe.exec(text);
	}

	return match;
}

The method is used twice: for filtering out and for highlighting the found pattern in options. Two regexps in the method do the same, they both capture the beginning of each word. However, they handle Unicode in different ways. So the first one accurately chooses options that match while the second one fetches out the parts of the options to be highlighted.

Below is the method responsible for highlighting

function markMatch(text, term, markup, escapeMarkup) {
	var wordMatch = wordStartMatcher(term, text, true);

	var match = wordMatch ? wordMatch.index : -1;
	var tl = term.length;

	if (match < 0) {
		markup.push(Select2.util.escapeMarkup(text));
		return;
	}

	markup.push(Select2.util.escapeMarkup(text.substring(0, match)));
	markup.push("<span class='select2-match'>");
	markup.push(Select2.util.escapeMarkup(text.substring(match, match + tl)));
	markup.push("</span>");
	markup.push(Select2.util.escapeMarkup(text.substring(match + tl, text.length)));
}

The method fetches out the string part which meets the pattern and wraps it in special tags.

Ok, now is how to apply the custom matcher and highlighing to the Select2 control

$("#magazines").select2({
	matcher: function (term, text) {
		return wordStartMatcher(term, text) != null;
	},
	formatResult: function (result, container, query, escapeMarkup) {
		var markup = [];
		markMatch(this.text(result), query.term, markup, escapeMarkup);
		return markup.join("");
	}
});

Where the “#magazines“-control from the example is specified as

<select id="magazines" name="magazines">
	<option value="">Select a subscription</option>
	<option value="Msdn">MSDN Magazine</option>
	<option value="VS">Visual Studio Magazine</option>
	<option value="Code">CODE Magazine</option>
	<option value="Dobbs">Dr. Dobbs Journal</option>
	<option value="GameDev">Game Developer Magazine</option>
	<option value="LinUsDev">Linux User and Developer</option>
</select>

So, for the above listed options and the pattern defined as “u” the “contains”-matcher would give

  • Visual Studio Magazine
  • Dr. Dobbs Journal
  • Linux User and Developer

while the custom matcher gives what we really need

  • Linux User and Developer

Categories: HTML, JavaScript, Select2 Tags: , ,

Select2: jqModal + Select2 = can’t type in

June 10th, 2015 No comments

    The Select2 control placed in the jqModal modal dialog doesn’t allow typing anything in. It’s unexpected, but explainable: jqModal eliminates all keypress and keydown events coming from without, i.e. coming from the controls not residing in the jqModal container. As I mentioned in the Select2: Make dropdown vertically wider, the Select2, in turn, dynamically appends the elements making up the dropdown (including the type-ahead search box) to the Body-tag, so they are literally out of jqModal dialog.

Select2 in jqModal - No Typing in

Where the contracted jqModal markup resembles the following:

<div id="myModalDialog" class="popup-6"...>
    <div class="pp6-header">
        <h3>Some Fancy Modal Dialog</h3>
        <span class="close" ...></span>
    </div>
    <div class="pp6-body" ...>
        
        <select id="mySelect2" ...>
			<option value="">Select an option</option>
			<option value="Product">Product</option>
			<option value="Producer">Producer</option>
			...
		</select>
			
    </div>
    <div class="pp6-footer">
        <a href="javascript: void(0)" class="ok" ...>OK</a>
        <a href="javascript: void(0)" class="close" ...>Cancel</a>
    </div>    
</div>

The Select2 and modal dialog are created as follows:

	$("#mySelect2").select2();
	...
	$("#myModalDialog").jqm({
            modal: true,
            trigger: false,
            onHide: function (h) {
                // hash object;
                //  w: (jQuery object) The modal element
                //  c: (object) The modal's options object 
                //  o: (jQuery object) The overlay element
                //  t: (DOM object) The triggering element                
                h.o.remove(); // remove overlay
                h.w.hide(); // hide window      
            },
            onShow: function (hash) {                
                hash.o.addClass("modalBackground").prependTo("body"); // add overlay
				
				// getNextDialogZIndex is my custom function, 
				// which increments z-index counter to place each new dialog atop the preceding one
                if (typeof getNextDialogZIndex === "function") {
                    var zIndex = getNextDialogZIndex();
                    hash.o.css("z-index", zIndex);
                    hash.w.css("z-index", zIndex + 1);
                }
                hash.w.css("left", 0);
                hash.w.css("right", 0);
                hash.w.css("position", "fixed");
                hash.w.css("margin-left", "auto");
                hash.w.css("margin-right", "auto");                
                hash.w.fadeIn();//show();
            }
        });		
	...
	$("#myModalDialog").jqmShow();

Below I quote the code from jqModal.js where the keypress, keydown, mousedown events are being handled, and their bubbling is being interrupted

F = function(t){
	// F: The Keep Focus Function (for modal: true dialos)
	// Binds or Unbinds (t) the Focus Examination Function (X) to keypresses and clicks
		
	$(document)[t]("keypress keydown mousedown",X);		
		
}, X = function(e){
	// X: The Focus Examination Function (for modal: true dialogs)

	var modal = $(e.target).data('jqm') || $(e.target).parents('.jqm-init:first').data('jqm'),
		activeModal = A[A.length-1].data('jqm');
		
	// allow bubbling if event target is within active modal dialog
	if(modal && modal.ID == activeModal.ID) return true; 

	// else, trigger focusFunc (focus on first input element and halt bubbling)
	return $.jqm.focusFunc(activeModal);
}

The code examines if the event target control or its first found parent marked with the ‘jqm-init‘ class have the associated datajqm‘. If so, the event will be allowed to pass through, otherwise it will be ignored.

Based on that, we can apply a workaround to Select2 control to unlock the type-ahead search box for typing in. The solution implies to associate the proper data named ‘jqm‘ with the Select2‘s dropdown. To catch the right dropdown (we don’t want to affect other Select2 controls on the page), a unique css class has to be applied to it. Plus the dropdown has to have the ‘jqm-init‘ class to be considered and analyzed by the jqModal code (note the following fragment in the listing above:)

$(e.target).parents('.jqm-init:first')

So, the solution should look like

// create Select2 and apply proper css classes to its dropdown
$("#mySelect2").select2({ dropdownCssClass: "someUniqueCssClass jqm-init" });

// capture the data associated with the jqModal container
var d = $("#myModalDialog").data("jqm");

// associate the captured data with the dropdown 
// to mimic the residence within the jqModal dialog
$(".someUniqueCssClass.jqm-init").data('jqm', d);

After that every keypress, keydown or mousedown event won’t be suppressed, but will be properly handled.

Select2: Make dropdown vertically wider

June 1st, 2015 No comments

    The Select2 is a quite useful and fancy replacement for ordinary Html Selects. Having a fixed number of items available to pick, you may want to make the dropdown of a Select2 control vertically wider to avoid extra scrolling.

Before - Select2 With Scrolling

Where the Select2 control is defined as follows

<select name="criteria" id="criteria">
	<option value="">Select a Search Criteria</option>
	<option value="Product">Product</option>
	<option value="Producer">Producer</option>
	<option value="Store">Store</option>
	<option value="State">State</option>
	<option value="Country">Country</option>
	<option value="Continent">Continent</option>
	<option value="Planet">Planet</option>
	<option value="Galaxy">Galaxy</option>
	<option value="Universe">Universe</option>
	<option value="Reality">Reality</option>
</select>
	$("#criteria").select2({
		minimumResultsForSearch: -1 // allows to hide the type-ahead search box,
		                            // so the Select2 acts as a regular dropdown list
	});

Whenever the Select2 has been clicked, the dropdown-elements will be dynamically added to the DOM at the end of the body-tag and will pop up. To make the dropdown wider we need to apply the proper ccs style to it. Of course, we can change the default css styles accompanying the Select2 control, but there is a way to apply desired changes to a certain control, not affecting other Select2 controls on the page. The custom css class can be attached to the target dropdown by the following call:

	$("#criteria").select2({
		minimumResultsForSearch: -1,
		dropdownCssClass: "verticallyWider" // the dropdownCssClass option is intended 
                                            // to specify a custom dropdown css class
	});                        

The Html markup dynamically added to the DOM when clicking the Select2 resembles the following (note the verticallyWider class, which among others has been applied to the outer div-tag):

<div class="select2-drop-mask" id="select2-drop-mask"></div>

<div class="select2-drop select2-display-none verticallyWider select2-drop-active" 
	id="select2-drop" 
	style="left: 828.49px; top: 112.61px; width: 300px; bottom: auto; display: block;">
	<div class="select2-search select2-search-hidden select2-offscreen">
		<label class="select2-offscreen" for="s2id_autogen2_search"></label>
		<input class="select2-input" id="s2id_autogen2_search" role="combobox" 
			aria-expanded="true" aria-activedescendant="select2-result-label-8" 
			aria-owns="select2-results-2" spellcheck="false" aria-autocomplete="list" 
			type="text" placeholder="" autocapitalize="off" 
			autocorrect="off" autocomplete="off" />
	</div>
   <ul class="select2-results" id="select2-results-2" role="listbox">
      <li class="select2-results-dept-0 select2-result select2-result-selectable" 
		role="presentation">
         <div class="select2-result-label" id="select2-result-label-3" role="option">
			<span class="select2-match"></span>Select a Search Criteria
		 </div>
      </li>
      <li class="select2-results-dept-0 select2-result select2-result-selectable" 
		role="presentation">
         <div class="select2-result-label" id="select2-result-label-4" role="option">
			<span class="select2-match"></span>Product
		 </div>
      </li>
      <li class="select2-results-dept-0 select2-result select2-result-selectable" 
		role="presentation">
         <div class="select2-result-label" id="select2-result-label-5" role="option">
			<span class="select2-match"></span>Producer
		 </div>
      </li>
      ...
      <li class="select2-results-dept-0 select2-result select2-result-selectable" 
		role="presentation">
         <div class="select2-result-label" id="select2-result-label-13" role="option">
			<span class="select2-match"></span>Reality
		 </div>
      </li>
   </ul>
</div>

The verticallyWider class, in turn, is defined as the following (so, specify the desired min/max heights in the styles):

.verticallyWider.select2-container .select2-results {
    max-height: 400px;
}
.verticallyWider .select2-results {
    max-height: 400px;
}
.verticallyWider .select2-choices {
    min-height: 150px; max-height: 400px; overflow-y: auto;
}

Below is the result of the modifications

After - Select2 With No Scrolling - Vertically Wider