/** @module delite/Widget */
define([
"dcl/dcl",
"dojo/dom", // dom.byId
"dojo/dom-class", // domClass.add domClass.replace
"dojo/dom-construct", // domConstruct.place
"dojo/dom-geometry", // isBodyLtr
"dojo/dom-style", // domStyle.set, domStyle.get
"dojo/has",
"dojo/_base/lang", // mixin(), hitch(), etc.
"./CustomElement",
"./register",
"dojo/has!dojo-bidi?./Bidi"
], function (dcl, dom, domClass, domConstruct, domGeometry, domStyle,
has, lang, CustomElement, register, Bidi) {
// module:
// delite/Widget
// Flag to enable support for textdir attribute
has.add("dojo-bidi", false);
// Used to generate unique id for each widget
var cnt = 0;
/**
* Base class for all widgets, i.e. custom elements that appear visually.
* @class module:delite/Widget
* @augments {module:delite/CustomElement}
*/
var Widget = dcl(CustomElement, /** @lends module:delite/Widget# */{
// summary:
// Base class for all widgets, i.e. custom elements that appear visually.
//
// Provides stubs for widget lifecycle methods for subclasses to extend, like buildRendering(),
// postCreate(), startup(), and destroy(), and also public API methods like watch().
// baseClass: [protected] String
// Root CSS class of the widget (ex: d-text-box)
baseClass: "",
_setBaseClassAttr: function (value) {
domClass.replace(this, value, this.baseClass);
this._set("baseClass", value);
},
// focused: [readonly] Boolean
// This widget or a widget it contains has focus, or is "active" because
// it was recently clicked.
/**
* This widget or a widget it contains has focus, or is "active" because
* it was recently clicked.
* @member {boolean}
* @default false
*/
focused: false,
/**
* Designates where children of the source DOM node will be placed.
* "Children" in this case refers to both DOM nodes and widgets.
* <p>
* containerNode must be defined for any widget that accepts innerHTML
* (like ContentPane or BorderContainer or even Button), and conversely
* is undefined for widgets that don't, like TextBox.
* </p>
* @member {DomNode} module:delite/Widget#containerNode
* @default undefined
*/
/*=====
// containerNode: [readonly] DomNode
// Designates where children of the source DOM node will be placed.
// "Children" in this case refers to both DOM nodes and widgets.
//
// containerNode must be defined for any widget that accepts innerHTML
// (like ContentPane or BorderContainer or even Button), and conversely
// is undefined for widgets that don't, like TextBox.
containerNode: undefined,
=====*/
// _started: [readonly] Boolean
// startup() has completed.
_started: false,
// register: delite/register
// Convenience pointer to register class. Used by buildRendering() functions produced from
// handlebars! / template.
/**
* Convenience pointer to register class. Used by buildRendering() functions produced from
* handlebars! / template.
* @member {delite/register}
*/
register: register,
// widgetId: [const readonly] Number
// Unique id for this widget, separate from id attribute (which may or may not be set).
// Useful when widget creates subnodes that need unique id's.
widgetId: 0,
//////////// INITIALIZATION METHODS ///////////////////////////////////////
/**
* Kick off the life-cycle of a widget.
*/
createdCallback: function () {
// summary:
// Kick off the life-cycle of a widget
// description:
// Create calls a number of widget methods (preCreate, buildRendering, and postCreate),
// some of which of you'll want to override.
//
// Of course, adventurous developers could override createdCallback entirely, but this should
// only be done as a last resort.
// tags:
// protected
this.preCreate();
// Render the widget
this.buildRendering();
this.postCreate();
},
/**
* Called when the widget is first inserted into the document.
* If widget is created programatically then app must call startup() to trigger this method.
*/
attachedCallback: function () {
// summary:
// Called when the widget is first inserted into the document.
// If widget is created programatically then app must call startup() to trigger this method.
this._attached = true;
// When Widget extends Invalidating some/all of this code should probably be moved to refreshRendering()
if (this.baseClass) {
domClass.add(this, this.baseClass);
}
if (!this.isLeftToRight()) {
domClass.add(this, "d-rtl");
}
// Since safari masks all custom setters for tabIndex on the prototype, call them here manually.
// For details see:
// https://bugs.webkit.org/show_bug.cgi?id=36423
// https://bugs.webkit.org/show_bug.cgi?id=49739
// https://bugs.webkit.org/show_bug.cgi?id=75297
var tabIndex = this.tabIndex;
// Trace up prototype chain looking for custom setter
for (var proto = this; proto; proto = Object.getPrototypeOf(proto)) {
var desc = Object.getOwnPropertyDescriptor(proto, "tabIndex");
if (desc && desc.set) {
if (this.hasAttribute("tabindex")) { // initial value was specified
this.removeAttribute("tabindex");
desc.set.call(this, tabIndex); // call custom setter
}
var self = this;
// begin watching for changes to the tabindex DOM attribute
/* global WebKitMutationObserver */
if ("WebKitMutationObserver" in window) {
// If Polymer is loaded, use MutationObserver rather than WebKitMutationObserver
// to avoid error about "referencing a Node in a context where it does not exist".
var MO = window.MutationObserver || WebKitMutationObserver; // for jshint
var observer = new MO(function () {
var newValue = self.getAttribute("tabindex");
if (newValue !== null) {
self.removeAttribute("tabindex");
desc.set.call(self, newValue);
}
});
observer.observe(this, {
subtree: false,
attributeFilter: ["tabindex"],
attributes: true
});
}
break;
}
}
},
/**
* Processing before buildRendering()
*/
preCreate: function () {
// summary:
// Processing before buildRendering()
// tags:
// protected
this.widgetId = ++cnt;
},
/**
* Construct the UI for this widget, filling in subnodes and/or text inside of this.
* Most widgets will leverage delite/handlebars! to implement this method.
*/
buildRendering: function () {
// summary:
// Construct the UI for this widget, filling in subnodes and/or text inside of this.
// Most widgets will leverage delite/handlebars! to implement this method.
// tags:
// protected
},
/**
* @summary
* Processing after the DOM fragment is created
* @description
* Called after the DOM fragment has been created, but not necessarily
* added to the document. Do not include any operations which rely on
* node dimensions or placement.
*/
postCreate: function () {
// summary:
// Processing after the DOM fragment is created
// description:
// Called after the DOM fragment has been created, but not necessarily
// added to the document. Do not include any operations which rely on
// node dimensions or placement.
// tags:
// protected
},
/**
* @summary
* Processing after the DOM fragment is added to the document
* @description
* Called after a widget and its children have been created and added to the page,
* and all related widgets have finished their create() cycle, up through postCreate().
* <p>
* Note that startup() may be called while the widget is still hidden, for example if the widget is
* inside a hidden deliteful/Dialog or an unselected tab of a deliteful/TabContainer.
* For widgets that need to do layout, it's best to put that layout code inside resize(), and then
* extend delite/_LayoutWidget so that resize() is called when the widget is visible.
* </p>
*/
startup: function () {
// summary:
// Processing after the DOM fragment is added to the document
// description:
// Called after a widget and its children have been created and added to the page,
// and all related widgets have finished their create() cycle, up through postCreate().
//
// Note that startup() may be called while the widget is still hidden, for example if the widget is
// inside a hidden deliteful/Dialog or an unselected tab of a deliteful/TabContainer.
// For widgets that need to do layout, it's best to put that layout code inside resize(), and then
// extend delite/_LayoutWidget so that resize() is called when the widget is visible.
if (this._started) {
return;
}
if (!this._attached) {
this.attachedCallback();
}
this._started = true;
this.getChildren().forEach(function (obj) {
if (!obj._started && !obj._destroyed && typeof obj.startup == "function") {
obj.startup();
obj._started = true;
}
});
},
//////////// DESTROY FUNCTIONS ////////////////////////////////
/**
* Destroy this widget and its descendants.
*/
destroy: function () {
// summary:
// Destroy this widget and its descendants.
if (this.bgIframe) {
this.bgIframe.destroy();
delete this.bgIframe;
}
},
/**
* Returns all direct children of this widget, i.e. all widgets or DOM node underneath
* this.containerNode whose parent is this widget. Note that it does not return all
* descendants, but rather just direct children.
* <p>
* The result intentionally excludes internally created widgets (a.k.a. supporting widgets)
* outside of this.containerNode.
* </p>
*/
getChildren: function () {
// summary:
// Returns all direct children of this widget, i.e. all widgets or DOM node underneath
// this.containerNode whose parent is this widget. Note that it does not return all
// descendants, but rather just direct children.
//
// The result intentionally excludes internally created widgets (a.k.a. supporting widgets)
// outside of this.containerNode.
// use Array.prototype.slice to transform the live HTMLCollection into an Array
return this.containerNode ? Array.prototype.slice.call(this.containerNode.children) : []; // []
},
/**
* Returns the parent widget of this widget.
*/
getParent: function () {
// summary:
// Returns the parent widget of this widget.
return this.getEnclosingWidget(this.parentNode);
},
/**
* Return this widget's explicit or implicit orientation (true for LTR, false for RTL)
*/
isLeftToRight: function () {
// summary:
// Return this widget's explicit or implicit orientation (true for LTR, false for RTL)
// tags:
// protected
return this.dir ? (this.dir === "ltr") : domGeometry.isBodyLtr(this.ownerDocument); //Boolean
},
/**
* Return true if this widget can currently be focused and false if not
*/
isFocusable: function () {
// summary:
// Return true if this widget can currently be focused
// and false if not
return this.focus && (domStyle.get(this, "display") !== "none");
},
/**
* @summary
* Place this widget somewhere in the DOM based
* on standard domConstruct.place() conventions.
* @description
* A convenience function provided in all _Widgets, providing a simple
* shorthand mechanism to put an existing (or newly created) Widget
* somewhere in the dom, and allow chaining.
* @param {String|DomNode|Widget} reference Widget, DOMNode, or id of widget or DOMNode
* @param {String|Int} [position] If reference is a widget (or id of widget), and that widget has an ".addChild" method,
* it will be called passing this widget instance into that method, supplying the optional
* position index passed. In this case position (if specified) should be an integer.
* <p>
* If reference is a DOMNode (or id matching a DOMNode but not a widget),
* the position argument can be a numeric index or a string
* "first", "last", "before", or "after", same as dojo/dom-construct::place().
* </p>
*/
placeAt: function (/* String|DomNode|Widget */ reference, /* String|Int? */ position) {
// summary:
// Place this widget somewhere in the DOM based
// on standard domConstruct.place() conventions.
// description:
// A convenience function provided in all _Widgets, providing a simple
// shorthand mechanism to put an existing (or newly created) Widget
// somewhere in the dom, and allow chaining.
// reference:
// Widget, DOMNode, or id of widget or DOMNode
// position:
// If reference is a widget (or id of widget), and that widget has an ".addChild" method,
// it will be called passing this widget instance into that method, supplying the optional
// position index passed. In this case position (if specified) should be an integer.
//
// If reference is a DOMNode (or id matching a DOMNode but not a widget),
// the position argument can be a numeric index or a string
// "first", "last", "before", or "after", same as dojo/dom-construct::place().
// returns: delite/Widget
// Provides a useful return of the newly created delite/Widget instance so you
// can "chain" this function by instantiating, placing, then saving the return value
// to a variable.
// example:
// | // create a Button with no srcNodeRef, and place it in the body:
// | var button = new Button({ label:"click" }).placeAt(document.body);
// | // now, 'button' is still the widget reference to the newly created button
// | button.on("click", function (e) { console.log('click'); }));
// example:
// | // create a button out of a node with id="src" and append it to id="wrapper":
// | var button = new Button({},"src").placeAt("wrapper");
// example:
// | // place a new button as the first element of some div
// | var button = new Button({ label:"click" }).placeAt("wrapper","first");
// example:
// | // create a contentpane and add it to a TabContainer
// | var tc = document.getElementById("myTabs");
// | new ContentPane({ href:"foo.html", title:"Wow!" }).placeAt(tc)
reference = dom.byId(reference);
if (reference && reference.addChild && (!position || typeof position === "number")) {
// Use addChild() if available because it skips over text nodes and comments.
reference.addChild(this, position);
} else {
// "reference" is a plain DOMNode, or we can't use refWidget.addChild(). Use domConstruct.place() and
// target refWidget.containerNode for nested placement (position==number, "first", "last", "only"), and
// refWidget otherwise ("after"/"before"/"replace").
var ref = reference ?
(reference.containerNode && !/after|before|replace/.test(position || "") ?
reference.containerNode : reference) : dom.byId(reference, this.ownerDocument);
domConstruct.place(this, ref, position);
// Start this iff it has a parent widget that's already started.
// TODO: for 2.0 maybe it should also start the widget when this.getParent() returns null??
if (!this._started && (this.getParent() || {})._started) {
this.startup();
}
}
return this;
},
/**
* Returns the widget whose DOM tree contains the specified DOMNode, or null if
* the node is not contained within the DOM tree of any widget
*/
getEnclosingWidget: function (/*DOMNode*/ node) {
// summary:
// Returns the widget whose DOM tree contains the specified DOMNode, or null if
// the node is not contained within the DOM tree of any widget
do {
if (node.nodeType === 1 && node.buildRendering) {
return node;
}
} while ((node = node.parentNode));
return null;
},
// Focus related methods. Used by focus.js.
/**
* Called when the widget becomes "active" because
* it or a widget inside of it either has focus, or has recently
* been clicked.
* @param {event} e - A focus event.
*/
onFocus: function () {
// summary:
// Called when the widget becomes "active" because
// it or a widget inside of it either has focus, or has recently
// been clicked.
// tags:
// callback
},
/**
* Called when the widget stops being "active" because
* focus moved to something outside of it, or the user
* clicked somewhere outside of it, or the widget was
* hidden.
*/
onBlur: function () {
// summary:
// Called when the widget stops being "active" because
// focus moved to something outside of it, or the user
// clicked somewhere outside of it, or the widget was
// hidden.
// tags:
// callback
},
_onFocus: function () {
// summary:
// This is where widgets do processing for when they are active,
// such as changing CSS classes. See onFocus() for more details.
// tags:
// protected
this.onFocus();
},
_onBlur: function () {
// summary:
// This is where widgets do processing for when they stop being active,
// such as changing CSS classes. See onBlur() for more details.
// tags:
// protected
this.onBlur();
}
});
if (has("dojo-bidi")) {
Widget = dcl(Widget, Bidi);
}
// Setup automatic chaining for lifecycle methods, except for buildRendering().
// destroy() is chained in Destroyable.js.
dcl.chainAfter(Widget, "preCreate");
dcl.chainAfter(Widget, "postCreate");
dcl.chainAfter(Widget, "startup");
return Widget;
});