Let's assume I want to get the following jstree in shiny (the part with the button is just to illustrate that the shinytree is not present from the beginning):
$(function() {
$('#create').on('click', function() {
$('#mytree').jstree({
'core' : {
'data' : {
"url" : "//www.jstree.com/fiddle/?lazy",
"data" : function (node) {
return { "id" : node.id };
}
}
},
contextmenu : {
items : {
'item1' : {
'label' : 'item1',
'action' : function () { /* action */ }
}
}
},
plugins : ["contextmenu"]
});
})
})
<link href="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.8/themes/default/style.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.8/jstree.min.js"></script>
<div id="mytree"></div>
<button id = "create">
Create Tree
</button>
The problem is that I cannot provide the contextmenu via shinyTree. So I need to fall back to JavaScript to add this functionality myself. On the console I would do it like follows:
$('#mytree').jstree(true).settings.contextmenu = {
items: {
item1 : {
'label' : 'item1',
'action': function() { /* action */ }
}
}
}
But where and when would I call that in my ShinyApp? My approach with another handler does not work, because I guess that the handler fires before the tree is rendered (a second press to the button does the trick then, showing at least that the JS code works as intended). Playing with the priority did not help either.
Could I use a javascript event handler which I would attach to $(document), which listens to the creation of the tree?
ShinyApp
library(shiny)
library(shinyTree)
library(shinyjs)
js_code <- "$('#mytree').jstree(true).settings.contextmenu = {
items: {
item1 : {
'label' : 'item1',
'action': function() { /* action */ }
}
}
};"
ui <- fluidPage(useShinyjs(),
actionButton("create", "Create Tree"),
shinyTree("mytree", contextmenu = TRUE))
server <- function(input, output, session) {
## does not work as intended
observeEvent(input$create, runjs(js_code), ignoreInit = TRUE, priority = -1)
output$mytree <- renderTree({
req(input$create)
list("Root Node" = list("Child Node 1" = list(
"Child Node 3" = "",
"Child Node 4" = ""),
"Child Node 2" = ""))
})
}
shinyApp(ui, server)
I found the solution. Basically one can use the ready.jstree or loaded.jstree event:
library(shiny)
library(shinyTree)
library(shinyjs)
js_code <- "$('.shiny-tree').on('ready.jstree', function() {
$(this).jstree(true).settings.contextmenu = {
items: {
item1 : {
'label' : 'item1',
'action': function() { /* action */ }
}
}
};
})"
ui <- fluidPage(useShinyjs(),
actionButton("create", "Create Tree"),
shinyTree("mytree", contextmenu = TRUE))
server <- function(input, output, session) {
session$onFlushed(function() runjs(js_code))
output$mytree <- renderTree({
req(input$create)
list("Root Node" = list("Child Node 1" = list(
"Child Node 3" = "",
"Child Node 4" = ""),
"Child Node 2" = ""))
})
}
shinyApp(ui, server)
I successfully created a PDF using a JavaScript plug-in (pdfmake) and it was great.
But when I try to render an ~8,000-row inventory/ledger printout, it freeze for over a minute.
This is how I usually declare my docDefinition
var docDefinition = {
pageOrientation: orientation,
footer: function(currentPage, pageCount) { return {text: currentPage.toString() + ' / ' + pageCount, fontSize:8, alignment:'center'}; },
content:[
printHeader,
{ fontSize: 8, alignment: 'right', style: 'tableExample',
table: {
widths: width,
headerRows: 1, body: arr },
layout: 'lightHorizontalLines' }] }
where
var printHeader = [ { text: 'COMPANY NAME',alignment:'center' },
{ text: 'Address 1',alignment:'center' },
{ text: 'Address 2',alignment:'center' },
{ text: 'Additional Details,alignment:'center' },
{ text: 'document title',alignment:'center' }];
and
var arr = [[{"text":"","alignment":"left"},"text":"Date","alignment":"left"},
{"text":"Trans #","alignment":"left"},{"text":"Description","alignment":"left"},
{"text":"Ref #","alignment":"left"},{"text":"Debit","alignment":"left"},
{"text":"Credit","alignment":"left"},{"text":"Amount","alignment":"left"},
{"text":"Balance","alignment":"left"}],[{"text":"ACCOUNT : Merchandise Inventory","alignment":"left","colSpan":8},"","","","","","","",
{"text":"1,646,101.06"}],["","10/13/2015","ST#0094",{"text":"","alignment":"left"},{"text":"","alignment":"left"},"546.94","0.00","546.94","1,646,648.00"],[{"text":"Total","alignment":"left","bold":true},"","","","",
{"text":"546.94","alignment":"right","bold":true},
{"text":"0.00","alignment":"right","bold":true},
{"text":"","alignment":"right","bold":true},
{"text":"546.94","alignment":"right","bold":true}],[{"text":"ACCOUNT : Accounts Payable-Main","alignment":"left","colSpan":8},"","","","","","","",
{"text":"-1,741,953.62"}],["","10/13/2015","ST#0094",
{"text":"","alignment":"left"},
{"text":"","alignment":"left"},"0.00","546.94","-546.94","-1,742,500.56"],
[{"text":"Total","alignment":"left","bold":true},"","","","",
{"text":"0.00","alignment":"right","bold":true},
{"text":"546.94","alignment":"right","bold":true},
{"text":"","alignment":"right","bold":true},
{"text":"-546.94","alignment":"right","bold":true}]
generated .
I searched about web workers and see that it can solve this UI freezing problem.
So I tried to create a web worker for it:
$('#makepdf').click(function(){
var worker = new Worker("<?php echo URL::to('/'); ?>/js/worker.js");
worker.addEventListener('message',function(e){
console.log('Worker said: ',e.data);
},false);
worker.postMessage(docDefinition);
//worker.js
self.addEventListener('message', function(e) {
self.postMessage(e.data);
}, false);
Output from console.log():
Worker said: Object {pageOrientation: "portrait", content: Array[7]}
its logging correctly the json structure.
So far so good.
But after I added pdfmake.min.js and vfs_font.js to the worker, I get the error Uncaught TypeError: Cannot read property 'createElementNS' of undefined.
I get the error before I even started using the worker.
Is it possible to implement web workers with the pdfmake plug-in?
Simple answer
Just provide a dummy constructor:
var document = { 'createElementNS': function(){ return {} } };
var window = this;
importScripts( 'pdfmake.min.js', 'vfs_fonts.js' );
Alternatively, if you think it is too dirty, import XML-JS (only 60k) and create a virtual document for pdfmake.
importScripts( 'tinyxmlw3cdom.js' );
var window = this;
var document = new DOMDocument( new DOMImplementation() );
importScripts( 'pdfmake.min.js', 'vfs_fonts.js' );
Explanation
pdfmake is known to be incompatible with worker.
By itself, pdfmake does not use createElementNS.
However its minified script pdfmake.min.js do, apparently to create a download link.
We don't need download link anyway, so just give it a dummy to keep it happy (for now).
If in the future it needs a real DOM,
bad news is document is unavailable in web worker.
Good news is we have a pure javascript implementation.
Download XML-JS,
extract and find tinyxmlw3cdom.js,
import it and you can create a functional document.
In addition to the document, since vfs_fonts.js get pdfmake through the window variable, we need to introduce window as an alias for global.
For me, these steps make pdfmake works in web worker.
To save the file, you need to convert the base64 data provided by pdfmake into a binary download.
A number of scripts are available, such as download.js.
In the code below I am using FileSaver.
Code
My worker is a Builder that can be cached.
If you compose the worker on server side,
you can get rid of the stack and build functions and directly call pdfmake,
making both js code much simpler.
Main HTML:
<script src='FileSaver.min.js'></script>
<script>
function base64ToBlob( base64, type ) {
var bytes = atob( base64 ), len = bytes.length;
var buffer = new ArrayBuffer( len ), view = new Uint8Array( buffer );
for ( var i=0 ; i < len ; i++ )
view[i] = bytes.charCodeAt(i) & 0xff;
return new Blob( [ buffer ], { type: type } );
}
//////////////////////////////////////////////////////////
var pdfworker = new Worker( 'worker.js' );
pdfworker.onmessage = function( evt ) {
// open( 'data:application/pdf;base64,' + evt.data.base64 ); // Popup PDF
saveAs( base64ToBlob( evt.data.base64, 'application/pdf' ), 'General Ledger.pdf' );
};
function pdf( action, data ) {
pdfworker.postMessage( { action: action, data: data } );
}
pdf( 'add', 'Hello WebWorker' );
pdf( 'add_table', { headerRows: 1 } );
pdf( 'add', [ 'First', 'Second', 'Third', 'The last one' ] );
pdf( 'add', [ { text: 'Bold value', bold: true }, 'Val 2', 'Val 3', 'Val 4' ] );
pdf( 'close_table' );
pdf( 'add', { text: 'This paragraph will have a bigger font', fontSize: 15 } );
pdf( 'gen_pdf' ); // Triggers onmessage when it is done
// Alternative, one-size-fit-all usage
pdf( 'set', { pageOrientation: 'landscape', footer: { text: 'copyright 2015', fontSize: 8, alignment:'center'}, content:[ "header", { fontSize: 8, alignment: 'right', table: { headerRows: 1, body: [[1,2,3],[4,5,6]] } }] } );
pdf( 'gen_pdf' );
</script>
Worker:
//importScripts( 'tinyxmlw3cdom.js' );
//var document = new DOMDocument( new DOMImplementation() );
var document = { 'createElementNS': function(){ return {} } };
var window = this;
importScripts( 'pdfmake.min.js', 'vfs_fonts.js' );
(function() { 'use strict';
var doc, current, context_stack;
function set ( data ) {
doc = data;
if ( ! doc.content ) doc.content = [];
current = doc.content;
context_stack = [ current ];
}
set( {} );
function add ( data ) {
current.push( data );
}
function add_table ( template ) {
if ( ! template ) template = {};
if ( ! template.table ) template = { table: template };
if ( ! template.table.body ) template.table.body = [];
current.push( template ); // Append table
push( template.table.body ); // Switch context to table body
}
function push ( data ) {
context_stack.push( current );
return current = data;
}
function pop () {
if ( context_stack.length <= 1 ) return console.warn( "Cannot close pdf root" );
context_stack.length -= 1;
return current = context_stack[ context_stack.length-1 ];
}
function gen_pdf() {
pdfMake.createPdf( doc ).getBase64( function( base64 ) {
postMessage( { action: 'gen_pdf', base64: base64 } );
} );
}
onmessage = function( evt ) {
var action = evt.data.action, data = evt.data.data;
switch ( action ) {
case 'set': set( data ); break;
case 'add': add( data ); break;
case 'add_table' : add_table( data ); break;
case 'close_table': pop(); break;
case 'gen_pdf': gen_pdf(); break;
}
};
})();
Prior to 4.2 we could override the default MCE gallery template with this code:
var galleryview = wp.mce.views.get('gallery');
galleryview.getContent = function(){
this.template = media.template( 'editor-gallery-dtbaker-flexslider' );
return this.template(this.shortcode.attrs.named);
}
this no longer works. Is there a way to override the default 'gallery' view without re-registering it? This works (by copying and modifying the default code from mce-view.js) but I feel there should be a better way than re-registering the entire view.
Is this possible after the mce view rewrite? https://core.trac.wordpress.org/ticket/31412
Here's code that works in 4.2:
add_action( 'admin_print_footer_scripts', 'my_admin_footer_scripts', 100 );
function my_admin_footer_scripts(){ ?>
<script type="text/javascript">
( function( $ ) {
// start code copied from mce-view.js
var postID = $( '#post_ID' ).val() || 0;
var custom_gallery = _.extend( {}, {
edit: function( text, update ) {
var media = wp.media[ this.type ],
frame = media.edit( text );
this.pausePlayers && this.pausePlayers();
_.each( this.state, function( state ) {
frame.state( state ).on( 'update', function( selection ) {
update( media.shortcode( selection ).string() );
} );
} );
frame.on( 'close', function() {
frame.detach();
} );
frame.open();
},
state: [ 'gallery-edit' ],
template: wp.media.template( 'editor-gallery' ),
initialize: function() {
var attachments = wp.media.gallery.attachments( this.shortcode, postID ),
attrs = this.shortcode.attrs.named,
self = this;
if(typeof attrs.slider != 'undefined'){
switch(attrs.slider){
case 'flexslider':
this.template = wp.media.template( 'editor-gallery-dtbaker-flexslider' );
break;
case 'flex':
this.template = wp.media.template( 'editor-gallery-dtbaker-flex' );
break;
default:
// leave the existing template (editor-gallery)
}
}
attachments.more()
.done( function() {
attachments = attachments.toJSON();
_.each( attachments, function( attachment ) {
if ( attachment.sizes ) {
if ( attrs.size && attachment.sizes[ attrs.size ] ) {
attachment.thumbnail = attachment.sizes[ attrs.size ];
} else if ( attachment.sizes.thumbnail ) {
attachment.thumbnail = attachment.sizes.thumbnail;
} else if ( attachment.sizes.full ) {
attachment.thumbnail = attachment.sizes.full;
}
}
} );
self.render( self.template( {
attachments: attachments,
columns: attrs.columns ? parseInt( attrs.columns, 10 ) : wp.media.galleryDefaults.columns
} ) );
} )
.fail( function( jqXHR, textStatus ) {
self.setError( textStatus );
} );
}
} );
wp.mce.views.unregister('gallery');
wp.mce.views.register('gallery',custom_gallery);
})(jQuery);
<?php }
I have tried this as well and no go:
var dtbaker_gallery = wp.mce.views.get('gallery');
dtbaker_gallery = dtbaker_gallery.extend({
template: wp.media.template( 'editor-gallery-dtbaker-flexslider' )
});
I am trying to define new attributes for a central controller (a backbone view actually) to make it more compartmentalised and structured. I try to separate the rendering I want to achieve in different cases by if statements, and implementing each case's rendering in those new attributes. But after insertion of the replacement code to achieve that and commenting of the code before it make my app not work.
What is the wrong thing I do.
Another way I can imagine to modularise it is to conditionally, again, to load different AMD/Require.js modeules I write, in a separate file/s, and load them. But How? Via require() inside the render:, or by new moduleName passed to the second argument to the define() below.
define([
'jquery',
'ratchet',
'underscore',
'backbone',
'models/login',
'text!templates/login.html',
'text!templates/app.html'
], function($, Ratchet, _, Backbone, LoginModel, LoginTmpl, AppTmpl){
var LoginView = Backbone.View.extend({
el: $("body"),
template_login: _.template(LoginTmpl),
template_app: _.template(AppTmpl),
initialize: function(){
this.model = new LoginModel;
this.listenTo(this.model, 'change', this.render);
this.render();
},
//Replacement code: two attributes:
render_login: function(){
this.$el.html(this.template_app( this.model.toJSON() ));
console.log(JSON.stringify(this.model) );
return this;
}
},
render_app: function(){
this.$("#content").html(this.template_login( this.model.toJSON() ));
console.log(JSON.stringify(this.model) );
return this;
},
render: function() {
/*
if (this.model.get('loginp') == true) {
this.$el.html(this.template_app( this.model.toJSON() ));
console.log(JSON.stringify(this.model) );
return this;
}
*/
/*
else {
this.$("#content").html(this.template_login( this.model.toJSON() ));
console.log(JSON.stringify(this.model) );
return this;
}
*/
//Replacement code:
if (this.model.get('loginp') == true) {
this.render_app;
} else {
this.render_login;
}
},
events: {
'click #loginbtn' : 'login',
'click #registerbtn' : 'register'
},
login: function(e){
e.preventDefault();
var formData = {};
$( 'input' ).each( function( i, el ) {
if( $(el).val() != '' ) {
if( el.id === 'username' ) {
formData[ el.id ] = $(el).val();
} else if( el.id === 'password' ) {
formData[ el.id ] = $(el).val();
}
}
console.log( i + ": " + $(el).val() );
console.log(JSON.stringify(formData) );
// Clear input field values
$( el ).val('');
});
this.model.save( formData );
}
});
return LoginView;
});
In render method your replacement code that call render_app and render_login, actualy did't execute them. Your forgot ()
if (this.model.get('loginp') == true) {
this.render_app();
} else {
this.render_login();
}
Summing up with the above answer, the forgotten brackets,
1-render_app and render_login to render_app() and render_login()
2-correcting the syntax error: the second bracket in render_login's function definition.
3-either interchanging render_app and render_login attribute names or the templates used in their anonym functions. they se each others'.
I've just started foundational work for a Backbone JS SPA (single page application). I'm using the basic Underscore templating support, and am having issues with unexpected routing occurring.
Basically, the sign up view is shown initially as expected, POSTs succesfully when I click a button and I have it navigate to a simple test view. However, the test view is quickly rendered and then I get re-routed to the default sign up view again.
I see the history of the test page and if I hit the back button I go back to that test view which works fine. I see there is some event being triggered in Backbone which is routing me back to the blank fragment (sign up page), but I have no idea why. I've tried messing with the replace and trigger options on the navigate call with no luck.
As a side note, the start() and stop() View functions were adapted from this article: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/ . I tried removing this and it had no effect.
$(document).ready( function(){
Backbone.View.prototype.start = function() {
console.debug('starting');
if (this.model && this.modelEvents) {
_.each(this.modelEvents,
function(functionName, event) {
this.model.bind(event, this[functionName], this);
}, this);
}
console.debug('started');
return this;
};
Backbone.View.prototype.stop = function() {
console.debug('stopping');
if (this.model && this.modelEvents) {
_.each(this.modelEvents,
function(functionName, event) {
this.model.unbind(event, this[functionName]);
}, this);
}
console.debug('stopped');
return this;
};
var myApp = {};
myApp.SignUp = Backbone.Model.extend({
urlRoot:"rest/v1/user",
defaults: {
emailAddress: "email#me.com",
firstName: "First Name",
lastName: "Last Name",
password: "",
confirmPassword: ""
}
});
myApp.SignUpView = Backbone.View.extend({
el: $('#bodyTarget'),
modelEvents: {
'change' : 'render'
},
render: function(){
document.title = "Sign Up Page";
// Compile the template using underscore
var template = _.template( $("#signUpTemplate").html(), this.model.toJSON());
// Load the compiled HTML into the Backbone "el"
this.$el.html( template );
return this;
},
events: {
"click .signUpButton": "signUp"
},
signUp: function() {
bindFormValues(this);
this.model.save(this.model.attributes, {error: this.signUpFailure});
myApp.router.navigate("test", {trigger: true});
},
signUpFailure: function(model, response) {
alert("Failure: " + response);
}
});
myApp.TestView = Backbone.View.extend({
el: $('#bodyTarget'),
modelEvents: {
'change' : 'render'
},
render: function() {
document.title = "Test Page";
this.$el.html( "<div>this is a test view</div>");
return this;
}
});
// for now, just pull values from form back into model
function bindFormValues(view) {
var mod = view.model;
var el = view.$el;
var updates = {};
for (var prop in mod.attributes) {
var found = el.find('* [name="' + prop + '"]');
if (found.length > 0) {
updates[prop] = found.val();
}
}
mod.set(updates/*, {error: errorHandler}*/);
}
// routers
myApp.Router = Backbone.Router.extend({
routes: {
'test': 'test',
'': 'home',
},
home: function() {
console.debug('home:enter');
this.signUpView = new myApp.SignUpView({model: new myApp.SignUp()});
this.showView(this.signUpView);
console.debug('home:exit');
},
test: function() {
console.debug('test:enter');
this.testView = new myApp.TestView({model: new myApp.SignUp()});
this.showView(this.testView);
console.debug('test:exit');
},
showView: function(view) {
if (this.currentView) {
this.currentView.stop();
}
this.currentView = view.start().render();
}
});
myApp.router = new myApp.Router();
Backbone.history.start();
});
My HTML page just brings in the relevant scripts and has the div element bodyTarget which is injected with the views when it loads.
Edit: Duh, I found the problem. It turns out I needed to prevent event propagation on the call to signUp() by returning false.