Modifying Selectize.js library to allow editing of selected options - javascript

Using Selectize.js in an Angular 9 application for selecting multiple values. Please see links to my UI at the end
https://selectize.github.io/selectize.js/
https://github.com/selectize/selectize.js
I'm trying enable the user to edit the already selected values by simply clicking on the selected item. Selectize has the concept of Plugins by which "features can be added to Selectize without modifying the main library." I'm making use of this concept to override onMouseDown event, where I'm attempting to make the clicked item editable. I have successfully used this method to override onKeyDown to implement editing of the last selected value by clicking on backspace. Please see code pasted at the bottom. this.onKeyDown = (function() {...
https://github.com/selectize/selectize.js/blob/master/docs/plugins.md
The already selected items are shown as a layer of div elements over the underlying input. To make a selected item editable, I'm removing the selected element div from the DOM, populating the underlying input element with the text from the div. That way that particular item becomes a input from a div and is editable.
There are a few issues im running into:
Its not possible to determine the caret position from the div that was clicked. I am able to get the div text and pre-populate the input element but not put the caret at the right place in input. By default the caret shows at the end and the user can move it around.
Corer cases around when a name is already being edited and the user clicks on another item to edit. The selectize library is giving api to insert selections only at the end of the already selected items. For me to keep deleting the div's and populating the input to mimic the editing effect I need to be able to insert at different positions but the library doesnt seem to have the capability for it.
Trying to see if anyone has worked on something similar or has any suggestions. Thanks in advance!
var Selectize = require('./selectize-standalone');
(function () {
Selectize.define('break_on_backspace_custom_plugin', function(options) {
var self = this;
options.text = options.text || function(option) {
return option[this.settings.labelField];
};
this.onMouseDown = (function() {
var original = self.onMouseDown;
return function(e) {
var index, option;
if (!this.$control_input.val().length && this.$activeItems.length > 0) {
index = this.caretPos - 1;
var toBeEdited = this.$activeItems[0];
var toBeEditedText = toBeEdited.textContent;
var text = toBeEditedText.substring(0, toBeEditedText.length - 1);
var prevEdit = localStorage.getItem("currentEdit");
if (index >= 0 && index < this.items.length) {
if (this.deleteSelection(e)) {
localStorage.setItem("currentEdit", text);
this.setTextboxValue(text);
this.refreshOptions(true);
if (prevEdit && prevEdit !== text) {
this.addItem(prevEdit);
}
}
//e.preventDefault();
//return;
}
}
//e.preventDefault();
return original.apply(this, arguments);
};
})();
this.onKeyDown = (function() {
var original = self.onKeyDown;
return function(e) {
var index, option;
if (e.keyCode === 8 && this.$control_input.val() === '' && !this.$activeItems.length) {
index = this.caretPos - 1;
if (index >= 0 && index < this.items.length) {
option = this.options[this.items[index]];
if (this.deleteSelection(e)) {
//option.value = option.value.substring(0, option.value.length - 1);
this.setTextboxValue(options.text.apply(this, [option]));
this.refreshOptions(true);
}
e.preventDefault();
return;
}
}
return original.apply(this, arguments);
};
})();
});
return Selectize;
})();
Pictures of UI and work in progress
Editing last element by clicking backspace
https://i.stack.imgur.com/wULcT.png
Editing middle element by clicking on it
https://i.stack.imgur.com/U5hxd.png

Related

Bootstrap-select, behavior like SO tags field

I have a bootstrap-select with multiple mode and live-search on. I need to add a behavior, something like Stack Overflow Tags select: meaning that if you enter a keyword that doesn't exist in the list, it should be added as soon as you press comma. Is there any easy way of doing this?
<select id="#Html.IdForModel()" name="#Html.NameForModel()"
class="selectpicker form-control" data-live-search="true" multiple></select>
Just for completeness: I'm also using https://github.com/truckingsim/Ajax-Bootstrap-Select on top of it, but I doubt this matters.
Okay, this appeared to be extremely complex problem to solve. Partially because I was using Ajax-Bootstrap-Select (thought this was insignificant, but it was not). This code has some problems, like spamming setTimeout - I for most don't care, but this code should give an idea to anybody who is struggling with the same issue I did.
var keywordsSelectItemTemplate = $('##Html.IdForModel()selectItemTemplate').html();
var keywordsSelectBox = $('##Html.IdForModel()');
var keywordsSelectedElements = [];
// list of words that were persisted
var keywordsAddedElementsStable = [];
// Remembered list of words that are currently in the input box (currently entered)
var keywordsAddedElements = [];
// The search box input
var keywordsSearchBox = keywordsSelectBox.parent().find("div.bs-searchbox").find("input");
// Subscribe to all kind of keypress events to track changes
keywordsSearchBox.bind("propertychange change click keyup input paste",
function(event) {
var value = keywordsSearchBox.val();
// Parse words by splitting input box value with ,
var keywords = value.split(",").map(function (str) {
return str.trim();
}).filter(function(str) {
return str != undefined && str.length > 0;
});
// Removing last word, because this is the word that is currently being typed
if (keywordsAddedElements.slice(-1).length > 0) {
keywordsAddedElements.splice(-1, 1);
}
// Remembering all words so Bootstrap-Select-Ajax could process them.
keywords.forEach(function(keyword) {
if ($.inArray(keyword, keywordsAddedElements) === -1) {
keywordsAddedElements.push(keyword);
}
});
// Adding entered words as selected and refresh the selectbox. The timeout should be biger than the timeout defined
// in Bootstrap-Select-Ajax, so this refresh will happen after Bootstrap-Select-Ajax process the selected list
setTimeout(function() {
keywordsSelectBox.val(keywordsSelectBox.val().concat(keywords));
keywords.forEach(function(keyword) {
keywordsSelectBox.find("[value=" + keyword + "]").attr('selected', '1');
});
keywordsSelectBox.selectpicker('refresh');
keywordsSelectBox.selectpicker('render');
}, 500);
});
// If we leave search box input - remember the entered items as persisted.
// The user is no longer able to change them, unless by clicking in Bootstrap-Select (as any other items)
keywordsSearchBox.focusout(function() {
// Refreshing selected items of Bootstrap-Select-Ajax, ensuring that it thinks
// these items were processed as any other.
var selectedItems = keywordsSelectBox.ajaxSelectPicker()
.data()
.AjaxBootstrapSelect.list.selected;
keywordsAddedElements.forEach(function(el) {
if (!selectedItems.some(function(element) { return element.value === el;})) {
selectedItems.push({
"class": "",
data: {
content: el
},
preserved: true,
selected: true,
text: el,
value: el
});
}
});
// Remember items as "persisted"
keywordsAddedElementsStable = keywordsAddedElementsStable.concat(keywordsAddedElements);
keywordsAddedElements = [];
And then in ajaxSelectPicker in preprocessData:
// Adding both "persisted" and "temporary" items to the Bootstrap-Select-Ajax list.
keywordsAddedElementsStable.concat(keywordsAddedElements).forEach(function(el) {
result.push({
value: el,
text: el,
data: {
content: el
}
});
Without using Ajax-Bootstrap-Select, the issue is very simple: Find search box:
var keywordsSearchBox = keywordsSelectBox.parent().find("div.bs-searchbox").find("input")
Subscribe to change:
keywordsSearchBox.bind("propertychange change click keyup input paste",
function(event) { ...
And add elements via selectBox.selectpicker('val', searchBox.val().split(","));

Event listener fails to attach or remove in some contexts

I've created a script that attaches an event listener to a collection of pictures by default. When the elements are clicked, the listener swaps out for another event that changes the image source and pushes the id of the element to an array, and that reverses if you click on the swapped image (the source changes back and the last element in the array is removed). There is a button to "clear" all of the images by setting the default source and resetting the event listener, but it doesn't fire reliably and sometimes fires with a delay, causing only the last element in a series to be collected.
TL;DR: An event fires very unreliably for no discernible reason, and I'd love to know why this is happening and how I should fix it. The JSFiddle and published version are available below.
I've uploaded the current version here, and you can trip the error by selecting multiple tables, pressing "Cancel", and selecting those buttons again. Normally the error starts on the second or third pass.
I've also got a fiddle.
The layout will be a bit wacky on desktops and laptops since it was designed for phone screens, but you'll be able to see the issue and inspect the code so that shouldn't be a problem.
Code blocks:
Unset all the selected tables:
function tableClear() {
//alert(document.getElementsByClassName('eatPlace')[tableResEnum].src);
//numResTables = document.getElementsByClassName('eatPlace').src.length;
tableArrayLength = tableArray.length - 1;
for (tableResEnum = 0; tableResEnum <= tableArrayLength; tableResEnum += 1) {
tableSrces = tableArray[tableResEnum].src;
//alert(tableSrcTapped);
if (tableSrces === tableSrcTapped) {
tableArray[tableResEnum].removeEventListener('click', tableUntap);
tableArray[tableResEnum].addEventListener('click', tableTap);
tableArray[tableResEnum].src = window.location + 'resources/tableBase.svg';
} /*else if () {
}*/
}
resTableArray.splice(0, resTableArray.length);
}
Set/Unset a particular table:
tableUntap = function () {
$(this).unbind('click', tableUntap);
$(this).bind('click', tableTap);
this.setAttribute('src', 'resources/tableBase.svg');
resTableArray.shift(this);
};
tableTap = function () {
$(this).unbind('click', tableTap);
$(this).bind('click', tableUntap);
this.setAttribute('src', 'resources/tableTapped.svg');
resTableArray.push($(this).attr('id'));
};
Convert the elements within the 'eatPlace' class to an array:
$('.eatPlace').bind('click', tableTap);
tableList = document.getElementsByClassName('eatPlace');
tableArray = Array.prototype.slice.call(tableList);
Table instantiation:
for (tableEnum = 1; tableEnum <= tableNum; tableEnum += 1) {
tableImg = document.createElement('IMG');
tableImg.setAttribute('src', 'resources/tableBase.svg');
tableImg.setAttribute('id', 'table' + tableEnum);
tableImg.setAttribute('class', 'eatPlace');
tableImg.setAttribute('width', '15%');
tableImg.setAttribute('height', '15%');
$('#tableBox').append(tableImg, tableEnum);
if (tableEnum % 4 === 0) {
$('#tableBox').append("\n");
}
if (tableEnum === tableNum) {
$('#tableBox').append("<div id='subbles' class='ajaxButton'>Next</div>");
$('#tableBox').append("<div id='cazzles' class='ajaxButton'>Cancel</div>");
}
}
First mistake is in tapping and untapping tables.
When you push a Table to your array, your pushing its ID.
resTableArray.push($(this).attr('id'));
It will add id's of elements, depending on the order of user clicking the tables.
While untapping its always removing the first table.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift
resTableArray.shift(this);
So, when user clicks tables 1, 2, 3. And unclicks 3, the shift will remove table 1.
Lets fix this by removing untapped table
tableUntap = function () {
$(this).unbind('click', tableUntap);
$(this).bind('click', tableTap);
this.setAttribute('src', 'http://imgur.com/a7J8OJ5.png');
var elementID = $(this).attr('id');
var elementIndex = resTableArray.indexOf(elementID);
resTableArray.splice(elementIndex, 1);
};
So you were missing some tables after untapping.
Well lets fix tableClear,
You have a array with tapped tables, but you are searching in main array.
function tableClear() {
tableLen = resTableArray.length;
for (var i = 0; i < tableLen; i++) {
var idString = "#" + resTableArray[i];
var $element = $(idString);
$element.unbind('click', tableUntap);
$element.bind('click', tableTap);
$element.attr("src", 'http://imgur.com/a7J8OJ5.png');
}
resTableArray = [];
}
Im searching only tapped tables, and then just untap them and remove handlers.
fiddle: http://jsfiddle.net/r9ewnxzs/
Your mistake was to wrongly remove at untapping elements.

Select dependence of one another (the choice of age)

Need to do dynamic filtering options , namely age and do.My code jsfiddle.net,but came across a problem that does not work in chrome method hide.Found answers(1, 2) but do not know how to put them in my code.
Problem in:
$("#age_from").change(function(){
$("#age_to option").each(function(i) {
if(parseInt($("#age_from").val()) > parseInt($(this).val())) {
$(this).hide();
}
else {
$(this).show();
}
});
});
It seems that chrome wont let you simply hide option tags. You may have to resort to dynamically filling the from options after you select the to option. I've updated your jQuery code below to allow passing a range of numbers in:
function fill(element, range_start, range_end){
if(typeof range_start == 'undefined') {
range_start = 1;
}
if(typeof range_end == 'undefined') {
range_end = 100;
}
// STORE THE PREVIOUSLY SELECTED VALUE
var selected = element.val();
// RESET THE HTML OF THE ELEMENT TO THE FIRST OPTION ONLY
element.html(element.find('option').first());
var age_list = [];
for (var i = range_start; i < range_end; i++){
age_list.push(i);
}
$.each(age_list, function(key, value) {
$(element)
.append($("<option></option>")
.attr("value", value)
.text(value));
});
// RESET THE VALUE
element.val(selected);
}
fill($('#age_from'));
fill($('#age_to'));
$("#age_from").change(function(){
// FILL THE SELECT ELEMENT WITH NUMBERS FROM THE RANGE #age_from.val() + 1 TO 100
fill($('#age_to'), parseInt($("#age_from").val()) + 1, 100);
});
$("#age_to").change(function(){
// FILL THE SELECT ELEMENT WITH NUMBERS FROM THE RANGE 1 TO #age_to.val()
fill($('#age_from'), 1, parseInt($("#age_to").val()));
});
I'm not entirely sure about performance on this, but as I've removed the .each() for going through the select options to hide them, it may still perform well.
updated working fiddle here:
http://jsfiddle.net/andyface/GKVxu/1/

Get HTML of selection in a specific div

I have found a code snippet (can't remember where), and it's working fine - almost :-)
The problem is, that it copies the selection no matter where the selection is made on the entire website, and it must only copy the selection if it is in a specific div - but how is that done?
function getHTMLOfSelection () {
var range;
if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
return range.htmlText;
}
else if (window.getSelection) {
var selection = window.getSelection();
if (selection.rangeCount > 0) {
range = selection.getRangeAt(0);
var clonedSelection = range.cloneContents();
var div = document.createElement('div');
div.appendChild(clonedSelection);
return div.innerHTML;
} else {
return '';
}
} else {
return '';
}
}
$(document).ready(function() {
$("#test").click(function() {
var kopitekst = document.getElementById("replytekst");
var kopitjek=getHTMLOfSelection(kopitekst);
if (kopitjek=='')
{
alert("Please select some content");
}
else
{
alert(kopitjek);
}
});
});
I have made a Jsfiddle
This is my first post here. Hopefully I done it right :-)
That's because it checks the entire document with:
if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
return range.htmlText;
}
Not a specific section. If you want to check specific sections for selected text, you need to identify that you are searching for them in the search selection, something that nails your range down to a particular div:
range = $('#replytekst');
Specify a particular DOM element instead of using document object.
var oDiv = document.getElementById( 'selDiv' );
then use
if ( oDiv.selection && oDiv.selection.createRange ) {
range = oDiv.selection.createRange();
return range.htmlText;
}
You need to check if the section contains the selection. This is separate from getting the selection. There is a method for doing this in this answer: How to know if selected text is inside a specific div
I've updated your fiddle
Basically you need to check the id of the parent/ascendant of the selected text node.
selection.baseNode.parentElement.id or selection.baseNode.parentElement.parentElement.id will give you that.
Edit: I've thought of another, somewhat hack-y, way of doing it.
If
kopitekst.innerHTML.indexOf(kopitjek) !== -1
gives true, you've selected the right text.
DEMO1
DEMO2
(these work in Chrome and Firefox, but you might want to restructure the getHTMLOfSelection function a little)
If it possible for you I recommend to use rangy framework. Then your code might look like this:
// get the selection
var sel = rangy.getSelection();
var ranges = sel.getAllRanges();
if (!sel.toString() || !sel.toString().length)
return;
// create range for element, where selection is allowed
var cutRange = rangy.createRange();
cutRange.selectNode(document.getElementById("replytekst"));
// make an array of intersections of current selection ranges and the cutRange
var goodRanges = [];
$.each(ranges, function(j, tr) {
var rr = cutRange.intersection(tr);
if (rr)
goodRanges.push(rr);
});
sel.setRanges(goodRanges);
// do what you want with corrected selection
alert(sel.toString());
// release
sel.detach();
In this code if text was selected in your specific div then it will be kept, if there was selection where other elements take part too, these selection ranges will be cut off.

Appending to a select leaks memory JavaScript JQuery

I have a select drop down menu, every time I refresh my page I have to re populate that select drop down. Which is resulting in a memory leak. This is the code any help would be great in refactoring the code. Also I have tried to make another method and calling it before this one, the other method would empty the options array and make it null. That did not help me.
var option = $(document.createElement("option"));
option.attr("value", List.id);
option.text(List.name);
if(List.name.length > maxSize) {
maxSize = List.name.length;
}
this.options.push(option);
//Mark the currently displayed list as the selected option
if (activeListId > 0) {
if (activeListId == List.id) {
option.attr("selected", true);
}
}
}
Toolbar.ListSelect.append(this.options);
It would be very helpful if you included more of the surrounding code so that we could know what is what, but here's my best shot at it given the current situation.
// Reference box
var $box = $('#id-of-select-box');
// Clear select box
$box.empty();
// START LOOP
// Create new option
var $option = $('<option value="'+List.id+'">"'+List.name+'"</option>');
// Append option to select box
$box.append($option);
// END LOOP
//Mark the currently displayed list as the selected option
if (activeListId > 0) {
$box.val(activeListId);
}
I whould suggest to create a new Element, then cut the provided name if exceeds thet maxSize using slice). Later we add the parameter "selected" if there is a match on activeListId and List.id. Latter we append the new option to Toolbar.ListSelect (I suposse it to be the element
var option = jQuery("<option />").attr('value', List.id);
var optionName = List.name.slice(maxSize);
option.text(optionName);
if ( activeListId && activeListId == List.id)
option.attr("selected", true);
option.appendTo(Toolbar.ListSelect)

Categories