c9-core/plugins/c9.ide.ui/ui.js

752 wiersze
30 KiB
JavaScript

/*global apf*/
define(function(require, module, exports) {
main.consumes = ["Plugin", "apf"];
main.provides = ["ui"];
return main;
function main(options, imports, register) {
var Plugin = imports.Plugin;
require("plugins/c9.ide.ui/codebox")(imports.apf);
var settings;
var packed = require("text!./style.less").length === 0;
var packedThemes = packed || options.packedThemes !== false;
/***** Initialization *****/
var plugin = new Plugin("Ajax.org", main.consumes);
var cssLibs = [];
var loaded = false;
function load() {
if (loaded || apf.uiLoaded) return false;
loaded = true;
apf.uiLoaded = true;
apf.preProcessCSS = insertLess;
// Load a basic document into APF
apf.initialize();
window.addEventListener("mousedown", function() {
apf.isMousePressed = true;
document.body.classList.add("disableIframe");
}, true);
window.addEventListener("mouseup", function() {
apf.isMousePressed = false;
document.body.classList.remove("disableIframe");
}, true);
}
function initSettings(v) {
settings = v;
function wrap(prototype, prop, isNumber) {
delete prototype.$booleanProperties[prop];
var oldHandler = prototype.$propHandlers[prop] || function(value) {
this[prop] = value;
};
var func = isNumber ? settings.getNumber : settings.getBool;
prototype.$propHandlers[prop] = function(value) {
var _self = this;
var dynProp = this.getAttribute(prop);
var isDynProp = ~(String(dynProp) || "").indexOf("/");
if (isNumber) {
if (parseInt(value, 10) == value || !value) {
isDynProp && settings.set(dynProp, value, null, null, true);
return oldHandler.call(_self, parseInt(value, 10));
}
}
else {
if (this.localName == "checkbox") {
var idx = this.$values && this.$values.indexOf(value);
if (!isNaN(idx) && idx > -1) {
isDynProp && settings.set(dynProp, value, null, null, true);
return oldHandler.call(_self, value);
}
}
if (apf.isTrue(value)) {
isDynProp && settings.set(dynProp, value, null, null, true);
return oldHandler.call(_self, true);
}
else if (apf.isFalse(value) || !value) {
isDynProp && settings.set(dynProp, value, null, null, true);
return oldHandler.call(_self, false);
}
}
if (isDynProp) {
oldHandler.call(_self, func(value));
settings.on(value, listen);
this.once("DOMNodeRemovedFromDocument", function() {
settings.off(value, listen);
});
}
function listen() {
var v = func(value);
if (_self[prop] != v)
oldHandler.call(_self, v);
}
};
}
wrap(apf.item.prototype, "checked", false);
wrap(apf.spinner.prototype, "value", true);
wrap(apf.group.prototype, "value", false);
wrap(apf.checkbox.prototype, "value", false);
wrap(apf.tab.prototype, "animate", false);
}
["model", "tree", "button", "menu", "item", "bar", "divider", "toolbar",
"list", "tab", "textbox", "textarea", "radiobutton", "checkbox", "page",
"splitter", "hsplitbox", "vsplitbox", "group", "img", "label", "spinner",
"dropdown", "BindingColumnRule", "datagrid", "hbox", "vbox", "colorbox",
"frame", "password", "modalwindow", "filler", "splitbutton", "codebox"].forEach(function(tag) {
plugin[tag] = function(struct) {
return new apf[tag](struct);
};
});
/***** Methods *****/
function defineLessLibrary(css, plugin) {
if (packed && css)
throw new Error("Can't add dynamic less library in packed mode!");
if (packedThemes) return;
cssLibs.push(css);
plugin.addOther(function() {
cssLibs.splice(cssLibs.indexOf(css), 1);
});
}
function insertLess(css, filename, staticPrefix, _callback, force) {
var callback = _callback || function(err) {
if (err) console.error(err);
};
css = css.trim();
filename = filename || "unknown.css";
staticPrefix = staticPrefix || "";
if (!css) return callback();
if (!force) {
if (packed && css)
throw new Error("Can't add dynamic less library in packed mode!");
if (packedThemes) return;
}
// use dynamic require because we don't want this in the packed version
require(["./lib_less1.5"], function(less) {
if (less) less.async = true;
var parser = new less.Parser({
filename: filename
});
// Parse Less Code
var baseLib = "@base-path : \"" + staticPrefix + "\";\n"
+ "@image-path : \"" + staticPrefix + "/images\";\n"
+ "@icon-path : \"" + staticPrefix + "/icons\";\n";
var code = baseLib + "\n" + cssLibs.join("\n") + "\n" + css;
parser.parse(code, function (e, tree) {
if (e)
return callback(e);
// Add css to the DOM
var style = apf.importCssString(tree.toCSS({
relativeUrls: true,
rootpath: staticPrefix || ""
}));
callback(null, style);
});
});
}
function insertCss(css, staticPrefix, plugin) {
if (!css) return;
if (staticPrefix instanceof Plugin) {
plugin = staticPrefix;
staticPrefix = "";
}
var force = packedThemes && String(staticPrefix).match(/^http/);
if (staticPrefix !== false && !packedThemes || force) {
var filename = staticPrefix + "/" + plugin.name + ".less";
insertLess(css, filename, staticPrefix, function(err, style) {
if (err)
return console.error(err);
// Cleanup
if (style) {
plugin.addOther(function() {
style.parentNode.removeChild(style);
});
}
}, force);
}
else {
if (!packed && packedThemes && staticPrefix !== false) {
return;
}
var style = apf.importCssString(css);
// Cleanup
plugin.addOther(function() {
style.parentNode.removeChild(style);
});
}
}
function insertSkin(skin, plugin) {
var data = skin.data;
delete skin.data;
skin.src = "<skins />";
skin.id = skin.name;
var skinNode = new apf.skin(skin);
skinNode.setProperty("src", data);
apf.document.documentElement.appendChild(skinNode);
plugin.addElement(skinNode);
}
function insertHtml(parent, markup, plugin) {
if (!parent)
parent = document.body;
var lastChild = parent.lastChild;
parent.insertAdjacentHTML("beforeend", markup);
var nodes = [];
if (!lastChild)
nodes.push(parent.lastChild);
else {
while (lastChild.nextSibling) {
nodes.push(lastChild = lastChild.nextSibling);
}
}
plugin.addOther(function() {
nodes.forEach(function(node) {
node.parentNode.removeChild(node);
});
});
return nodes;
}
function insertMarkup(parent, markup, plugin) {
//@todo find a way to get a list of all elements added
if (!parent)
parent = apf.document.documentElement;
if (parent.$amlDestroyed) return; // Parent is already destroyed
//var allMarker = apf.all.length;
var childMarker = parent.childNodes.length;
parent.insertMarkup(markup);
//var allNodes = apf.all.slice(allMarker);
var childNodes = parent.childNodes.slice(childMarker);
return plugin.addElement.apply(plugin, childNodes);
}
function insertByIndex(parent, item, index, plugin) {
var beforeNode, diff = 100000000, nodes = parent.childNodes;
for (var i = 0, l = nodes.length; i < l; i++) {
var d = nodes[i].$position - index;
if (d > 0 && d < diff) {
beforeNode = nodes[i];
diff = d;
}
}
if (typeof item == "string") {
var bar = new apf.bar({ htmlNode: document.createElement("div") });
bar.insertMarkup(item);
item = bar.childNodes.slice();
bar.childNodes.length = 0;
bar.destroy();
}
if (Array.isArray(item)) {
for (var i = 0; i < item.length; i++) {
var node = parent.insertBefore(item[i], beforeNode);
node.$position = index;
if (plugin !== false)
plugin.addElement(node);
}
return item[0];
} else {
var node = parent.insertBefore(item, beforeNode);
node.$position = index;
if (plugin !== false)
plugin.addElement(node);
return node;
}
}
function addClass(html, name) {
html.className =
html.className.replace(new RegExp(" " + name, "g"), "")
+ " " + name;
}
function removeClass(html, name) {
html.className =
html.className.replace(new RegExp(" " + name, "g"), "");
}
/***** Lifecycle *****/
plugin.on("load", function() {
load();
});
plugin.on("unload", function() {
loaded = false;
});
/***** Register and define API *****/
/**
* The Cloud9 UI Library is exposed through this plugin. The UI widgets
* that Cloud9 uses are called AMLElements. These elements can be
* created programmatically, or using a markup language, called AML.
*
* This plugin has three main functions:
*
* * A set of functions to insert HTML, CSS and AML into the UI.
* * A set of constructors for the AMLElements.
* * A set of helper methods that allow for easy manipulation of the
* HTML DOM, the XML DOM, Stylesheets and AML Elements.
*
* ## Insertion Functions ##
*
* The five insertions functions ({@link #insertSkin}, {@link #insertCss},
* {@link #insertHtml}, {@link #insertMarkup}, {@link #insertByIndex})
* are commonly used in the draw method of a plugin. See the source of
* {@link Template} for an elaborate example.
*
* Here's a simple example:
*
* // Import Skin
* ui.insertSkin({
* name : "c9statusbar",
* data : require("text!./skin.xml"),
* "media-path" : options.staticPrefix + "/images/",
* "icon-path" : options.staticPrefix + "/icons/"
* }, plugin);
*
* // Create UI elements
* ui.insertMarkup(parentElement, require("text!./markup.xml"), plugin);
*
* // Insert CSS
* ui.insertCss(require("text!./style.css"), plugin);
*
* The following example shows how you can use the AML markup language
* to create a window.
*
* <a:window center="true" buttons="close" resizable="true" title="Hello World">
* <h1>Hello World!</h1>
*
* <p>Hello there! How are you?</p>
*
* <a:hbox edge="23 0 10" pack="end" padding="8">
* <a:button>Cancel</a:button>
* <a:button default="2">OK</a:button>
* </a:hbox>
* </a:window>
*
* ## AMLElement Constructors ##
*
* The following list of widgets are offered by the ui plugin:
*
* {@link ui.button}, {@link ui.menu},
* {@link ui.item}, {@link ui.bar}, {@link ui.divider}, {@link ui.toolbar},
* {@link ui.tab}, {@link ui.textbox}, {@link ui.textarea},
* {@link ui.radiobutton}, {@link ui.checkbox}, {@link ui.page},
* {@link ui.hsplitbox}, {@link ui.vsplitbox},
* {@link ui.label}, {@link ui.spinner},
* {@link ui.dropdown}, {@link ui.colorbox}, {@link ui.frame},
* {@link ui.password}.
*
* This example shows how to create a button using the {@link ui.button}
* constructor. When pressed the button will execute the save command.
*
* var button = new ui.button({
* id : "saveButton",
* caption : "Save",
* tooltip : "Save",
* skin : "c9-toolbarbutton-glossy",
* command : "save"
* });
*
* @singleton
*/
plugin.freezePublicAPI({
/**
* @property {Boolean} whether c9 was started in the packed version
*/
get packed() { return packed; },
/**
* @property {Boolean} whether to load packed less files
*/
get packedThemes() { return packedThemes; },
/**
* @ignore
*/
set settings(v) { initSettings(v); },
/**
*
*/
defineLessLibrary: defineLessLibrary,
/**
* Imports a CSS string into the browser as a new stylesheet.
*
* Example:
*
* var cssString = require("text!./myplugin.css");
* ui.insertCss(cssString, options.staticPrefix, plugin);
*
* You can also generate the css with javascript:
*
* var cssString = ".myplugin { background: url(images/test.png); }";
* ui.insertCss(cssString, options.staticPrefix, plugin);
*
* Make sure to always point to images using the "images/" path. The
* `staticPrefix` parameter will prefix those paths with the path
* for your plugin. Your images should always recide in a folder
* called "images".
*
* @param {String} css The css definition as you would write in a .css file.
* @param {String} [staticPrefix] The path or url that will prefix
* all the image urls. Make sure that this comes from the options
* of your plugin (See {@link Architect}). By making this an option
* of your plugin the path can be set to a different one for the
* when all cloud9 plugins are packaged and the images are loaded
* from a different url (a static file server).
* @param {Plugin} plugin The plugin responsible for
* inserting the css. This is needed for cleanup during the unload
* phase of the plugin.
*/
insertCss: insertCss,
/**
* Load a skin definition into Cloud9. A skin definition
* describes the HTML and CSS used for AML widgets.
*
* When you wish customize the styling of AML elements, copy the
* skin from the main skins.xml file into your own skin.xml file
* and load that file using this method.
*
* Example:
*
* // Import Skin
* ui.insertSkin({
* name : "mypluginname",
* data : require("text!./skin.xml"),
* "media-path" : options.staticPrefix + "/images/",
* "icon-path" : options.staticPrefix + "/icons/"
* }, plugin);
*
* You can then reference the skin, using the skinset attribute:
*
* <a:button skinset="mypluginname" />
*
* Or set the skinset attribute on a parent to apply it to all it's
* children:
*
* <a:window skinset="mypluginname">
* <!-- your children -->
* </a:window>
*
* @param {String} skin The skin definition.
* @param {Plugin} plugin The plugin responsible for
* loading the skin. This is needed for cleanup during the unload
* phase of the plugin.
*/
insertSkin: insertSkin,
/**
* Inserts AMLElements from their xml definition.
*
* Example:
*
* // Create UI elements
* ui.insertMarkup(parentElement, require("text!./markup.xml"), plugin);
*
* The following example shows how you can use the AML markup language
* to create a window.
*
* <a:window center="true" buttons="close" resizable="true" title="Hello World">
* <h1>Hello World!</h1>
*
* <p>Hello there! How are you?</p>
*
* <a:hbox edge="23 0 10" pack="end" padding="8">
* <a:button>Cancel</a:button>
* <a:button default="2">OK</a:button>
* </a:hbox>
* </a:window>
*
* @param {AMLElement} parent The parent where the newly created
* elements are inserted into.
* @param {String} markup The markup that describes the UI elements.
* @param {Plugin} plugin The plugin responsible for
* inserting the markup. This is needed for cleanup during the unload
* phase of the plugin.
*/
insertMarkup: insertMarkup,
/**
* Inserts an html string into the DOM and returns the inserted
* HTML elements in an array. This method is very similar to
* `.innerHTML += "markup"`, but it's faster and it makes sure these
* elements are cleaned up when the plugin is unloaded.
*
* Example:
*
* var html = "<div>Hello World</div>";
* var nodes = ui.insertHtml(document.body, html, plugin);
* nodes[0].className = "red";
*
* @param {HTMLElement} parent The parent of the newly created
* elements.
* @param {String} markup The html string.
* @param {Plugin} plugin The plugin responsible for
* inserting the html. This is needed for cleanup during the unload
* phase of the plugin.
*/
insertHtml: insertHtml,
/**
* Appends an AMLElement to a parent where the child position is
* determined by a number. The higher the number the higher the
* child position compared to other children.
*
* The use of index numbers to determine the position is a concept
* that is used throughout Cloud9. It allows different plugins
* to determine the position of UI elements, while being oblivious
* of other plugins.
*
* Example:
*
* // Insert a divider on the right of the button
* ui.insertByIndex(amlToolbar, amlButton, 100, plugin);
* ui.insertByIndex(amlToolbar, amlDivider, 200, plugin);
*
* @param {AMLElement} parent The ui element to which the `child`
* is added.
* @param {AMLElement} child The ui element to add to the `parent`.
* @param {Number} index The position in the stack of child
* elements of the `parent`.
* @param {Plugin} [plugin] The plugin responsible for
* inserting the aml. This is needed for cleanup during the unload
* phase of the plugin. Only pass the plugin argument if the
* element that is inserted has not yet been added to the plugin.
*/
insertByIndex: insertByIndex,
/**
*
*/
buildDom: apf.buildDom,
/**
* Escapes "&amp;", greater than, less than signs, quotation marks,
* and others into the proper XML entities.
*
* @param {String} str The XML string to escape.
* @param {Boolean} [strictMode] By default, this function
* attempts to NOT double-escape XML entities. This flag turns
* that behavior off when set to `true`.
* @return {String} The escaped string
*/
escapeXML: apf.escapeXML,
/**
* Retrieves the absolute x- and y-coordinates, relative to the
* browser's drawing area or the specified `refParent`.
* @param {HTMLElement} htmlNode The element to check
* @param {HTMLElement} [refParent] The reference parent
* @param {Boolean} [inclSelf] Whether to include the position of the element to check in the return value.
* @returns {Number[]} The x- and y-coordinates of `htmlNode`.
*/
getAbsolutePosition: apf.getAbsolutePosition,
/**
* Constructs a stylesheet.
* @param {Object} [win] A reference to a browser window
* @returns {String} The created CSS stylesheet
*/
createStylesheet: apf.createStylesheet,
/**
* Imports a stylesheet defined by a multidimensional array.
* @param {Array} def A multidimensional array specifying stylesheets to import
* @param {Object} [win] A reference to a window
*/
importStylesheet: apf.importStylesheet,
/**
* Retrieves the value of a single CSS rule.
* @param {String} name The CSS name of the rule (i.e. `.class` or `#id`).
* @param {String} type The CSS property to retrieve.
* @param {String} [stylesheet] The name of the stylesheet to change.
* @param {Object} [win] A reference to a window
* @return {String/Number}
*/
getStyleRule: apf.getStyleRule,
/**
* Sets a property of a CSS rule.
* @param {String} name The CSS name of the rule (i.e. `.class` or `#id`).
* @param {String} type The CSS property to change.
* @param {String} value The CSS value of the property.
* @param {String} [stylesheet] The name of the stylesheet to change.
* @param {Object} [win] A reference to a window
*/
setStyleRule: apf.setStyleRule,
/**
* @ignore
*/
removeStyleRule: apf.removeStyleRule,
/**
* @ignore
*/
setStyleClass: apf.setStyleClass,
/**
* Returns the distance between the border left and border right
* values of an element, taking padding into consideration.
* @param {HTMLElement} oHtml The element to check
* @returns {Number} The final calculation, or 0, if there's no difference
*/
getWidthDiff: apf.getWidthDiff,
/**
* Returns the distance between the border top and border bottom
* values of an element, taking padding into consideration.
* @param {HTMLElement} oHtml The element to check
* @returns {Number} The final calculation, or 0, if there's no difference
*/
getHeightDiff: apf.getHeightDiff,
/**
* Returns an array with two elements. The first is the distance
* between the margin left and margin right values of an element;
* the second is the distance between the margin top top and margin
* bottom values of an element.
* @param {HTMLElement} oHtml The element to check
* @returns {Number[]} An array containing the differences
*/
getMargin: apf.getMargin,
/**
* Determines whether a node is a child of another node.
*
* @param {HTMLElement} pNode The potential parent element.
* @param {HTMLElement} childnode The potential child node.
* @param {Boolean} [orItself] Whether the method also returns `true` when `pNode` is the `childnode`.
* @return {Boolean} `false` if the second argument is not a child of the first.
*/
isChildOf: apf.isChildOf,
/**
* @ignore
*/
layout: apf.layout,
/**
* Performs an async function serially on each of the list items.
*
* @param {Array} list A list of elements to iterate over asynchronously
* @param {Function} async An ssync function of the form `function(item, callback)`
* @param {Function} callback Called after all items have been processed
* @param {Error} callback.err Error if any has occured.
*/
asyncForEach: apf.asyncForEach,
/**
* Adds a className to an html element. This method leaves
* existing classes applied to the html element.
*
* @param {HTMLElement} html The element to apply the class to.
* @param {String} name The name of the CSS class to add.
*/
addClass: addClass,
/**
* Removes a className to an html element. This method leaves
* other classes applied to the html element.
*
* @param {HTMLElement} html The element to remove the class from.
* @param {String} name The name of the CSS class to remove.
*/
removeClass: removeClass,
/**
* Determines whether a string is true (in the HTML attribute sense).
* @param {Mixed} value The variable to check. Possible truth values include:
*
* * true
* * 'true'
* * 'on'
* * 1
* * '1'
*
* @return {Boolean} Whether the string is considered to imply truth.
*/
isTrue: apf.isTrue,
/**
* Determines whether a string is false (in the HTML attribute sense).
* @param {Mixed} value The variable to check. Possible false values include:
*
* * false
* * 'false'
* * 'off'
* * 0
* * '0'
*
* @return {Boolean} whether the string is considered to imply untruth.
*/
isFalse: apf.isFalse,
/**
* This method retrieves the current value of a CSS property on an
* HTML element.
* @param {HTMLElement} html the element to read the property from
* @param {String} name the property to read
* @returns {String}
*/
getStyle: apf.getStyle
});
register("", {
ui: plugin
});
}
});