/**
* @fileoverview
* Utils and classes to identify and load Kekule Widget in HTML tag when a page is loaded.
*
* Generally, the auto launcher will iterate through elements in document. If an element is
* with a data-widget attribute, it will be regarded as a Kekule related one. Then a
* specified Kekule widget will be created (according to data-widget attribute) on this tag.
* Widget property will also be set from "data-XXX" attribute.
*
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /core/kekule.common.js
* requires /utils/kekule.utils.js
* requires /xbrowsers/kekule.x.js
* requires /widgets/kekule.widget.base.js
*/
(function(){
var DU = Kekule.DomUtils;
var AU = Kekule.ArrayUtils;
/**
* Helper class to create and bind Kekule widgets while loading HTML page.
* Generally, the auto launcher will iterate through elements in document. If an element is
* with a data-widget attribute, it will be regarded as a Kekule related one. Then a
* specified Kekule widget will be created (according to data-widget attribute) on this tag.
* Widget property will also be set from "data-XXX" attribute.
*
* @class
*/
Kekule.Widget.AutoLauncher = Class.create(ObjectEx,
/** @lends Kekule.Widget.AutoLauncher# */
{
/** @private */
CLASS_NAME: 'Kekule.Widget.AutoLauncher',
/** @private */
WIDGET_ATTRIB: 'data-widget',
/** @private */
WIDGET_ATTRIB_ALT: 'data-kekule-widget',
/** @private */
PLACEHOLDER_ATTRIB: 'data-placeholder',
/** @private */
FIELD_PARENT_WIDGET_ELEM: '__$kekule_parent_widget_elem$__',
/** @constructs */
initialize: function($super)
{
$super();
this._executingFlag = 0; // private
this._pendingWidgetRefMap = new Kekule.MapEx(true); // non-weak, to get all keys
this._pendingElems = []; // elements that need to be launched
this._handlingPendings = false; // private flag
this._execOnPendingBind = this._execOnPending.bind(this);
var self = this;
// delegate Kekule.Widget.Util method for auto launch purpose
/** @ignore */
Kekule.Widget.Utils._setWidgetRefPropFromId = function(widget, propName, id)
{
if (id)
{
var refWidget = Kekule.Widget.getWidgetById(id, widget.getDocument());
if (refWidget)
widget.setPropValue(propName, refWidget);
else // in auto launch mode, perhaps the corresponding widget has not been created
{
var elem = widget.getDocument().getElementById(id);
if (elem && self.isExecuting()) // has the corresponding element, just save it and try to set widget again after auto launch
{
self.addPendingWidgetRefItem(elem, widget, propName);
}
}
}
};
},
/** @private */
finalize: function($super)
{
this._pendingElems = null;
this._pendingWidgetRefMap = null;
$super();
},
// Methods about lanuchingElems
/** @private */
getPendingElems: function()
{
return this._pendingElems;
},
/** @private */
addPendingElem: function(doc, elem, parent, widgetClass, execOnChildren)
{
//console.log('pending', elem, parent);
this._pendingElems.push({'doc': doc, 'elem': elem, 'parent': parent, 'widgetClass': widgetClass, 'execOnChildren': execOnChildren});
},
/** @private */
handlePendingElems: function(callback)
{
if (!this._handlingPendings) // avoid duplicated call
{
this._handlingPendings = true;
if (callback)
{
var elemItems = this._pendingElems;
elemItems.push({'callback': callback}); // set callback to tail element item
}
this.beginExec();
//this._execOnPendingBind.defer();
setTimeout(this._execOnPendingBind, 0);
}
},
/** @private */
_execOnPending: function()
{
var currItem = this._pendingElems.shift(); // handle the first one
if (!currItem) // sequence is empty
{
this._handlingPendings = false;
this.endExec();
}
else // do actual create
{
try
{
if (currItem.elem) // normal element lauch item
this.createWidgetOnElem(currItem.doc, currItem.elem, currItem.parent);
else if (currItem.callback) // special callback item
setTimeout(currItem.callback, 0);
}
finally
{
//this._execOnPendingBind.defer();
setTimeout(this._execOnPendingBind, 0);
}
}
},
// Methods about pendingWidgetRefMap
/** @private */
getPendingWidgetRefMap: function()
{
return this._pendingWidgetRefMap;
},
/** @private */
addPendingWidgetRefItem: function(refElem, widget, propName)
{
this._pendingWidgetRefMap.set(refElem, {'widget': widget, 'propName': propName});
},
/** @private */
removePendingWidgetRefItem: function(refElem)
{
this._pendingWidgetRefMap.remove(refElem);
},
/** @private */
handlePendingWidgetRef: function()
{
var pendingElems = this._pendingWidgetRefMap.getKeys();
for (var i = 0, l = pendingElems.length; i < l; ++i)
{
var elem = pendingElems[i];
var refWidget = Kekule.Widget.getWidgetOnElem(elem);
if (refWidget)
{
var setting = this._pendingWidgetRefMap.get(elem);
setting.widget.setPropValue(setting.propName, refWidget);
}
this.removePendingWidgetRefItem(elem);
}
},
/** @private */
beginExec: function()
{
++this._executingFlag;
//this._startTime = Date.now();
},
/** @private */
endExec: function()
{
if (this._executingFlag > 0)
--this._executingFlag;
if (this._executingFlag <= 0) // finally execution done
{
this.handlePendingWidgetRef();
}
/*
this._endTime = Date.now();
console.log('elapse', this._endTime - this._startTime);
*/
},
/** @private */
isExecuting: function()
{
return this._executingFlag > 0;
},
/**
* Launch all widgets inside element.
* @param {HTMLElement} rootElem
* @param {Bool} deferCreation
* @param {Func} callback A callback function that will be called when the task is done (since deferCreation may be true).
*/
execute: function(rootElem, deferCreation, callback)
{
var deferring = Kekule.ObjUtils.notUnset(deferCreation)? deferCreation: Kekule.Widget.AutoLauncher.deferring;
if (!deferring)
this.beginExec();
//var _tStart = Date.now();
try
{
if (typeof(rootElem.querySelector) === 'function') // support querySelector func, use fast approach
this.executeOnElemBySelector(rootElem.ownerDocument, rootElem, null, deferring);
else
this.executeOnElem(rootElem.ownerDocument, rootElem, null, deferring);
if (deferring)
this.handlePendingElems(callback);
}
finally
{
if (!deferring)
{
this.endExec();
if (callback)
callback();
}
}
//var _tEnd = Date.now();
//console.log('Launch in ', _tEnd - _tStart, 'ms');
},
/**
* Execute launch process on element and its children. Widget created will be set as child of parentWidget.
* This method will use traditional element iterate method for heritage browsers that do not support querySelector.
* @param {HTMLDocument} doc
* @param {HTMLElement} elem
* @param {Variant} parentWidgetOrElem Can be null.
* @private
*/
executeOnElem: function(doc, elem, parentWidgetOrElem, deferring)
{
/*
if (elem.isContentEditable && !Kekule.Widget.AutoLauncher.enableOnEditable)
return;
*/
if (!this.isElemLaunchable(elem))
return;
/*
// if elem already binded with a widget, do nothing
if (Kekule.Widget.getWidgetOnElem(elem))
return;
// check if elem has widget specified attribute.
var widgetName = elem.getAttribute(this.WIDGET_ATTRIB);
if (!widgetName)
widgetName = elem.getAttribute(this.WIDGET_ATTRIB_ALT);
if (widgetName) // may be a widget
{
var widgetClass = this.getWidgetClass(widgetName);
if (widgetClass)
{
widget = this.createWidgetOnElem(doc, elem, widgetClass);
if (widget) // create successful
{
if (parentWidget)
widget.setParent(parentWidget);
currParent = widget;
}
}
}
*/
var widget = null;
var widgetClass = this.getElemWidgetClass(elem);
var currParent = parentWidgetOrElem;
if (deferring) // deferring creation on element
{
if (widgetClass)
{
this.addPendingElem(doc, elem, parentWidgetOrElem, widgetClass, false/*Kekule.Widget.AutoLauncher.enableCascadeLaunch*/);
currParent = elem;
}
}
else // create directly on element
{
var parentWidget = parentWidgetOrElem;
if (parentWidget && !(parentWidget instanceof Kekule.Widget.BaseWidget)) // is element
parentWidget = Kekule.Widget.getWidgetOnElem(parentWidgetOrElem);
widget = this.createWidgetOnElem(doc, elem, parentWidget, widgetClass);
if (widget)
currParent = widget;
else
currParent = elem;
}
{
var shouldCascade = (deferring && !widgetClass) || (!deferring && !widget) || Kekule.Widget.AutoLauncher.enableCascadeLaunch;
//if (!widget || Kekule.Widget.AutoLauncher.enableCascadeLaunch)
if (shouldCascade)
{
// check child elements further
var children = DU.getDirectChildElems(elem);
for (var i = 0, l = children.length; i < l; ++i)
{
var child = children[i];
this.executeOnElem(doc, child, currParent, deferring);
}
}
}
},
/**
* Execute launch process on element and its children. Widget created will be set as child of parentWidget.
* This method will use querySelector method to perform a fast launch on supported browser.
* @param {HTMLDocument} doc
* @param {HTMLElement} rootElem
* @param {Kekule.Widget.BaseWidget} parentWidget Can be null.
*/
executeOnElemBySelector: function(doc, rootElem, parentWidget, deferring)
{
//console.log('Using selector');
var selector = '[' + this.WIDGET_ATTRIB + '],[' + this.WIDGET_ATTRIB_ALT + ']';
//var selector = '[' + this.WIDGET_ATTRIB + ']';
var allElems = rootElem.querySelectorAll(selector);
var rootWidgetClass = this.getElemWidgetClass(rootElem); // if root element is a widget, shift it into allElems
if (rootWidgetClass)
{
allElems = Array.prototype.slice.call(allElems);
allElems.unshift(rootElem);
}
if (allElems && allElems.length)
{
/*
// turn node list to array
if (Array.from)
allElems = Array.from(allElems);
else
{
var temp = [];
for (var i = 0, l = allElems.length; i < l; ++i)
{
temp.push(allElems[i]);
}
allElems = temp;
}
*/
//console.log(allElems, typeof(allElems));
// build tree relation of all those elements
for (var i = 0, l = allElems.length; i < l; ++i)
{
var elem = allElems[i];
//var candidateParentElems = allElems.slice(0, i - 1);
// only leading elems can be parent of curr one
var parentElem = this._findParentCandidateElem(elem, allElems, 0, i - 1);
if (parentElem)
{
elem[this.FIELD_PARENT_WIDGET_ELEM] = parentElem;
//console.log('Parent relation', elem.id + '/' + elem.getAttribute('data-widget'), parentElem.id + '/' + parentElem.getAttribute('data-widget'));
}
}
// then create corresponding widgets
for (var i = 0, l = allElems.length; i < l; ++i)
{
var elem = allElems[i];
/*
if (elem.isContentEditable && !Kekule.Widget.AutoLauncher.enableOnEditable)
continue;
*/
if (!this.isElemLaunchable(elem))
continue;
var parentWidgetElem = elem[this.FIELD_PARENT_WIDGET_ELEM] || null;
// create widget only on top level elem when enableCascadeLaunch is false
if (Kekule.Widget.AutoLauncher.enableCascadeLaunch || !parentWidgetElem)
{
/*
var pWidget = null;
if (parentWidgetElem)
{
// we can be sure that the parentWidgetElem is before this one in array
// and the widget on it has already been created
var pWidget = Kekule.Widget.getWidgetOnElem(parentWidgetElem);
}
this.createWidgetOnElem(doc, elem, pWidget);
*/
if (deferring) // deferring
this.addPendingElem(doc, elem, parentWidgetElem, null, false);
else // create directly
this.createWidgetOnElem(doc, elem, parentWidgetElem);
}
}
}
},
/** @private */
_findParentCandidateElem: function(elem, candidateElems, fromIndex, toIndex)
{
var result= null;
var parent = elem.parentNode;
while (parent && !result)
{
for (var i = toIndex; i >= fromIndex; --i)
{
if (parent === candidateElems[i])
{
result = candidateElems[i];
return result;
}
}
if (!result)
parent = parent.parentNode;
}
return result;
},
/**
* Create new widget on an element.
* @param {HTMLDocument} doc
* @param {HTMLElement} elem
* @param {Class} widgetClass
* @returns {Kekule.Widget.BaseWidget}
* @private
*/
createWidgetOnElem: function(doc, elem, parentWidgetOrElem, widgetClass)
{
var result = null;
// if elem already binded with a widget, do nothing
var old = Kekule.Widget.getWidgetOnElem(elem);
if (old)
return old;
//console.log('Create widget on elem', elem, parentWidgetOrElem && parentWidgetOrElem.getElement());
if (!widgetClass)
widgetClass = this.getElemWidgetClass(elem);
if (widgetClass)
{
var AL = Kekule.Widget.AutoLauncher;
// check if using place holder
var usingPlaceHolder = false;
if (AL.placeHolderStrategy !== AL.PlaceHolderStrategies.DISABLED)
{
var attrPlaceholder = elem.getAttribute(this.PLACEHOLDER_ATTRIB) || '';
usingPlaceHolder = ((AL.placeHolderStrategy === AL.PlaceHolderStrategies.EXPLICIT) && Kekule.StrUtils.strToBool(attrPlaceholder))
|| (AL.placeHolderStrategy === AL.PlaceHolderStrategies.IMPLICIT);
usingPlaceHolder = usingPlaceHolder && ClassEx.getPrototype(widgetClass).canUsePlaceHolderOnElem(elem);
}
//usingPlaceHolder = true;
if (usingPlaceHolder)
{
result = new Kekule.Widget.PlaceHolder(elem, widgetClass);
}
else
result = new widgetClass(elem);
if (result) // create successful
{
var parentWidget = parentWidgetOrElem;
if (parentWidget && !(parentWidget instanceof Kekule.Widget.BaseWidget)) // is element
parentWidget = Kekule.Widget.getWidgetOnElem(parentWidgetOrElem);
if (parentWidget)
result.setParent(parentWidget);
}
}
return result;
},
/**
* Return whether the element should be launched as a widget.
* @param {HTMLElement} elem
* @returns {Bool}
*/
isElemLaunchable: function(elem)
{
if (elem.isContentEditable && !Kekule.Widget.AutoLauncher.enableOnEditable)
return false;
else
return true;
},
/** @private */
getElemWidgetClass: function(elem)
{
var result = null;
// check if elem has widget specified attribute.
var widgetName = elem.getAttribute(this.WIDGET_ATTRIB);
if (!widgetName)
widgetName = elem.getAttribute(this.WIDGET_ATTRIB_ALT);
if (widgetName) // may be a widget
{
result = this.getWidgetClass(widgetName);
}
return result;
},
/** @private */
getWidgetClass: function(widgetName)
{
// TODO: here we simply create class from widget class name
return ClassEx.findClass(widgetName);
}
});
Kekule.ClassUtils.makeSingleton(Kekule.Widget.AutoLauncher);
Kekule.Widget.autoLauncher = Kekule.Widget.AutoLauncher.getInstance();
/**
* PlaceHolder creation strategy for autolauncher
* @enum
*/
Kekule.Widget.AutoLauncher.PlaceHolderStrategies = {
/** PlaceHolder will be totally disabled. */
DISABLED: 'disabled',
/** Placeholder will be created when possible. */
IMPLICIT: 'implicit',
/** Placeholder will only be created when attribute placeholder is explicitly set to true in element. */
EXPLICIT: 'explicit'
};
/** A flag to turn on or off auto launcher. */
Kekule.Widget.AutoLauncher.enabled = true;
/** A flag to enable or disable launching child widgets inside a widget element. */
Kekule.Widget.AutoLauncher.enableCascadeLaunch = true;
/** A flag to enable or disable checking dynamic inserted content in HTML page. */
Kekule.Widget.AutoLauncher.enableDynamicDomCheck = true;
/** A flag to enable or disable launching widgets on element in HTML editor (usually should not). */
Kekule.Widget.AutoLauncher.enableOnEditable = false;
/** If true, Placeholder maybe created during auto launching. */
Kekule.Widget.AutoLauncher.placeHolderStrategy = Kekule.Widget.AutoLauncher.PlaceHolderStrategies.EXPLICIT;
/** If true, the launch process on each element will be deferred, try not to block the UI. */
Kekule.Widget.AutoLauncher.deferring = false;
/**
* A helper class to notify widget system is ready.
* @class
*/
Kekule.Widget.WidgetsReady = {
isReady: false,
funcs: [],
ready: function(fn)
{
if (WR.isReady)
{
fn();
}
else
{
WR.funcs.push(fn);
}
},
fireReady: function()
{
if (WR.isReady)
return;
WR.isReady = true;
var funcs = WR.funcs;
while (funcs.length)
{
var fn = funcs.shift();
fn();
}
}
};
var WR = Kekule.Widget.WidgetsReady;
/**
* Invoked when widget system is constructed.
* @param {Func} fn Callback function.
* @function
*/
Kekule.Widget.ready = WR.ready;
var _doAutoLaunch = function()
{
//console.log('do autolaunch', _doAutoLaunch.done, Kekule.Widget.AutoLauncher.enabled);
if (_doAutoLaunch.done)
return;
if (!Kekule._isLoaded()) // the whole library is not completely loaded yet, may be some widget class unavailable, waiting
{
Kekule._registerAfterLoadSysProc(_doAutoLaunch);
return;
}
// if deferring launch, must intercept the DOM ready handlers, ensures they are called after widgets created (for compatibility)
var resumeDomReady = Kekule.Widget.AutoLauncher.deferring? function()
{
Kekule.X.DomReady.resume();
}: null;
var done = function()
{
try
{
WR.fireReady();
}
finally
{
if (resumeDomReady)
resumeDomReady();
}
};
if (Kekule.Widget.AutoLauncher.enabled)
{
//console.log('do autolaunch on body', document.body);
if (Kekule.Widget.AutoLauncher.deferring)
Kekule.X.DomReady.suspend();
Kekule.Widget.autoLauncher.execute(document.body, null, done);
}
else
done();
// add dynamic node inserting observer
if (Kekule.X.MutationObserver)
{
var observer = new Kekule.X.MutationObserver(
function(mutations)
{
if (Kekule.Widget.AutoLauncher.enableDynamicDomCheck && Kekule.Widget.AutoLauncher.enabled)
{
for (var i = 0, l = mutations.length; i < l; ++i)
{
var m = mutations[i];
if (m.type === 'childList') // dom tree changes
{
var addedNodes = m.addedNodes;
for (var j = 0, k = addedNodes.length; j < k; ++j)
{
var node = addedNodes[j];
if (node.nodeType === Node.ELEMENT_NODE)
{
Kekule.Widget.autoLauncher.execute(node);
}
}
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
else // traditional DOM event method
{
Kekule.X.Event.addListener(document, 'DOMNodeInserted',
function(e)
{
if (Kekule.Widget.AutoLauncher.enableDynamicDomCheck && Kekule.Widget.AutoLauncher.enabled)
{
var target = e.getTarget();
if (target.nodeType === (Node.ELEMENT_NODE || 1)) // is element
{
Kekule.Widget.autoLauncher.execute(target);
}
}
}
);
}
_doAutoLaunch.done = true;
};
Kekule.X.domReady(_doAutoLaunch);
/*
if ($jsRoot && $jsRoot.addEventListener && $jsRoot.postMessage)
{
// response to special message, force autolaunch widget.
// This is usually requested by browser addon.
$jsRoot.addEventListener('message', function(event)
{
console.log('receive message', event, event.source == $jsRoot);
if (event.data && event.data.msg === 'kekule-widget-force-autolaunch' && event.source == $jsRoot)
{
console.log('force autolaunch');
_doAutoLaunch();
}
}, false);
}
*/
})();