How to tell if anything is selected in CKEditor - ckeditor

I'm trying to determine with Javascript if anything is selected within the CKEditor. I wish there was a bool like editor.hasSelection(). I started out using editor.getSelection().getSelectedText() === "", but if an element with no "text" is selected (like an img) then that will be a blank string, giving me a false negative. I also looked into editor.getSelection().getSelectedElement(), but that gives null if more than one element is selected.
Is there anything that does this that I'm not seeing in the API?

Looks to me as though there is nothing in the CKEditor selection API to do this directly. However, I think the following will do it, although I agree that it's a shame (and surprising) there is no equivalent of the isCollapsed property of the native browser Selection object.
This is untested but looks as though it will work:
function hasSelection(editor) {
var sel = editor.getSelection();
var ranges = sel.getRanges();
for (var i = 0, len = ranges.length; i < len; ++i) {
if (!ranges[i].collapsed) {
return true;
}
}
return false;
}
// Example:
alert( hasSelection(editor) );

Related

How can I replace an image in Google Documents?

I'm trying to insert images into Google Docs (other GSuite apps later) from an Add-On. I've succeeded in fetching the image and inserting it when getCursor() returns a valid Position. When there is a selection (instead of a Cursor), I can succeed if it's text that's selected by walking up to the Parent of the selected text and inserting the image at the start of the paragraph (not perfect, but OK).
UPDATE: It seems that I was using a deprecated method (getSelectedElements()), but that didn't fix the issue. It seems the issue is only with wrapped images as well (I didn't realize that the type of the object changed when you changed it to a wrapped text).
However, when an wrapped-text Image (presumably a PositionedImage) is highlighted (with the rotate and resize handles visible in blue), both getSelection() and getCursor() return null. This is a problem as I would like to be able to get that image and replace it with the one I'm inserting.
Here's my code... any help would be great.
var response = UrlFetchApp.fetch(imageTokenURL);
var selection = DocumentApp.getActiveDocument().getSelection();
if (selection)
{
Logger.log("Got Selection");
var replaced = false;
var elements = selection.getRangeElements();
if (elements.length === 1
&& elements[0].getElement().getType() === DocumentApp.ElementType.INLINE_IMAGE)
{
//replace the URL -- this never happens
}
//otherwise, we take the first element and work from there:
var firstElem = elements[0].getElement();
Logger.log("First Element Type = " + firstElem.getType());
if (firstElem.getType() == DocumentApp.ElementType.PARAGRAPH)
{
var newImage = firstElem.asParagraph().insertInlineImage(0, response);
newImage.setHeight(200);
newImage.setWidth(200);
}
else if (firstElem.getType() == DocumentApp.ElementType.TEXT)
{
var p = firstElem.getParent();
if (p.getType() == DocumentApp.ElementType.PARAGRAPH)
{
var index = p.asParagraph().getChildIndex(firstElem);
var newImage = p.asParagraph().insertInlineImage(index, response);
newImage.setHeight(200);
newImage.setWidth(200);
}
}
} else {
Logger.log("Checking Cursor");
var cursor = DocumentApp.getActiveDocument().getCursor();
if (cursor)
{
Logger.log("Got Cursor: " + cursor);
var newImage = cursor.insertInlineImage(response);
var p = cursor.getElement();
var size=200;
newImage.setHeight(size);
newImage.setWidth(size);
}
}
You are using the deprecated 'getSelectedElements()' method of the Range class. You may notice it's crossed out in the autocomplete selection box.
Instead, use the 'getRangeElements()' method. After selecting the image in the doc, the code below worked for me:
var range = doc.getSelection();
var element = range.getRangeElements()[0].getElement();
Logger.log(element.getType() == DocumentApp.ElementType.INLINE_IMAGE); //logs 'true'

editor.getSelection().getRanges()[0] don't return the same result in IE11

I have been looking into this issue for a few hours now and I can't find a way to fix it. I am using ckeditor 4.3(also try 4.5) with a custom colour picker to change font colour.
All work well in chrome, firefox, opera and safari yet not in IE. The problem come from
editor.getSelection().getRanges()[0].startContainer
which gave me a span in chrome which is what I want and p in IE which is one level high.
Here a litle exemple :
_me.editor.focus();
var range = _me.editor.getSelection().getRanges()[0];
AddLinkColor(range.startContainer, value.Value);
var AddLinkColor = function (element, color)
{
var selectedChild = null;
if (element.getChildren) { selectedChild = element.getChildren(); }
if (selectedChild)
{
if (selectedChild.count)
{
for (var i = 0; i < selectedChild.count() ; i++)
{
var childElement = selectedChild.getItem(i);
if (childElement.getStyle && childElement.getStyle('color') != '' && childElement.getStyle('color') != color) childElement.setStyle('color',` color);
if (childElement.getChildCount && childElement.getChildCount() > 0) AddLinkColor(childElement, color);
if (element.$.tagName == 'A') element.setStyle('color', color);
}
}
}
};
Any other face the same issue?
I have tried all the variant of startContainer that gave a dom element, like commonAncestor and such still same problem.
Selection behaves differently in different browsers, because there's no specification as well as the platforms behave differently in general. As long as the results reflect the real position of the selection everything is fine. That's nothing surprising and nothing to worry about. The only problem is that you need to handle all those different selections and this makes creating an editor so hard. Therefore, CKEditor API contains many tools to simplify this job.
Yea I was afraid so, thanks, made a litle fix not quite sexy but it work
if(element.getName() != "span") { selectedChild = element.getChildren().getItem(0).getChildren(); }
else { selectedChild = element.getChildren(); }

How to tell if a cell value has passed validation

I am familiar with the Google Apps script DataValidation object. To get and set validation criteria. But how to tell programatically if a cell value is actually valid. So I can see the little red validation fail message in the spreadsheet but can the fact the cell is currently failing validation be picked up thru code?
I have tried to see if there is a cell property that tells you this but there is not. Also I looked for some sort of DataValidation "validate" method - i.e. test a value against validation rules, but nothing there either
Any ideas? Is this possible??
Specific answer to your question, there is no method within Google Apps Script that will return the validity of a Range such as .isValid(). As you state, you could reverse engineer a programatic one using Range.getDataValidations() and then parsing the results of that in order to validate again the values of a Range.getValues() call.
It's a good suggestion. I've added a feature request to the issue tracker -> Add a Star to vote it up.
I've created a workaround for this issue that works in a very ugly -technically said- and slightly undetermined way.
About the workaround:
It works based on the experience that the web browser implementation of catch() function allows to access thrown errors from the Google's JS code parts.
In case an invalid input into a cell is rejected by a validation rule then the system will display an error message that is catchable by the user written GAS. In order to make it work first the reject value has to be set on the specified cell then its vale has to be re-entered (modified) then -right after this- calling the getDataValidation() built in function allows the user to catch the necessary error.
Only single cells can be tested with this method as setCellValues() ignores any data validation restriction (as of today).
Disadvantages:
The validity won't be necessarily re-checked for this function:
it calls a cell validation function right after the value is inserted into the cell.
Therefore the result of this function might be faulty.
The code messes up the history as cells will be changed - in case they are
valid.
I've tested it successfully on both Firefox and Chromium.
function getCellValidity(cell) {
var origValidRule = cell.getDataValidation();
if (origValidRule == null || ! (cell.getNumRows() == cell.getNumColumns() == 1)) {
return null;
}
var cell_value = cell.getValue();
if (cell_value === '') return true; // empty cell is always valid
var is_valid = true;
var cell_formula = cell.getFormula();
// Storing and checking if cell validation is set to allow invalid input with a warning or reject it
var reject_invalid = ! origValidRule.getAllowInvalid();
// If invalid value is allowed (just warning), then changing validation to reject it
// IMPORTANT: this will not throw an error!
if (! reject_invalid) {
var rejectValidRule = origValidRule.copy().setAllowInvalid(false).build();
cell.setDataValidation(rejectValidRule);
}
// Re-entering value or formula into the cell itself
var cell_formula = cell.getFormula();
if (cell_formula !== '') {
cell.setFormula(cell_formula);
} else {
cell.setValue(cell_value);
}
try {
var tempValidRule = cell.getDataValidation();
} catch(e) {
// Exception: The data that you entered in cell XY violates the data validation rules set on this cell.
// where XY is the A1 style address of the cell
is_valid = false;
}
// Restoring original rule
if (rejectValidRule != null) {
cell.setDataValidation(origValidRule.copy().setAllowInvalid(true).build());
}
return is_valid;
}
I still recommend starring the above Google bug report opened by Jonathon.
I'm using this solution. Simple to learn and fast to use! You may need to adapt this code for your needs. Hope you enjoy
function test_corr(link,name) {
var ss = SpreadsheetApp.openByUrl(link).getSheetByName(name);
var values = ss.getRange(2,3,200,1).getValues();
var types = ss.getRange(2,3,200,1).getDataValidations()
var ans
for (var i = 0; i < types.length; i++) {
if (types[i][0] != null){
var type = types[i][0].getCriteriaType()
var dval_values = types[i][0].getCriteriaValues()
ans = false
if (type == "VALUE_IN_LIST") {
for (var j = 0; j < dval_values[0].length; j++) {
if (dval_values[0][j] == values[i][0]) { ans = true }
}
} else if (type == "NUMBER_BETWEEN") {
if (values[i][0] >= dval_values[0] && values[i][0] <= dval_values[1]) { ans = true }
} else if (type == "CHECKBOX") {
if (values[i][0] == "Да" || values[i][0] == "Нет") { ans = true }
}
if (!ans) { return false }
}
}
return true;
}

Creating a new kendo binding for "associative arrays"

I'll start off by stating that I don't know if this is possible at all, but I'm reading over the Kendo UI documentation and trying to figure out how to at least try it, but I'm running into a lot of difficulties with making a custom binding. This is a followup to another question I am still working on, which is posted here. If this is not an appropriate question, please kindly let me know, and I will close it or rephrase it. I'm just really lost and confused at this point.
As I understand it, based on what I've been told and tried, Kendo cannot bind to an Associative Array not because the data isn't good, but because it is an array of objects, each as a separate individual entity - under normal circumstances, an array would be a bit different and contain a length property, as well as some other functions in the array prototype that make iteration through it possible.
So I was trying to conjecture how to get around this. I succeeded in getting what I think was a workaround to function. I preface that with "think" because I'm still too inexperienced with Javascript to truly know the ramifications of doing it this way (performance, stability, etc)
Here is what I did;
kendo template
<script type="text/x-kendo-template" id="display-items-many">
# for(var key in data) { #
# if (data.hasOwnProperty(key) && data[key].hasOwnProperty("Id")) { #
<tr>
<td>
<strong>#= data[key].Id #</strong>
</td>
<td class="text-right">
<code>#= data[key].Total #</code>
</td>
</tr>
# } #
# } #
</script>
html
<table class="table borderless table-hover table-condensed" data-bind="source: Associative data-template="display-items-many">
</table>
Now to me, immediately off hand, this gave me the illusion of functioning. So I got to thinking a bit more on how to fix this ...
I want to create a new binding called repeat. The goal of this binding is as follows;
repeat the template for each instance of an object within the given root object that meets a given criteria
In my head, this would function like this;
<div data-template="repeater-sample" data-bind="repeat: Associative"></div>
<script type="text/x-kendo-template" id="repeater-sample">
<div> ${ data.Id }</div>
</script>
And the criteria would be a property simply called _associationKey. So the following would, in theory, work.
$.ajax({
// get data from server and such.
}).done(function(results){
// simple reference to the 'associative array' for easier to read code
var associative = results.AssociativeArray;
// this is a trait that everything in the 'associative array' should have to match
// this is purely, purely an example. Obviously you would use a more robust property
var match = "Id";
// go through the results and wire up the associative array objects
for(var key in associative ) {
if(associative.hasOwnProperty(key) && associative[key].hasOwnProperty(match)) {
associative[key]._associationKey = 10; // obviously an example value
}
}
// a watered down example implementation, obviously a real use would be more verbose
viewModel = kendo.observable({
// property = results.property
// property = results.property
associativeArray = associative
});
kendo.bind('body', viewModel);
});
So far this actually seems to work pretty well, but I have to hard code the logic in the template using inline scripting. That's kind of what I want to avoid.
Problem
The big issue is that I'm vastly confused on telerik's documentation for custom bindings (available here). I do have their examples to draw from, yes - but it's a bit confusing to me how it interacts with the object. I'll try to explain, but I'm so lost that it may be difficult.
This is what telerik gives for an example custom binding, and I've pruned it a bit for space concerns;
<script>
kendo.data.binders.repeater = kendo.data.Binder.extend({
init: function(element, bindings, options) {
//call the base constructor
kendo.data.Binder.fn.init.call(this, element, bindings, options);
var that = this;
// how do we interact with the data that was bound?
}
});
</script>
So essentially that's where I am lost. I'm having a big disconnect figuring out how to interact with the actual "associative array" that is bound using data-bind="repeat: associativeArray"
So ..
I need to interact with the bound data (the entire 'associative array')
I need to be able to tell it to render the target template for each instance that matches
Further Updates
I have been digging through the kendo source code, and this is what I have so far - by taking the source binding as an example... but I'm still not getting the right results. Unfortunately this poses a few problems;
some of the functions are internal to kendo, I'm not sure how to get access to them without re-writing them. While I have the source and can do that, I'd prefer to make version agnostic code so that it can "plug in" to newer releases
I'm totally lost about what a lot of this does. I basically made a copy of the source binding and replaced it with my own syntax where possible, since the concept is fundamentally the same. I cannot figure out where to do the test for qualification to be rendered, if that makes sense.
I'm having a big logic disconnect here - there should ideally be some place where I can basically say ... If the current item that kendo is attempting to render in a template matches a criteria, render it. If not, pass it over and then another place where I tell it to iterate over every object in the 'associative array' so as to get to the point where I test it.
I feel just forcing a for loop in here will actually make this fire too many times, and I am getting pretty lost. Any help is greatly appreciated.
kendo.data.binders.repeat = kendo.data.Binder.extend({
init: function(element, bindings, options) {
kendo.data.Binder.fn.init.call(this, element, bindings, options);
var source = this.bindings.repeat.get();
if (source instanceof kendo.data.DataSource && options.autoBind !== false) {
source.fetch();
}
},
refresh: function(e) {
var that = this,
source = that.bindings.repeat.get();
if (source instanceof kendo.data.ObservableArray|| source instanceof kendo.data.DataSource) {
e = e || {};
if (e.action == "add") {
that.add(e.index, e.items);
} else if (e.action == "remove") {
that.remove(e.index, e.items);
} else if (e.action != "itemchange") {
that.render();
}
} else {
that.render();
}
},
container: function() {
var element = this.element;
if (element.nodeName.toLowerCase() == "table") {
if (!element.tBodies[0]) {
element.appendChild(document.createElement("tbody"));
}
element = element.tBodies[0];
}
return element;
},
template: function() {
var options = this.options,
template = options.template,
nodeName = this.container().nodeName.toLowerCase();
if (!template) {
if (nodeName == "select") {
if (options.valueField || options.textField) {
template = kendo.format('<option value="#:{0}#">#:{1}#</option>',
options.valueField || options.textField, options.textField || options.valueField);
} else {
template = "<option>#:data#</option>";
}
} else if (nodeName == "tbody") {
template = "<tr><td>#:data#</td></tr>";
} else if (nodeName == "ul" || nodeName == "ol") {
template = "<li>#:data#</li>";
} else {
template = "#:data#";
}
template = kendo.template(template);
}
return template;
},
add: function(index, items) {
var element = this.container(),
parents,
idx,
length,
child,
clone = element.cloneNode(false),
reference = element.children[index];
$(clone).html(kendo.render(this.template(), items));
if (clone.children.length) {
parents = this.bindings.repeat._parents();
for (idx = 0, length = items.length; idx < length; idx++) {
child = clone.children[0];
element.insertBefore(child, reference || null);
bindElement(child, items[idx], this.options.roles, [items[idx]].concat(parents));
}
}
},
remove: function(index, items) {
var idx, element = this.container();
for (idx = 0; idx < items.length; idx++) {
var child = element.children[index];
unbindElementTree(child);
element.removeChild(child);
}
},
render: function() {
var source = this.bindings.repeat.get(),
parents,
idx,
length,
element = this.container(),
template = this.template();
if (source instanceof kendo.data.DataSource) {
source = source.view();
}
if (!(source instanceof kendo.data.ObservableArray) && toString.call(source) !== "[object Array]") {
source = [source];
}
if (this.bindings.template) {
unbindElementChildren(element);
$(element).html(this.bindings.template.render(source));
if (element.children.length) {
parents = this.bindings.repeat._parents();
for (idx = 0, length = source.length; idx < length; idx++) {
bindElement(element.children[idx], source[idx], this.options.roles, [source[idx]].concat(parents));
}
}
}
else {
$(element).html(kendo.render(template, source));
}
}
});
I would propose as a simpler solution transform transmitted associative array in an array. This is pretty simple and (for most cases) can solve your problem.
Lets say that you get the following associative array received from the server:
{
"One" : { Name: "One", Id: "id/one" },
"Two" : { Name: "Two", Id: "id/two" },
"Three" : { Name: "Three", Id: "id/three" }
}
That is store in a variable called input. Transform it from associative to no associative is as easy as:
var output = [];
$.each(input, function(idx, elem) {
elem.index = idx;
output.push(elem);
});
Now, you have in output an equivalent array where I saved the index field into a field called index for each element of the associative array.
Now you can use out-of-the-box code for displaying the data received from the server.
See it in action here : http://jsfiddle.net/OnaBai/AGfWc/
You can even use KendoUI DataSource for retrieving and transforming the data by using DataSource.schema.parse method as:
var dataSource = new kendo.data.DataSource({
transport: {
read: ...
},
schema : {
parse: function (response) {
var output = [];
$.each(response, function(idx, elem) {
elem.index = idx;
output.push(elem);
});
return output;
}
}
});
and your model would be:
var viewModel = new kendo.data.ObservableObject({
Id: "test/id",
Associative: dataSource
});
You can see it in action here: http://jsfiddle.net/OnaBai/AGfWc/1/

How can I disable a hotkey in GreaseMonkey while editing?

I'm using Ctrl+Left / Ctrl+Right in a GreaseMonkey script as a hotkey to turn back / forward pages. It seems to works fine, but I want to disable this behavior if I'm in a text edit area. I'm trying to use document.activeElement to get the page active element and test if it's an editable area, but it always returns "undefined".
document.activeElement works for me in FF3 but the following also works
(function() {
var myActiveElement;
document.onkeypress = function(event) {
if ((myActiveElement || document.activeElement || {}).tagName != 'INPUT')
// do your magic
};
if (!document.activeElement) {
var elements = document.getElementsByTagName('input');
for(var i=0; i<elements.length; i++) {
elements[i].addEventListener('focus',function() {
myActiveElement = this;
},false);
elements[i].addEventListener('blur',function() {
myActiveElement = null;
},false);
}
}
})();
element.activeElement is part of HTML5 spec but is not supported by most browsers. It was first introduced by IE.

Resources