How to print ExtJS component? - extjs

How do I pop up the Print dialog that will print out a component when OK-ed?

var targetElement = Ext.getCmp('PrintablePanelId');
var myWindow = window.open('', '', 'width=200,height=100');
myWindow.document.write('<html><head>');
myWindow.document.write('<title>' + 'Title' + '</title>');
myWindow.document.write('<link rel="Stylesheet" type="text/css" href="http://dev.sencha.com/deploy/ext-4.0.1/resources/css/ext-all.css" />');
myWindow.document.write('<script type="text/javascript" src="http://dev.sencha.com/deploy/ext-4.0.1/bootstrap.js"></script>');
myWindow.document.write('</head><body>');
myWindow.document.write(targetElement.body.dom.innerHTML);
myWindow.document.write('</body></html>');
myWindow.print();
write your extjs printable component into document.

I like Gopal Saini's answer! I took his approach and wrote a function for one of my apps. Here's the code. Tested on FF and Safari. Haven't tried it on IE but it should work.
print: function(el){
var win = window.open('', '', 'width='+el.getWidth()+',height='+el.getHeight());
if (win==null){
alert("Pop-up is blocked!");
return;
}
Ext.Ajax.request({
url: window.location.href,
method: "GET",
scope: this,
success: function(response){
var html = response.responseText;
var xmlDoc;
if (window.DOMParser){
xmlDoc = new DOMParser().parseFromString(html,"text/xml");
}
else{
xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
xmlDoc.async = false;
xmlDoc.loadXML(html);
}
win.document.write('<html><head>');
win.document.write('<title>' + document.title + '</title>');
var xml2string = function(node) {
if (typeof(XMLSerializer) !== 'undefined') {
var serializer = new XMLSerializer();
return serializer.serializeToString(node);
} else if (node.xml) {
return node.xml;
}
}
var links = xmlDoc.getElementsByTagName("link");
for (var i=0; i<links.length; i++){
win.document.write(xml2string(links[i]));
}
win.document.write('</head><body>');
win.document.write(el.dom.innerHTML);
win.document.write('</body></html>');
win.print();
},
failure: function(response){
win.close();
}
});
}

ExtJS 4.1:
https://github.com/loiane/extjs4-ux-gridprinter

Printing in ExtJS is not paticularly easy. The best resource I've found on making components printable can be found on a Sencha architect's blog. The post describes how to set up custom print renderers for components, and other details about printing. However, this information is for ExtJS 3.x; it's possible that ExtJS 4 has made printing easier.

You can also add a component to be printed to the Ext.window.Window with a modal property set to true and just open a standard print dialog which will only print the desired component.
var view = this.getView();
var extWindow = Ext.create('Ext.window.Window', { modal: true });
extWindow.add(component); // move component from the original panel to the popup window
extWindow.show();
window.print(); // prints only the content of a modal window
// push events to the event queue to be fired on the print dialog close
setTimeout(function() {
view.add(component); // add component back to the original panel
extWindow.close();
}, 0);

Another option to consider is to render the component to an image or pdf. While the pop-up window/print option is nice, some browsers don't print correctly. They tend to ignore background images, certain css properties, etc. To get the component to print exactly the way it appears in the pop-up window, I ended up writing some server side code to transform the html into an image.
Here's what the client code looks like:
print: function(el){
var waitMask = new Ext.LoadMask(Ext.getBody(), {msg:"Please wait..."});
waitMask.show();
//Parse current url to set up the host and path variables. These will be
//used to construct absolute urls to any stylesheets.
var currURL = window.location.href.toString();
var arr = currURL.split("/");
var len = 0;
for (var i=0; i<arr.length; i++){
if (i<3) len+=(arr[i].length+1);
}
var host = currURL.substring(0, len);
if (host.substring(host.length-1)=="/") host = host.substring(0, host.length-1);
var path = window.location.pathname;
if (path.lastIndexOf("/")!=path.length-1){
var filename = path.substring(path.lastIndexOf("/")+1);
if (filename.indexOf(".")!=-1){
path = path.substring(0, path.lastIndexOf("/")+1);
}
else{
path += "/";
}
}
//Start constructing an html document that we will send to the server
var html = ('<html><head>');
html += ('<title>' + document.title + '</title>');
//Insert stylesheets found in the current page. Update href attributes
//to absolute URLs as needed.
var links = document.getElementsByTagName("link");
for (var i=0; i<links.length; i++){
var attr = links[i].attributes;
if (attr.getNamedItem("rel")!=null){
var rel = attr.getNamedItem("rel").value;
var type = attr.getNamedItem("type").value;
var href = attr.getNamedItem("href").value;
if (href.toLowerCase().indexOf("http")!=0){
if (href.toString().substring(0, 1)=="/"){
href = host + href;
}
else{
href = host + path + href;
}
}
html += ('<link type="' + type + '" rel="' + rel+ '" href="' + href + '"/>');
}
}
html += ('</head><body id="print">');
html += (el.dom.innerHTML);
html += ('</body></html>');
//Execute AJAX request to convert the html into an image or pdf -
//something that will preserve styles, background images, etc.
//This, of course, requires some server-side code. In our case,
//our server is generating a png that we return to the client.
Ext.Ajax.request({
url: "/WebServices/Print?action=submit",
method: "POST",
rawData: html,
scope: this,
success: function(response){
var url = "/WebServices/Print?action=pickup&id="+response.responseText;
window.location.href = url;
waitMask.hide();
},
failure: function(response){
win.close();
waitMask.hide();
var msg = (response.responseText.length>0 ? response.responseText : response.statusText);
alert(msg);
}
});
}
Again, this requires some server-side magic to transform the html into an image. In my case, I implemented a "Print" service. Clients submit job requests via the "submit" action and retrieve output products via the "pickup" action.
To convert html to images, I ended up using a free command line app called Web Screen Capture. It only works on windows and I don't know how scalable it is so use at your risk.

Related

Creating new HTML images using a For loop through an array of image URLs

I have an array of 40 different image URLs being returned from an AJAX request. I'm trying to create a new HTML image element for each URL in the array using a For loop, as seen in the below code. For some reason, it's only displaying the image at the first URL and that's it. Any idea why the other 39 aren't showing up?
$(document).ready(function() {
$.ajax({
type: 'GET',
dataType: 'json',
url: 'https://****/images',
success: function(data) {
console.log(data);
let container = document.getElementById('feed');
let image = document.createElement("img");
for (let i = 0; i < data.length; i++) {
image.setAttribute('src', data[i]);
container.appendChild(image);
}
}
});
});
<body>
<div id="feed">
</div>
</body>
Try to create the element inside the loop.
for (let i = 0; i < data.length; i++) {
let image = document.createElement("img");
image.setAttribute('src', data[i]);
container.appendChild(image);
}
The way to create images is with new Image() and when appending multiple nodes to the DOM at once, it's better to first append the image nodes into a document fragment, and only when all the images have been appended to the fragment, then append the fragment itself into the Document (prevents redundant repaints)
// dummy data
const data = ['http://placekitten.com/100/100',
'http://placekitten.com/100/150',
'http://placekitten.com/100/180',
'http://placekitten.com/100/200']
// create a dumpster-node for the images to reside in
const fragment = document.createDocumentFragment();
// iterate the data and create <img> elements
data.forEach(url => {
let image = new Image()
image.src = url;
fragment.appendChild(image);
})
// dump the fragment into the DOM all the once (FTW)
document.body.appendChild(fragment);
I've used Array forEach iterator in my example, because it's easier in my opinion, but you can use a for loop (or for..of loop)

CQ/AEM extjs get selection dropdown box text, and get page path

Say I have a component with dialog drop to parsys on a content page /content/phonegap/ss/en_gb/login/home/test1/jcr:content/par/productimage
now inside the dialog i have something like
I wish to get $PATH append to URL and send selected device e.g the text 'Device ID:HTC_10_GOLD', to servlet in this dialog listener extjs:
<deviceAndColour
jcr:primaryType="cq:Widget"
allowBlank="{Boolean}false"
fieldLabel="Device and Colour"
name="./deviceAndColour"
options="/bin/reference/data/device.devices.json$PATH"
type="select"
xtype="selection">
<listeners
jcr:primaryType="nt:unstructured"
selectionchanged="function(pathfield) {
var selected = this.findParentByType('form').find('name', './deviceAndColour')[0].getText();
console.log( this.findParentByType('form').find('name', './deviceAndColour')[0].getText());
$.getJSON('/bin/reference/data/device/availablecolour.availablecolour.json$PATH?selectedDevice=' + selected + '&colour=red', function(jsonData){
selectBox.setOptions(jsonData);
});
}" />
</deviceAndColour>
So bascially, the console.log( this.findParentByType('form').find('name', './deviceAndColour')[0].getText()); is not working as I expected, neither the $PATH inside the dialog listener js, it does not retrieve the path at all.
Apart from above attempt, I know var selected = this.findParentByType('form').find('name', './deviceAndColour')[0].getValue(); this will get the value asscociated with the select correctly, but I do not need value, I just wish to getText(), and get the current $PATH in extjs.
another questions, you may noticed $.getJSON('/bin/reference/data/device/availablecolour.availablecolour.json$PATH?selectedDevice=' + selected + '&colour=red'
how do I skip the & in this listner, as if i use & directly, project won't even build. there must be someway to skip & and let extjs treat as part of the string to send request
Anyone expereinced this before? please suggest with code example.
Thanks
Getting the text of the option selected:
function(field,value){
for(var i = 0; i < field.options.length; i++) {
if(field.options[i].value === value) {
console.log('Selected: ' + field.options[i].text);
break;
}
}
}
Getting the path to the resource being edited:
function(field,value){
console.log("Resource:" + field.findParentByType("dialog").path);
}
Documentation: https://docs.adobe.com/docs/en/cq/5-6/widgets-api/index.html?class=CQ.form.Selection
UPDATE
Please try the following code adapted to your scenario (I also refactored the code to make use of params when providing query parameters. There is no reason why this shouldn't work.
function(field, value) {
var selected = '';
var path = field.findParentByType("dialog").path;
// get text of option selected
for(var i = 0; i < field.options.length; i++) {
if(field.options[i].value === value) {
selected = field.options[i].text;
break;
}
}
var params = {
selectedDevice: selected,
colour: 'red'
}
$.getJSON('/bin/reference/data/device/availablecolour.availablecolour.json'+path, params, function(jsonData){
// FIXME: how are you getting the "selectBox" reference?
selectBox.setOptions(jsonData);
});
}

Prevent search changes from spamming history

I'm using links like #!/OrderList?id=123, which shows the list of all orders and more details of the order 123. With reloadOnSearch=false and watching $routeUpdate it works fine, except for one thing: All such links get put into the browsers history, while I'd prefer to have only one such link there. For example, instead of
#!/OrderList?id=123
#!/OrderList?id=124
#!/OrderList?id=125
#!/AnotherList?id=678
#!/AnotherList?id=679
just the last member of each group, i.e.,
#!/OrderList?id=125
#!/AnotherList?id=679
I'm aware of $location.replace(), but I can't see where to place it when the change happens via following a link. I tried to place it in $scope.$on("$routeUpdate", ...), but it did nothing, probably because it's too late when the route has already changed.
I'm not using neither router-ui nor the HTML5 mode (just plain angular-route).
I'm afraid, I wasn't clear about me insisting on using href rather than a custom handler. I want the links to work with middle mouse click and bookmarks and everything. A combination of ng-href and ng-click might do what I want, but I've found a simple solution working with plain links.
Looks like you may want to update the URL query parameter using an ng-click function instead of relying on a link, then call a function like the one below to update the parameter... With replace state, the history should only track the current value. I haven't tested this case so if you try it, let me know if it works.
function changeUrlParam (param, value) {
var currentURL = window.location.href;
var urlObject = currentURL.split('?');
var newQueryString = '?';
value = encodeURIComponent(value);
if(urlObject.length > 1){
var queries = urlObject[1].split('&');
var updatedExistingParam = false;
for (i = 0; i < queries.length; i++){
var queryItem = queries[i].split('=');
if(queryItem.length > 1){
if(queryItem[0] == param){
newQueryString += queryItem[0] + '=' + value + '&';
updatedExistingParam = true;
}else{
newQueryString += queryItem[0] + '=' + queryItem[1] + '&';
}
}
}
if(!updatedExistingParam){
newQueryString += param + '=' + value + '&';
}
}else{
newQueryString += param + '=' + value + '&';
}
window.history.replaceState('', '', urlObject[0] + newQueryString.slice(0, -1));
}
Maybe what you can do is, istead of a regular <a ng-href="#!/OrderList?id={{your.id}}">Link to your ID</a> you can create a link with an ng-clickdirective bound to a function which retrieves the data and passes it to the view.
Your HTML
`<span ng-click="loadListItem(your.id)">Link to your ID</span>`
<div id="your-item-data">
{{item.id}} - {{item.name}}
</div>
Your controller
myApp.controller('someController', function($scope) {
$scope.loadListItem(itemId) = function (
var myItem;
// Get item by 'itemId' and assign it to 'myItem' var
$scope.item = myItem;
);
});
This way instead of changing your URL, you can retrieve the item data in your controller and pass it to your view.
You don't give much detail of your controller/service implementation, but I hope this helps.
I think you were on the right track with the $scope.$on("$routeUpdate", ...) thing. Rather than $routeUpdate, however, try binding on $routeChangeStart:
$scope.$on("$routeChangeStart", function(event, nextRoute, currentRoute){
if (nextRoute.yourCriteria === currentRoute.yourCriteria){
//do your location replacement magic
}
});
If you wanted, you could even define a dontUpdateHistory boolean property in your route definitions, and then check for that property in your run block:
myApp.config(function($routeProvider){
$routeProvider.when('/whatever' {
templateUrl: 'whatever',
dontUpdateHistory: true //something like this
});
}).run(function($rootScope){
$rootScope.on('$routeChangeStart', function(event, nextRoute, currentRoute){
if (nextRoute.dontUpdateHistory){
//do your location replacement magic
}
});
I haven't tested any of this, but hopefully it gets the idea across.
I wasn't satisfied with any answer and after quite some debugging I found this solution:
.run(function($rootScope, $location) {
var replacing;
$rootScope.$on("$locationChangeStart", function(event, newUrl, oldUrl) {
if (oldUrl === newUrl) return; // Nobody cares.
// Make urls relative.
var baseLength = $location.absUrl().length - $location.url().length;
newUrl = newUrl.substring(baseLength);
oldUrl = oldUrl.substring(baseLength);
// Strip search, leave path only.
var newPath = newUrl.replace(/\?.*/, "");
var oldPath = oldUrl.replace(/\?.*/, "");
// Substantial change, history should be written normally.
if (oldPath !== newPath) return;
// We're replacing, just let it happen.
if (replacing) {
replacing = false;
return;
}
// We're NOT replacing, scratch it ...
event.preventDefault();
// ... and do the same transition with replace later.
$rootScope.$evalAsync(function() {
$location.url(newUrl).replace();
replacing = true;
});
});
})

Routing in SAPUI5: How to implement passing of URL? Model data not initialy loaded

My goal is to write a SAPUI5 Fiori app with routing support. One mail goal is to have passable URLs. For example in an E-Mail like "please approve this: link". The link is an URL matched by my rounting config, e.g.index.html#/applicants/8.
I use a typical sap.m.SplitApp kind of application. Clicking a list item in masterview changes the URL to index.html#/applicants/[id of entry in JSON]. I can click on the list, my defined routes are getting matched and the apps loads the (applicant) data as expected.
However, and here comes my question, this doeas not work when using an URL directly, say pasting [my url]/index.html#/applicants/8 into my browser. The app is launched but no detail data is loaded. I have to click on another list item again to get the data.
Actually, the controller is called when passing the URL, but it seems the model is not initiated and undefined. My JSON model is bound in the createContent function of my Component.js
// Update 2015-05-14
The problems seems to be around the getData() function. I have the model, it has the entries, but getData() returns undefined for the first time my app is loaded. I recently read getData() is deprecated. How should I improve my coding below?
// Component.js
ui5testing.Component.prototype.createContent = function(){
// create root view
var oView = sap.ui.view({
id : "app",
viewName : "ui5testing.view.Main",
type : "JS",
viewData : {
component : this
}
var oModel = new sap.ui.model.json.JSONModel("model/mock_applicants.json");
oView.setModel(oModel);
[...]
return oView;
});
// Master controller
handleApplicantSelect : function (evt) {
var oHashChanger = sap.ui.core.routing.HashChanger.getInstance();
var context = evt.getParameter("listItem").getBindingContext();
var path = context.getPath();
var model = this.getView().getModel();
var item = model.getProperty(path);
oHashChanger.setHash("applicants/" + item.id);
},
// Detail controller
onInit: function() {
this.router = sap.ui.core.UIComponent.getRouterFor(this);
this.router.attachRoutePatternMatched(this._handleRouteMatched, this);
},
_handleRouteMatched : function(evt){
var objectId = evt.getParameter("arguments").id;
var model = this.getView().getModel();
var data = model.getData()["applicants"];
var pathId;
if (data) {
for (var i = 0; data.length; i++) {
if ( objectId == data[i].id ) {
pathId = i;
break;
}
}
var sPath = "/applicants/" + pathId;
var context = new sap.ui.model.Context(model, sPath)
this.getView().setBindingContext(context);
}
},
As you've figured out that getData() returns undefined for the first time, which means the model data is still not yet loaded. So you can make use of attachRequestCompleted method of the model & fire an event from the component & listen to that event in the detail controller to ensure the routerPatternMatched() gets executed only after the data is loaded.
//Component.js
var oModel = new sap.ui.model.json.JSONModel("model/mock_applicants.json");
oModel.attachRequestCompleted(jQuery.proxy(function(){
this.fireEvent("MockDataLoaded"); // fireEvent through component
},this));
oView.setModel(oModel);
//Detail controller
onInit : function(){
this.router = sap.ui.core.UIComponent.getRouterFor(this);
var oComponent = this.getOwnerComponent();
oComponent.attachEvent("MockDataLoaded",jQuery.proxy(function(){
this.router.attachRoutePatternMatched(this._handleRouteMatched, this);
},this));
}
Or the simplest & but the dirty way would be to make an synchronous request instead of an async request to load data.
var oModel = new sap.ui.model.json.JSONModel();
oModel.loadData(""model/mock_applicants.json",{bAsync:false});
oView.setModel(oModel);

passing an array in a message from content script to background page in google chrome extension

I am writing a Google Chrome extension.
I want to pass a small array from a content script to background page in a message. Can I simply reference the array name or need I construct a JSON object from it first?
Here is the code:
IN THE CONTENT SCRIPT
var req;
var detailWin;
//drag off the f_foto class
var searchResult = document.getElementsByClassName("f_foto");
alert("Found Class f_foto "+searchResult.length+" times.");
//collect profile links
for (var i = 0; i<searchResult.length; ++i)
{
var profileLink=searchResult[i].getElementsByTagName("a");
profileLinks[i]=profileLink[0].href;
// alert(i+1+" of "+searchResult.length+" "+profileLinks[i]+" length of "+profileLinks[i].length);
}
for (var i = 0; i<searchResult.length; ++i)
{
//tell bkgd page to open link
chrome.extension.sendRequest({cmd: "openProfile", url: profileLinks[i]});
//BETTER TO SEND WHOLE ARRAY.
//LIKE THIS? chrome.extension.sendRequest({cmd: "openProfile", urlList: profileLinks});
//OR SHOULD I MAKE A JSON OBJECT OUT OF IT?
}
//IN THE BACKGROUND PAGE
var detailTabId = null;
var profileLinks = new Array();
chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
if(request.cmd == "openProfile") {
//IF RECEIVING AN ARRAY, PROCESS IT LIKE THIS?
// profileLinks= request.urlList;
// console.log=("Received "+ urlList.length + " links.");
chrome.tabs.create({url: request.url}, function(tab){
//save tab id so we can close this tab later
detailTabId = tab.id;
//profile tab is created, inject profile script
chrome.tabs.executeScript(tab.id, {file: "profile.js"});
});
}
});
An Array is a construct of a JSON object so there is no need to do anything other than what you are doing now.

Resources