This document is a compendium of the bugs, hidden features and other weirdnesses lurking inside the JavaScript engine used to run extensions in Adobe Fireworks. Feel free to fork this repo and contribute back your own findings.
To explore the JS examples here, it's helpful to install the Fireworks Console extension. You can then enter some JS, evaluate it, and see the output.
If you’re a real masochist, you can take a look at the source code for the JavaScript interpreter, which is installed with the app in Configuration\Third Party Source Code\JavaScript Interpreter. Doing so was how I discovered some of the undocumented features of the JS engine, which appears to be based on version 1.5 of the Mozilla interpreter. The version string in the source says "JavaScript-C 1.5 pre-release 3 2001-03-07"
, so it looks like the code hasn’t been updated since 2001.
- Native objects
- Bugs
- Assignment in if statements
- Function.toString() reformats the source code
- Regular expressions
- decodeURI(),decodeURIComponent(),encodeURI(), and encodeURIComponent() crash Fireworks
- [].sort() returns undefined
- Naming a variable nodes in auto shape code causes an error
- System.osName is wrong
- fw.appName is wrong
- dom.setElementLocked() and dom.setElementVisible() index elements differently than dom.frames[0].layers[0].elements
- Setting the locked, visible or disclosure property of a sub-layer throws an exception
- dom.frames[m].layers[n] always returns the layer from the current frame
- The frame index in all dom methods is ignored
- Calling fw.saveDocument() via MMExecute() throws an exception if the file doesn’t already exist
- Rich symbol scripts get called multiple times unnecessarily
- dom.moveSelectionTo() doesn’t move gradient handles of elements inside groups
- Auto shape icons in the toolbox and Shapes panel get mixed up if auto shapes have the same name
- Undocumented features
- toSource()
- File class
- const
- Getters and setters
- watch() and unwatch()
__call__
and__parent__
- dom.topLayers
- fw.getDocumentDOM() can access the DOM inside a symbol
- fw.takeScreenshot()
- fw.getFWLaunchLanguage()
- fw.commonLibraryDir
- fw.reloadPatterns() and fw.reloadTextures()
- fw.browseForFileURL()
- fw.setDocumentImageSize()
- Tools
- $ global object
- fw.shiftKeyDown(), fw.ctrlCmdKeyDown(), fw.altOptKeyDown()
- onFwWidgetLibraryChange
- fw.undo() and fw.redo()
- fw.uiOK
In addition to standard JS objects, the Fireworks environment exposes access to various Fireworks-specific objects, which don’t always behave like normal JS objects.
A number of Fireworks objects that are exposed in the JS environment look like arrays but are actually instances of the FwDict
and FwArray
class:
fw.selection instanceof Array; // false
fw.selection instanceof FwArray; // true
fw.selection[0].customData instanceof FwDict; // true
fw.selection[0].customData.foo = [42];
fw.selection[0].customData.foo instanceof Array; // false
fw.selection[0].customData.foo instanceof FwArray; // true
dom.frames instanceof FwArray; // true
This is why the fw.selection
object doesn’t have any useful Array
methods, like slice()
:
typeof fw.selection.slice; // "undefined"
One way to work around the lack of standard Array
methods is to use the underscore
module in the fwlib
library. After requiring the module via FWRequireJS, you can do things like _(fw.selection).slice(2)
.
The FwDict
and FwArray
classes inherit from Object
and seem to be roughly interchangeable.
Unfortunately, the in
operator and hasOwnProperty()
method don't seem to work with instances of native Fireworks objects. In some cases, neither work, in others, just hasOwnProperty()
doesn’t work:
fw.selection[0].customData.foo = [42];
"foo" in fw.selection[0].customData; // false
dom.layers[0].name; // "Layer 1"
"name" in dom.layers[0]; // true
dom.layers[0].hasOwnProperty("name"); // false
The customData
property on elements in a Fireworks document is extremely useful for storing extra data about the element that can then be used by a JS extension. Unfortunately, there are some quirks to using customData
that can trip you up if you’re not careful.
One surprising thing is that if you add an object or array to an element's customData
and then duplicate that element, the data is shared among the duplicates.
dom.addNewRectangle({ left: 0, top: 0, right: 100, bottom: 100 }, 0);
fw.selection[0].customData.foo = [42];
dom.cloneSelection();
fw.selection[0].customData.foo[1] = "bar"; // foo is now [42, "bar"]
dom.selectAll();
log(fw.selection[0].customData.foo, fw.selection[1].customData.foo);
This outputs [42, "bar"] [42, "bar"]
, showing that the customData
on both the original and the duplicate are pointing at the same array. If you save and reopen the file, however, the two elements will be pointing at different arrays that can be updated independently.
The delete
operator also doesn’t work with customData
properties:
fw.selection[0].customData.foo = [42];
delete fw.selection[0].customData.foo;
log(fw.selection[0].customData.foo); // still shows [42]
To remove a property from customData
, the best you can do is simply set it to null
.
Another weirdness occurs if you try to store an object on customData
that contains the properties top
, right
, bottom
, and left
. Fireworks will force the values of those properties to be numbers, even if they’re set to a different value:
el.customData.foo = { left: "a", top: "b", right: "c", bottom: "d", bar: 42 };
log(el.customData.foo);
This leaves customData.foo
actually set to { bottom: 0, left: 0, right: 0, top: 0 }
. It appears that Fireworks treats any object on customData
with those properties as a bounds object, forces all the values to be numeric and strips off any properties beyond the four sides. The only way to work around this is to change the names of your properties so they don’t trigger this special handling.
The same thing happens with an object containing x
and y
properties:
el.customData.foo = { x: "a", y: "b" };
log(el.customData.foo); // { x: 0, y: 0 }
The pngText
property of Fireworks documents is an FwDict
that can be used to persistently store arbitrary data in the document. While somewhat similar to customData
, it's even more limited, as it supports storing only string values. So if you do:
dom.pngText.foo = [1, 2, 3];
and then save the file, the next time you reopen it, the array value will have been turned into the source string version of that value: "[1,2,3]"
.
Even more confusing, dom.pngText.foo
will appear to still be an array immediately after you set it. It's only after the document is saved, closed and reopened do you discover it's been converted into a string.
And to make matters worse, each property on dom.pngText
is limited to 1023 characters. So if you think you can just stringify some data and store it, think again. It will get cut off if it's too long.
The DomStorage
class offers a way to more easily store arbitrary data on dom.pngText
by turning it into a JSON string and then automatically chunking it up into 1023-character strings. When the data is restored, the chunks are combined and then evaluated.
Note that like customData
, there’s no way to delete a property from dom.pngText
. You can only set the property to null
.
Also note that each page in a Fireworks document has its own independent pngText
property. The easiest way to deal with this in an extension is just to always store data on the first page's pngText
.
Files.readLine()
will return only the first 2047 characters of a line of text in a file, so if a file has lines longer than that, your JS code will not be able to successfully read all of it. This may particularly be an issue with JSON files, if you don’t take care to break the lines up when initially saving it.
There are some flat-out bugs in the JS engine, many of which can’t really be worked around.
Although usually unintended, the JavaScript syntax allows you to assign a value to a variable within the expression of an if
statement. However, the Fireworks JS engine interprets an assignment as a check for equality and doesn’t perform the assignment:
var a = 0;
if (a = 1) {
log("a!");
}
a; // 0
Usually you don’t want to write code like this, but some JS minifiers, like Google's Closure Compiler, do make this micro-optimization, so the output from such compressors won’t work correctly in Fireworks. (Fortunately, minification isn’t a big deal for Fireworks extensions, since the code isn’t being loaded from the internet.)
Most JS implementations seem to store the original source text and use that to return a function's string representation, which means that comments within the function are visible in the returned string. The JS engine in Fireworks, however, seems to decompile the bytecodes to produce the string representation of a function, which means that comments are lost. Other changes are made to the source, like adding semi-colons where they’re optional and reformatting and re-indenting the code.
For example, this code:
function foo(
a,
b)
{
// this comment won't appear
if (a) log("a!") // note the lack of braces
else if (b) log("b!")
}
foo.toString();
Produces this output:
'
function foo(a, b) {
if (a) {
log("a!");
} else {
if (b) {
log("b!");
}
}
}
'
This normally isn’t a big deal, but some code libraries may expect to be able to see comments in a function's source code.
There is one case Function.toString()
actually returns incorrect code. If a function contains an object with a quoted property name:
function foo()
{
var o = {
"bar baz": [42]
};
};
foo.toString();
then calling toString()
on that function returns code with a syntax error:
"
function foo() {
var o = {bar baz:[42]};
}
"
Note that the "bar baz"
property isn’t quoted in the output, which it should be, so if you try to eval()
the output, you’ll get an error.
There are a number of annoying bugs in the JS engine's handling of regular expressions. This code:
"foo bar baz".match(/(?:\w+\s*){1,5}/)
will return ["foo bar baz"]
in a browser but only ["foo "]
in Fireworks.
In a browser, this expression:
"http://api.twitter.com/oauth/request_token".match(/^([^:\/?#]+?:\/\/)*([^\/:?#]*)?(:[^\/?#]*)*([^?#]*)(\?[^#]*)?(#(.*))*/)
returns
["http://api.twitter.com/oauth/request_token", "http://", "api.twitter.com", undefined, "/oauth/request_token", undefined, undefined, undefined]
But in Fireworks, it just returns:
["http://api.twitter.com/oauth/request_token", "http://", "", "", ""]
Using a captured group token, like $1
, in the replacement string in a call to replace()
will fail if the token is immediately followed by a number. So a call like "***".replace(/(\*)/g, "$12|")
will return "*2|*2|*2|"
in a browser but "|||"
in Fireworks. It seems like the parser is interpreting the token in "$12|"
as $12
, rather than $1
followed by a 2.
Calling any of these functions will either make the JS engine unstable or cause Fireworks to crash completely. The only workaround is to use escape()
and unescape()
, which aren’t perfect replacements, or write your own versions of those functions.
In browsers, the sort()
method returns the sorted array, but in Fireworks, it returns undefined
:
[3, 2, 1].sort() instanceof Array; // false
The array does get sorted, but you just can’t chain the call.
In auto shape code, you often want to refer to the nodes in one of the shape's path contours. You might think of storing a reference to the nodes in a variable called nodes
, like var nodes
. But you’d be sadly mistaken. Doing so will cause an exception to be thrown. You have to use a different name for the variable, like nds
. Senocular also notes this bug at the end of his list of auto shape gotchas.
There is a special circle of hell reserved for whoever is responsible for this bug.
Checking System.osName
on Windows 7 returns "Windows XP"
.
fw.appName
returns "Fireworks 10"
in Fireworks CS6, even though CS6 is version 12.
dom.setElementLocked()
and dom.setElementVisible()
index elements differently than dom.frames[0].layers[0].elements
The elements
array of a layer is indexed from the top-most element in the layer downwards. When you call dom.setElementLocked()
, the third parameter is supposed to be the index of the element you want to lock or unlock. But passing in 0
actually toggles the lock state of the bottom-most element.
So it seems like dom.setElementLocked()
indexes the layer's elements in the opposite direction from the elements
array. To convert from the elements
index to the index used by dom.setElementLocked()
, subtract it from the length of the array minus 1. For example, if there are 5 elements on the layer and you want to change the locked state of the second one from the top (with index 1), you’d do (5 – 1) – 1 == 3
.
The dom.setElementVisible()
method has the same bug.
You can lock or hide a layer by doing something like dom.layers[1].frames[0].visible = true
. But if layer 1 is actually a sub-layer, then this code will throw an exception that says Could not run the script. A parameter was incorrect.. The workaround is to use the dom.setLayerLocked()
, dom.setLayerVisible()
and dom.setLayerDisclosure()
methods instead.
Just accessing the locked
, visible
or disclosure
properties without changing them works fine for sub-layers. Note that the disclosure
property is accessed directly on the layer, like dom.layers[1].disclosure
.
If your document has multiple frames and sub-layers, then the layer named "Content"
, say, may have a different index on different frames. This is because sub-layers don’t appear on every frame, yet they’re included in the layers
array.
However, if your document has 3 frames and dom.currentFrameNum
is 0
, then dom.frames[1].layers[2].name
doesn’t return the name of the third layer in the layers
array on the second frame. It actually returns the third layer on the first frame. No matter which index you use for dom.frames[]
, the layers
array for that frame always lists the layers from the current frame. The only way to work around this is to change dom.currentFrameNum
to the desired frame before accessing that frame's layers.
Methods like dom.setLayerLocked()
have a parameter that is supposed to specify which frame the given layer is on, since layers can have different locked and visible states on different frames. Unfortunately, the frame parameter seems to be ignored. If you call dom.setLayerLocked(1, 2, true, false)
to lock layer 1 on frame 2 while dom.currentFrameNum
is 0, then layer 1 on frame 0 will get locked instead. The only work around is to change dom.currentFrameNum
to the desired frame before calling the method.
The buggy methods include:
dom.setLayerLocked()
dom.setLayerVisible()
dom.setElementLocked()
dom.setElementName()
dom.setElementVisible()
If a Flash panel creates a new document and then calls fw.saveDocument()
via a call to MMExecute()
, two An internal error occurred alerts are displayed, even though the file is successful saved. Any code that runs as a result of a JSML panel receiving an event will also trigger the error. This doesn’t happen if fw.saveDocument()
is called from a .jsf
script.
To work around this, you need to create a throwaway file at the location where you’re saving the document. Calling fw.saveDocument()
from MMExecute()
should then not trigger any error dialogs. One quick way is to use the files
module in the fwlib repo to write out the empty file:
fw.createDocument();
files.write(path, "");
fw.saveDocument(null, path);
Every time you enter and then exit symbol edit mode, every instance of that symbol on every page and state in the document runs its symbol script twice, first with Widget.opCode == 1
, then with Widget.opCode == 2
. The instance that was edited also gets one additional call with Widget.opCode == 2
.
If you drag a symbol in from the Document Library panel, its symbol script code is run six times. The first four calls happen during the drag, with Widget.opCode
alternating between 1 and 2, then the last two calls occur after the symbol is dropped.
If you drag a symbol in from the Common Library panel, its symbol script code is run just three times, first with Widget.opCode == 1
and then twice with Widget.opCode == 2
.
If you insert a symbol with dom.insertSymbolAt()
, the script is called only twice. Copying or pasting a symbol also calls the script twice.
These unnecessary calls likely add to slowdowns in documents with large numbers of symbol instances.
Usually you want the handles to move with the shape, so that it maintains its visual appearance. But for gradients inside groups, dom.moveSelectionTo()
leaves the handles in their original location, which can cause the gradient to become a flat color. Elements with gradients that aren’t in a group look okay.
One workaround is to use dom.moveSelectionBy()
, which move the gradient handles even for elements inside a group. If you don’t want to calculate the delta needed to move the selection to the desired location, you could also
use dom.setSelectionBounds()
and set the left
and top
properties of the first parameter to the desired location, and calculate right
and bottom
properties based on that location plus the selection's width and height. You will also need to pass "autoTrimImages transformAttributes"
as the second parameter.
Auto shape icons in the Configuration/Auto Shapes folder are GIFs, while those in the Configuration/Auto Shape Tools folder are PNGs. The same .jsf
file can run in both locations, so you can have a version of the auto shape that’s accessing the Tools and one that’s available in the Auto Shapes panel. If those two files have the same name, Fireworks seems to confuse their icons. You may see the small PNG icon in the Auto Shapes panel or the larger GIF icon shrunk down in the Tools panel.
The only workaround is to give the auto shape files different names. So if your auto shape is called Placeholder, you could have a Placeholder.jsf
file in the Auto Shapes folder and a Placeholder tool.jsf
file in the Auto Shape Tools folder, with corresponding icon files.
Many of these undocumented features aren’t unique to Fireworks. They were standard features in version 1.5 of the Mozilla JS engine, but not necessarily standard across other browsers.
Most browsers don’t have the toSource()
method, but it's incredibly useful. It returns a JavaScript representation of the object you call it on. It's basically a poor-man's JSON, without the quoted properties.
var foo = {
bar: [42]
};
foo.toSource(); // "({bar:[42]})"
Since you can take the string returned by toSource()
and pass it to eval()
to recreate the original object, this method provides a simple way of making a deep copy of an object:
function copyObject(
inObject)
{
if (typeof inObject == "object" && inObject !== null) {
return eval("(" + inObject.toSource() + ")");
} else {
return inObject;
}
}
There’s a documented Files
object in Fireworks, which provides methods for working with files. Unfortunately, this object doesn’t offer any way to get a file's size or modification date. Fortunately, there’s also an undocumented File
class lurking in the JS engine:
var f = new File("C:\\Projects\\test.js");
log(f.exists); // true if the file exists
Note that unlike the methods of the documented Files
object, the path passed to the File
constructor must be a standard OS path, not a file://
URL. So on Windows it would contain backslashes, and on the Mac it would contain forward slashes.
A File
instance has the following properties and methods:
exists
: flag indicating whether the file at the path exists.length
: the size of the file in bytes.created
: the creation date of the file, as a JavaScriptDate
.modified
: the last modified date of the file, as a JavaScriptDate
.open(mode)
: pass"append"
to open the file for appending, which is otherwise impossible to do.writeln(text)
: writes the text to the file without appending a newline, despite the method name.write(text)
: writes the text to the file with appending a newline, despite the method name.
There’s a read()
method as well, though it doesn’t work very well. Its first parameter should be the number of bytes to read, but it only ever seems to read one byte. It's better to use Files.open()
to open a file for reading and then call readline()
to read in the whole file.
Note that the modified
and created
dates have a bug where they’re exactly a month in the future compared to the actual date. The files
module in the fwlib repo provides getCreatedDate()
and getModifiedDate()
methods that correct this bug.
This statement defines a read-only constant value in the current function scope:
const foo = 42;
foo = "bar";
log(foo); // still 42
Assignments to a const
are silently ignored. This statement has been available within Firefox for a long time.
The cutting edge ECMAScript 5 includes getters and setters as part of the specification, but it turns out that Fireworks has had these features for years:
var o =
{
get property() { log("gotten!"); return "get"; },
set property(v) { log("sotten! " + v); }
};
var v = o.property; // prints "gotten!", v === "get"
o.property = "new"; // prints "sotten! new"
There are two other syntaxes for adding getters and setters:
var o = {};
o.property getter = function() { log("gotten!"); return "get" };
o.property setter = function() { log("sotten!"); };
var v = o.property; // prints "gotten!", v === "get"
o.property = "new"; // prints "sotten!"
var o = {};
o.__defineGetter__("property", function(n, o) { log("gotten!"); return "get"; });
o.__defineSetter__("property", function(n, o) { log("sotten!"); });
var v = o.property; // prints "gotten!", v === "get"
o.property = "new"; // prints "sotten!"
Unfortunately, there doesn’t appear to be a way to get access to the getter or setter function via __lookupGetter__
or __lookupSetter__
methods that were added in later versions of the Mozilla engine. There also is no way to control whether the getters/setters are enumerable. If you use the __defineGetter__
syntax, the resulting property seems to not be enumerable, whereas if you use the object literal syntax, it is.
See this blog post for more information on this old getter/setter syntax, which has since been removed from the Mozilla engine.
Calling the watch()
method on an object and passing the name of a property on that object and a function handler will cause that function to be called whenever the property's value changes. This works for pure JS objects, but it unfortunately doesn’t work with the native Fireworks objects, which makes it a lot less useful. The Mozilla developer site has more information.
The console.watch()
and console.unwatch()
methods of the Fireworks Console use these functions.
The __call__
property of a function is an object containing a property for every parameter and local variable in the function. This lets a called function actually peek into the state of the function that called it, via arguments.callee.caller.__call__
. In fact, the __call__
property is mutable, which lets you reach into a function and actually create local variables:
(function() {
function foo()
{
bar();
log(baz);
}
function bar()
{
arguments.callee.caller.__call__.baz = "hello, foo";
}
foo(); // hello, foo
})();
In this example, even though the baz
variable isn’t defined in the foo()
function, it exists after the call to bar()
, which modifies foo()
's __call__
property.
The somewhat related __parent__
property of objects provides access to the scope that contains that object. For a global variable, this would be the global scope, but for a function defined within another function, the inner function's __parent__
would be the outer function. The property contains an attribute for every variable or function defined within that scope.
While these properties are fairly arcane, they were crucial for adding the trace()
method to the Fireworks Console.
This property is an array of the top-level layers (no sub-layers), which is otherwise a pain to figure out from the dom.layers
array. This can also be accessed via a frame object, like dom.frames[0].topLayers
.
The docs claim that the one parameter to fw.getDocumentDOM()
is there only for compatibility with Dreamweaver. But you can actually pass in a symbol ID to get a reference to that symbol's DOM, like var dom = fw.getDocumentDOM(fw.selection[0].symbolID)
. You can then use that reference to manipulate the elements inside the symbol without having to enter symbol edit mode. For instance, you could paste an element into the symbol or modify the name of one of the elements inside it. This is much faster than entering and exiting symbol edit mode, which causes a lot of screen updates.
You can also usually access the symbol DOM from within a rich symbol script. Assuming the instance is currently selected, fw.getDocumentDOM(Widget.elem.symbolID)
will return the symbol's DOM.
Displays a dialog telling you to switch to the window you want to take a screenshot of. Once you have, you click the OK button to switch to a crosshair mode. You can then drag out a rectangle over the area you want to capture. When you release the mouse, that part of the screen is copied to the clipboard as an image.
Returns a string with the code for the language and locale in which Fireworks is displaying its UI, like "en_US"
.
Returns the path to the Common Library directory, which is :\Users<username>\Appdata\Roaming\Adobe\Fireworks CS6\Common Library on Windows and /Library/Users//Library/Application Support/Adobe/Fireworks CS6/Common Library on OS X. This was added in Fireworks CS6.
These methods reload the patterns and textures directories, respectively, so that the drop-downs in the Properties panel update to show the currently available files. This is useful if your extension creates a new pattern or texture and you want it to be immediately available to the user without restarting Fireworks. This was added in Fireworks CS6.
In Fireworks CS6, this method takes a fourth parameter that specifies the root directory for the dialog box that appears.
In Fireworks CS6, this method takes a fourth parameter that specifies the interpolation method to use when the document is resized. The parameter can be a number from 1 to 4, which corresponds to these interpolation methods:
- Bicubic
- Bilinear
- Soft
- Nearest neighbor
In Fireworks CS6, the global Tools
object is a hash that maps a language-independent name for tools, like "marquee"
or "paintBucket"
, to the localized string that’s used for the tool in the UI, like "Marquee"
or "Paint Bucket"
. The only reason this is necessary is that the fw.activeTool
property returns the localized name of the currently selected tool. So if you want your extension to do something only when fw.activeTool == "Paint Bucket"
, that check will work in the English version of Fireworks but not the Japanese version.
To make the extension work with different languages, you’d want to check if fw.activeTool == Tools.paintBucket
. It would have probably been more useful to have the hash go the other way, so that you could check if Tools[fw.activeTool] == "paintBucket"
, but it's easy enough to build your own object with those mappings.
Interestingly, Tools
is an instance of Object
, rather than FwDict
.
This global object contains a number of Fireworks-specific properties, some of which are useful and some which are not:
build
: returns the detailed build version of Fireworks, e.g."12.0.0.236"
.buildDate
: this seems to return the same string as$.build
.fileName
: returns"not implemented"
.locale
: returns"English"
for a US build of Fireworks.os
: returns"Windows XP"
for Windows 7, so this is not reliable.version
: returns"12.0"
for Fireworks CS6, which is the correct version number, unlikefw.appName
.sleep()
: pass in the number of milliseconds the script should sleep before continuing. Unfortunately, this method blocks the UI, so there’s no way to use it to cause code to run after a delay.
These undocumented methods return the current state of the shift, ctrl/command and alt/option keys, respectively. They could be used to make a command behave differently depending on whether a key was held down while it was selected from the Commands menu. But you wouldn’t be able to tell whether the command was run from the menu or via a keyboard shortcut that included the given key.
An event that Fireworks dispatches to SWF panels, possibly when the Common Library is refreshed.
These undo()
and redo()
methods seem to behave the same as the equivalent methods on the dom
object.
This can be set to true
or false
, but it's not clear what effect it has on anything.