I have an associative array that I've verified (via console.log) is originally
this.RegionsChecked = {"US":true,"APAC":true,"Canada":true,"France":true,"Germany":true,"India":true,"Japan":true,"LATAM":true,"MEA":true,"UK":true,"WE":true};
and I have an event handler that attempts to toggle the value of a corresponding item when it is checked/unchecked in the HTML:
Calculator.prototype.UpdateGraphs = function ( $elem )
{
// $elem : input[type="checkbox"] element that was clicked, as a jQuery object
var $parent = $elem.parent(); // Parent of input that was clicked.
// Will either be a th.region or a td#guiderow-[metric_name]
if ( $parent.hasClass('region') )
{
var region_name = $parent.text();
this.RegionsChecked[region_name] = !this.RegionsChecked[region_name]; // toggle region name
console.log(JSON.stringify(this.RegionsChecked)); // TEST
}
// ...
What is strange is that when I change the check value of Canada, for example, the array sometimes turns to
this.RegionsChecked = {"US":true,"APAC":true,"Canada":true,"France":true,"Germany":true,"India":true,"Japan":true,"LATAM":true,"MEA":true,"UK":true,"WE":true,"\n\t\t\t\t\tCanada":true};
(look at the last key, what previously was "Unknown")
instead of the expected
this.RegionsChecked = {"US":true,"APAC":true,"Canada":false,"France":true,"Germany":true,"India":true,"Japan":true,"LATAM":true,"MEA":true,"UK":true,"WE":true};
which is does some of the time, I think (but still have to verify). I'm still trying to figure out how consistently it is happening, but you have any ideas on why?
EDIT: Weird ... It just did it correctly. I can't find any discernable pattern in when it works and doesn't .. I am using Microsoft Sharepoint Designer, which can do strange things ...
In general, it's not a very good idea to depend on the actual text of a DOM node as a key for information, especially if that DOM is dynamically generated by a large unwieldy CMS-like like SharePoint that performs various transforms on your text before it ends up in the DOM (using the Designer adds another fun step to that chain). That is what constructs like data attributes are for.
That said, if you have to do it, you are probably better off trimming your text before using it. I.e.,
var region_name = $.trim($parent.text());
Related
I am trying to process an insert event from the CKEditor 5.
editor.document.on("change", (eventInfo, type, data) => {
switch (type) {
case "insert":
console.log(type, data);
break;
}
});
When typing in the editor the call back is called. The data argument in the event callback looks like approximately like this:
{
range: {
start: {
root: { ... },
path: [0, 14]
},
end: {
root: { ... },
path: [0, 15]
}
}
}
I don't see a convenient way to figure out what text was actually inserted. I can call data.range.root.getNodeByPath(data.range.start.path); which seems to get me the text node that the text was inserted in. Should we then look at the text node's data field? Should we assume that the last item in the path is always an offset for the start and end of the range and use that to substring? I think the insert event is also fired for inserting non-text type things (e.g. element). How would we know that this is indeed a text type of an event?
Is there something I am missing, or is there just a different way to do this all together?
First, let me describe how you would do it currently (Jan 2018). Please, keep in mind that CKEditor 5 is now undergoing a big refactoring and things will change. At the end, I will describe how it will look like after we finish this refactoring. You may skip to the later part if you don't mind waiting some more time for the refactoring to come to an end.
EDIT: The 1.0.0-beta.1 was released on 15th of March, so you can jump to the "Since March 2018" section.
Until March 2018 (up to 1.0.0-alpha.2)
(If you need to learn more about some class API or an event, please check out the docs.)
Your best bet would be simply to iterate through the inserted range.
let data = '';
for ( const child of data.range.getItems() ) {
if ( child.is( 'textProxy' ) ) {
data += child.data;
}
}
Note, that a TextProxy instance is always returned when you iterate through the range, even if the whole Text node is included in the range.
(You can read more about stringifying a range in CKEditor5 & Angular2 - Getting exact position of caret on click inside editor to grab data.)
Keep in mind, that InsertOperation may insert multiple nodes of a different kind. Mostly, these are just singular characters or elements, but more nodes can be provided. That's why there is no additional data.item or similar property in data. There could be data.items but those would just be same as Array.from( data.range.getItems() ).
Doing changes on Document#change
You haven't mentioned what you want to do with this information afterwards. Getting the range's content is easy, but if you'd like to somehow react to these changes and change the model, then you need to be careful. When the change event is fired, there might be already more changes enqueued. For example:
more changes can come at once from collaboration service,
a different feature might have already reacted to the same change and enqueued its changes which might make the model different.
If you know exactly what set of features you will use, you may just stick with what I proposed. Just remember that any change you do on the model should be done in a Document#enqueueChanges() block (otherwise, it won't be rendered).
If you would like to have this solution bulletproof, you probably would have to do this:
While iterating over data.range children, if you found a TextProxy, create a LiveRange spanning over that node.
Then, in a enqueueChanges() block, iterate through stored LiveRanges and through their children.
Do your logic for each found TextProxy instance.
Remember to destroy() all the LiveRanges afterwards.
As you can see this seems unnecessarily complicated. There are some drawbacks of providing an open and flexible framework, like CKE5, and having in mind all the edge cases is one of them. However it is true, that it could be simpler, that's why we started refactoring in the first place.
Since March 2018 (starting from 1.0.0-beta.1)
The big change coming in 1.0.0-beta.1 will be the introduction of the model.Differ class, revamped events structure and a new API for big part of the model.
First of all, Document#event:change will be fired after all enqueueChange blocks have finished. This means that you won't have to be worried whether another change won't mess up with the change that you are reacting to in your callback.
Also, engine.Document#registerPostFixer() method will be added and you will be able to use it to register callbacks. change event still will be available, but there will be slight differences between change event and registerPostFixer (we will cover them in a guide and docs).
Second, you will have access to a model.Differ instance, which will store a diff between the model state before the first change and the model state at the moment when you want to react to the changes. You will iterate through all diff items and check what exactly and where has changed.
Other than that, a lot of other changes will be conducted in the refactoring and below code snippet will also reflect them. So, in the new world, it will look like this:
editor.document.registerPostFixer( writer => {
const changes = editor.document.differ.getChanges();
for ( const entry of changes ) {
if ( entry.type == 'insert' && entry.name == '$text' ) {
// Use `writer` to do your logic here.
// `entry` also contains `length` and `position` properties.
}
}
} );
In terms of code, it might be a bit more of it than in the first snippet, but:
The first snippet was incomplete.
There are a lot fewer edge cases to think about in the new approach.
The new approach is easier to grasp - you have all the changes available after they are all done, instead of reacting to a change when other changes are queued and may mess up with the model.
The writer is an object that will be used to do changes on the model (instead of Document#batch API). It will have methods like insertText(), insertElement(), remove(), etc.
You can check model.Differ API and tests already as they are already available on master branch. (The internal code will change, but API will stay as it is.)
#Szymon Cofalik's answer went into a direction "How to apply some changes based on a change listener". This made it far more complex than what's needed to get the text from the Document#change event, which boils down to the following snippet:
let data = '';
for ( const child of data.range.getChildren() ) {
if ( child.is( 'textProxy' ) ) {
data += child.data;
}
}
However, reacting to a change is a tricky task and, therefore, make sure to read Szymon's insightful answer if you plan to do so.
JS comparative newbie... spent about 3 hours on this so far... Please consider this code if you'd be so kind:
function updateDataFields( mapIDToNewValue )
{
$.each(dataFields, function(key, value)
{
var dataField = $( value );
// var dataField = $(this); // same difference
console.log( dataField );
var iD = dataField.attr('id');
// DOES NOT WORK!!! i.e. going val() on this object does not update the INPUT element on the page!
// dataField.val(mapIDToNewValue[ iD ]);
var thePageElement = $( '#' + iD );
console.log( thePageElement );
console.log( '£ is dataField the same object as thePageElement?' + ( dataField === thePageElement ? 'yes' : 'no, you fool' ));
// DOES WORK:
thePageElement.val( mapIDToNewValue[ iD ] );
});
}
Explanation: dataFields, a file-global variable*, is passed by an outside script which calls this one, using a (fully) global variable which attaches dataFields to itself in piggyback fashion. This data structure, dataFields, consists of all the page elements with the class .dataField. All of these are in fact INPUT HTML elements.
Using the supplied param mapIDToNewValue (from an AJAX call to a dbase), I want to update the respective contents (i.e. text) of these INPUTs. mapIDToNewValue is a map in which the key is the same as the attr( 'id' ) of these dataFields, and the value is the new value which needs to be displayed in the INPUT.
It turns out that thePageElement is not the same object as dataField. When I examine the console output for them both they appear virtually the same... except that dataField, for example, has 0 height, which is enough to chill the soul!
My working hypothesis is that somehow dataFields, when passed from the calling script via this piggyback global variable, somehow turned its contents into "phantom" objects: they have the same id as thePageElement... but are incapable of having any effect on the real page elements themselves!
NB there is no possibility of duplicate ids here, or anything like that.
Any explanation welcome!
* implemented using anonymous function as per here.
later, in response to Patrick Barr's comment:
It's quite involved. Given your use of the word "interesting", I was moved to find out what your rep might be. If you had been a proven JS guru (you may nonetheless be one of course) I'd have been inclined to think that I need to embark on a forensic fault-finding mission.
The context: I'm developing a sort of "MSAccess Forms for MySQL front end" type of a thing, which is sorely lacking out there IMHO. At the moment I'm tackling subforms, and a major aim is to re-use code as much as is humanly possible. I'm rapidly getting out of my depth, not least because of the asynchronicity/concurrency issues that spring up, gorgon-like, at every turn. I'm thinking about how to answer your question in an informative, useful way.
To anyone else: I now realise I have to strip down my project to the bare bones to find out what's going on here... and if still baffled post an SSCCE (as we call it in Java) ... just thought initially that an expert out there might recognise a well-known issue and be able to set me straight.
Having examined things here I now understand what happened (to a degree).
In fact the dataFields Array was obtained from the result of an AJAX call which loaded (or more accurately returned, as the data param in the callback function) an HTML fragment, containing several page elements with class .dataField, which I then gathered by going data.find( '.dataField' ).
But in fact these "page elements", despite having hundreds of properties, just like objects directly "mapping" to page elements, were indeed "phantom" objects: specifically, properties such as clientHeight were 0, seemingly an indication that this is a non-visible object.
It was only when the HTML returned in the AJAX callback was inserted into the document structure (and made visible by setting hidden to false for the encompassing DIV) that this HTML generated real page elements. I then had to select these page elements with class .dataField to get hold of the non-phantom objects (having clientHeight 20 or whatever).
Quite strange, as these initial "phantom" objects did NOT become "real" objects as a consequence of the HTML being added to the document. They remained ... useless (and confusing!).
I have objects who's only references are the DOM elements they are tied to and I'm not sure if calling $element.remove() actually removes the reference or just the DOM element. Here's some example code.
var Foo = function() {
var constructor = function Foo() {
var col = '#'+(Math.random()*0xFFFFFF<<0).toString(16);
var $container = $('<div class="element" style="background-color:'+col+';"></div>');
var $remove = $('<input type="button" value="Delete" />');
$container.append($remove);
$('#wrapper').append($container);
$remove.on('click', function() {
$container.remove();
});
};
return constructor;
}();
$('#addElement').on('click', function() {
new Foo();
});
And a jsfiddle for people to play around with. Click one button to add elements, click each element's "Delete" button to remove it; from the DOM at least.
It's not necessarily a problem for my current project because of the small scale, but I think it'd be really useful to know for future reference if I'm ever working on something large.
If it doesn't, is there any way to remove the objects without holding them in some array?
EDIT: Ok, so I've kinda answered my own question using the chrome heap snapshots and adding 10,000 elements to the page. Here's the important data. (I've included HTMLInputElements because the majority of what I'm inserting are those)
Initial 10,000 Removed
Memory 2.7MB 135MB 4.0MB
Objects 1,676 81,693 11,703
InputElements 1 59,995 1
So yeah, it seems that the garbage collector is smart enough to remove the objects when the DOM element referencing them is removed. Though it's not perfect.
The objects will be garbage collected as soon as you do not have a reference to the object anymore. I'm not sure if it would be considered a "best practice" to do this though. From MDN:
On the other hand, JavaScript values are allocated when things (objects, strings, etc.) are
created and "automatically" free'd when they are not used anymore.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management
The "automatically" described above in modern browsers means that objects that are considered "unreachable" will be marked for garbage collection.
Calling $.element.remove() will only remove the DOM element because it only knows about the DOM element. It's not possible that it could remove any other object associated with the DOM element because it does not know about that object.
http://api.jquery.com/remove/
I am authoring a simple jQuery plugin that turns an input tag into a time-formatted element (on blur it will change 245p into 2:45 pm).
Since I do not want to apply the time format events to the same element twice, I need a way to detect that the specific element in the list provided has not already had the format applied.
This is the relevant part of the code:
var methods = {
init : function(sel) {
var $this = $(sel);
return $this.each(function(){
var data = $(this).data('time_formatted');
if (data) {
return;
} else {
$(this).data('time_formatted', true);
I have heard that using $(sel).data() in a plugin is not a good idea; instead, use $.data(). I don't know why, that's just what I've heard; honestly, I don't know what the difference is.
So my question is, is this the way to accomplish checking if a specific element has had the time formatter applied to it in a plugin?
If you care to see the plugin in it's current development state, see http://jsfiddle.net/userdude/xhXCR/.
Thanks!
Jared
Where have you heard that using .data() is not good? jQuery's plugin autoring page says:
Often times in plugin development, you may need to maintain state or check if your plugin has already been initialized on a given element. Using jQuery's data method is a great way to keep track of variables on a per element basis. However, rather than keeping track of a bunch of separate data calls with different names, it's best to use a single object literal to house all of your variables, and access that object by a single data namespace.
So it should be perfectly fine.
I have an ext combobox which uses a store to suggest values to a user as they type.
An example of which can be found here: combobox example
Is there a way of making it so the suggested text list is rendered to an element in the DOM. Please note I do not mean the "applyTo" config option, as this would render the whole control, including the textbox to the DOM element.
You can use plugin for this, since you can call or even override private methods from within the plugin:
var suggested_text_plugin = {
init: function(o) {
o.onTypeAhead = function() {
// Original code from the sources goes here:
if(this.store.getCount() > 0){
var r = this.store.getAt(0);
var newValue = r.data[this.displayField];
var len = newValue.length;
var selStart = this.getRawValue().length;
if(selStart != len){
this.setRawValue(newValue);
this.selectText(selStart, newValue.length);
}
}
// Your code to display newValue in DOM
......myDom.getEl().update(newValue);
};
}
};
// in combobox code:
var cb = new Ext.form.ComboBox({
....
plugins: suggested_text_plugin,
....
});
I think it's even possible to create a whole chain of methods, calling original method before or after yours, but I haven't tried this yet.
Also, please don't push me hard for using non-standard plugin definition and invocation methodics (undocumented). It's just my way of seeing things.
EDIT:
I think the method chain could be implemented something like that (untested):
....
o.origTypeAhead = new Function(this.onTypeAhead.toSource());
// or just
o.origTypeAhead = this.onTypeAhead;
....
o.onTypeAhead = function() {
// Call original
this.origTypeAhead();
// Display value into your DOM element
...myDom....
};
#qui
Another thing to consider is that initList is not part of the API. That method could disappear or the behavior could change significantly in future releases of Ext. If you never plan on upgrading, then you don't need to worry.
So clarify, you want the selected text to render somewhere besides directly below the text input. Correct?
ComboBox is just a composite of Ext.DataView, a text input, and an optional trigger button. There isn't an official option for what you want and hacking it to make it do what you want would be really painful. So, the easiest course of action (other than finding and using some other library with a component that does exactly what you want) is to build your own with the components above:
Create a text box. You can use an Ext.form.TextField if you want, and observe the keyup event.
Create a DataView bound to your store, rendering to whatever DOM element you want. Depending on what you want, listen to the 'selectionchange' event and take whatever action you need to in response to the selection. e.g., setValue on an Ext.form.Hidden (or plain HTML input type="hidden" element).
In your keyup event listener, call the store's filter method (see doc), passing the field name and the value from the text field. e.g., store.filter('name',new RegEx(value+'.*'))
It's a little more work, but it's a lot shorter than writing your own component from scratch or hacking the ComboBox to behave like you want.
#Thevs
I think you were on the right track.
What I did was override the initList method of Combobox.
Ext.override(Ext.form.ComboBox, {
initList : function(){
If you look at the code you can see the bit where it renders the list of suggestions to a dataview. So just set the apply to the dom element you want:
this.view = new Ext.DataView({
//applyTo: this.innerList,
applyTo: "contentbox",
#qui
Ok. I thought you want an extra DOM field (in addition to existing combo field).
But your solution would override a method in the ComboBox class, isn't it? That would lead to all your combo-boxes would render to the same DOM. Using a plugin would override only one particular instance.