So I have a problem and it's most likely because I still don't get JavaScript... Cytoscape has their own 'this' and Polymer has their 'this'
<div id="labelFld">{{node.label}}</div>
<div id="measureFld">{{node.measure}}</div>
<div id="timesignatureFld">{{node.time_signature}}</div>
<div id="voiceFld">{{node.voice}}</div>
<div id="beatFld">{{node.beat}}</div>
<div id="meventFld">{{node.event}}</div>
var cy;
cytoscape({
ready : function () {
Polymer: ({
...
properties : {
node : {
type : Object,
notify : true,
readOnly : true
}
},
...
// Fires when the local DOM has been fully prepared
ready : function () {
var self_node = this.node; // <- 'this' via Polymer
try {
cy = cytoscape({
container : this.$.rhythmgraph,
ready : function (e) {}
});
} catch (err) {
console.log(err);
}
// Assign handler to Cytoscape click event for node elements
cy.on('click', 'node', {
"nodedata" : self_node // <- Seems to pass by value, not reference
}, function (e) {
self_node = this.data(); // <- 'this' via Cytoscape
console.log(self_node);
// e.data.nodedata = this.data();
});
},
But in order to update my <div>{{node.label}}</div> I have to be able to do this.node.label = "N42" // Polymer but I can't do it in the cy.on('click','node', ... ) because I need this.data() // Cytoscape inside there.
Scope is really killing me on this.
EDIT
In the end, I created an Observer to watch and update:
self_node = this.selectedNode;
var poly = this;
Object.observe(self_node, function(changes) {
changes.forEach(function(change) {
if(change.type == "update") {
poly.selectedNode = {
"id": change.object.id,
... }
};
poly.notifyPath('selectedNode.id', change.object.id);
}
});}.bind(poly));
I find this a common gotcha among JS dev beginners. You'll have to bind the function to its proper this reference.
cy.on('click', 'node', {
"nodedata" : rhynode
}, function (e) {
e.data.nodedata = this.data();
console.log(e.data.nodedata);
}.bind(this)); // here
With ES2015, arrow functions would bind automatically to the proper this:
cy.on('click', 'node', {
"nodedata" : rhynode
}, (e) => {
e.data.nodedata = this.data();
console.log(e.data.nodedata);
});
Related
I am working on a webapp with drag 'n drop functionality.
Up to now I used basic HTML5 Dragging but now I switched to using the library interact.js.
I don't know if my problem is specific to this library or of more general kind:
Events when dragging and dropping usually fire multiple times (if I have watched it correctly, it also seems to always be exactly 4 times, but no guarantee on that).
I am also using Vue.js and this is my code:
<template>
<v-card
elevation="0"
:id="id"
class="board device-dropzone"
>
<slot class="row"/>
<div
Drop Card here
</div>
</v-card>
</template>
In the slot, an image and div with text get added. Also this is the script:
<script>
import interact from 'interactjs';
export default {
name: 'Devices',
props: ['id', 'acceptsDrop'],
data() {
return {
extendedHover: false,
hoverEnter: false,
timer: null,
totalTime: 2,
};
},
methods: {
resetHover() {
alert('reset');
},
drop(e) {
let wantedId = e.relatedTarget.id.split('-')[0];
console.log(wantedId);
console.warn(e.target);
e.target.classList.remove('hover-drag-over');
this.extendedHover = false;
console.log('-------------dropped');
console.warn('dropped onto device');
this.$emit('dropped-component', cardId);
e.target.classList.remove('hover-drag-over'); */
},
dragenter() {
console.log('------------dragenter');
this.hoverEnter = true;
setTimeout(() => {
this.extendedHover = true;
console.log('extended hover detected');
}, 2000);
} */
this.timerID = setTimeout(this.countdown, 3000);
},
dragover(e) {
if (this.acceptsDrop) {
e.target.classList.add('hover-drag-over');
}
},
dragleave(e) {
if (this.acceptsDrop) {
clearInterval(this.timer);
this.timer = null;
this.totalTime = 2;
e.target.classList.remove('hover-drag-over');
this.extendedHover = false;
this.hoverEnter = false;
console.log('................');
console.warn(this.extendedHover);
// this.$emit('cancel-hover');
}
},
countdown() {
console.log('!!!!!!!!!!!!!!!!!!!!');
if (this.hoverEnter === true) {
this.extendedHover = true;
console.log('-------------------');
console.warn(this);
console.warn(this.extendedHover);
this.$emit('long-hover');
}
},
},
mounted() {
// enable draggables to be dropped into this
const dropzone = this;
interact('.device-dropzone').dropzone({
overlap: 0.9,
ondragenter: dropzone.dragenter(),
ondrop: function (event) {
dropzone.drop(event);
},
})
},
};
</script>
The draggable component is this one:
<template>
<v-card
class="primary draggable-card"
:id = "id"
:draggable = "false"
#dragover.stop
ref="interactElement"
>
<slot/>
</v-card>
With the script:
<script>
import interact from 'interactjs';
export default {
props: ['id', 'draggable'],
data() {
return {
isInteractAnimating: true,
position: { x: 0, y: 0 },
};
},
methods: {
/* dragStart: (e) => {
e.stopPropagation(); // so dragStart of ParentBoard does not get triggered as well
// eslint-disable-next-line
const target = e.target;
e.dataTransfer.setData('card_id', target.id);
e.dataTransfer.setData('type', 'widget');
// for some delay
setTimeout(() => {
console.log('started dragging');
}, 0);
}, */
dragEndListener: (event) => {
console.warn('+++++++++++++++++++++++++++');
// console.warn(event.currentTarget.id);
if (document.getElementById(event.currentTarget.id)) {
event.currentTarget.parentNode.removeChild(event.currentTarget);
}
},
dragMoveListener: (event) => {
/* eslint-disable */
var target = event.target;
// keep the dragged position in the data-x/data-y attributes
const xCurrent = parseFloat(target.getAttribute('data-x')) || 0;
const yCurrent = parseFloat(target.getAttribute('data-y')) || 0;
const valX = xCurrent + event.dx;
const valY = yCurrent + event.dy;
// translate the element
event.target.style.transform =
`translate(${valX}px, ${valY}px)`
// update the postion attributes
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
}
/* eslint-enable */
},
mounted() {
const element = this.$refs.interactElement;
console.log(element);
// interact(element).draggable({
const component = this;
interact('.draggable-card')
.draggable({
manualStart: true,
onmove: component.dragMoveListener,
onend:component.dragEndListener,
})
.on('move', function (event) {
var interaction = event.interaction;
// if the pointer was moved while being held down
// and an interaction hasn't started yet
if (interaction.pointerIsDown && !interaction.interacting()) {
var original = event.currentTarget;
// create a clone of the currentTarget element
const clone = event.currentTarget.cloneNode(true);
clone.id = clone.id + "-clone";
clone.classname += " dragged-clone";
// insert the clone to the page
document.body.appendChild(clone);
clone.style.opacity = 0.5;
// start a drag interaction targeting the clone
interaction.start({ name: 'drag' },
event.interactable,
clone);
}
})
.on('end', function (event) {
console.error('end drag');
});
},
/* eslint-enable */
};
</script>
In general the dragging and dropping works.
But I don't get why e.g. the drop-event would trigger four times when only dropping a single card.
Can anybody help me with this?
I was facing a similar issue using the same framework and library. Since there was some logic based on the event firing, it was breaking my app when it fired multiple times.
I suspected that the issue was related to bubbling of events.
The solution in my case was therefore to add event.stopImmediatePropagation() inside my drop(event) handler.
As noted in the referenced article, one should take care when stopping event bubbling that it doesn't have unforeseen consequences elsewhere.
I made this JS to add a functionality on a form (backend) that computes a field when the event click is triggered. So far the code recomputes when I use ".include" but the whole JS in all views fail since I'm using ".include". When I try to use extend my code does nothing. Looks like Odoo doesn't add the extended code to the JS engine so my question is, what am I doing wrong here? Is there something else I need to add so my code works as extended?
odoo.define('med_care.TestRenderer', function (require) {
"use strict";
var viewRegistry = require('web.view_registry');
var FormRenderer = require('web.FormRenderer');
var FormView = require('web.FormView');
var TestFormRenderer = FormRenderer.extend({
events: _.extend({}, FormRenderer.prototype.events, {
'click .sign_selector': '_onSignSelectorClicked',
}),
init: function (parent, state, params) {
this._super.apply(this, arguments);
this.fields = state.fields;
this._onSignSelectorClicked = _.debounce(this._onSignSelectorClicked, 300, true);
},
confirmChange: function (state, id, fields, e) {
var self = this;
if (state.model == 'med.test') {
return this._super.apply(this, arguments).then(function () {
self.canBeSaved(self.state.id);
});
}
},
_onSignSelectorClicked: function (event) {
this.state.data.telephone = '333';
if (this.state.model == 'med.test') {
var info_test = {
dataPointID: this.state.id,
changes: {telephone: '333'},
viewType: "form",
notifyChange: true
};
var odoo_event = this.trigger_up('field_changed', info_test);
this.confirmChange(this.state, this.state.id, "telephone",
odoo_event)
}
},
});
var TestFormView = FormView.extend({
config: _.extend({}, FormView.prototype.config, {
Renderer: TestFormRenderer,
}),
});
viewRegistry.add('test_form', TestFormView);
return TestFormView;
});
I have several fields which need to be initialized with CKEditor, for this I have created an helper class that contains the initEditor method.
The method below should return the initialized editor but it doesn't:
window.CKEditorHelper = window.CKEditorHelper || {};
(function (exports) {
exports.initEditor = function (input, myEditor) {
ClassicEditor
.create(document.querySelector(input), {
language: {
ui: 'en'
content: 'en'
}
})
.then(editor => {
myEditor = editor;
});
};
})(window.CKEditorHelper);
this is called in the following way:
let editor = null;
CKEditorHelper.initEditor('#description', editor);
so when I click on a button:
$('#save').on('click', function(){
console.log(editor.getData());
});
I get:
Cannot read property 'getData' of null
what I did wrong?
There are some issues on your code
let editor = null;
the let keyword only define a variable within function scope, when you use editor on another scope (your click handle event), it could be undefined
Another line
myEditor = editor;
This just simple made the reference to your original editor object will gone
Here is my solution to fix it
Change the way you init an editor like bellow
window.editorInstance = {editor: null};
CKEditorHelper.initEditor('#description', editorInstance);
Change your CKEditorHelper to
window.CKEditorHelper = window.CKEditorHelper || {};
(function (exports) {
exports.initEditor = function (input, myEditorInstance) {
ClassicEditor
.create(document.querySelector(input), {
language: {
ui: 'en'
content: 'en'
}
})
.then(editor => {
myEditorInstance.editor = editor;
});
};
})(window.CKEditorHelper);
And when you want to use your editor
console.log(editorInstance.editor.getData());
You can give this in javascript
$(document).ready(function () {
CKEDITOR.replace('tmpcontent', { height: '100px' })
})
take the value by using following
$('#save').on('click', function(){
var textareaValue = CKEDITOR.instances.tmpcontent.getData();
});
<label class="control-label">Message</label>
<textarea name="tmpcontent" id="tmpcontent" class="form-control"></textarea>
//OR in latest version
var myEditor;
ClassicEditor
.create( document.querySelector( '#description' ) )
.then( editor => {
console.log( 'Editor was initialized', editor );
myEditor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
and then get data using
myEditor.getData();
This could be a very basic Javascript I know but I just can't get it..
Ext.regController('Chat', {
initSocketConnection: function() {
this.chatStore = new App.Store.Chat();
...
this.socket = io.connect(settings.get('server'), {port: 8080});
this.socket.on(
'message',
this.addMessageToChatStore
);
},
addMessageToChatStore: function(message) {
console.log(message);
console.log(this); << console shows the 'this' has become SocketNameSpace
this.chatStore.add(message); << this line error with "Undefined" chatStore
this.send(message);
},
Console print out shows that the "this" in the addMessageToChatStore function is "SocketNamespace"
How to I get rid of the error?
To generalize the problem. I think it's better to describe it as function chain calling dilemma.
A class has some local var that's instance of another class. When this var listens on certain events, it calls the parent's class's method. The problem is when this method is called, it's under the context of the other class and hence the scope has changed and the access to the original parent's class methods are denied.
In javascript, the this variable is determined by the caller. You could use a self executing function to ensure you're referencing the correct entity rather than using this:
Ext.regController('Chat', (function() {
var self = {
initSocketConnection: function() {
self.chatStore = new App.Store.Chat();
...
self.socket = io.connect(settings.get('server'), {port: 8080});
self.socket.on(
'message',
this.addMessageToChatStore
);
},
addMessageToChatStore: function(message) {
console.log(message);
self.chatStore.add(message);
self.send(message);
}
};
return self;
}()));
Update
Based on the full snippet, I'm not sure where the render function is defined, if render is a global function then self.viewChat = render({xtype : 'App.View.Chat'}); will suffice, otherwise it may be defined on this (the this that is defined for the call to index) so the following may suffice self.viewChat = this.render({xtype : 'App.View.Chat'});.
If I were a betting man, I'd go for the latter, so the code would be (with commented out code removed):
Ext.regController('Chat', (function() {
var self = {
index: function() {
if (!self.socket) {
self.initSocketConnection();
}
self.showChat();
},
/**
* init the socket connection to the node.js server
* using user settings
*
*/
initSocketConnection: function() {
self.chatStore = new App.Store.Chat();
self.chatStore.add({
user: "Figo",
message: "Welcome!"
});
self.configStore = Ext.StoreMgr.get('ConfigStore');
var settings = self.configStore.getAt(0);
self.socket = io.connect(settings.get('server'), {
port: 8080
});
// Event Listener
self.socket.on('connect', self.registerUser);
self.socket.on('message', self.addMessageToChatStore);
App.on('newMsg', self.sendMessageToServer);
},
sendMessageToServer: function(msg) {
self.socket.send(msg);
},
addMessageToChatStore: function(message) {
console.log(message);
console.log(this);
console.log(this.parent);
self.chatStore.add(message);
self.socket.send(message);
},
registerUser: function() {
self.configStore = Ext.StoreMgr.get('ConfigStore');
var settings = self.configStore.getAt(0);
var user = {
nickname: settings.get('nickname'),
gravatar: settings.get('gravatar')
};
console.log(user);
self.socket.send(user);
},
/**
* Show chat view
*/
showChat: function() {
if (!self.viewChat) {
self.viewChat = this.render({
xtype: 'App.View.Chat'
});
self.viewChat.query('#settingsButton')[0].on('tap', self.showConfig, self);
}
self.application.viewport.setActiveItem(
self.viewChat, {
type: 'slide',
direction: 'left'
});
},
/**
* Show config View
*/
showConfig: function() {
Ext.dispatch({
controller: 'Viewport',
action: 'showConfig'
});
}
};
return self;
}()));
For #Rich.okelly
Here's the error (highlighted in code)
Uncaught TypeError: Object #<Object> has no method 'render' app.all.js:438
self.showChat app.all.js:438
self.index app.all.js:351
Ext.util.Dispatcher.Ext.extend.dispatch sencha-touch-debug.js:10630
Ext.dispatch sencha-touch-debug.js:10667
Ext.regController.showChat app.all.js:293
fire sencha-touch-debug.js:979
Ext.util.Observable.Ext.extend.fireEvent sencha-touch-debug.js:595
And here's the full code (other code within the same project not shown):
Ext.regController('Chat',(function() {
var self = {
/**
* Index action
*
* #return {void}
*/
index: function() {
if (!self.socket) {
self.initSocketConnection();
}
self.showChat();
},
/**
* init the socket connection to the node.js server
* using user settings
*
*/
initSocketConnection: function() {
self.chatStore = new App.Store.Chat();
self.chatStore.add({user: "Figo", message: "Welcome!"});
self.configStore = Ext.StoreMgr.get('ConfigStore');
//this.configStore = new App.Store.Config()
var settings = self.configStore.getAt(0);
//this.socket = new App.util.Socketio(settings.get('server'), {port: 4000});
//this.socket.connect();
self.socket = io.connect(settings.get('server'), {port: 8080});
/*
this.socket.on('message', function (data) {
console.log(data);
this.parent.addMessageToChatStore(data);
this.emit('message', data);
});
this.socket.on('message',
this.addMessageToChatStore
);
*/
// Event Listener
self.socket.on(
'connect',
self.registerUser
);
self.socket.on(
'message',
self.addMessageToChatStore
);
App.on(
'newMsg',
self.sendMessageToServer
);
},
sendMessageToServer: function(msg){
self.socket.send(msg);
},
addMessageToChatStore: function(message) {
console.log(message);
console.log(this);
console.log(this.parent);
//if (!this.chatStore)
//this.chatStore = new App.Store.Chat();
self.chatStore.add(message);
//App.Controller.Chat.chatStore.add(message);
self.socket.send(message);
},
registerUser: function() {
self.configStore = Ext.StoreMgr.get('ConfigStore');
var settings = self.configStore.getAt(0);
var user = {
nickname: settings.get('nickname'),
gravatar: settings.get('gravatar')
};
console.log(user);
self.socket.send(user);
},
/**
* Show chat view
*/
showChat: function() {
if (!self.viewChat) {
self.viewChat = self.render({ << fails over here
xtype: 'App.View.Chat'
});
self.viewChat.query('#settingsButton')[0].on(
'tap',
self.showConfig,
self
);
}
self.application.viewport.setActiveItem(
self.viewChat,
{
type: 'slide',
direction: 'left'
}
);
},
/**
* Show config View
*/
showConfig: function() {
Ext.dispatch({
controller: 'Viewport',
action : 'showConfig'
});
}
};
return self;
}()));
I have a Parent and Child view. The Child view extends the Parent events with:
initialize : function() {
// don't overwrite parent's events
this.events = _.extend( {}, ExerciseRowView.prototype.events, this.events);
},
However, the Parent expects a ParentModel and the Child expects a ChildModel, so when an event is passed to the Parent, the model is the ChildModel. How can I set the Parent model to be different from the Child model?
Thanks!
Here's the source, as requested.
ParentView aka ExerciseRowView:
var ExerciseRowView = Parse.View.extend( {
tagName : 'div',
className : 'exerciseWrapper',
template : _.template(exerciseElement),
events : {
'click .icon_delete' : 'confirmDelete',
'click .name' : 'showDetailsPopup'
},
confirmDelete : function() {
var that = this;
if(confirm("Are you sure you want to delete this exercise?")) {
this.destroy({
success: function(exercise) {
// log the action
Log.add(Log.ACTION_EXERCISE_DELETED, exercise.get("name"));
that.$el.fadeOut();
}
});
}
},
showDetailsPopup : function() {
(new ExerciseDetailsView({model: (this.model.constructor == Exercise ? this.model : this.model.get("exercise"))})).render();
},
// accept data as a parameter for workoutexercises
render : function(data) {
_.defaults(data, {
exercise: this.model,
Muscle : Muscle,
Equipment : Equipment,
Exercise : Exercise,
Break : Break,
HTMLHelper : HTMLHelper,
User : User
});
$(this.el).html(this.template(data));
return this;
}
});
ChildView aka WorkoutExerciseRowView:
var WorkoutExerciseRowView = ExerciseRowView.extend( {
events : {
"click .icon_randomize" : "changeToRandomExercise"
},
initialize : function() {
// don't overwrite parent's events
this.events = _.extend( {}, ExerciseRowView.prototype.events, this.events);
},
render: function() {
// override the template data with workout exercise template data
return ExerciseRowView.prototype.render.call(this, {
workoutExercise : this.model,
exercise : this.model.get("exercise"),
workoutSection : this.model.get("section"),
isEditable : true,
number : this.options.number,
WorkoutExercise : WorkoutExercise,
WorkoutSection : WorkoutSection
});
},
changeToRandomExercise : function(e) {
// pick a random alternative exercise
var newExerciseId;
do {
newExerciseId = _.keys(this.model.get("alternativeExercises"))[ Math.floor(Math.random() * _.keys(this.model.get("alternativeExercises")).length) ];
} while(newExerciseId == this.model.get("exercise").id);
// grab it
var that = this;
(new Parse.Query(Exercise)).get(newExerciseId, {
success: function(exercise) {
// update the workout exercise
that.model.set("exercise", exercise);
// render it
that.render();
}
});
}
});
Currently (as you can see), I test to see if this.model.constructor == Exercise inside ExerciseRowView. If it is not, I know that I have a WorkoutExercise, inside which is an Exercise, so I use this.model.get("exercise"):
showDetailsPopup : function() {
(new ExerciseDetailsView({model: (this.model.constructor == Exercise ? this.model : this.model.get("exercise"))})).render();
},
This doesn't seem like the cleanest possible solution, though.
what I could think of is that you define function for each view
ParentView
getExercise: function() {
return this.model;
}
ChildView
getExercise: function() {
return this.model.get('exercise');
}
And then change the function
showDetailsPopup: function() {
(new ExerciseDetailsView({model: this.getExercise()})).render();
}
How about that?