flex-sdk
Version:
A Node.js module to download and "install" the latest open source Apache/Adobe Flex SDK.
1,621 lines (1,510 loc) • 86.1 kB
JavaScript
/* AIRSourceViewer.js - Revision: 1.5 */
/*
ADOBE SYSTEMS INCORPORATED
Copyright 2007-2008 Adobe Systems Incorporated. All Rights Reserved.
NOTICE: Adobe permits you to modify and distribute this file only in accordance with
the terms of Adobe AIR SDK license agreement. You may have received this file from a
source other than Adobe. Nonetheless, you may modify or
distribute this file only in accordance with such agreement.
*/
// @the Source Viewer uses the <air> namespace:
if (typeof air == 'undefined') {air = {}};
air.SourceViewer = function() {
throw ( new Error (
"\n\n" +
"You cannot instantiate the 'air.SourceViewer' class. " +
"Instead, use 'air.SourceViewer.getDefault()' to retrieve the " +
"class' unique instance." +
"\n\n"
));
}
air.SourceViewer.getDefault = function() {
// The Source Viewer only works in the AIR application sandbox:
if (typeof window.runtime == "undefined") {
throw ( new Error (
"\n\n" +
"The Source Browser module can only work inside the <application " +
"sandbox>. Please include the SourceViewer.js file in the " +
"application's main *.html file." +
"\n\n"
));
}
// @return the existing instance if there is one; creating it otherwise:
var context = arguments.callee;
if (context.instance) { return context.instance };
/**
* CLASS
* air.SourceViewer
* DESCRIPTION
* A self-contained module to embed in HTML-based AIR applications.
* It will display a customizable selection of source files in
* an expandable tree structure. The user will be able
* to select a file to view its source code.
* SAMPLE USAGE
* var viewer1 = air.SourceViewer.getDefault();
* viewer1.viewSource();
*
* var viewer2 = air.SourceViewer.getDefault();
* var oConfig = { exclude: ['/icons', '/images'] };
* viewer2.setup(oConfig);
* viewer2.viewSource();
*
* @class
* @public
* @singleton
*/
function _SourceViewer() {
/**
* CONSTANTS
* Private members that aren't ment to be altered in any way.
* @private
* @constant
*/
// @AIR runtime:
var HTMLLoader = window.runtime.flash.html.HTMLLoader;
var NativeWindowInitOptions = window.runtime.flash.display.
NativeWindowInitOptions;
var Rectangle = window.runtime.flash.geom.Rectangle;
var NativeWindowType = window.runtime.flash.display.
NativeWindowType;
var File = window.runtime.flash.filesystem.File;
var FileStream = window.runtime.flash.filesystem.
FileStream;
var Screen = window.runtime.flash.display.Screen;
// @application
var TEXT_EXTENSIONS = ['txt', 'xml', 'mxml', 'htm', 'html',
'js', 'as', 'css', 'properties',
'config', 'ini', 'bat', 'readme'];
var IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif'];
var TEXT_TYPE = "text";
var IMAGE_TYPE = "image";
// @events
var WINDOW_CREATED_EVENT = 'windowCreatedEvent';
var FILES_LIST_READY_EVENT = 'filesListReadyEvent';
var FILE_LISTED_EVENT = 'fileListedEvent';
var FOLDER_CHECKED_EVENT = 'folderCheckedEvent';
var FOLDER_FIRST_CLICKED_EVENT = 'folderFirstClickedEvent';
var FOLDER_STATE_CHANGED_EVENT = 'folderStateChanged';
var ITEM_MOUSE_OVER_EVENT = 'itemMouseOverEvent';
var ITEM_MOUSE_OUT_EVENT = 'itemMouseOutEvent';
var ITEM_MOUSE_CLICK_EVENT = 'itemMouseClickEvent';
var FILE_ITEM_CLICKED = 'fileItemClicked';
var FILE_CONTENT_READY_EVENT = 'fileContentReady';
// @strings
var CANNOT_READ_TEXT_MESSAGE = 'Cannot retrieve text content from ' +
'this filetype.';
var IO_ERROR_MESSAGE = 'An IO Error occured while trying to ' +
'read this text.';
var ADOBE_TOKEN = 'ADOBE';
var COPYRIGHT_TOKEN = '©';
var AIR_TOKEN = 'AIR';
var TRADEMARK_TOKEN = '™';
var APP_LEGAL_NAME = 'HTML View Source Framework';
var APP_VERSION_MAJOR_MINOR = '1.5';
var TREE_DESCRIPTION_MESSAGE = 'Select a source file in the tree to '+
'see its content in the right pane:';
var COPYRIGHT_MESSAGE = '';
/**
* Flag to raise when the Source Viewer's window is open.
* @field
* @private
*/
var isMainWindowOpen;
/**
* The DOMProvider instance shared by all application components. It
* is instantiated by WindowsManager.makeMainWindow().
* @field
* @private
*/
var domProvider;
/**
* The CSSProvider instance shared by all application components. It
* is instantiated by WindowsManager.makeMainWindow().
* @field
* @private
*/
var cssProvider;
/**
* The UIBuilder instance shared by all application components. It
* is instantiated by WindowsManager.makeMainWindow().
* @field
* @private
*/
var uiBuilder;
/**
* The EventManager instance shared by all application components.
* @field
* @private
*/
var eventManager = new EventManager();
/**
* The WindowsManager instance shared by all application components.
* @field
* @private
*/
var windowManager = new WindowsManager();
/**
* The LayoutProvider instance shared by all application components.
* @field
* @private
*/
var layoutProvider = new LayoutProvider();
/**
* The FileSystemWalker instance shared by all application components.
* @field
* @private
*/
var fileSystemWalker = new FileSystemWalker();
/**
* The RequestedFilesRegistry instance shared by all application
* components.
* @field
* @private
*/
var requestedFilesRegistry = new RequestedFilesRegistry();
/**
* A hash that gets populated with references to DOM Elements
* representing application's UI main sections. Namely, the hash's
* structure is:
* - header { HTML DOM element }
* A reference to the DOM Element representing the header
* (top most section) of the application's UI
* - sidebar { HTML DOM element }
* A reference to the DOM Element representing the sidebar
* (left hand section) of the application's UI
* - tree { HTML DOM element }
* The <ul> HTML DOM Element that represents the root
* of the tree UI component (the one that displays
* clickable entries for applications files and folders)
* - contentArea { HTML DOM element }
* A reference to the DOM Element representing the content
* area (right hand section) of the application's UI.
* - ruler { HTML DOM element }
* A reference to the DOM Element representing the
* scrollable ruller that displays line numbers
* - sourceArea { HTML DOM element }
* The <div> HTML DOM Element that actually contains the
* text of the file being displayed.
* - footer { HTML DOM element }
* A reference to the DOM Element representing the footer
* (bottom most section) of the application's UI.
* @field
* @private
*/
var ui;
/**
* The main method to be called by the client programmer. Opens the
* Source Viewer UI and lists the first level of source files.
* @method
* @public
*/
this.viewSource = function() {
if(isMainWindowOpen){ return };
var windowCreatedHandler = function(event) {
initUI(event.body.window.document);
eventManager.removeListener(
WINDOW_CREATED_EVENT,
windowCreatedHandler
);
};
eventManager.addListener(
WINDOW_CREATED_EVENT,
windowCreatedHandler
);
windowManager.makeMainWindow (function(oWindow) {});
}
/**
* Holds the configuration object provided bythe client programmer.
* @field
* @private
*/
var oConfig = {};
/**
* Holds the user preference regarding Source Viewer's modal state.
* A value of "true" will make our window modal, whereas a value of
* "false" will leave it a regular window. Default is "true".
* @field
* @private
*/
var isToBeModal = false;
/**
* Holds the user preference regarding a default file that is to be
* displayed by the Source Viewer when it starts. Will contain an
* application's root relative file path.
* @field
* @private
*/
var defaultFilePath;
/**
* There is a list of file extensions that are 'recognized', and hence,
* displayed. Should the user also want some other file extensions, he
* must declare them here.
* @field
* @private
*/
var typesToAdd = [];
/**
* There is a list of file extensions that are 'recognized', and hence,
* displayed. Should the user want some of these file extensions
* removed, he must declare them here.
* Note:
* The "typesToRemove" exclusion list superseedes the "typesToAdd"
* addition list.
* @field
* @private
*/
var typesToRemove = [];
/**
* Holds the user preference regarding the color scheme to be used for
* the Source Viewer's UI. There are currently two color schemes
* available:
* - professionalBlue
* - nightScape
* @field
* @private
*/
var userColorScheme;
/**
* Holds the user preference regarding the initial [x, y] position of
* the Source Viewer's window on the screen. This is expected to be an
* array of the form [x, y].
* @field
* @private
*/
var initPosition;
/**
* Also part of the public API. Transmits the settings to the internal
* core.
* @method
* @public
* @param cfg { Object }
* Object literal containing settings for the Source Viewer.
* Currently only supports:
* - exclude { Array }
* An app root relative array of paths. Files or folders
* starting with one of these paths will not show in the tree.
* - modal { Boolean }
* Whether the Source viewer's window should be modal to the
* host application's window.
*/
this.setup = function (cfg) {
oConfig = cfg;
// @collect user's modal preference:
if ((typeof oConfig['modal'] != "undefined") &&
oConfig['modal'] !== null) {
isToBeModal = oConfig['modal']? true: false;
}
// @collect user's default file preference:
if ((typeof oConfig['defaultFile'] != "undefined") &&
oConfig['defaultFile'] !== null) {
defaultFilePath = oConfig['defaultFile'];
}
// @collect user's file extensions list preference:
if((typeof oConfig['typesToAdd'] != "undefined")
&& oConfig['typesToAdd'] !== null) {
typesToAdd = oConfig['typesToAdd'];
}
if((typeof oConfig['typesToRemove']
!= "undefined") && oConfig['typesToRemove'] !== null) {
typesToRemove = oConfig['typesToRemove'];
}
var existingList = {};
for(var i=0; i<TEXT_EXTENSIONS.length; i++) {
var ext = TEXT_EXTENSIONS[i];
existingList['text.'+ext] = 'text';
}
for(var j=0; j<IMAGE_EXTENSIONS.length; j++) {
var ext = IMAGE_EXTENSIONS[j];
existingList['image.'+ext] = 'image';
}
for(var k=0; k<typesToAdd.length; k++) {
var newType = typesToAdd[k];
var match1 = newType.match(/(?:^text\.(\w+)$)/);
if(match1) {
existingList['text.' + match1[1]] = 'text';
}
var match2 = newType.match(/(?:^image\.(\w+)$)/);
if(match2) {
existingList['image.' + match2[1]] = 'text';
}
}
for(var l=0; l<typesToRemove.length; l++) {
var type = typesToRemove[l];
var match3 = type.match(/(?:^text\.(\w+)$)/);
if(match3) { delete existingList['text.' + match3[1]] };
var match4 = type.match(/(?:^image\.(\w+)$)/);
if(match4) { delete existingList['image.' + match4[1]] };
}
TEXT_EXTENSIONS = [];
IMAGE_EXTENSIONS = [];
for (var key in existingList) {
var value = existingList[key];
var extension = key.substr(key.indexOf('.')+1);
if (value == 'text') {
TEXT_EXTENSIONS.push(extension);
} else if(value == 'image') {
IMAGE_EXTENSIONS.push(extension);
}
}
// @collect user's color scheme preference:
if((typeof oConfig['colorScheme'] != "undefined")
&& oConfig['colorScheme'] !== null) {
userColorScheme = oConfig['colorScheme'];
}
// @collect user's initial window position preference:
if(
typeof oConfig['initialPosition'] != "undefined"
&& oConfig['initialPosition'] !== null
&& oConfig['initialPosition'].length == 2
&& !isNaN(parseInt(oConfig['initialPosition'][0]))
&& !isNaN(parseInt(oConfig['initialPosition'][1]))
) {
initPosition = oConfig['initialPosition'];
}
}
/**
* Checks whether the given 'file' is to be hidden according to config
* regulations.
* @method
* @private
* @param fileUrl { String }
* The file url to check.
*/
function isFileToBeHidden (file) {
var relUrl = Utils.getRelativePath(file);
var toHide = oConfig.exclude;
if (toHide) {
for(var i=0; i<toHide.length; i++) {
var testVal = toHide[i];
testVal = testVal.replace(/\\/g, '/');
testVal = testVal.replace(/\/$/g, '');
if (testVal[0] != '/') { testVal = '/' + testVal };
if(testVal == relUrl) { return true };
if(relUrl.indexOf(testVal) == 0) {
if (relUrl[testVal.length] == '/') {
return true;
}
}
}
}
return false;
}
/**
* Initializes the application UI.
* @method
* @private
* @param oDocument { HTML Document Object }
* The document object to be used by classes responsible with UI
* creation.
*/
function initUI (oDocument) {
cssProvider = new CSSProvider(oDocument);
cssProvider.changeColorScheme(userColorScheme);
domProvider = new DOMProvider(oDocument);
uiBuilder = new UIBuilder(domProvider);
ui = uiBuilder.createMainLayout();
populateTree(ui.tree);
// @attempt to load default file's content, if there is one:
if (typeof defaultFilePath != 'undefined') {
var segments = String(defaultFilePath).split('/');
var urlSegments = [];
for(var i=0; i<segments.length; i++) {
var sgm = segments[i];
if (!/\w/.test(sgm)) { continue };
urlSegments.push(sgm);
};
var relURL = urlSegments.join('/');
var appDir = File.applicationDirectory;
var file = appDir.resolvePath(relURL);
// @the path must finally point to a file:
if(file.exists) {
if(!file.isDirectory) {
if (!isFileToBeHidden(file)) {
fileSystemWalker.queryFileContent(file);
}
}
}
}
}
/**
* Fills the tree with the first level of files.
* @method
* @private
* @param treeRoot { HTML Element }
* An HTML Element representing the root of the tree.
*/
function populateTree (treeRoot) {
var fileListReadyHandler = function(event){
uiBuilder.displayFilesList(treeRoot, event.body.filesList);
eventManager.removeListener(
FILES_LIST_READY_EVENT,
fileListReadyHandler
);
}
eventManager.addListener(
FILES_LIST_READY_EVENT,
fileListReadyHandler
);
fileSystemWalker.makeInitialQuery();
}
/**
* Clears all data that would naturally persist over sessions.
* @method
* @private
*/
function purge() {
uiBuilder.unsetClickableItems();
eventManager.removeListenersFor (FOLDER_STATE_CHANGED_EVENT);
requestedFilesRegistry.reset();
context.instance = null;
delete context.instance;
}
/**
* CLASS
* WindowsManager
* DESCRIPTION
* Handles window creation and manipulation for this application.
* SAMPLE USAGE
* N/A (internal use only)
* @class
* @private
*/
function WindowsManager () {
/**
* Returns the default display options for a newly created window.
* @method
* @private
* @return { NativeWindowInitOptions }
* An object specifying display options for a new window.
*/
function getDefWindowOptions () {
var options = new NativeWindowInitOptions();
options.type = NativeWindowType.UTILITY;
return options;
}
/**
* Returns the screen the host application is currently in (we call
* "host application" the application that has launched the Source
* Viewer).
* If the host application spans more than a single screen, the one
* displaying a larger area of the host application UI will be
* considered.
* If the host application almost equally spans two screns, or if it
* spans more than two screens, the primary screen will be returned.
* Needless to say, the single screen is returned for single screen
* hardware configurations.
* @method
* @private
* @return { Screen } The screen object.
*/
function getCurrentScreen () {
// Get all screens on which our host application is displayed:
var hostBounds = window.nativeWindow.bounds;
var screens = Screen.getScreensForRectangle(hostBounds);
// Get the most important screen showing a portion of the app:
var largestArea = 0;
var mostImportantScreen = null;
var screenOffset = -1;
for (var i=0; i<screens.length; i++) {
var screen = screens[i];
var intersection = screen.bounds.intersection(hostBounds);
var area = intersection.width * intersection.height;
if (area > largestArea) {
largestArea = area;
mostImportantScreen = screen;
screenOffset = i;
}
}
return mostImportantScreen;
}
/**
* Will translate the absolute, agnostic coordinates of the given
* rectangle into a relative, screen-dependant set of coordinates.
*
* This is required, as screen coordinates may drammatically vary
* based on that screen geographic position -- to the left or to the
* right of the primary screen, for instance.
* @method
* @private
* @return { Rectangle } The translated coordinates.
*/
function positionRectangleOnScreen (screen, rectangle) {
// Adjust the 'x' coordinate:
if ((rectangle.x + screen.visibleBounds.x)
< screen.visibleBounds.x) {
rectangle.x = screen.visibleBounds.x;
} else {
var leftPoint = screen.visibleBounds.x + rectangle.x;
var rightPoint = leftPoint + rectangle.width;
if (rightPoint <= screen.visibleBounds.width +
screen.visibleBounds.x) {
rectangle.x = leftPoint;
} else {
var adjustedLeftPoint = screen.visibleBounds.width -
rectangle.width + screen.visibleBounds.x;
rectangle.x = Math.max(adjustedLeftPoint,
screen.visibleBounds.x);
}
}
// Adjust the 'y' coordinate:
if (rectangle.y < screen.visibleBounds.y) {
rectangle.y = screen.visibleBounds.y;
} else if(rectangle.y + rectangle.height >
screen.visibleBounds.height) {
rectangle.y = screen.visibleBounds.height -
rectangle.height;
}
return rectangle;
}
/**
* Centers the given rectangle on the given screen.
* @method
* @private
* @see positionRectangleOnScreen
* @return { Rectangle} The centered rectangle
*/
function centerRectangleOnScreen (screen, rectangle) {
rectangle.x = Math.floor(screen.visibleBounds.width -
rectangle.width) / 2;
rectangle.y = Math.floor(screen.visibleBounds.height -
rectangle.height) / 2;
return positionRectangleOnScreen(screen, rectangle);
}
/**
* Returns the default display boundaries for a newly created
* window.
* @method
* @private
* @return { Rectangle }
* A rectangle defining the boundaries of this new window.
*/
function getDefaultBoundaries () {
return new Rectangle (0, 0, 800, 600);
}
/**
* Creates the main window of the application.
* @method
* @public
*/
this.makeMainWindow = function () {
var screen = getCurrentScreen();
var bounds = getDefaultBoundaries();
if (initPosition) {
bounds.x = initPosition[0];
bounds.y = initPosition[1];
bounds = positionRectangleOnScreen (screen,
bounds);
} else {
bounds = centerRectangleOnScreen(screen, bounds);
}
var htmlLoader = HTMLLoader.createRootWindow (
true,
getDefWindowOptions(),
false,
bounds
);
var domInitHandler = function() {
isMainWindowOpen = true;
if(isToBeModal) {
makeWindowModal (htmlLoader.window, self);
} else {
// @close our window before parent window gets closed:
var parWinClosingHandler = function() {
self.nativeWindow.removeEventListener('closing',
parWinClosingHandler);
self.nativeWindow.removeEventListener('activate',
parWindowActivateHandler);
if(htmlLoader && htmlLoader.window &&
htmlLoader.window.nativeWindow) {
WindowsManager.closeWindow(
htmlLoader.window.nativeWindow);
}
}
self.nativeWindow.addEventListener ('closing',
parWinClosingHandler);
// @activate our window when parent gets activated:
var parWindowActivateHandler = function() {
if(htmlLoader &&
htmlLoader.window &&
htmlLoader.window.nativeWindow) {
htmlLoader.window.nativeWindow.
orderInFrontOf(self.nativeWindow);
}
}
self.nativeWindow.addEventListener ('activate',
parWindowActivateHandler);
}
htmlLoader.window.nativeWindow.addEventListener (
'close',
function(evt) {
isMainWindowOpen = false;
}
);
var unconditionedClosingHandler = function (event) {
if (!event.isdefaultPrevented) {
purge();
}
htmlLoader.window.nativeWindow.removeEventListener (
'closing',
unconditionedClosingHandler
);
}
htmlLoader.window.nativeWindow.addEventListener (
'closing',
unconditionedClosingHandler
);
var event = eventManager.createEvent(
WINDOW_CREATED_EVENT,
{'window': htmlLoader.window}
);
eventManager.fireEvent(event);
// @this is a once-in-a-lifetime run:
htmlLoader.removeEventListener('htmlDOMInitialize',
domInitHandler);
}
htmlLoader.addEventListener('htmlDOMInitialize',domInitHandler);
try{
//since AIR1.5 the htmlLoader will not allow string load in app sandbox
//surrounded with try/catch in case we are running in older runtime
htmlLoader.placeLoadStringContentInApplicationSandbox= true;
}catch(e){}
htmlLoader.loadString('');
}
/**
* Makes a window modal to a certain parent window.
* @method
* @private
* @param oWindow { Object Window }
* The window to be made modal.
* @param oParentWindow { Object Window }
* The parent of the modal window. Any attempt to access the
* parent while the modal window is open will fail.
*/
function makeWindowModal (oWindow, oParentWindow) {
// @prevent parent window closing:
var closingHandler = function (event) {
if (isMainWindowOpen) {
event.preventDefault();
return;
};
oParentWindow.nativeWindow.removeEventListener(
'closing', closingHandler
);
}
oParentWindow.nativeWindow.addEventListener (
'closing', closingHandler
);
// @prevent parent window minimizing or maximizing:
var stateChangingHandler = function (event) {
if (isMainWindowOpen) {
event.preventDefault();
return;
};
oParentWindow.nativeWindow.removeEventListener(
'displayStateChanging', stateChangingHandler
);
}
oParentWindow.nativeWindow.addEventListener (
'displayStateChanging', stateChangingHandler
);
// @prevent parent window moving:
var movingHandler = function(event) {
if (isMainWindowOpen) {
event.preventDefault();
return;
};
oParentWindow.nativeWindow.removeEventListener(
'moving', movingHandler
);
}
oParentWindow.nativeWindow.addEventListener (
'moving', movingHandler
);
// @prevent parent window resizing:
var resizingHandler = function(event) {
if(isMainWindowOpen) {
event.preventDefault();
return;
};
oParentWindow.nativeWindow.removeEventListener(
'resizing', resizingHandler
);
}
oParentWindow.nativeWindow.addEventListener (
'resizing', resizingHandler
);
// @make sure parent window will stay behind the modal window:
var ensureProperOrder = function() {
if (oWindow && oWindow.nativeWindow &&
oParentWindow && oParentWindow.nativeWindow) {
oWindow.nativeWindow.activate();
oParentWindow.nativeWindow
.orderInBackOf(oWindow.nativeWindow);
}
}
// @works by default on Windows, hacked on Mac:
var osString = runtime.flash.system.Capabilities.os;
if(osString.indexOf('Windows') != -1) {
oWindow.nativeWindow.addEventListener ('deactivate',
ensureProperOrder, false);
} else {
var EPO_INTERVAL = window.setInterval(ensureProperOrder, 0);
}
// @prevent user from interacting with the parent's content:
var parentContentBlocker;
var parentDomProvider = new DOMProvider(oParentWindow.document);
var parentLayoutProvider = new LayoutProvider();
parentContentBlocker = parentDomProvider.makeDiv();
parentLayoutProvider.setupBox(parentContentBlocker);
parentLayoutProvider.setupStretched(parentContentBlocker);
parentContentBlocker.style.backgroundColor = 'black';
parentContentBlocker.style.opacity = '0.1';
var bodyElement = parentContentBlocker.parentNode;
var localCSSProvider = new CSSProvider(oParentWindow.document);
// @restore parent windows' state:
var modalWindowClosingHandler = function(evt) {
if (!evt.isDefaultPrevented()) {
window.clearInterval(EPO_INTERVAL);
if(parentContentBlocker) {
var parEl = parentContentBlocker.parentNode;
parEl.removeChild(parentContentBlocker);
}
oWindow.nativeWindow.removeEventListener('closing',
modalWindowClosingHandler);
}
}
oWindow.nativeWindow.addEventListener ('closing',
modalWindowClosingHandler);
}
}
/**
* Programmatically closes the given native window, while still allowing
* it to opt out, via preventing the default behavior of the 'closing'
* event.
* @method
* @public
* @static
* @param oWindow { NativeWindow }
* The instance of the NativeWindow class that is to be closed.
* @return
* True, if the given window has been closed; false otherwise.
*/
WindowsManager.closeWindow = function (oWindow) {
var closeEvent = new runtime.flash.events.Event (
runtime.flash.events.Event.CLOSING, true, true );
oWindow.dispatchEvent(closeEvent);
if (!closeEvent.isDefaultPrevented()){
oWindow.close();
return true;
} else {
return false;
}
}
/**
* CLASS
* UIBuilder
* DESCRIPTION
* Private class that handles the application's layout creation.
* SAMPLE USAGE
* N/A (internal use only)
* @class
* @private
* @param oDomProvider { DOMProvider }
* An instance of the DOMProvider class. Required, in order to be
* able to build the layout blocks.
*/
function UIBuilder (oDomProvider) {
// @let private methods see own class' instance:
var that = this;
/**
* Updates the title shown by the Source Viewer window.
* @method
* @private
* @param fileUrl { String }
* The title to show.
*/
this.updateTitle = function(file) {
var relPath = Utils.getRelativePath(file);
var oDocument = dProvider.getClientDocument ();
oDocument.title = APP_LEGAL_NAME + " - " + relPath;
}
/**
* Custom initialization for the class UIBuilder.
* @method
* @private
*/
function init() {
eventManager.addListener(FOLDER_STATE_CHANGED_EVENT,
function (event) {
uiBuilder.toggleItemContent(event.body.folder);
}
)
var fileContentReadyHandler = function (event) {
var content = event.body.content;
var type = event.body.type? event.body.type : 'text';
if (type == 'text') {
uiBuilder.showText(content);
} else if (type == 'image'){
uiBuilder.showImage(content);
}
that.updateTitle(event.body.file);
}
eventManager.addListener (
FILE_CONTENT_READY_EVENT, fileContentReadyHandler);
}
/**
* Displays the content of the selected file item inside the source
* area element.
* @method
* @public
* @param content { String }
* The content to be displayed.
* @param oDocument { HTML Document Object }
* The document object to display the content in.
* @param uid { String }
* The id for an HTML Element that will host provided content.
*/
this.showText = function(content) {
var oDocument = dProvider.getClientDocument();
var el = oDocument.getElementById('sourceCodeArea');
var txtNode = (el.getElementsByTagName('span')[0]||
dProvider.makeText(' ', el, 'sourceCodeText')).firstChild;
txtNode.nodeValue = content;
if(el.style.visibility != 'visible') {
el.style.visibility = 'visible';
}
var noContentText = oDocument.getElementById('srcAreaBgText');
if (noContentText.style.visibility != 'hidden') {
noContentText.style.visibility = 'hidden';
}
var ruler = oDocument.getElementById('lineNoRuler');
uiBuilder.initRuler (ruler, content);
if(ruler.style.visibility != 'visible') {
ruler.style.visibility = 'visible';
}
var img;
while (img = el.getElementsByTagName('img')[0]){
el.removeChild(img);
}
el.scrollTop = 0;
el.scrollLeft = 0;
ruler.scrollTop = 0;
}
/**
* Displays the selected file item, provided it is an image.
* @method
* @public
* @param { String }
* The application root relative url of the image to display.
*/
this.showImage = function(url) {
var oDocument = dProvider.getClientDocument();
var el = oDocument.getElementById('sourceCodeArea');
el.innerHTML = "";
var ruler = oDocument.getElementById('lineNoRuler');
ruler.innerHTML = "";
var img = dProvider.makeElement ('img', el, 'imageContent');
img.src = url;
el.scrollTop = 0;
el.scrollLeft = 0;
}
/**
* The DOM provider instance used for building UI elements.
* @field
* @private
*/
var dProvider = oDomProvider;
/**
* Creates an item in the files list on the left.
* @method
* @public
* @param parentEl { HTML Element }
* The HTML element to build the new item in. Both 'ul' or 'li'
* elements can be specified here.
* @param text { String }
* The text to display inside the newly created item (i.e, file
* name).
* @param className { String }
* The css class to apply to the newly created element.
* @return { HTML Element }
* The 'li' element created
*/
this.makeItem = function(parentEl, text, className) {
var item = makeTreeItem(parentEl, text, className);
return item;
}
/**
* Transparently creates a generic tree item, regardless of the
* actual HTML Element given as parent.
* @method
* @private
* @param parentEl { HTML Element }
* The HTML element to build the new item in. Both 'ul' or 'li'
* elements can be specified here.
* @param text { String }
* The text to display inside the newly created item (i.e, file
* name).
* @param className { String }
* The css class to apply to the newly created element.
*/
function makeTreeItem (parentEl, text, className) {
var isUL =
parentEl.nodeName &&
parentEl.nodeName.toLowerCase() == 'ul';
var isLI =
parentEl.nodeName &&
parentEl.nodeName.toLowerCase() == 'li';
var clsName = className? className : 'item';
var item;
if(isUL) {item = dProvider.makeElement('li', parentEl, clsName)}
else if(isLI) {
var wrapper =
parentEl.getElementsByTagName('ul')[0] ||
dProvider.makeElement('ul', parentEl);
item = dProvider.makeElement('li', wrapper, clsName);
}
var textClsName = className? className+'Text' : 'itemText';
var txt = dProvider.makeText(text, item, textClsName);
txt.addEventListener('mouseover', function (event){
var evt = eventManager.createEvent (
ITEM_MOUSE_OVER_EVENT,
{ htmlElement: event.target }
);
eventManager.fireEvent(evt);
}, false);
txt.addEventListener('mouseout', function (event){
var evt = eventManager.createEvent (
ITEM_MOUSE_OUT_EVENT,
{ htmlElement: event.target }
);
eventManager.fireEvent(evt);
}, false);
txt.addEventListener('click', function( event ) {
var evt = eventManager.createEvent (
ITEM_MOUSE_CLICK_EVENT,
{ htmlElement: event.target }
);
eventManager.fireEvent(evt);
}, false);
return item;
}
/**
* Possibly returns a tree item that matches the given path.
* @method
* @public
* @param treeRoot { DOM Element }
* The root, <ul> DOM Element of the tree to search.
* @param path { String }
* The path to look for. It is expected to be an application
* root relative path. Illegal or absolute paths will fail
* silently.
* @return { DOM Element }
* The <li> DOM Element that matches the given path, or null in
* case of failure.
*/
this.getTreeItemByPath = function (treeRoot, path) {
var getItemsLabel = function(item) {
var sp = item.getElementsByTagName('span')[0];
var txt = sp.innerText;
txt = txt.replace(/^\[([^\]]+)\]$/, '$1');
return txt;
}
var getChildByLabel = function(parentEl, childLabel) {
var children = parentEl.childNodes;
for (var i=0; i<children.length; i++) {
var child = children.item(i);
if(child.nodeType != 1) { continue };
if(child.nodeName.toLowerCase() != 'li') { continue };
var labelValue = getItemsLabel(child);
if (labelValue == childLabel) {
return child;
}
}
}
var segments = path.split('/');
segments.reverse();
var current = treeRoot;
do {
if(segments.length == 0) { break };
var segment = segments.pop();
if(segment.length == 0) { continue };
var item = getChildByLabel(current, segment);
if(item) { current = item }
else { break };
} while(true);
return (current && (segments.length == 0))? current: null;
}
/**
* Displays all given files as sibling items in the tree.
* @method
* @public
* @param parentEl { HTML Element }
* The HTML element to build the list in. This can be either a
* 'li' node or the root, 'ul' node.
* Note:
* Any required wrapping is done transparently.
* @param files { Array }
* An array containing File objects that are to be
* listed.
*/
this.displayFilesList = function(parentEl, files) {
this.removeProgressIndicator(parentEl);
for(var i=0; i<files.length; i++) {
var file = files[i];
var class_name = 'leaf';
var name = file.name;
if(file.isDirectory) {
name = '[' +name+ ']';
class_name = 'branch';
}
var item = this.makeItem(parentEl, name, class_name);
var event = eventManager.createEvent(FILE_LISTED_EVENT,
{'file': file, 'item': item});
eventManager.fireEvent(event);
}
}
/**
* Adds visual clues that the given item has children i.e., a
* different CSS style, a progress indicator.
* @method
* @private
* @param item { HTML Element }
* The element that is to be marked as non empty.
*/
this.markItemAsNonEmpty = function(item) {
item.className = 'nonEmptyBranch';
var textEl = item.getElementsByTagName('span')[0];
textEl.className = 'nonEmptyBranchText';
this.addProgressIndicator(item);
this.toggleItemContent(item);
this.setupAsClickableFolderItem(textEl);
}
/**
* Maintains a list with all clickable items.
* @field
* @private
*/
var clickableItems = [];
/**
* Explicitelly unsets all items previously set as clickable.
* @method
* @public
*/
this.unsetClickableItems = function() {
for(var i=0; i<clickableItems.length; i++) {
var item = clickableItems[i];
item.onclick = "";
}
clickableItems = [];
}
/**
* Sets the given item as a clickable folder, i.e., clicking on it
* will fetch its children, and will collapse/expand it afterwards.
* @method
* @public
* @param item { HTML Element }
* The element that is to be setup as a clickable folder.
*/
this.setupAsClickableFolderItem = function(item) {
var cb = function(DOMEvent) {
var el = DOMEvent.target;
var notYetExpanded = hasProgressIndicator(el.parentNode);
if(notYetExpanded) {
var event = eventManager.createEvent(
FOLDER_FIRST_CLICKED_EVENT,
{'folder': el.parentNode}
);
eventManager.fireEvent(event);
} else {
var event = eventManager.createEvent(
FOLDER_STATE_CHANGED_EVENT, {'folder':
el.parentNode});
eventManager.fireEvent(event);
}
}
item.onclick = cb;
clickableItems.push(item);
}
/**
* Sets the given file as clickable, i.e., clicking on it will
* display its content in the right pane.
* @method
* @private
* @param item { HTML Element }
* The element that is to be setup as a clickable file.
* @param file { File }
* The file object associated to the item to setup.
*/
this.setupAsClickableFileItem = function(item, file) {
var fileItemClickHandler = function(){
var event = eventManager.createEvent(FILE_ITEM_CLICKED, {
'item': item, 'file': file });
eventManager.fireEvent(event);
}
item.onclick = fileItemClickHandler;
clickableItems.push(item);
}
/**
* Sets up visual feedback in response to user interacting with the
* tree items (i.e., mouse-hovering an item, etc).
* @method
* @private
* @see CSSProvider.dynamicCSS
*/
function hookItemsVisualFeedback() {
var current = arguments.callee;
// @visual effect for mouse over on tree items.
var hoverCb = function(event) {
var el = event.body.htmlElement;
if (current.lastClicked &&
current.lastClicked === el) {
return;
}
cssProvider.setStyle(el, 'background-color', 'darkNeutral');
};
eventManager.addListener(ITEM_MOUSE_OVER_EVENT, hoverCb);
// @visual effect for mouse out on tree items.
var outCb = function(event) {
var el = event.body.htmlElement;
if (current.lastClicked &&
current.lastClicked === el) {
return;
}
el.style.backgroundColor = 'transparent';
};
eventManager.addListener (ITEM_MOUSE_OUT_EVENT, outCb);
// @visual effect for mouse click on tree items.
var clickCb = function(event) {
var el = event.body.htmlElement;
var old = current.lastClicked;
if (old) {
old.style.backgroundColor = 'transparent';
}
cssProvider.setStyle(el, 'background-color',
'lighterColorAccent');
current.lastClicked = el;
};
eventManager.addListener(ITEM_MOUSE_CLICK_EVENT, clickCb);
}
/**
* Alternatively collapses or expands the children of a tree item.
* @method
* @public
* @param { HTML Element }
* The item to be collapsed or expanded (based on its current
* state).
*/
this.toggleItemContent = function(item) {
var wrapper = item.getElementsByTagName('ul')[0];
if (wrapper) {
var visible = wrapper.style.display == 'none'? false: true;
wrapper.style.display = visible? 'none' : 'block';
}
}
/**
* Adds a special, temporary child to the given item, indicating
* that its actual children are being loaded in background.
* @method
* @public
* @param item { HTML Element }
* The element to signalize background loading for.
*/
this.addProgressIndicator = function(item) {
if (!hasProgressIndicator(item)) {
makeTreeItem (item, 'loading...', 'progress');
}
}
/**
* Removes the 'progress indicator' from the given item, should it
* have one.
* @method
* @public
* @param item { HTML Element }
* The element to remove the progress indicator from.
*/
this.removeProgressIndicator = function(item) {
var children = item.getElementsByTagName('span');
for(var i=0; i<children.length; i++) {
var child = children[i];
var value = child.innerHTML;
if (value == 'loading...') {
var itemToRemove = child.parentNode;
var _parent = itemToRemove.parentNode;
_parent.removeChild(itemToRemove);
return;
}
}
}
/**
* Checks whether a given item currently presents a 'progress
* indicator' instead of its actual content.
* @method
* @private
* @param item { HTML Element }
* The element to be checked.
* @return { Boolean }
* True if the element has a 'progress indicator' in place,
* false otherwise.
*/
function hasProgressIndicator (item) {
var firstWrapper = item.getElementsByTagName('ul')[0];
if(firstWrapper) {
var firstTextEl = firstWrapper.
getElementsByTagName('span')[0];
if (firstTextEl) {
var value = firstTextEl.innerHTML;
if (value == 'loading...') {return true};
}
}
return false;
}
/**
* Creates the header of the application UI.
* @method
* @private
* @param parentEl { HTML Element }
* The HTML Element to build in.
* @return { HTML Element }
* The HTML Element holding the UI header.
*/
function createHeader (parentEl) {
var header = dProvider.makeDiv( parentEl, 'rcHeader' );
layoutProvider.setupBox(header, {h: 3})
layoutProvider.setupStretched(header, { bottom: -1 })
dProvider.makeText(ADOBE_TOKEN, header, 'hdrToken');
dProvider.makeText(COPYRIGHT_TOKEN, header, 'hdrToken');
dProvider.makeText(AIR_TOKEN, header, 'hdrToken');
dProvider.makeText(TRADEMARK_TOKEN, header, 'hdrToken');
dProvider.makeText(APP_LEGAL_NAME, header, 'hdrToken');
dProvider.makeText(
APP_VERSION_MAJOR_MINOR,
header,
'vToken'
);
return header;
}
/**
* Creates the left side of the application UI -- the side bar that
* contains the tree list showing files and folders.
* @method
* @private
* @param parentEl { HTML Element }
* The HTML Element to build in.
* @return { HTML Element }
* The HTML Element holding the UI sidebar.
*/
function createSideBar (parentEl) {
var left = dProvider.makeDiv( parentEl, 'rcTree' );
layoutProvider.setupBox(left, {w: 13.85});
layoutProvider.setupStretched(left, {top: 3.2, bottom: 2, right: -1});
dProvider.makeText(TREE_DESCRIPTION_MESSAGE, left,
'listDescr');
var lstBackground = dProvider.makeDiv(left, 'listBackground');
layoutProvider.setupBox(lstBackground);
layoutProvider.setupStretched(lstBackground, {top:3, right:0.5, bottom:1,
left:0.5});
return left;
}
/**
* Creates the tree list that displays source files.
* @method
* @private
* @param parentEl { HTML Element }
* The HTML element to build the tree in.
* @return { HTML Element }
* The HTML Element holding the UI tree's root.
*/
function createTree (parentEl) {
var root = dProvider.makeElement('ul', parentEl, 'tree');
layoutProvider.setupBox(root);
layoutProvider.setupStretched(root, {top:2.5, right:1,
bottom:0.5, left: 1});
root.style.overflow = 'auto';
return root;
}
/**
* Creates the right side of the application UI -- the area that
* displays selected file's content.
* @method
* @private
* @param parentEl { HTML Element }
* The HTML Element to build in.
* @return { HTML Element}
* The HTML Element holding the UI content area.
*/
function createContentArea (parentEl) {
var right = dProvider.makeDiv( parentEl, 'rcContent' );
layoutProvider.setupBox(right);
layoutProvider.setupStretched(right, {top: 3.2, left: 14, bottom: 2});
var txt = dProvider.makeText('no content to display', right,
'noContent');
layoutProvider.setupBox(txt, {w:10, h:1});
layoutProvider.setupCentered(txt);
txt.setAttribute('id', 'srcAreaBgText');
return right;
}
/**
* Creates the ruler showing line numbering for displayed file's
* content.
* @method
* @private
* @param parentEl { HTML Element }
* The HTML Element to build in.
* @return { HTML Element }
* The HTML Element holding the UI ruler
*/
function createRuler(parentEl) {
var lnNumb = dProvider.makeDiv(parentEl, 'ruler');
layoutProvider.setupBox (lnNumb, {w:4});
layoutProvider.setupStretched (lnNumb, {left:0.5, top:0.5,
right:-1, bottom:0.8 });
lnNumb.setAttribute('id', 'lineNoRuler');
return lnNumb;
}
/**
* Creates the UI element that will display the selected source
* file's source code.
* @method
* @private
* @param parentEl { HTML Element }
* The HTML Element to build in.
* @return { HTML Element }
* The HTML Element holding the UI source area.
*/
function createSourceArea(parentEl) {
var srcArea = dProvider.makeDiv(parentEl, 'srcCodeArea');
layoutProvider.setupBox (srcArea);
layoutProvider.setupStretched (srcArea, {left:4.5, top:0.5,
right:0.35, bottom:0.8, });
srcArea.setAttribute('id', 'sourceCodeArea');
return srcArea;
}
/**
* Creates the footer of the application UI.
* @method
* @private
* @param parentEl { HTML Element }
* The HTML Element to build in.
* @return { HTML Element }
* The HTML Element holding the UI footer area.
*/
function createFooter (parentEl) {
var footer = dProvider.makeDiv( parentEl, 'rcFooter' );
layoutProvider.setupBox(footer, {h: 2});
layoutProvider.setupStretched(footer, {top: -1});
dProvider.makeText(COPYRIGHT_MESSAGE, footer,
'copyrightText');
return footer;
}
/**
* Creates the application user interface.
* @method
* @public
* @param parentEl { HTML Element }
* The HTML Element to build in.
* @return { Object }
* A hash with all UI elements created.
*/
this.createMainLayout = function(parentEl) {
var header = createHeader(parentEl);
var sidebar = createSideBar(parentEl);
var tree = createTree(sidebar);
this.addProgressIndicator (tree);
var contentArea = createContentArea(parentEl);
var ruler = createRuler(contentArea);
var sourceArea = createSourceArea(contentArea);
linkScrollable(ruler, sourceArea);
var footer = createFooter(parentEl);
cssProvider.applyCSS();
hookItemsVisualFeedback();
return {
'header' : header,
'sidebar' : sidebar,
'tree' : tree,
'contentArea' : contentArea,
'ruler' : ruler,
'sourceArea' : sourceArea,
'footer' : footer
};
}
/**
* Initiates the linked ruler that shows line numbers for the file
* content being displayed.
* @method
* @public
* @param parentEl { HTML Element }
* The HTML Element to build in.
* @assocText { String }
* A formatted string to show line numbers for. The ruler will
* add a new number for each occurence of the new line
* character in this string.
*/
this.initRuler = function(parentEl, assocText) {
var count = 0;
var index = 0
var str = '';
domProvider.destroyContent(parentEl);
do {
var searchIndex = assocText.indexOf("\n", index);
count++;
str += count + "\n";
index = searchIndex + 1;
if (searchIndex == -1) {
str+="EOF";
break;
};
} while (true);
dProvider.makeText(str, parentEl, 'rulerText');
}
/**
* Links two elements, so that they scroll together.
* @method
* @private
* @param scrollable { HTML Element }
* The target element to be linked.
* @param related { HTML Element }
* The source element to link with.
* @param proxy { Function }
* An optional function that specifies how the scroll value of
* the 'related' element applies to the 'scrollable' element.
* Default is to just pass the 'scrollTop' value from 'related'
* to 'scrollable'.
*/
function linkScrollable (scrollable, related, proxy) {
// @private function; scrolls a given element.
var scrollElement = function(element, offset) {
element.scrollTop = offset;
}
related.addEventListener('scroll', function() {
scrollElement (scrollable,
Utils.isFunction(proxy)? proxy.call(this, related.scrollTop):
related.scrollTop
);
}, false );
}
// @perform custom initialization of this class.
init();
}
/**
* CLASS
* FileSystemWalker
* DESCRIPTION
* Private class that encapsulates functionality related to file
* system traversal (i.e., recursively iterating through a folder's
* children).
* SAMPLE USAGE
* N/A (internal use only)
* @class
* @private
*/
function FileSystemWalker() {
// @let private methods see own class' instance:
var that = this;
/**
* Keeps a record of all listed files.
* @field
* @private
*/
var listedFiles = {};
/**
* Custom initialization for the