How to programmatically create multiple cursors and set their position? - javascript

I'm making my Lua engine on CodeMirror (+ luaparser), I changed the show-hint.js addon code a bit, and I want to make sure that the cursor is created on each "$" character, which will be replaced with a void before inserting into the text. I already have an array of all coordinates for cursors, but I can't find an API anywhere on the Internet to create cursors.
ps. Cursors, I mean, those that are created by the combination "Ctrl + LMB"
pick: function(data, i) {
const completion = data.list[i], self = this;
this.cm.operation(function() {
const txt = completion.text;
const indices = [];
let minuses = 0;
for(let i=0; i<txt.length; i++) { // Find "$"
if (txt[i] === "$") {
indices.push(i-minuses);
minuses++;
}
}
completion.text = completion.text.replace('$', ''); // Remove "$"
if (completion.hint)
completion.hint(self.cm, data, completion);
else
self.cm.replaceRange(getText(completion), completion.from || data.from,
completion.to || data.to, "complete");
CodeMirror.signal(data, "pick", completion);
if(indices.length > 0){
for (const indicesKey in indices) {
// TODO: Creating cursors
// Bad try: self.cm.setCursor(data.from.line, data.from.ch+indices[indicesKey]);
}
}
self.cm.scrollIntoView();
});
if (this.options.closeOnPick) {
this.close();
}
}
code

Related

Unexpected Identifier while selecting elements from array

I'm tearing my hair out about a Syntax Error: Unexpected Identifier that I can't figure out. I know what the error means, but as far as I can tell there's nothing wrong.
I've posted the entirety of the script I'm using; what the code is meant to do is allow a user to step through a replay of a gomoku-like game one move at a time. The game data is stored in a csv file that has a row for every move and contains multiple games. Games are identified by an index value.
var replayArray = [],
rawData=[[]];
function importData(matchID,gI) {
var dataPromise = $.ajax({
url:"./data/" + matchID + ".csv",
dataType: 'text'
})
dataPromise.then(function(data) {
rawData = data;
rawData = String(rawData);
rawData = rawData.split(/\n/);
for (h = 0; h < rawData.length; h++){
rawData[h] = String(rawData[h]).split(",");
}
}).done(function(data){
dataToArray(gI,actionReplayKeydown);
})
}
function dataToArray(gI,cb) {
var f = 0;
var g = 0;
for (var i = 0; i < rawData.length; i++) {
var turnArray = [];
if (parseInt(eval(rawData[i][1])) === gI) {
turnArray[0] = colorToNumber(eval(rawData[i][5]));
turnArray[1] = parseInt(eval(rawData[i][6]));
replayArray[g] = turnArray;
g++;
} else {
doNothing();
}
}
cb(replayArray);
}
The dataToArray function is where the problem occurs, in the line
if (parseInt(eval(rawData[i][1])) === gI) {
I think dev tools has been indicating the problem occurs at rawData[i][1], but rawData is a two dimensional array and the indexing should work fine (the first column of rawData contains the game index, and I want all rows where the value of the game index equals the index of the queried game).
The rest of the code follows but is not afaik problematic.
function colorToNumber(inputColor) {
if (inputColor === "B" ) {
return 0
} else {
return 1
}
}
function actionReplay(inputArray) {
addStone(parseInt(inputArray[f][1]),parseInt(inputArray[f][0]));
f++;
$('#whiteLastMove').remove();
$('#blackLastMove').remove();
if ((f+1)===inputArray.length){
$(document).off('keyup').on('keyup',function(e){
if (e.keyCode === 32) {
clearBoard();
createTiles(M,N);
replayArray = [];
rawData="";
}
});
}
}
function actionReplayKeydown() {
$(document).off('keyup').on('keyup',function(e) {
if (e.keyCode === 13) {
actionReplay(replayArray);
evaluateWin(0);
evaluateWin(1);
} else if (e.keyCode === 32) {
clearBoard();
createTiles(M,N);
replayArray = [];
rawData="";
} else {
doNothing();
}
});
}
function playReplay(matchID,gI) {
openCurtain(doNothing);
importData(matchID,gI);
}
I'm sure I'm missing something obvious, but I'm just not figuring it out on my own.
The issue is that there is a js syntax error in the value of rawData[i][1]. If you use your debugger you can see the value and check whether it's valid js for eval to execute.

charAt is not a function

I'm trying to create a key mapping that keeps track of the frequency for each character of a string in my createArrayMap() function but I keep getting this error from firebug: TypeError: str.charAt(...) is not a function
I found the charAt() function on Mozilla's developer website it should be a function that exists.
var input;
var container;
var str;
var arrMapKey = [];
var arrMapValue = [];
function initDocElements() {
container = document.getElementById("container");
input = document.getElementById("inputbox");
}
function createArrayMap() {
str = input.value;
for (var i = 0; i < str.length; i++) {
if (arrMapKey.find(str.charAt(i)) == undefined) {
arrMapKey.push(str.charAt(i));
arrMapValue.push(1);
}
}
}
function keyPressHandler() {
createArrayMap();
console.log(arrMapKey);
console.log(arrMapValue);
}
function prepareEventHandlers() {
input.onfocus = function() {
if (this.value == "Start typing here!") {
this.value = "";
}
};
input.onblur = function() {
if (this.value == "") {
this.value = "Start typing here!";
}
};
input.onkeyup = keyPressHandler;
}
window.onload = function() {
initDocElements();
prepareEventHandlers();
};
The problem is not with String.charAt(), but with Array.find().
The first argument to find is a callback, but the result of str.charAt(i) is a character and not a callback function.
To search for an element in your array, you could use Array.indexOf() as #adeneo already suggested in a comment
function createArrayMap() {
var str = input.value;
for (var i = 0; i < str.length; i++) {
if (arrMapKey.indexOf(str.charAt(i)) == -1) {
arrMapKey.push(str.charAt(i));
arrMapValue.push(1);
}
}
}
See JSFiddle
You're not going about things in the most efficient manner... What if you changed it to look like this so you are continually updated with each keypress?
var keyMap = {};
...
input.onkeyup = keyPressHandler;
function keyPressHandler(e) {
var char = String.fromCharCode(e.keyCode);
if(!(char in keyMap))
keyMap[char] = 1;
else
keyMap[char]++;
}
This has been answered, but here's my version of your problem JSBIN LINK (also has an object option in addition to the array solution).
I moved some variables around so you'll have less global ones, added comments, and mocked with the output so it'll show it on the page instead of the console.
besides the Array.find() issues, you weren't initializing your arrays on the build method, and so, you would have probably ended with the wrong count of letters.
HTML:
<div id="container">
<textArea id="inputbox"></textArea></div>
<p id="output">output will show here</p>
JS:
var input, // Global variables
container, //
output; //
/**
* Initialize components
*/
function initDocElements() {
container = document.getElementById("container");
input = document.getElementById("inputbox");
output = document.getElementById("output");
}
/**
* Creates the letters frequency arrays.
* Note that every time you click a letter, this is done from scratch.
* Good side: no need to deal with "backspace"
* Bad side: efficiency. Didn't try this with huge texts, but you get the point ...
*/
function createArrayMap() {
var index, // obvious
tempChar, // temp vars for: char
tempStr = input.value, // string
len = tempStr.length, // for loop iteration
arrMapKey = [], // our keys
arrMapValue = []; // our values
for (var i = 0 ; i <len ; i++) {
// These 2 change each iteration
tempChar = tempStr.charAt(i);
index = arrMapKey.indexOf(tempChar);
// If key exists, increment value
if ( index > -1) {
arrMapValue[index]++;
}
// Otherwise, push to keys array, and push 1 to value array
else {
arrMapKey.push(tempChar);
arrMapValue.push(1);
}
}
// Some temp output added, instead of cluttering the console, to the
// a paragraph beneath the text area.
output.innerHTML = "array keys: "+arrMapKey.toString() +
"<br/>array values:"+arrMapValue.toString();
}
function keyPressHandler() {
createArrayMap();
}
function prepareEventHandlers() {
input.onfocus = function() {
if (this.value == "Start typing here!") {
this.value = "";
}
};
input.onblur = function() {
if (this.value === "") {
this.value = "Start typing here!";
}
};
input.onkeyup = keyPressHandler;
}
window.onload = function() {
initDocElements();
prepareEventHandlers();
};
BTW, as the comments suggest, doing this with an object will is much nicer and shorter, since all you care is if the object has the current char as a property:
/**
* Same as above method, using an object, instead of 2 arrays
*/
function createObject() {
var index, // obvious
tempChar, // temp vars for: char
tempStr = input.value, // string
len = tempStr.length, // for loop iteration
freqObj = {}; // our frequency object
for (var i = 0 ; i <len ; i++) {
tempChar = tempStr.charAt(i); // temp char value
if (freqObj.hasOwnProperty(tempChar))
freqObj[tempChar]++;
else
freqObj[tempChar] = 1;
}
}

Get rid of "PageMap asked for range which it does not have" exception

Occassionally I get the exception "PageMap asked for range which it does not have" from my Ext Js 4.2.1 infinite scrolling grid. It is raised in data/PageMap.js on line 211. Of course one should not ask for non-existing entries, but this is sometimes done by the framework itself. Seems to be somehow connected to adding/removing records or reloading the grid. There are already some threads on this topic in the Sencha forum, e.g. this, but no killer solution or bugfix was proposed yet.
Meanwhile, I have to keep this exception from the users' eyes. What would be a good way to do so? Tricky thing is that it is sometimes provoked just by the user moving the scrollbar, so there is no single line of my code directly involved.
I found the root cause to be that when it's rendering rows, it determines if it's before a selected row. If it's working on the last row, it still looks for row + 1. (Ext.view.Table:931 in 4.2.1)
My simple solution is to just make it return false:
Ext.override(Ext.selection.RowModel,
{
isRowSelected: function (record, index)
{
try
{
return this.isSelected(record);
}
catch (e)
{
return false;
}
}
});
Christoph,
I have similar troubles with "PageMap asked for range which it does not have" during asynchronuous refreshing of grids. I catched some of sources of errors in the ExtJS 4.2.1 code and created simple override, that works for me. You can try if it will work for you. I will be happy for your feedback.
Ext.override(Ext.view.Table, {
getRecord: function (node) {
node = this.getNode(node);
if (node) {
var recordIndex = node.getAttribute('data-recordIndex');
if (recordIndex) {
recordIndex = parseInt(recordIndex, 10);
if (recordIndex > -1) {
// Eliminates one of sources of "PageMap asked for range which it does not have" error
if (this.store.getCount() > 0) {
return this.store.data.getAt(recordIndex);
}
}
}
return this.dataSource.data.get(node.getAttribute('data-recordId'));
}
},
renderRow: function (record, rowIdx, out) {
var me = this,
isMetadataRecord = rowIdx === -1,
selModel = me.selModel,
rowValues = me.rowValues,
itemClasses = rowValues.itemClasses,
rowClasses = rowValues.rowClasses,
cls,
rowTpl = me.rowTpl;
rowValues.record = record;
rowValues.recordId = record.internalId;
rowValues.recordIndex = rowIdx;
rowValues.rowId = me.getRowId(record);
rowValues.itemCls = rowValues.rowCls = '';
if (!rowValues.columns) {
rowValues.columns = me.ownerCt.columnManager.getColumns();
}
itemClasses.length = rowClasses.length = 0;
if (!isMetadataRecord) {
itemClasses[0] = Ext.baseCSSPrefix + "grid-row";
if (selModel && selModel.isRowSelected) {
var storeRows = this.getStore().getCount();
// Eliminates one of sources of "PageMap asked for range which it does not have" error
if (rowIdx + 1 < storeRows) {
if (selModel.isRowSelected(rowIdx + 1)) {
itemClasses.push(me.beforeSelectedItemCls);
}
}
if (selModel.isRowSelected(record)) {
itemClasses.push(me.selectedItemCls);
}
}
if (me.stripeRows && rowIdx % 2 !== 0) {
rowClasses.push(me.altRowCls);
}
if (me.getRowClass) {
cls = me.getRowClass(record, rowIdx, null, me.dataSource);
if (cls) {
rowClasses.push(cls);
}
}
}
if (out) {
rowTpl.applyOut(rowValues, out);
} else {
return rowTpl.apply(rowValues);
}
}
});
all these codes don't work for me, after many debugging I wrote this override which solve the problem.
Ext.define('overrides.LruCache', {
override: 'Ext.util.LruCache',
// private. Only used by internal methods.
unlinkEntry: function (entry) {
// Stitch the list back up.
if (entry) {
if (this.last && this.last.key == entry.key)
this.last = entry.prev;
if (this.first && this.first.key == entry.key)
this.first = entry.next;
if (entry.next) {
entry.next.prev = entry.prev;
} else {
this.last = entry.prev;
}
if (entry.prev) {
entry.prev.next = entry.next;
} else {
this.first = entry.next;
}
entry.prev = entry.next = null;
}
}
});
This is my solution for my specific case with the same error
it somehow lost DOM element for child
this code fix that
Ext.define('override.Ext.view.Table', {
/**
* Returns the node given the passed Record, or index or node.
* #param {HTMLElement/String/Number/Ext.data.Model} nodeInfo The node or record
* #param {Boolean} [dataRow] `true` to return the data row (not the top level row if wrapped), `false`
* to return the top level row.
* #return {HTMLElement} The node or null if it wasn't found
*/
override: 'Ext.view.Table',
getNode: function (nodeInfo, dataRow) {
// if (!dataRow) dataRow = false
var fly,
result = this.callParent(arguments)
if (result && result.tagName) {
if (dataRow) {
if (!(fly = Ext.fly(result)).is(this.dataRowSelector)) {
result = fly.down(this.dataRowSelector, true)
}
} else if (dataRow === false) {
if (!(fly = Ext.fly(result)).is(this.itemSelector)) {
result = fly.up(this.itemSelector, null, true)
}
if (this.xtype == 'gridview' && !this.body.dom.querySelector(`#${result.id}`)) {
result = null
}
}
}
return result
},
})

Mutable variable is accessible from closure. How can I fix this?

I am using Typeahead by twitter. I am running into this warning from Intellij. This is causing the "window.location.href" for each link to be the last item in my list of items.
How can I fix my code?
Below is my code:
AutoSuggest.prototype.config = function () {
var me = this;
var comp, options;
var gotoUrl = "/{0}/{1}";
var imgurl = '<img src="/icon/{0}.gif"/>';
var target;
for (var i = 0; i < me.targets.length; i++) {
target = me.targets[i];
if ($("#" + target.inputId).length != 0) {
options = {
source: function (query, process) { // where to get the data
process(me.results);
},
// set max results to display
items: 10,
matcher: function (item) { // how to make sure the result select is correct/matching
// we check the query against the ticker then the company name
comp = me.map[item];
var symbol = comp.s.toLowerCase();
return (this.query.trim().toLowerCase() == symbol.substring(0, 1) ||
comp.c.toLowerCase().indexOf(this.query.trim().toLowerCase()) != -1);
},
highlighter: function (item) { // how to show the data
comp = me.map[item];
if (typeof comp === 'undefined') {
return "<span>No Match Found.</span>";
}
if (comp.t == 0) {
imgurl = comp.v;
} else if (comp.t == -1) {
imgurl = me.format(imgurl, "empty");
} else {
imgurl = me.format(imgurl, comp.t);
}
return "\n<span id='compVenue'>" + imgurl + "</span>" +
"\n<span id='compSymbol'><b>" + comp.s + "</b></span>" +
"\n<span id='compName'>" + comp.c + "</span>";
},
sorter: function (items) { // sort our results
if (items.length == 0) {
items.push(Object());
}
return items;
},
// the problem starts here when i start using target inside the functions
updater: function (item) { // what to do when item is selected
comp = me.map[item];
if (typeof comp === 'undefined') {
return this.query;
}
window.location.href = me.format(gotoUrl, comp.s, target.destination);
return item;
}
};
$("#" + target.inputId).typeahead(options);
// lastly, set up the functions for the buttons
$("#" + target.buttonId).click(function () {
window.location.href = me.format(gotoUrl, $("#" + target.inputId).val(), target.destination);
});
}
}
};
With #cdhowie's help, some more code:
i will update the updater and also the href for the click()
updater: (function (inner_target) { // what to do when item is selected
return function (item) {
comp = me.map[item];
if (typeof comp === 'undefined') {
return this.query;
}
window.location.href = me.format(gotoUrl, comp.s, inner_target.destination);
return item;
}}(target))};
I liked the paragraph Closures Inside Loops from Javascript Garden
It explains three ways of doing it.
The wrong way of using a closure inside a loop
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
Solution 1 with anonymous wrapper
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
Solution 2 - returning a function from a closure
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}
Solution 3, my favorite, where I think I finally understood bind - yaay! bind FTW!
for(var i = 0; i < 10; i++) {
setTimeout(console.log.bind(console, i), 1000);
}
I highly recommend Javascript garden - it showed me this and many more Javascript quirks (and made me like JS even more).
p.s. if your brain didn't melt you haven't had enough Javascript that day.
You need to nest two functions here, creating a new closure that captures the value of the variable (instead of the variable itself) at the moment the closure is created. You can do this using arguments to an immediately-invoked outer function. Replace this expression:
function (item) { // what to do when item is selected
comp = me.map[item];
if (typeof comp === 'undefined') {
return this.query;
}
window.location.href = me.format(gotoUrl, comp.s, target.destination);
return item;
}
With this:
(function (inner_target) {
return function (item) { // what to do when item is selected
comp = me.map[item];
if (typeof comp === 'undefined') {
return this.query;
}
window.location.href = me.format(gotoUrl, comp.s, inner_target.destination);
return item;
}
}(target))
Note that we pass target into the outer function, which becomes the argument inner_target, effectively capturing the value of target at the moment the outer function is called. The outer function returns an inner function, which uses inner_target instead of target, and inner_target will not change.
(Note that you can rename inner_target to target and you will be okay -- the closest target will be used, which would be the function parameter. However, having two variables with the same name in such a tight scope could be very confusing and so I have named them differently in my example so that you can see what's going on.)
In ecmascript 6 we have new opportunities.
The let statement declares a block scope local variable, optionally initializing it to a value.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
Since the only scoping that JavaScript has is function scope, you can simply move the closure to an external function, outside of the scope you're in.
Just to clarify on #BogdanRuzhitskiy answer (as I couldn't figure out how to add the code in a comment), the idea with using let is to create a local variable inside the for block:
for(var i = 0; i < 10; i++) {
let captureI = i;
setTimeout(function() {
console.log(captureI);
}, 1000);
}
This will work in pretty much any modern browser except IE11.

Javascript text highlight function

Scenario
I am trying to develop a Javascript text highlight function.
It receives in input a text to search inside, an array of tokens to be searched, a class to wrap the found matches:
var fmk = fmk || {};
fmk.highlight = function (target, tokens, cls) {
var token, re;
if (tokens.length > 0) {
token = tokens.pop();
re = new RegExp(token, "gi");
return this.highlight(
target.replace(re, function (matched) {
return "<span class=\"" + cls + "\">" + matched + "</span>";
}), tokens, cls);
}
else { return target; }
};
It is based on a recursive replace that wraps a <span> tag around the found matches.
JsFiddle demo.
Issues
if there are two tokens, and the latter is a substring of the former then only the latter token will be highligthed. In the jsFiddle example try these tokens: 'ab b'.
if the tokens contains a substring of the wrapper sequence (i.e. <span class="[className]"></span>) and another matching token, then the highlight fails and returns a dirty result. In the jsFiddle example try these tokens: 'red ab'.
Note that single character tokens are admitted in the actual application.
Questions
How to avoid these errors? I figured out these approaches:
To pre-process the tokens, removing the tokens that are substrings of others. Disadvantages: it requires O(n^2) searches in the pre-processing phase in case of n tokens; good matches are cut off.
To pre-process the matches BEFORE applying the wrapper, in order to cut off only the substrings matches. Disadvantages: again, further computation required. Anyway, I don't know where to start from implement this inside the replace callback function.
I think the way to handle this is to loop through all descendants of an element, check if it's a text node, and replace the appropriate content wrapped with a span/class.
var MyApp = {};
MyApp.highlighter = (function () {
"use strict";
var checkAndReplace, func,
id = {
container: "container",
tokens: "tokens",
all: "all",
token: "token",
className: "className",
sensitiveSearch: "sensitiveSearch"
};
checkAndReplace = function (node, tokenArr, classNameAll, sensitiveSearchAll) {
var nodeVal = node.nodeValue, parentNode = node.parentNode,
i, j, curToken, myToken, myClassName, mySensitiveSearch,
finalClassName, finalSensitiveSearch,
foundIndex, begin, matched, end,
textNode, span;
for (i = 0, j = tokenArr.length; i < j; i++) {
curToken = tokenArr[i];
myToken = curToken[id.token];
myClassName = curToken[id.className];
mySensitiveSearch = curToken[id.sensitiveSearch];
finalClassName = (classNameAll ? myClassName + " " + classNameAll : myClassName);
finalSensitiveSearch = (typeof sensitiveSearchAll !== "undefined" ? sensitiveSearchAll : mySensitiveSearch);
if (finalSensitiveSearch) {
foundIndex = nodeVal.indexOf(myToken);
} else {
foundIndex = nodeVal.toLowerCase().indexOf(myToken.toLowerCase());
}
if (foundIndex > -1) {
begin = nodeVal.substring(0, foundIndex);
matched = nodeVal.substr(foundIndex, myToken.length);
end = nodeVal.substring(foundIndex + myToken.length, nodeVal.length);
if (begin) {
textNode = document.createTextNode(begin);
parentNode.insertBefore(textNode, node);
}
span = document.createElement("span");
span.className += finalClassName;
span.appendChild(document.createTextNode(matched));
parentNode.insertBefore(span, node);
if (end) {
textNode = document.createTextNode(end);
parentNode.insertBefore(textNode, node);
}
parentNode.removeChild(node);
}
}
};
func = function (options) {
var iterator,
tokens = options[id.tokens],
allClassName = options[id.all][id.className],
allSensitiveSearch = options[id.all][id.sensitiveSearch];
iterator = function (p) {
var children = Array.prototype.slice.call(p.childNodes),
i, cur;
if (children.length) {
for (i = 0; i < children.length; i++) {
cur = children[i];
if (cur.nodeType === 3) {
checkAndReplace(cur, tokens, allClassName, allSensitiveSearch);
} else if (cur.nodeType === 1) {
iterator(cur);
}
}
}
};
iterator(options[id.container]);
};
return func;
})();
window.onload = function () {
var container = document.getElementById("container");
MyApp.highlighter({
container: container,
all: {
className: "highlighter"
},
tokens: [{
token: "sd",
className: "highlight-sd",
sensitiveSearch: false
}, {
token: "SA",
className: "highlight-SA",
sensitiveSearch: true
}]
});
};
DEMO: http://jsfiddle.net/UWQ6r/1/
I set it up so you can change the values in id so that you can use different keys in the {} passed to highlighter.
The two settings in the all object refer to a class being added no matter what, as well as a case sensitive search override. For each token, you specify the token, class, and whether the match should be case sensitive.
References:
nodeType: https://developer.mozilla.org/en-US/docs/DOM/Node.nodeType
childNodes: https://developer.mozilla.org/en-US/docs/DOM/Node.childNodes
substr: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/substr
substring: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/substring
insertBefore: https://developer.mozilla.org/en-US/docs/DOM/Node.insertBefore
This seems to work for me:
(line 17 in your JsFiddle demo)
Issue 1: var tokens = [['ab','b'].join("|")];
Issue 2: var tokens = ['<span'.replace(/</g,"<")];
All together, then:
var tokens = [[..my tokens..].sort().join("|").replace(/</g,"<")];
(by the way, I did test tokens such as '"', '"s' or 'span' and they seem to work fine. Also, I'm not sure why .sort() is important here but I left it in since I like to stay close to the original code.)

Categories