Cytoscape.js cy.style().fromJson() add instead of replace? - css

I want to separate the style from color schemes in a Cytoscape.js 3.1 graph. According to http://js.cytoscape.org/#cy.style, I initialize and then add to cy.style():
var mystyle = [
{
"selector": "node",
"css":
{
"border-opacity": 1.0,
'label': function(ele)
{
if(ele.data('Labels_EN')) {return ele.data('Labels_EN')[0];}
...
}, ...
];
var bright = [
{
"selector": "node",
"css":
{"background-color": "white";}
}];
cy = cytoscape(
{
container: mycontainer,
style: mystyle
});
cy.style().fromJson(bright).update();
Unfortunately, the call to cy.style().fromJson() seems to automatically invoke cy.style().resetToDefault(), as it removes the existing style.
How can I prevent Cytoscape.js from deleting my existing style and instead add to it using JSON? I need this functionality so that I don't have to put the complete style information in all my color scheme files, which makes it harder to maintain.
P.S.: As a workaround, I merged two style files like this:
function mergeJsonArraysByKey(a1,a2)
{
let map1 = new Map();
let map2 = new Map();
for(i=0;i<a1.length;i++) {if(a1[i].selector) {map1.set(a1[i].selector,a1[i]);}}
for(i=0;i<a2.length;i++) {if(a2[i].selector) {map2.set(a2[i].selector,a2[i]);}}
let merged = [];
map1.forEach((value,key,map) =>
{
if(map2.has(key))
{
merged.push($.extend(true,{},value,map2.get(key)));
} else
{
merged.push(value);
}
});
map2.forEach((value,key,map) =>
{
if(!map1.has(key))
{
merged.push(value);
}
});
return merged;
}
function initGraph(container, graph)
{
let merged = mergeJsonArraysByKey(mystyle,bright);
cy = cytoscape(
{
container: container,
style: merged
});
}

Merge the json rather than appending to the stylesheet. You can do whatever you want with the json, whereas cy.style() can be modified in only particular ways to prevent issues.
cy.style().fromJson( sheetA.concat( sheetB ) )

Related

Vue js custom select - binding to the

I have a custom select box.
<select-box :options="['Male', 'Female', ]"
title="Gender"
v-bind:value="selected"
v-model="person.gender"
>
</select-box>
The .vue code
<script>
export default {
props:['title', 'options'],
data () {
return {
selected: this.title,
dropdownVisible: false,
}
},
methods: {
toggleOptions() {
this.dropdownVisible = !this.dropdownVisible
},
selectValue(option) {
this.selected = option;
this.toggleOptions();
}
}
}
How can I bind the selected value directly to the model (person.gender)?
I assume that above .vue code belongs to select-box component.
Because I saw you use v-model, to bind value directly to v-model, you need to $emit inside children component.
You can change your selectValue function
selectValue(option) {
this.selected = option;
this.$emit('input', option);
this.toggleOptions();
}

How do I get translated column headers with Meteor and aldeed:tabular?

I'm running into the same problem as issue #53 of aldeed:tabular. When defining the table as suggested in the documentation, it is too soon to invoke a translation function (TAPi18n.__ or other), since the I18N variables are not yet set.
What is the nice, reactive way of feeding the translated column titles into DataTables, either directly as suggested by aldeed himself upon closing the issue, or through aldeed:tabular?
With .tabular.options
There is a way with the template's .tabular.options reactive
variable, but it is quirky. Here is a variation of the library
example using
tap-i18n to translate the
column headers:
function __(key) {
if (Meteor.isServer) {
return key;
} else {
return TAPi18n.__(key);
}
}
Books = new Meteor.Collection("Books");
TabularTables = {};
TabularTables.Books = new Tabular.Table({
name: "Books",
collection: Books,
columns: [] // Initially empty, reactively updated below
});
var getTranslatedColumns = function() {
return [
{data: "title", title: __("Title")},
{data: "author", title: __("Author")},
{data: "copies", title: __("Copies Available")},
{
data: "lastCheckedOut",
title: __("Last Checkout"),
render: function (val, type, doc) {
if (val instanceof Date) {
return moment(val).calendar();
} else {
return "Never";
}
}
},
{data: "summary", title: __("Summary")},
{
tmpl: Meteor.isClient && Template.bookCheckOutCell
}
];
}
if (Meteor.isClient) {
Template.tabular.onRendered(function() {
var self = this;
self.autorun(function() {
var options = _.clone(self.tabular.options.get());
options.columns = getTranslatedColumns();
self.tabular.options.set(_.clone(options));
});
});
}
With a forked version
I created a pull request against branch devel of meteor-tabular to enable the straightforward, reactive-based approach like so:
<template name="MyTemplateWithATable">
{{> tabular table=makeTable class="table table-editable table-striped table-bordered table-condensed"}}
</template>
var MyColumns = ["title", "author"];
// Assume translations are set up for "MyTable.column.title", "MyTable.column.author"
// in other source files; see TAPi18n documentation for how to do that
function makeTable() {
return new Tabular.Table({
name: "MyTable",
collection: MyCollection,
columns: _.map(MyColumns,
function(colSymbol) {
return {
data: colSymbol,
title: TAPi18n.__("MyTable.column." + colSymbol)
};
})
});
}
if (Meteor.isServer) {
// Called only once
makeTable();
} else if (Meteor.isClient) {
// Reactively called multiple times e.g. when switching languages
Template.MyTemplateWithATable.helpers({makeTable: makeTable});
}
Recent versions of aldeed:tabular allow to specify a function for setting the column titles.
import {TAPi18n} from 'meteor/tap:i18n';
TabularTables = {};
TabularTables.Departments= new Tabular.Table({
name: 'Departments',
collection: Departments,
responsive: true,
autoWidth: true,
stateSave: false,
columns: [
{data: "name", titleFn: function() {
return TAPi18n.__("name");
}},
{data: "description", titleFn: function() {
return TAPi18n.__("description");
}}
]
});
The language change is reactive. If you have translations you can switch and columns will be translated.
TAPi18n.setLanguage("en");
TAPi18n.setLanguage("de");
Word of warning:
This currently does not work when you include invisible columns in your table data. The offset is wrong and you get wrong column titles.

ChartJS rendering old and new data

I am creating an application using Meteor and ReactJS. I have a collection which I subscribe to and the client side, and I want to render a ChartJS Polar Area chart based on the data from the collection.
The problem is that whenever I update the collection, both the new data and the old data appears on the chart (when I hover, I get two different values for the segments, the old data and the new data).
Here is my code:
ChartComponent = React.createClass({
mixins: [ReactMeteorData],
getMeteordata() {
let data = Meteor.subscribe("sentiment");
return {
chartData: [
{
value: MyCollection.find({"item": "test1"}).count(),
color: "green",
label: "Test1"
},
{
value: MyCollection.find({"item": "test2"}).count(),
color: "yellow",
label: "Test2"
},
{
value: MyCollection.find({"item": "test3"}).count(),
color: "red",
label: "Test3"
}
]
}
},
_drawGraph() {
let ctx = document.getElementById("my-canvas").getContext("2d");
let polarChart = new Chart(ctx).Polararea(this.data.chartData, {
animateScale: true,
});
return polarChart;
},
componentDidMount() {
this._drawGraph();
},
componentDidUpdate() {
this._drawGraph().update();
},
render() {
return <canvas id="my-canvas"></canvas>
}
});
Help would be appreciated. Thank you very much!
Note:
I am using react version 0.14. The official react-chartjs component does not work, due to some of the api changes.

How to make Chrome Extension run for each new Iframe added?

I created a Chrome Extension as a solution to override the helpText bubbles in SalesForce Console pages. The helpText bubbles show up the text without the ability to link URLs. It looks like this:
The extension is taking the helpText bubble (which in the SalesForce console window, is inside an iFrame) and makes the URL click-able. It also adds word wrap and marks the links in blue.
The solution works fine when the page loads with the initial iFrame (or iFrames) on it, meaning when you open the SalesForce console the first time (https://eu3.salesforce.com/console).
When a new tab is created at the SalesForce console, my inject script doesn't run.
Can you please assist in understanding how to inject the script on each and every new Tab SalesForce Console is creating?
The Extension as follows:
manifest.js:
{
"browser_action": {
"default_icon": "icons/icon16.png"
},
"content_scripts": [ {
"all_frames": true,
"js": [ "js/jquery/jquery.js", "src/inject/inject.js" ],
"matches": [ "https://*.salesforce.com/*", "http://*.salesforce.com/*" ]
} ],
"default_locale": "en",
"description": "This extension Fix SalesForce help bubbles",
"icons": {
"128": "icons/icon128.png",
"16": "icons/icon16.png",
"48": "icons/icon48.png"
},
"manifest_version": 2,
"name": "--Fix SalesForce bubble text--",
"permissions": [ "https://*.salesforce.com/*", "http://*.salesforce.com/*" ],
"update_url": "https://clients2.google.com/service/update2/crx",
"version": "5"
}
And this is the inject.js:
chrome.extension.sendMessage({}, function(response) {
var readyStateCheckInterval = setInterval(function() {
if (document.readyState === "complete") {
clearInterval(readyStateCheckInterval);
var frame = jQuery('#servicedesk iframe.x-border-panel');
frame = frame.contents();
function linkify(inputText) {
var replacedText, replacePattern1, replacePattern2, replacePattern3;
var originalText = inputText;
//URLs starting with http://, https://, file:// or ftp://
replacePattern1 = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&##\/%?=~_|!:,.;]*[-A-Z0-9+&##\/%=~_|])/gim;
replacedText = inputText.replace(replacePattern1, '$1');
//URLs starting with "www." (without // before it, or it'd re-link the ones done above).
replacePattern2 = /(^|[^\/f])(www\.[\S]+(\b|$))/gim;
replacedText = replacedText.replace(replacePattern2, '$1$2');
//Change email addresses to mailto:: links.
replacePattern3 = /(([a-zA-Z0-9\-\_\.])+#[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
replacedText = replacedText.replace(replacePattern3, '$1');
//If there are hrefs in the original text, let's split
// the text up and only work on the parts that don't have urls yet.
var count = originalText.match(/<a href/g) || [];
if(count.length > 0){
var combinedReplacedText;
//Keep delimiter when splitting
var splitInput = originalText.split(/(<\/a>)/g);
for (i = 0 ; i < splitInput.length ; i++){
if(splitInput[i].match(/<a href/g) == null){
splitInput[i] = splitInput[i].replace(replacePattern1, '$1').replace(replacePattern2, '$1$2').replace(replacePattern3, '$1');
}
}
combinedReplacedText = splitInput.join('');
return combinedReplacedText;
} else {
return replacedText;
}
}
var helpOrbReady = setInterval(function() {
var helpOrb = frame.find('.helpOrb');
if (helpOrb) {
clearInterval(helpOrbReady)
} else {
return;
}
helpOrb.on('mouseout', function(event) {
event.stopPropagation();
event.preventDefault();
setTimeout(function() {
var helpText = frame.find('.helpText')
helpText.css('display', 'block');
helpText.css('opacity', '1');
helpText.css('word-wrap', 'break-word');
var text = helpText.html()
text = text.substr(text.indexOf('http'))
text = text.substr(0, text.indexOf(' '))
var newHtml = helpText.html()
helpText.html(linkify(newHtml))
}, 500); });
}, 1000);
}
}, 1000);
});
It is possible (I have not tested it, but it sounds plausible from a few questions I've seen here) that Chrome does not automatically inject manifest-specified code into newly-created <iframe> elements.
In that case, you will have to use a background script to re-inject your script:
chrome.runtime.onMessage.addListener( function(request, sender, sendResponse) {
if(request.reinject) {
chrome.tabs.executeScript(
sender.tab.id,
{ file: "js/jquery/jquery.js", "all_frames": true },
function(){
chrome.tabs.executeScript(
sender.tab.id,
{ file: "js/inject/inject.js", "all_frames": true }
);
}
);
});
Content script:
// Before everything: include guard, ensure injected only once
if(injected) return;
var injected = true;
function onNewIframe(){
chrome.runtime.sendMessage({reinject: true});
}
Now, I have many questions about your code, which are not directly related to your question.
Why the pointless sendMessage wrapper? No-one is even listening, so your code basically returns with an error set.
Why all the intervals? Use events instead of polling.
If you are waiting on document to become ready, jQuery offers $(document).ready(...)
If you're waiting on DOM modifications, learn to use DOM Mutation Observers, as documented and as outlined here or here. This would be, by the way, the preferred way to call onNewIframe().

AngularJS - computing CSS class using plain javascript or a service?

Let's say we are fetching some data from the server.
data = {
"value": "123456",
"included": true,
"invalid": true,
"warning": false,
"error": false,
}
Depending on the booleans state, the value needs to be displayed with a specific style.
What I am currently doing is formatting the data into a JS constructor
$scope.model = new SomePrototype(data);
to deduce the CSS you compute the rules (in pseudo code):
var SomePrototype = function (data) {
this.computeCSS = function () {
if data.error then css = 'danger'
if (data.included and data.invalid) then css = 'danger'
/*other rules*/
return css
}
}
then you call computeCSS() in the HTML view
<p class="{{model.computeCSS()}}">{{model.value}}</p> which renders as
`<p class="danger">123456</p>`
ISSUE: first, I haven't seen anything like this elsewhere. So I might do something wrong.
Usually you get an object under $scope to hold the class value.
Secondly, it requires a call to SomePrototype into each controllers.
I wonder if using a service/factory would be more legal. The end result looks basically the same for me.
You don't need a function to set class based on scope values, use ng-class which accepts some javascript conditionals
<p ng-class="{danger: data.error || data.included && data.invalid} ">{{data.value}}</p>
function Ctrl($scope) {
$scope.data = {
"value": "123456",
"included": true,
"invalid": true,
"warning": false,
"error": false,
}
}
DEMO
You are on the right track however I would use ng-class as charlietfl suggested.
Keeping the logic inside of a function like you mentioned you are able to unit test your rule of what is considered an invalid state of the model. (your conditional is simple, but having logic in your view is usually not ideal)
Model
var SomePrototype = function (data) {
this.isDataInValid = function () {
return data.error || data.included && data.invalid;
}
}
Test
it('should be invalid with error or included && invalid', function () {
var data = {
"value": "123456",
"included": true,
"invalid": true,
"warning": false,
"error": false,
}
var someModel = new SomePrototype(data);
var isDataInValid = someModel.isDataInValid();
expect(isDataInValid).toBe(true);
});
In your <html/>
<p ng-class="{danger : model.isDataInValid() } ">{{model.value}}</p>

Resources