Why popup window stay front after refresh parent window? [duplicate] - javascript

I can't seem to get this to work.
In response to a click, window A opens window B (which then has focus). Then, in response to a click on B, the window calls window.opener.focus(), but the focus does not go back to A.
I have found a strange, strange workaround for Chrome (29, possibly others).
If I run:
window.opener.name = 'somename';
window.open(window.opener.location.href, window.opener.name);
window.opener.focus();
it does work (and doesn't reload window A). But this doesn't work for Firefox, and it is probably a fluke anyway.
It seems very clear to me what opener and focus are supposed to do, but window.opener.focus() doesn't work. What am I missing?

From the fine manual:
Makes a request to bring the window to the front. It may fail due to user settings and the window isn't guaranteed to be frontmost before this method returns.
Emphasis mine. Calling focus() is just a request and the browser is free to ignore you and you should generally expect to be ignored. Consider what sorts of nefarious things you could get up to by switching focus to a tiny window while someone is typing if you need some reasons why a browser would ignore your request.
If you need focus() to work for your application to work then you need to redesign your application so that it doesn't need to call focus().

I can see why a browser/OS will not allow a child windows to take over the focus (abuse of power). Here is a workaround:
In the parent window, declare a function in "window.external" that will trigger Javascript "alert()" or "confirm()".
Invoke that function from the child window.
The browser might ignore a request from a child window that wants to control the focus (e.g. window.opener.focus()), but the browser should honor a request from a parent window that triggers an alert() or a confirm() action, which requires to focus on the parent window.
JS Parent:
var child = window.open('child.html', 'child');
window.external.comeback = function() {
var back = confirm('Are you sure you want to comback?');
if(back) {
child.close();
} else {
child.focus();
}
}
JS Child:
// assuming you have jQuery
$('.btn').click() {
window.opener.external.comeback();
};
--I am using this code in a real world application to handle a checkout request that runs in child window, and I need to gracefully return to the parent window.

Web-facing or private intranet?
Window management is up to the Browser and the OS. HTML & ECMAscript have nothing to say about it.
If this is for a public-facing website, then just don't bother -- as they say, "Don't break the web."
But I really wanna!
If this is for some tightly managed (say Intranet) application of some kind then you'll need to resort to writing Addons/Extensions. It's certaintly easier if you can restrict yourself to a single browser & platform.
EDIT: Example for Firefox on Win32...
This solution works as a custom addon for Firefox which uses jsctypes internally to load a Win32 DLL. The window_focus() JavaScript function is exposed which does what you want.
There are 3 parts to this solution:
The privileged JavaScript code to load/bind the Win32 APIs
The CPP header file for our external DLL
The CPP source file for our external DLL
I built a simple GUI DLL project in MSVC++ with the later two files & compiled wmctrl.dll, depending on msvcr100.dll, and used Dependency Walker to find the "plain C" symbols exported by the DLL for use by js-ctypes. E.g: ?wmctrl_find_window##YAKPAD#Z is the "plain C" symbol for the C++ api function called wmctrl_find_window.
As a caveat, this code relies on temporarily being able to change the title of the window that needs to be focused so that Win32 APIs can examine all windows on your desktop to find the correct Firefox window.
You need to have access to privileged Mozilla platform APIs, i.e: JavaScript inside a Firefox Addon.
In your privileged JavaScript code:
// get API constants (might already be available)
const {Cc,Ci,Cu} = require("chrome");
// import js-ctypes
var file=null, lib=null, ctypes = {};
Cu.import("resource://gre/modules/ctypes.jsm", ctypes);
var ctypes = ctypes.ctypes;
// build platform specific library path
var filename = ctypes.libraryName("wmctrl"); // automatically adds '.dll'
var comp = "#mozilla.org/file/directory_service;1";
var file = Cc[comp].getService(Ci.nsIProperties).get("CurProcD", Ci.nsIFile);
file.append("browser_code"); // or whereever you put your DLL
file.append(filename);
// get the JavaScript library interface (load the library)
var lib = ctypes.open(file.path);
// wmctrl_find_window: returing unsigned 32bit (long) "window handle"
// takes string "window title".
var find_window = lib.declare(
"?wmctrl_find_window##YAKPAD#Z", /* plain "C" DLL symbol */
ctypes.stdcall_abi, ctypes.uint32_t, /* return type: uint32 */
ctypes.char.ptr); /* parameter: string */
// wmctrl_window_focus: takes unsigned 32bit (long) "window handle".
var window_focus = lib.declare(
"?wmctrl_window_focus##YAXK#Z", /* plain "C" DLL symbol */
ctypes.stdcall_abi, ctypes.void_t, /* return type: void */
ctypes.uint32_t); /* parameter: uint32 */
wmctrldll.h
#ifdef WMCTRLDLL_EXPORTS
#define WMCTRLDLL_API __declspec(dllexport)
#else
#define WMCTRLDLL_API __declspec(dllimport)
#endif
WMCTRLDLL_API void wmctrl_window_focus (unsigned long wid);
WMCTRLDLL_API unsigned long wmctrl_find_window(char* find_title);
wmctrldll.cpp
typedef struct {
HWND hWnd;
char title[255];
} myWinSpec;
BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam) {
char String[255];
myWinSpec* to_find = (myWinSpec*) lParam;
// not a window
if (!hWnd) return TRUE;
// not visible
if (!IsWindowVisible(hWnd)) return TRUE;
// no window title
if (!GetWindowTextA(hWnd, (LPSTR)String, 255)) return TRUE;
// no title match
if (strcmp(String, to_find->title) != 0) return TRUE;
to_find->hWnd = hWnd;
return FALSE;
}
WMCTRLDLL_API void wmctrl_window_focus(unsigned long wid) {
SetForegroundWindow((HWND) wid);
}
WMCTRLDLL_API unsigned long wmctrl_find_window(char* find_title) {
myWinSpec to_find;
sprintf_s(to_find.title, sizeof(to_find.title), "%s", find_title);
to_find.hWnd = 0;
EnumWindows(EnumWindowsProc, (LPARAM)&to_find);
return (unsigned long) to_find.hWnd;
}

I found a reasonable workaround for this using web Notifications.
As mentioned in some of the other answers here there are some constraints with browsers and the OS where window.opener.focus() may not work.
I was able to get this to work using postMessage and Notifications.
To try this out:
From the parent (opener) tab add a 'message' event listener that creates a notification with a click listener:
window.addEventListener("message", (event) => {
if (!("Notification" in window)) {
alert("This browser does not support desktop notification");
}
// Let's check whether notification permissions have already been granted
else if (Notification.permission === "granted") {
// If it's okay let's create a notification
var notification = new Notification("The opener needs your attention");
// Add click listener that will perform a window focus
notification.onclick = function (x) {
window.focus();
this.close();
};
}
// Otherwise, we need to ask the user for permission
else if (Notification.permission !== "denied") {
Notification.requestPermission().then(function (permission) {
// If the user accepts, let's create a notification
if (permission === "granted") {
var notification = new Notification("The opener needs your attention");
// Add click listener that will perform a window focus
notification.onclick = function (x) {
window.focus();
this.close();
};
}
});
}
});
Then from the child tab you need to use postMessage when focus back to parent tab is needed:
// Called from child tab
window.opener.postMessage(
{
message: "focus",
},
"*" // cross-origin
);
Once you call postMessage you should see a OS Web notification. If a user clicks this they should be redirected to the parent (opener) tab.

Workaround
In main window added script function:
function here() {
alert('Welcome Back') // seems needed to wake up document
window.focus()
}
In opened window invoke script function:
function HomeTab() {
O = window.opener;
if (O)
if (O.closed) alert('Home page has been closed')
else O.here()
else alert('This tab has no home page')
}
Works widely differently in different browsers
Some will have the parent tab blink
Some mark the parent tab, and you have to notice it
Some you have to click on home tab the first time and you can then give it permission to go directly to home tab without a confirm box.

Related

Get nsIDOMWindow of a Tab to do Text Input

Is it possible to use Text Input Processor on a specific Tab of a Firefox window?
Do all the tabs share the same nsIDOMWindow object? If so, then is there any other solution?
Rephrasing my problem: I want to type text to a specific tab, and if there is more than one, then it might not be the current.
Below is an example code to write text on any Firefox window, but only for current tab:
function myTestFunc() {
// var windows = Services.wm.getEnumerator("navigation:browser");
var windows = require("sdk/windows");
for (let browserWindow of windows.browserWindows) {
//console.log(window.title);
var chromeWindow = viewFor(browserWindow);
console.log(chromeWindow.document.location.href);
var idomWindow = chromeWindow.QueryInterface(Ci.nsIDOMWindow);
var TIP = Cc["#mozilla.org/text-input-processor;1"].
createInstance(Ci.nsITextInputProcessor);
if(!TIP.beginInputTransaction(idomWindow, onNotifyImpl)) {
console.log("Error TIP can't start");
continue;
}
TIP.setPendingCompositionString("foo-bar-buzz");
if (!TIP.flushPendingComposition()) {
console.log("Failed to flush");
continue; // Composition is not started
}
}
}
nsIDOMWindow (+ various related interfaces like the docshell) is the XPCOM representation of regular window objects in the w3c specs. And since window objects can be nested (e.g. via iframes) so can be nsIDOMWindows. When you're accessing browser windows you're generally accessing the outer windows representing the browser chrome, not the content.
In principle you can access a tab's content window directly from its <browser> XUL element, but to be forward-compatible with e10s you should use framescripts instead.

Save web pages with Firefox addon using file -> save as pop-up window

Let me start off by saying I am new to add-on development. Using the Add-on SDK, I am trying to create a simple Firefox add-on that, when a button is pressed, acts like pressing the Ctrl-S hotkey, or following the file -> save page as to get the save page pop up window. I have looked at similar questions on here, but they seem to be going around the built in save functions, and not utilizing the "save page as" window.
The end goal is to run other functions before the save call is made. The user will only see the save page window as normal.
I am unaware of ways to send hotkey signals, or accessing the file drop down menu from within an add-on.
One way to do this is to invoke the Save As dialog exactly as if the user had clicked on the "Save Page As..." menu item (id="menu_savePage"). You can do this by executing the doCommand() method of that menu item. The following assumes that the event passed in is the command event for the button that the user clicked.
function launchSaveAsFromButton(event) {
var window = event.view;
//Create some common variables if they do not exist.
// This should work from any Firefox context.
// Depending on the context in which the function is being run,
// this could be simplified.
if (window === null || typeof window !== "object") {
//If you do not already have a window reference, you need to obtain one:
// Add a "/" to un-comment the code appropriate for your add-on type.
//* Add-on SDK:
var window = require('sdk/window/utils').getMostRecentBrowserWindow();
//*/
/* Overlay and bootstrap (from almost any context/scope):
var window=Components.classes["#mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator)
.getMostRecentWindow("navigator:browser");
//*/
}
if (typeof document === "undefined") {
//If there is no document defined, get it
var document = window.content.document;
}
if (typeof gBrowser === "undefined") {
//If there is no gBrowser defined, get it
var gBrowser = window.gBrowser;
}
let menuSavePage = gBrowser.ownerDocument.getElementById("menu_savePage");
menuSavePage.doCommand();
}
Finding out the ID for the "Save Page As..." dialog is made easier by using the DOM Inspector in combination with the add-on Element Inspector.

window.opener.focus() doesn't work

I can't seem to get this to work.
In response to a click, window A opens window B (which then has focus). Then, in response to a click on B, the window calls window.opener.focus(), but the focus does not go back to A.
I have found a strange, strange workaround for Chrome (29, possibly others).
If I run:
window.opener.name = 'somename';
window.open(window.opener.location.href, window.opener.name);
window.opener.focus();
it does work (and doesn't reload window A). But this doesn't work for Firefox, and it is probably a fluke anyway.
It seems very clear to me what opener and focus are supposed to do, but window.opener.focus() doesn't work. What am I missing?
From the fine manual:
Makes a request to bring the window to the front. It may fail due to user settings and the window isn't guaranteed to be frontmost before this method returns.
Emphasis mine. Calling focus() is just a request and the browser is free to ignore you and you should generally expect to be ignored. Consider what sorts of nefarious things you could get up to by switching focus to a tiny window while someone is typing if you need some reasons why a browser would ignore your request.
If you need focus() to work for your application to work then you need to redesign your application so that it doesn't need to call focus().
I can see why a browser/OS will not allow a child windows to take over the focus (abuse of power). Here is a workaround:
In the parent window, declare a function in "window.external" that will trigger Javascript "alert()" or "confirm()".
Invoke that function from the child window.
The browser might ignore a request from a child window that wants to control the focus (e.g. window.opener.focus()), but the browser should honor a request from a parent window that triggers an alert() or a confirm() action, which requires to focus on the parent window.
JS Parent:
var child = window.open('child.html', 'child');
window.external.comeback = function() {
var back = confirm('Are you sure you want to comback?');
if(back) {
child.close();
} else {
child.focus();
}
}
JS Child:
// assuming you have jQuery
$('.btn').click() {
window.opener.external.comeback();
};
--I am using this code in a real world application to handle a checkout request that runs in child window, and I need to gracefully return to the parent window.
Web-facing or private intranet?
Window management is up to the Browser and the OS. HTML & ECMAscript have nothing to say about it.
If this is for a public-facing website, then just don't bother -- as they say, "Don't break the web."
But I really wanna!
If this is for some tightly managed (say Intranet) application of some kind then you'll need to resort to writing Addons/Extensions. It's certaintly easier if you can restrict yourself to a single browser & platform.
EDIT: Example for Firefox on Win32...
This solution works as a custom addon for Firefox which uses jsctypes internally to load a Win32 DLL. The window_focus() JavaScript function is exposed which does what you want.
There are 3 parts to this solution:
The privileged JavaScript code to load/bind the Win32 APIs
The CPP header file for our external DLL
The CPP source file for our external DLL
I built a simple GUI DLL project in MSVC++ with the later two files & compiled wmctrl.dll, depending on msvcr100.dll, and used Dependency Walker to find the "plain C" symbols exported by the DLL for use by js-ctypes. E.g: ?wmctrl_find_window##YAKPAD#Z is the "plain C" symbol for the C++ api function called wmctrl_find_window.
As a caveat, this code relies on temporarily being able to change the title of the window that needs to be focused so that Win32 APIs can examine all windows on your desktop to find the correct Firefox window.
You need to have access to privileged Mozilla platform APIs, i.e: JavaScript inside a Firefox Addon.
In your privileged JavaScript code:
// get API constants (might already be available)
const {Cc,Ci,Cu} = require("chrome");
// import js-ctypes
var file=null, lib=null, ctypes = {};
Cu.import("resource://gre/modules/ctypes.jsm", ctypes);
var ctypes = ctypes.ctypes;
// build platform specific library path
var filename = ctypes.libraryName("wmctrl"); // automatically adds '.dll'
var comp = "#mozilla.org/file/directory_service;1";
var file = Cc[comp].getService(Ci.nsIProperties).get("CurProcD", Ci.nsIFile);
file.append("browser_code"); // or whereever you put your DLL
file.append(filename);
// get the JavaScript library interface (load the library)
var lib = ctypes.open(file.path);
// wmctrl_find_window: returing unsigned 32bit (long) "window handle"
// takes string "window title".
var find_window = lib.declare(
"?wmctrl_find_window##YAKPAD#Z", /* plain "C" DLL symbol */
ctypes.stdcall_abi, ctypes.uint32_t, /* return type: uint32 */
ctypes.char.ptr); /* parameter: string */
// wmctrl_window_focus: takes unsigned 32bit (long) "window handle".
var window_focus = lib.declare(
"?wmctrl_window_focus##YAXK#Z", /* plain "C" DLL symbol */
ctypes.stdcall_abi, ctypes.void_t, /* return type: void */
ctypes.uint32_t); /* parameter: uint32 */
wmctrldll.h
#ifdef WMCTRLDLL_EXPORTS
#define WMCTRLDLL_API __declspec(dllexport)
#else
#define WMCTRLDLL_API __declspec(dllimport)
#endif
WMCTRLDLL_API void wmctrl_window_focus (unsigned long wid);
WMCTRLDLL_API unsigned long wmctrl_find_window(char* find_title);
wmctrldll.cpp
typedef struct {
HWND hWnd;
char title[255];
} myWinSpec;
BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam) {
char String[255];
myWinSpec* to_find = (myWinSpec*) lParam;
// not a window
if (!hWnd) return TRUE;
// not visible
if (!IsWindowVisible(hWnd)) return TRUE;
// no window title
if (!GetWindowTextA(hWnd, (LPSTR)String, 255)) return TRUE;
// no title match
if (strcmp(String, to_find->title) != 0) return TRUE;
to_find->hWnd = hWnd;
return FALSE;
}
WMCTRLDLL_API void wmctrl_window_focus(unsigned long wid) {
SetForegroundWindow((HWND) wid);
}
WMCTRLDLL_API unsigned long wmctrl_find_window(char* find_title) {
myWinSpec to_find;
sprintf_s(to_find.title, sizeof(to_find.title), "%s", find_title);
to_find.hWnd = 0;
EnumWindows(EnumWindowsProc, (LPARAM)&to_find);
return (unsigned long) to_find.hWnd;
}
I found a reasonable workaround for this using web Notifications.
As mentioned in some of the other answers here there are some constraints with browsers and the OS where window.opener.focus() may not work.
I was able to get this to work using postMessage and Notifications.
To try this out:
From the parent (opener) tab add a 'message' event listener that creates a notification with a click listener:
window.addEventListener("message", (event) => {
if (!("Notification" in window)) {
alert("This browser does not support desktop notification");
}
// Let's check whether notification permissions have already been granted
else if (Notification.permission === "granted") {
// If it's okay let's create a notification
var notification = new Notification("The opener needs your attention");
// Add click listener that will perform a window focus
notification.onclick = function (x) {
window.focus();
this.close();
};
}
// Otherwise, we need to ask the user for permission
else if (Notification.permission !== "denied") {
Notification.requestPermission().then(function (permission) {
// If the user accepts, let's create a notification
if (permission === "granted") {
var notification = new Notification("The opener needs your attention");
// Add click listener that will perform a window focus
notification.onclick = function (x) {
window.focus();
this.close();
};
}
});
}
});
Then from the child tab you need to use postMessage when focus back to parent tab is needed:
// Called from child tab
window.opener.postMessage(
{
message: "focus",
},
"*" // cross-origin
);
Once you call postMessage you should see a OS Web notification. If a user clicks this they should be redirected to the parent (opener) tab.
Workaround
In main window added script function:
function here() {
alert('Welcome Back') // seems needed to wake up document
window.focus()
}
In opened window invoke script function:
function HomeTab() {
O = window.opener;
if (O)
if (O.closed) alert('Home page has been closed')
else O.here()
else alert('This tab has no home page')
}
Works widely differently in different browsers
Some will have the parent tab blink
Some mark the parent tab, and you have to notice it
Some you have to click on home tab the first time and you can then give it permission to go directly to home tab without a confirm box.

Javascript detect closing popup loaded with another domain

I am opening a popup window and attaching an onbeforeunload event to it like this:
win = window.open("http://www.google.com", "", "width=300px,height=300px");
win.onbeforeunload = function() {
//do your stuff here
alert("Closed");
};
If I leave the URL empty, the new popup opens with "about:blank" as the address but when I close it, I see the alert.
If I open in as you see it (with an external URL), once it's closed, I cannot see the alert anymore. Any idea why this is happening?
As mentioned, same origin policy prevents Javascript from detecting such events. But there's a quite simple solution which allows you to detect closure of such windows.
Here's the JS code:
var openDialog = function(uri, name, options, closeCallback) {
var win = window.open(uri, name, options);
var interval = window.setInterval(function() {
try {
if (win == null || win.closed) {
window.clearInterval(interval);
closeCallback(win);
}
}
catch (e) {
}
}, 1000);
return win;
};
What it does: it creates new window with provided parameters and then sets the checker function with 1s interval. The function then checks if the window object is present and has its closed property set to false. If either ot these is not true, this means, that the window is (probably) closed and we should fire the 'closeCallback function' callback.
This function should work with all modern browsers. Some time ago Opera caused errors when checking properties from windows on other domains - thus the try..catch block. But I've tested it now and it seems it works quite ok.
We used this technique to create 'facebook-style' login popups for sites which doesn't support them via SDK (ehem... Twitter... ehem). This required a little bit of extra work - we couldn't get any message from Twitter itself, but the Oauth redireced us back to our domain, and then we were able to put some data in popup window object which were accessible from the opener. Then in the close callback function we parsed those data and presented the actual results.
One drawback of this method is that the callback is invoked AFTER the window has been closed. Well, this is the best I was able to achieve with cross domain policies in place.
You could listen to the 'focus' event of the opener window which fires when the user closes the popup.
Unfortunately, you're trying to communicate across domains which is prohibited by JavaScript's same origin policy. You'd have to use a server-side proxy or some other ugly hack to get around it.
You could try creating a page on your site that loads the external website in an iframe. You could then pop open that page and listen for it to unload.
I combined #ThomasZ's answer with this one to set an interval limit (didn't want to use setTimeout).
Example (in Typescript, declared anonymously so as not lose reference to "this"):
private _callMethodWithInterval = (url: string, callback: function, delay: number, repetitions: number) => {
const newWindow = window.open(url, "WIndowName", null, true);
let x = 0;
let intervalID = window.setInterval(() => {
//stops interval if newWindow closed or doesn't exist
try {
if (newWindow == null || newWindow.closed) {
console.info("window closed - interval cleared")
callback();
window.clearInterval(intervalID);
}
}
catch (e) {
console.error(`newWindow never closed or null - ${e}`)
}
//stops interval after number of intervals
if (++x === repetitions) {
console.info("max intervals reached - interval cleared")
window.clearInterval(intervalID);
}
}, delay)
}//end _callMethodWithInterval

help with Firefox extension in multiple windows

I'm writing a Firefox extension that creates a socket server which will output the active tab's URL when a client makes a connection to it. I have the following code in my javascript file:
var serverSocket;
function startServer()
{
var listener =
{
onSocketAccepted : function(socket, transport)
{
try {
var outputString = gBrowser.currentURI.spec + "\n";
var stream = transport.openOutputStream(0,0,0);
stream.write(outputString,outputString.length);
stream.close();
} catch(ex2){ dump("::"+ex2); }
},
onStopListening : function(socket, status){}
};
try {
serverSocket = Components.classes["#mozilla.org/network/server-socket;1"]
.createInstance(Components.interfaces.nsIServerSocket);
serverSocket.init(7055,true,-1);
serverSocket.asyncListen(listener);
} catch(ex){ dump(ex); }
document.getElementById("status").value = "Started";
}
function stopServer ()
{
if (serverSocket)
serverSocket.close();
}
window.addEventListener("load", function() { startServer(); }, false);
window.addEventListener("unload", function() { stopServer(); }, false);
As it is, it works for multiple tabs in a single window. If I open multiple windows, it ignores the additional windows. I think it is creating a server socket for each window, but since they are using the same port, the additional sockets fail to initialize. I need it to create a server socket when the browser launches and continue running when I close the windows (Mac OS X). As it is, when I close a window but Firefox remains running, the socket closes and I have to restart firefox to get it up an running. How do I go about that?
Firefox extension overlays bind to window objects. One way around this is to create an XPCOM component or find one that someone else already created to allow you to build functionality without binding it to the window objects.
Of course, section #2 below on Observer Notifications may be helpful as well.
Possible workaround: #1
Instead of calling "startServer()" each time a window is opened, you could have a flag called windowCount that you could increment each time you open a new window. If windowCount is greater than 0, don't call startServer().
As windows close, you could decrement the count. Once it hits 0, stop the server.
Here is information from the Mozilla forums on this problem:
http://forums.mozillazine.org/viewtopic.php?f=19&t=2030279
Possible workaround #2:
With that said, I've also found documentation for Observer Notifications, which may be helpful as there is a section on Application Startup and Shutdown:
https://developer.mozilla.org/en/Observer_Notifications
UPDATE:
Here are some resources on creating XPCOM components in JavaScript and in C++:
https://developer.mozilla.org/en/how_to_build_an_xpcom_component_in_javascript
http://www.codeproject.com/KB/miscctrl/XPCOM_Creation.aspx
https://developer.mozilla.org/en/creating_xpcom_components
You probably want to:
Move your code into a JavaScript component
Register your component as a profile-after-change observer
Whenever someone makes a connection to your socket, find the active window and return its URL.
Use something like
var wm = Components.classes["#mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var win = wm.getMostRecentWindow("navigator:browser");
var spec = win ? win.getBrowser().currentURI.spec : "";
var outputString = spec + "\n";
etc.

Categories

Resources