Set selection on tekst inside CKEditor - ckeditor

I'm having trouble to select text in CKEditor(3.6). As we use plain text i dont know how to use correctly the range selectors.
HTML code of the CKEditor:
<body spellcheck="false" class="rf-ed-b" contenteditable="true">
<br>
Cross those that apply:<br>
<br>
<br>
[«dummy»] If he/she is tall<br>
<br>
[«dummy»] If he/she is a male<br>
<br>
[«dummy»] If he/shi is a minor<br>
<br>
Specialties:<br>
<br>
[«dummy»] «Write here the specialties if known»<br>
<br>
<br>
«You are now done with filling in this form»<br>
</body>
With the keys 'CRTL+N' I want to go to the next filleble spot:
«[label]»
I tried stuff like:
var editor = CKEDITOR.instances['MyEditor'];
var findString = '«';
var element = editor.document.getBody();
var ranges = editor.getSelection().getRanges();
var startIndex = element.getHtml().indexOf(findString);
if (startIndex != -1) {
ranges[0].setStart(element.getFirst(), startIndex);
ranges[0].setEnd(element.getFirst(), startIndex + 5);
editor.getSelection().selectRanges([ranges[0]]);
}
Error:
Exception: Index or size is negative or greater than the allowed amount
While totally stripepd down it kinda works a bit:
var editor = CKEDITOR.instances['MyEditor'];
var ranges = editor.getSelection().getRanges();
var startIndex = 10;
if (startIndex != -1) {
ranges[0].setStart(element.getFirst(), startIndex);
ranges[0].setEnd(element.getFirst(), startIndex + 5);
editor.getSelection().selectRanges([ranges[0]]);
}
here it selects 5th till 10th char on first row.
I used the following sources:
example on Stackoverflow
Another stackoverflow example
CKEditor dom selection API
All solutions i can find work with html nodes.
How can set selection range on the '«' till next '»'

I've managed to solve this solution. Meanwhile i also upgraded CKeditor to 4.0.
This shouldnt have an impact on the solution.
It is a lot of code in JS.
On my keybinding i call the following JS function: getNextElement()
In this solution it also searches behind the cursor, this makes it possible to step through multiple find results.
Also the view gets scrolled to the next search result
var textNodes = [], scrollTo=0,ranges = [];
function getNextElement(){
var editor =null;
ranges = [];
// I dont know the ID of the editor, but i know there is only one the page
for(var i in CKEDITOR.instances){
editor = CKEDITOR.instances[i];
}
if(editor ==null){
return;
}
editor.focus();
var startRange = editor.getSelection().getRanges()[0];
var cursorData ="",cursorOffset=0,hasCursor = false;
if(startRange != null && startRange.endContainer.$.nodeType == CKEDITOR.NODE_TEXT){
cursorOffset = startRange.startOffset;
cursorData = startRange.endContainer.$.data;
hasCursor = true;
}
var element;
element = editor.document.getBody().getLast().getParent();
var selection = editor.getSelection();
// Recursively search for text nodes starting from root.
textNodes = [];
getTextNodes( element );
var foundElement = false;
foundElement = iterateEditor(editor,hasCursor,cursorData,cursorOffset);
if(!foundElement){
foundElement =iterateEditor(editor,false,"",0);
}
if(foundElement){
// Select the range with the first << >>.
selection.selectRanges( ranges );
jQuery(".cke_wysiwyg_frame").contents().scrollTop(scrollTo);
}
}
function iterateEditor(editor,hasCursor,cursorData,cursorOffset){
var foundElement = false;
var rowNr = 0;
var text, range;
var foundNode = false;
if(!hasCursor){
foundNode = true;
}
// Iterate over and inside the found text nodes. If some contains
// phrase "<< >>", create a range that selects this word.
for (var i = textNodes.length; i--; ) {
text = textNodes[ i ];
if ( text.type == CKEDITOR.NODE_ELEMENT && text.getName() == "br" ){
rowNr++;
} else if ( text.type == CKEDITOR.NODE_TEXT ) {
var sameNode = false;
if(text.$.data == cursorData){
foundNode = true;
sameNode = true;
}
if(foundNode){
var startIndex = -1;
var endIndex = 1;
if(sameNode){
// Check inside the already selected node if the text has multiple hits on the searchphrase
var indicesStart = getIndicesOf('\u00AB', text.getText());
var indicesEnd = getIndicesOf('\u00BB', text.getText());
for (var j = indicesStart.length; j--; ) {
if(indicesStart[j] > cursorOffset){
startIndex = indicesStart[j];
endIndex = indicesEnd[j];
}
}
} else{
startIndex = text.getText().indexOf( '\u00AB' );
endIndex = text.getText().indexOf( '\u00BB' );
}
if ( startIndex > -1 && (!sameNode || startIndex > cursorOffset)) {
range = editor.createRange();
range.setStart( text, startIndex );
foundElement = true;
// calculate the height the window should scroll to focus the selected element
scrollTo = (rowNr)*20;
}
if ( endIndex > -1 && foundElement ) {
range.setEnd( text, endIndex+1 );
ranges.push( range );
return true;
}
}
}
}
}
function getIndicesOf(searchStr, str) {
var startIndex = 0, searchStrLen = searchStr.length;
var index, indices = [];
while ((index = str.indexOf(searchStr, startIndex)) > -1) {
indices.push(index);
startIndex = index + searchStrLen;
}
return indices;
}
function getTextNodes( element ) {
var children = element.getChildren(), child;
for ( var i = children.count(); i--; ) {
child = children.getItem( i );
textNodes.push( child );
}
}

Related

Is there a way I can make my script more efficient?

I have created the below script to add a timestamp when a cell equals a specific value. This script repeats for each row but it is not efficient. How can I make this simpler and quicker? Below is an extract of the script, it repeats for each row
function MyFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet()
var sheet = ss.getSheetByName('Overview')
var cell = sheet.getRange('H3').getValue()
var sent = sheet.getRange('p3').getValue()
if (cell == "Full" && sent == "") {
sheet.getRange('p3').setValue(new Date())
}
else if (cell == "Open" && sent == "") {
sheet.getRange('p3').setValue("")
}
}
Try this.
function MyFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Overview');
// get range H3:P were H=8, P=16, 9 columns total or index 0 to 8
var data = sheet.getRange(3,8,sheet.getLastRow()-2,9).getValues(); // get range H3:P
var i = 0;
var row = null;
for( i=0; i<data.length; i++ ) {
row = data[i];
if( ( row[0] === "Full" ) && ( row[8] === "" ) ) {
row[8] = new Date();
}
// Your else doesn't do anything if blank set blank ??
row.splice(0,8); // extract only column P
}
sheet.getRange(3,16,data.length,1).setValues(data);
}
Try using setvalues() method of class range
function MyFunction() {
const ss = SpreadsheetApp.getActive()
const sh = ss.getSheetByName('Overview');
const rg = sh.getDataRange();
const vs = rg.getValues();
let vo = vs.map(r => {
if( r[7] == "Full") r[15]=new Date()
if( r[7] == "Open") r[15]='';
return [r[15]];
});
sh.getRange(2,16,vo.length,1).setValues(vo);
}

In Boomla, how can I easily find the next sibling of a file

I'm quite used to nextSiblingand nextElementSibling in the DOM. Is there an easy way of doing a similar thing with Boomla files?
I would need the next sibling within the same placeholder (and null if this is the last), but I'd be interested about finding the next sibling in any placeholder (and null if this is the last file in the last placeholder).
Currently, there is no built-in method for this.
Here are 2 methods for the sjs-4 engine for getting the next in the placeholder or parent:
var nextInBucket = function(f) {
var bucket = f.bucketId();
var bucketSiblings = f.query("../:" + bucket);
var path = f.path();
var index = 0;
var found = false;
bucketSiblings.each(function(t) {
if (t.path() == path) {
found = true;
return false;
}
index++;
});
if ( ! found) {
return null;
}
return bucketSiblings.eq(index + 1);
}
var nextInParent = function(f) {
var parentChildren = f.query("../*");
var path = f.path();
var index = 0;
var found = false;
parentChildren.each(function(t) {
if (t.path() == path) {
found = true;
return false;
}
index++;
});
if ( ! found) {
return null;
}
return parentChildren.eq(index + 1);
}

How to create all possible variations from single string presented in special format?

Let's say, I have following template.
Hello, {I'm|he is} a {notable|famous} person.
Result should be
Hello, I'm a notable person.
Hello, I'm a famous person.
Hello, he is a notable person.
Hello, he is a famous person.
The only possible solution I have in mind - full search, but it is not effective.
May be there is a good algorithm for such kind of job but I do not know what task about. All permutations in array is very close to this but I have no idea how to use it here.
Here is working solution (it's part of object, so here is only relevant part).
generateText() parses string and converts 'Hello, {1|2}, here {3,4}' into ['Hello', ['1', '2'], 'here', ['3', '4']]]
extractText() takes this multidimensional array and creates all possible strings
STATE_TEXT: 'TEXT',
STATE_INSIDE_BRACKETS: 'INSIDE_BRACKETS',
generateText: function(text) {
var result = [];
var state = this.STATE_TEXT;
var length = text.length;
var simpleText = '';
var options = [];
var singleOption = '';
var i = 0;
while (i < length) {
var symbol = text[i];
switch(symbol) {
case '{':
if (state === this.STATE_TEXT) {
simpleText = simpleText.trim();
if (simpleText.length) {
result.push(simpleText);
simpleText = '';
}
state = this.STATE_INSIDE_BRACKETS;
}
break;
case '}':
if (state === this.STATE_INSIDE_BRACKETS) {
singleOption = singleOption.trim();
if (singleOption.length) {
options.push(singleOption);
singleOption = '';
}
if (options.length) {
result.push(options);
options = [];
}
state = this.STATE_TEXT;
}
break;
case '|':
if (state === this.STATE_INSIDE_BRACKETS) {
singleOption = singleOption.trim();
if (singleOption.length) {
options.push(singleOption);
singleOption = '';
}
}
break;
default:
if (state === this.STATE_TEXT) {
simpleText += symbol;
} else if (state === this.STATE_INSIDE_BRACKETS) {
singleOption += symbol;
}
break;
}
i++;
}
return result;
},
extractStrings(generated) {
var lengths = {};
var currents = {};
var permutations = 0;
var length = generated.length;
for (var i = 0; i < length; i++) {
if ($.isArray(generated[i])) {
lengths[i] = generated[i].length;
currents[i] = lengths[i];
permutations += lengths[i];
}
}
var strings = [];
for (var i = 0; i < permutations; i++) {
var string = [];
for (var k = 0; k < length; k++) {
if (typeof lengths[k] === 'undefined') {
string.push(generated[k]);
continue;
}
currents[k] -= 1;
if (currents[k] < 0) {
currents[k] = lengths[k] - 1;
}
string.push(generated[k][currents[k]]);
}
strings.push(string.join(' '));
}
return strings;
},
The only possible solution I have in mind - full search, but it is not effective.
If you must provide full results, you must run full search. There is simply no way around it. You don't need all permutations, though: the number of results is equal to the product of the number of alternatives in each template.
Although there are multiple ways to implement this, recursion is among the most popular approaches. Here is some pseudo-code to get you started:
string[][] templates = {{"I'm", "he is"}, {"notable", "famous", "boring"}}
int[] pos = new int[templates.Length]
string[] fills = new string[templates.Length]
recurse(templates, fills, 0)
...
void recurse(string[][] templates, string[] fills, int pos) {
if (pos == fills.Length) {
formatResult(fills);
} else {
foreach option in templates[pos] {
fills[pos] = option
recurse(templates, fills, pos+1);
}
}
}
It seems like the best solution here is going to be n*m where n=the first array and m= the second array . There are nm required lines of output, which means that as long as you are only doing nm you aren't doing any extra work
The generic running time for this is where there is more than 2 arrays with options, it would be
n1*n2...*nm where each of those is equal to the size of the respective list
A nested loop where you just print out the value for the current index of the outer loop along with the current value for the index of the inner loop should do this properly

Google sheets, get range of rows, where column intersect cell is empty

Basically I want to use an auto-sorting script through the script editor in Google sheets, but I want it to smartly pick a range of rows, by only selecting those rows where a specific column has an empty cell.
All other rows that have that column's cell populated would stay put. Here is the sort code I've found.
/**
* Automatically sorts the 1st column (not the header row) Ascending.
*/
function onEdit(event){
var sheet = event.source.getActiveSheet();
var editedCell = sheet.getActiveCell();
var columnToSortBy = 1;
var tableRange = "A2:T99"; // What to sort.
if(editedCell.getColumn() == columnToSortBy){
var range = sheet.getRange(tableRange);
range.sort( { column : columnToSortBy, ascending: true } );
}
}
Thanks.
Note: this will only sort the values in the cells (metadata such as formats, notes etc will not be sorted, as they would with the GAS range.sort() method).
function onEdit(event)
{
var sheet = event.source.getActiveSheet();
var editedCell = sheet.getActiveCell();
var columnToSortBy = 0; // zero-based index
if (editedCell.getColumn() == columnToSortBy)
{
var columnToWatch = 5; // checking this column for blank cells (zero-based index)
var tableRange = "A2:T99"; // what to sort
var range = sheet.getRange(tableRange);
var data = range.getValues();
var dataCopy = data.slice();
dataCopy.sort(function(a, b) {return a[columnToSortBy] > b[columnToSortBy] ? 1 : (a[columnToSortBy] < b[columnToSortBy] ? -1 : 0);});
var j = -1;
for (var i = 0, length = data.length; i < length; i++)
{
if (data[i][columnToWatch].toString().length)
{
do j++; while (!dataCopy[j][columnToWatch].toString().length);
data[i] = dataCopy[j];
}
}
range.setValues(data);
}
}

Column sort reset of ascending/descending on 3rd click

Suppose we've a grid with sortable columns.
If a user clicks on a column, it gets sorted by 'asc', then if a user clicks the column header again, it gets sorted by 'desc', now I want similar functionality when a user clicks the column third time, the sorting is removed, i.e. returned to previous style, the css is changed back to normal/non-italic etc?
Today I was trying to achieve same thing. I've skimmed through SlickGrid code but didn't find any 'Reset' function. So, I've tweaked the slick.grid.js v2.2 a little bit.
Basically you just need to add a new array - 'latestSortColumns' which will store sort columns state (deep copy of internal SlickGrid sortColumns array).
var latestSortColumns = [];
Optionally add new setting to opt-in for sort reset.
var defaults = {
(...),
autoResetColumnSort: false
};
Change setupColumnSort to reset sort after third click on column's header.
function setupColumnSort() {
$headers.click(function (e) {
// temporary workaround for a bug in jQuery 1.7.1 (http://bugs.jquery.com/ticket/11328)
e.metaKey = e.metaKey || e.ctrlKey;
if ($(e.target).hasClass("slick-resizable-handle")) {
return;
}
var $col = $(e.target).closest(".slick-header-column");
if (!$col.length) {
return;
}
var column = $col.data("column");
if (column.sortable) {
if (!getEditorLock().commitCurrentEdit()) {
return;
}
var sortOpts = null;
var i = 0;
for (; i < sortColumns.length; i++) {
if (sortColumns[i].columnId == column.id) {
sortOpts = sortColumns[i];
sortOpts.sortAsc = !sortOpts.sortAsc;
break;
}
}
**if ((e.metaKey || (options.autoResetColumnSort && latestSortColumns[i] != null && latestSortColumns[i].sortAsc === !column.defaultSortAsc)) && options.multiColumnSort) {**
if (sortOpts) {
sortColumns.splice(i, 1);
**latestSortColumns.splice(i, 1);**
}
}
else {
if ((!e.shiftKey && !e.metaKey) || !options.multiColumnSort) {
sortColumns = [];
}
if (!sortOpts) {
sortOpts = { columnId: column.id, sortAsc: column.defaultSortAsc };
sortColumns.push(sortOpts);
} else if (sortColumns.length == 0) {
sortColumns.push(sortOpts);
}
}
setSortColumns(sortColumns);
if (!options.multiColumnSort) {
trigger(self.onSort, {
multiColumnSort: false,
sortCol: column,
sortAsc: sortOpts.sortAsc}, e);
} else {
trigger(self.onSort, {
multiColumnSort: true,
sortCols: $.map(sortColumns, function(col) {
return {sortCol: columns[getColumnIndex(col.columnId)], sortAsc: col.sortAsc };
})}, e);
}
}
});
}
Store new state after every sort change in latestSortColumns:
function setSortColumns(cols) {
sortColumns = cols;
var headerColumnEls = $headers.children();
headerColumnEls
.removeClass("slick-header-column-sorted")
.find(".slick-sort-indicator")
.removeClass("slick-sort-indicator-asc slick-sort-indicator-desc");
$.each(sortColumns, function(i, col) {
if (col.sortAsc == null) {
col.sortAsc = true;
}
var columnIndex = getColumnIndex(col.columnId);
if (columnIndex != null) {
headerColumnEls.eq(columnIndex)
.addClass("slick-header-column-sorted")
.find(".slick-sort-indicator")
.addClass(col.sortAsc ? "slick-sort-indicator-asc" : "slick-sort-indicator-desc");
}
});
**for (var i = 0; i < sortColumns.length; i++)
latestSortColumns[i] = { columnId: sortColumns[i].columnId, sortAsc: sortColumns[i].sortAsc };**
}
That's all, should work now.
This is a function I call to reset all columns back to their original order.
It requires one of columns to be set up as not sortable.
In the example below I use the first column of the table, columns[0], which has the field id "locus".
function removeSorting() {
columns[0].sortable = true;
$('.slick-header-columns').children().eq(0).trigger('click');
columns[0].sortable = false;
// clear other sort columns
grid.setSortColumns( new Array() );
}
Then in the typical dataView.sort() function you make an exception for this column:
grid.onSort.subscribe(function(e,args) {
var cols = args.sortCols;
dataView.sort(function (dataRow1, dataRow2) {
for( var i = 0; i < cols.length; i++ ) {
var field = cols[i].sortCol.field;
// reset sorting to original indexing
if( field === 'locus' ) {
return (dataRow1.id > dataRow2.id) ? 1 : -1;
}
var value1 = dataRow1[field];
var value2 = dataRow2[field];
if( value1 == value2 ) continue;
var sign = cols[i].sortAsc ? 1 : -1;
return (value1 > value2) ? sign : -sign;
}
return 0;
});
});
Will the table always be sorted in some order on some column?
If not then you'll have to store a lot of state the first time a column is clicked just in case you want to restore it after a third click.

Resources