/**
* @fileoverview
* Widget is a control embeded in HTML element and react to UI events (so it can interact with users).
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /core/kekule.common.js
* requires /utils/kekule.utils.js
* requires /utils/kekule.domUtils.js
* requires /xbrowsers/kekule.x.js
*/
(function(){
var AU = Kekule.ArrayUtils;
var EU = Kekule.HtmlElementUtils;
/**
* Namespace for UI Widgets.
* @namespace
*/
Kekule.Widget = {
/** @private */
DEF_EVENT_HANDLER_PREFIX: 'react_',
/** @private */
getEventHandleFuncName: function(eventName)
{
return Kekule.Widget.DEF_EVENT_HANDLER_PREFIX + eventName;
},
/** @private */
getTouchGestureHandleFuncName: function(touchGestureName)
{
//return Kekule.Widget.DEF_TOUCH_GESTURE_HANDLER_PREFIX + touchGestureName;
return Kekule.Widget.DEF_EVENT_HANDLER_PREFIX + touchGestureName;
}
};
/**
* Enumeration of predefined widget element class names.
* @ignore
*/
Kekule.Widget.HtmlClassNames = {
/** A class name should add to all widget elements. */
BASE: 'K-Widget',
/** Child widget dynamic created by parent widget. */
DYN_CREATED: 'K-Dynamic-Created',
/* A top most layer. */
/*TOP_LAYER: 'K-Top-Layer',*/
NORMAL_BACKGROUND: 'K-Normal-Background',
/** Indicate text in widget can not be selected. */
NONSELECTABLE: 'K-NonSelectable',
SELECTABLE: 'K-Selectable',
// State classes
/** Class name for all widget elements in normal (enabled) state. */
STATE_NORMAL: 'K-State-Normal',
/** Class name for all widget elements in disabled state. */
STATE_DISABLED: 'K-State-Disabled',
STATE_HOVER: 'K-State-Hover',
STATE_ACTIVE: 'K-State-Active',
STATE_FOCUSED: 'K-State-Focused',
STATE_SELECTED: 'K-State-Selected',
STATE_CHECKED: 'K-State-Checked',
// show type
SHOW_POPUP: 'K-Show-Popup',
SHOW_DIALOG: 'K-Show-Dialog',
SHOW_ACTIVE_MODAL: 'K-Show-ActiveModal',
// section
SECTION: 'K-Section',
// parts
PART_CONTENT: 'K-Content',
PART_TEXT_CONTENT: 'K-Text-Content',
PART_ASSOC_TEXT_CONTENT: 'K-Assoc-Text-Content',
PART_IMG_CONTENT: 'K-Img-Content',
PART_GLYPH_CONTENT: 'K-Glyph-Content',
PART_PRI_GLYPH_CONTENT: 'K-Pri-Glyph-Content',
PART_ASSOC_GLYPH_CONTENT: 'K-Assoc-Glyph-Content',
PART_DECORATION_CONTENT: 'K-Decoration-Content',
PART_ERROR_REPORT: 'K-Error-Report',
// container
FIRST_CHILD: 'K-First-Child',
LAST_CHILD: 'K-Last-Child',
/*
BTN_GROUP_H: 'K-ButtonGroup-H',
BTN_GROUP_V: 'K-ButtonGroup-V',
*/
// text control
TEXT_NO_WRAP: 'K-No-Wrap',
// layout
LAYOUT_H: 'K-Layout-H',
LAYOUT_V: 'K-Layout-V',
// outlook/decoration classes
CORNER_ALL: 'K-Corner-All',
CORNER_LEFT: 'K-Corner-Left',
CORNER_RIGHT: 'K-Corner-Right',
CORNER_TOP: 'K-Corner-Top',
CORNER_BOTTOM: 'K-Corner-Bottom',
CORNER_TL: 'K-Corner-TL',
CORNER_TR: 'K-Corner-TR',
CORNER_BL: 'K-Corner-BL',
CORNER_BR: 'K-Corner-BR',
CORNER_LEADING: 'K-Corner-Leading',
CORNER_TAILING: 'K-Corner-Tailing',
FULLFILL: 'K-Fulfill',
NOWRAP: 'K-No-Wrap',
HIDE_TEXT: 'K-Text-Hide',
HIDE_GLYPH: 'K-Glyph-Hide',
SHOW_TEXT: 'K-Text-Show',
SHOW_GLYPH: 'K-Glyph-Show',
MODAL_BACKGROUND: 'K-Modal-Background',
DUMB_WIDGET: 'K-Dumb-Widget',
PLACEHOLDER: 'K-PlaceHolder'
};
var CNS = Kekule.Widget.HtmlClassNames;
/**
* Enumeration of layout of widget group.
*/
Kekule.Widget.Layout = {
HORIZONTAL: 1,
VERTICAL: 2
};
/**
* Enumeration of relative position of widget.
*/
Kekule.Widget.Position = {
AUTO: 0,
TOP: 1,
LEFT: 2,
BOTTOM: 4,
RIGHT: 8,
TOP_LEFT: 3,
TOP_RIGHT: 9,
BOTTOM_LEFT: 6,
BOTTOM_RIGHT: 12
};
/**
* Enumeration of directions.
* In some case, use can use the combination of directions, e.g. LTR | TTB.
*/
Kekule.Widget.Direction = {
/** Automatic direction. */
AUTO: 0,
/** Left to right. */
LTR: 1,
/** Top to bottom. */
TTB: 2,
/** Right to left. */
RTL: 4,
/** Bottom to top. */
BTT: 8,
/**
* Check if direction has horizontal component (LTR/RTL).
* @param {Int} direction
* @returns {Bool}
*/
isInHorizontal: function(direction)
{
var D = Kekule.Widget.Direction;
return !!((direction & D.LTR) || (direction & D.RTL));
},
/**
* Check if direction has vertical component (TTB/BTT).
* @param {Int} direction
* @returns {Bool}
*/
isInVertical: function(direction)
{
var D = Kekule.Widget.Direction;
return !!((direction & D.TTB) || (direction & D.BTT));
}
};
/**
* Enumeration of state of widget.
* @enum
*/
Kekule.Widget.State = {
NORMAL: 0,
FOCUSED: 1,
HOVER: 2,
ACTIVE: 3,
DISABLED: -1
};
/** @ignore */
var WS = Kekule.Widget.State;
/**
* Enumeration of mode of showing widget.
* @enum
*/
Kekule.Widget.ShowHideType = {
DROPDOWN: 1,
POPUP: 2,
DIALOG: 3,
DEFAULT: 0
};
/**
* A series of interactive events that may be handled by widget.
* @ignore
*/
Kekule.Widget.UiEvents = [
/*'blur', 'focus',*/ 'click', 'dblclick', 'mousedown',/*'mouseenter', 'mouseleave',*/ 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'mousewheel',
'keydown', 'keyup', 'keypress',
'touchstart', 'touchend', 'touchcancel', 'touchmove',
'pointerdown', 'pointermove', 'pointerout', 'pointerover', 'pointerup'
];
/**
* A series of interactive events that must be listened on local element.
* @ignore
*/
Kekule.Widget.UiLocalEvents = [
'blur', 'focus', 'mouseenter', 'mouseleave', 'pointerenter', 'pointerleave'
];
/**
* A series of interactive touch gestures that may be handled by widget.
* @ignore
*/
Kekule.Widget.TouchGestures = [
//'press', 'pressup'
'hold', 'tap', 'doubletap',
'swipe', 'swipeup', 'swipedown', 'swipeleft', 'swiperight',
'transform', 'transformstart', 'transformend',
'rotate', 'rotatestart', 'rotatemove', 'rotateend', 'rotatecancel',
'pinch', 'pinchstart', 'pinchmove', 'pinchend', 'pinchcancel', 'pinchin', 'pinchout',
'pan', 'panstart', 'panmove', 'panend', 'pancancel', 'panleft', 'panright', 'panup', 'pandown'
];
/** @private */
Kekule.Widget._PointerHoldParams = {
DURATION_THRESHOLD: 1000, // ms
MOVEMENT_THRESHOLD: 10 // px
};
/** @ignore */
var widgetBindingField = '__$kekule_widget__';
/**
* An abstract UI widget.
* Event param invoked by widget will always has a 'widget' field indicate the widget raise the event.
* This value may not be same as event.target, e.g., a widget containing child widgets, when child widget
* invokes an event and bubbles to parent widget, parent widget may overwrite event.widget.
* @class
* @augments ObjectEx
*
* @param {Variant} HTMLElement or HTMLDocument or {@link Kekule.Widget.BaseWidget}.
* If it is an HTML element, the widget will bind to this one.
* If it is an HTML document, the widget will be created in it.
* If it is Kekule.Widget.BaseWidget, a new HTML element will be created and append in parent widget.
* @param {Bool} isDumb Whether the widget is a dumb one (do not react to events).
* This type of dumb widget is used to create some very light-weighted static widgets, in other word,
* just used to bind widget styles to some HTML element.
* @param {Bool} bubbleUiEvents Defaultly, ui event (mouseenter, keyup and so on) will only be handled by
* widget itself and will not bubble to higher level widget. Set this property to true to pass such events
* to parent widget.
* @param {Bool} inheritBubbleUiEvents When bubbleUiEvents value is inherited from parent widget.
* For example, if this.getBubbleUiEvents() == false but this.getParent().getBubbleUiEvents() == true,
* the ui events will still bubbled to parent widget.
*
* @property {Kekule.Widget.BaseWidget} parent Parent widget.
* @property {HTMLDocument} document HTML document contains this widget.
* @property {HTMLElement} element HTML element bind with this widget.
* @property {Bool} isDumb Whether the widget is a dumb one (do not react to events). Readonly.
* This type of dumb widget is used to create some very light-weighted static widgets, in other word,
* just used to bind widget styles to some HTML element.
* @property {Bool} observeElementAttribChanges If this property is true, when the attribute of binded element changed in DOM,
* the widget will also reflect to it.
* @property {String} id ID of corresponding HTML element.
* @property {String} width Width style of element.
* @property {String} height Height style of element.
* @property {String} innerHTML Current element's innerHTML value.
* @property {Object} style CSS style object of current binding element.
* @property {String} cssText CSS text of current binding element.
* @property {String} htmlClassName HTML class of current binding element. This property will include all values in element's class attribute.
* @property {String} customHtmlClassName HTML class set by user. This property will exclude some predefined class names.
* //@property {Array} outlookStyleClassNames Classes used to control the outlook of widget. Usually user do not need to access this value.
* @property {String} touchAction Touch action style value of widget element.
* You should set this value (e.g., to 'none') to enable pointer event on touch as describle by pep.js.
* @property {Hash} minDimension A {width, height} hash defines the min size of widget.
* @property {Bool} enableDimensionTransform If true, when setting size of widget by setDimension method
* and the size is less than minDimension, CSS3 transform scale will be used.
* @property {Bool} useCornerDecoration
* @property {Int} layout Layout of child widgets. Value from {@link Kekule.Widget.Layout}.
* @property {Bool} allowTextWrap
* @property {Bool} showText Whether show text content in widget.
* @property {Bool} showGlyph Whether show glyph content in widget.
* @property {Bool} visible Whether current bind element's visibility style is not 'hidden'.
* @property {Bool} displayed Whether current bind element's display style is not 'none'.
* @property {Bool} finalizeAfterHiding If true, this widget will be automatically be finalize
* after {@link Kekule.Widget.BaseWidget.hide} is called.
* @property {Bool} enabled Whether widget can reflect to user input. Default is true.
* @property {Bool} inheritEnabled If set to true, widget will be turned to disabled when parent is disabled.
* @property {Bool} static Whether this widget can react to interaction events.
* @property {Bool} inheritStatic If set to true, widget will be static if parent is static.
* @property {Int} state State (normal, focused, hover, active) of widget, value from {@link Kekule.Widget.State}. Readonly.
* @property {Bool} inheritState If set to true, widget will has the same state value of parent.
* @property {String} hint Hint of widget, actually mapping to title attribute of HTML element.
* @property {String} cursor CSS cusor property for widget.
* @property {Kekule.Action} action Action associated with widget. Excute the widget will invoke that action.
* @property {Bool} enablePeriodicalExec If this property is true, the execute event will be invoked repeatly between startPeriodicalExec and stopPeriodicalExec methods.
* (for instance, mousedown on button).
* @property {Int} periodicalExecDelay How many milliseconds should periodical execution begin after startPeriodicalExec is called.
* Available only when enablePeriodicalExec property is true.
* @property {Int} periodicalExecInterval Milliseconds between two execution in periodical mode.
* Available only when enablePeriodicalExec property is true.
* @property {Hash} autoResizeConstraints A hash of {width, height}, each value from 0-1 indicating the ratio of widget width/height to client.
* If this property is set, widget will automatically adjust its size when the browser window is resized.
* @property {Bool} autoAdjustSizeOnPopup Whether shrink to browser visible client size when popping up or dropping down.
* @property {Bool} isPopup Whether this is a "popup" widget, when click elsewhere on window, the widget will automatically hide itself.
* @property {Bool} isDialog Whether this is a "dialog" widget, when press ESC key, the widget will automatically hide itself.
* @property {Kekule.HashEx} iaControllerMap Interaction controller map (id= > controller) linked to this component. Read only.
* @property {String} defIaControllerId Id of default interaction controller in map.
* @property {Kekule.Widget.InteractionController} defIaController Default interaction controller object.
* @property {String} activeIaControllerId Id of active interaction controller in map.
* @property {Kekule.Widget.InteractionController} activeIaController Active interaction controller object.
*/
/**
* Invoked when a widget object is bind to an HTML element.
* event param of it has fields: {widget, element}
* @name Kekule.Widget.BaseWidget#bind
* @event
*/
/**
* Invoked when a widget object is unbind from an HTML element.
* event param of it has fields: {widget, element}
* @name Kekule.Widget.BaseWidget#unbind
* @event
*/
/**
* Invoked when a widget is executed (such as click on button, select on menu and so on).
* event param of it has field: {widget}
* @name Kekule.Widget.BaseWidget#execute
* @event
*/
/**
* Invoked when a widget is activated (such as mouse down or enter key down on button).
* event param of it has field: {widget}
* @name Kekule.Widget.BaseWidget#activate
* @event
*/
/**
* Invoked when a widget is deactivated (such as mouse up or enter key up on button).
* event param of it has field: {widget}
* @name Kekule.Widget.BaseWidget#deactivate
* @event
*/
/**
* Invoked when a widget is shown or hidden.
* event param of it has field: {widget, isShown, isDismissed}
* @name Kekule.Widget.BaseWidget#showStateChange
* @event
*/
/**
* Invoked when a widget's width or height changed.
* event param of it has field: {widget}
* Note: This event will only be invoked when using width/height property or setDimension method to change size.
* Set CSS styles directly will not fire this event.
* @name Kekule.Widget.BaseWidget#resize
* @event
*/
Kekule.Widget.BaseWidget = Class.create(ObjectEx,
/** @lends Kekule.Widget.BaseWidget# */
{
/** @private */
CLASS_NAME: 'Kekule.Widget.BaseWidget',
/** @private */
BINDABLE_TAG_NAMES: null,
/** @private */
DEF_PERIODICAL_EXEC_DELAY: 500,
/** @private */
DEF_PERIODICAL_EXEC_INTERVAL: 100,
/** @private */
STYLE_RES_FIELD: '__$style_resources__',
/** @constructs */
initialize: function($super, parentOrElementOrDocument, isDumb)
{
this._stateClassName = null;
this._isDismissed = false;
this._pendingHtmlClassNames = '';
this._enableShowHideEvents = true;
this._reactElemAttribMutationBind = this._reactElemAttribMutation.bind(this);
this.reactTouchGestureBind = this.reactTouchGesture.bind(this);
this.setPropStoreFieldValue('inheritEnabled', true);
this.setPropStoreFieldValue('inheritStatic', true);
this.setPropStoreFieldValue('selfEnabled', true);
this.setPropStoreFieldValue('selfStatic', false);
this.setPropStoreFieldValue('periodicalExecDelay', this.DEF_PERIODICAL_EXEC_DELAY);
this.setPropStoreFieldValue('periodicalExecInterval', this.DEF_PERIODICAL_EXEC_INTERVAL);
this.setPropStoreFieldValue('useNormalBackground', true);
//this.setPropStoreFieldValue('touchAction', 'none'); // debug: set to none disable default touch actions
this._touchActionNoneTouchStartHandlerBind = this._touchActionNoneTouchStartHandler.bind(this);
$super();
this.setPropStoreFieldValue('isDumb', !!isDumb);
if (!isDumb)
this.reactUiEventBind = this.reactUiEvent.bind(this);
/*
this.setShowText(true);
this.setShowGlyph(true);
*/
if (parentOrElementOrDocument)
{
if (parentOrElementOrDocument instanceof Kekule.Widget.BaseWidget)
{
this.setDocument(parentOrElementOrDocument.getDocument());
this.createElement();
this.setParent(parentOrElementOrDocument);
}
else if (parentOrElementOrDocument.documentElement) // is document
{
this.setDocument(parentOrElementOrDocument);
this.createElement();
}
else // is HTML element
{
this.setDocument(parentOrElementOrDocument.ownerDocument);
this.setElement(parentOrElementOrDocument);
}
}
this._stateClassName = null;
this._layoutClassName = null;
if (!this.getLayout())
this.setLayout(Kekule.Widget.Layout.HORIZONTAL);
this.setDraggable(false);
this._periodicalExecBind = this._periodicalExec.bind(this);
//this.setBubbleEvent(false); // disallow event bubble
this.setBubbleEvent(true);
this.setInheritBubbleUiEvents(true);
this.stateChanged();
/*
if (Kekule.Widget.globalManager)
Kekule.Widget.globalManager.notifyWidgetCreated(this);
*/
var gm = this.getGlobalManager();
if (gm)
gm.notifyWidgetCreated(this);
},
/** @private */
initProperties: function()
{
this.defineProp('isDumb', {'dataType': DataType.BOOL, 'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'setter': function(value)
{
if (this.getIsDumb() != value)
{
this.setPropStoreFieldValue('isDumb', value);
var elem = this.getElement();
if (elem)
{
if (value)
this.uninstallUiEventHandlers(elem);
else
this.installUiEventHandlers(elem);
}
}
}
});
this.defineProp('bubbleUiEvents', {'dataType': DataType.BOOL, 'scope': Class.PropertyScope.PUBLIC});
this.defineProp('inheritBubbleUiEvents', {'dataType': DataType.BOOL, 'scope': Class.PropertyScope.PUBLIC});
this.defineProp('touchAction', {'dataType': DataType.STRING, 'scope': Class.PropertyScope.PUBLIC,
'setter': function(value)
{
//var elem = this.getElement();
var elem = this.getCoreElement();
if (elem)
{
//elem.setAttribute('touch-action', value); // for polyfill pep.js lib (PointerEvent)
elem.style.touchAction = value; // CSS touch-action
if (value === 'none')
{
//console.log('add none handler', this.getClassName());
// Add a dummy touchstart handler to prevent default action
Kekule.X.Event.addListener(elem, 'touchstart', this._touchActionNoneTouchStartHandlerBind, {passive: false});
}
else
{
// remove the dummy touchstart handler to prevent default action
Kekule.X.Event.removeListener(elem, 'touchstart', this._touchActionNoneTouchStartHandlerBind, {passive: false});
}
}
}
});
this.defineProp('parent', {'dataType': 'Kekule.Widget.BaseWidget', 'serializable': false,
'scope': Class.PropertyScope.PUBLISHED,
'setter': function(value)
{
var old = this.getParent();
if (old) // remove from old
old._removeChild(this);
if (value) // append to new parent
value._addChild(this);
this.setPropStoreFieldValue('parent', value);
}
});
this.defineProp('childWidgets', {'dataType': DataType.ARRAY, 'serializable': false, 'setter': null,
'scope': Class.PropertyScope.PUBLIC,
'getter': function()
{
var r = this.getPropStoreFieldValue('childWidgets');
if (!r)
{
r = [];
this.setPropStoreFieldValue('childWidgets', r);
}
return r;
}
});
this.defineProp('document', {'dataType': DataType.OBJECT, 'serializable': false, 'scope': Class.PropertyScope.PUBLIC});
this.defineProp('element', {'dataType': DataType.OBJECT, 'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'setter': function(value)
{
var old = this.getElement();
if (value !== old)
{
this.setPropStoreFieldValue('element', value);
this.elementChanged(value, old);
}
}
});
this.defineProp('observeElementAttribChanges', {'dataType': DataType.BOOL, //'scope': Class.PropertyScope.PUBLIC,
'setter': function(value)
{
if (!!value !== !!this.getObserveElementAttribChanges())
{
this.setPropStoreFieldValue('observeElementAttribChanges', value);
this.observeElementAttribChangesChanged(!!value);
}
}
});
this.defineElemAttribMappingProp('id', 'id');
this.defineElemAttribMappingProp('draggable', 'draggable');
this.defineElemStyleMappingProp('width', 'width');
this.defineElemStyleMappingProp('height', 'height');
this.defineProp('offsetParent', {'dataType': DataType.OBJECT, 'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'getter': function() { return this.getElement().offsetParent; },
'setter': null
});
this.defineProp('offsetLeft', {'dataType': DataType.INT, 'serializable': false,
'getter': function() { return this.getElement().offsetLeft; },
'setter': null
});
this.defineProp('offsetTop', {'dataType': DataType.INT, 'serializable': false,
'getter': function() { return this.getElement().offsetTop; },
'setter': null
});
this.defineProp('offsetWidth', {'dataType': DataType.INT, 'serializable': false,
'getter': function() { return this.getElement().offsetWidth; },
'setter': null
});
this.defineProp('offsetHeight', {'dataType': DataType.INT, 'serializable': false,
'getter': function() { return this.getElement().offsetHeight; },
'setter': null
});
this.defineProp('innerHTML', {'dataType': DataType.STRING, 'serializable': false,
'scope': Class.PropertyScope.PUBLIC, // this prop value usually should not be shown in objInspector to avoid modification of essential HTML structure
'getter': function() { return this.getElement().innerHTML; },
'setter': function(value) { this.getElement().innerHTML = value; }
});
this.defineProp('style', {'dataType': DataType.OBJECT, 'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'getter': function() { return this.getElement().style; },
'setter': null
});
this.defineProp('cssText', {'dataType': DataType.STRING, 'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'getter': function() { return this.getElement().style.cssText; },
'setter': function(value)
{
this.getElement().style.cssText = value;
}
});
this.defineProp('htmlClassName', {'dataType': DataType.STRING, 'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'getter': function() { return this.getElement().className; },
'setter': function(value) { this.getElement().className = value; }
});
this.defineProp('customHtmlClassName', {'dataType': DataType.STRING,
'scope': Class.PropertyScope.PUBLIC,
'setter': function(value)
{
var elem = this.getElement();
var old = this.getCustomHtmlClassName();
if (elem && (old !== value))
{
if (old)
EU.removeClass(elem, old);
if (value)
EU.addClass(elem, value);
this.setPropStoreFieldValue('customHtmlClassName', value);
}
}
});
/*
this.defineProp('outlookStyleClassNames', {'dataType': DataType.ARRAY, 'serializable': false,
'setter': function(value)
{
var old = this.getOutlookStyleClassNames();
if (old)
this.removeClassName(old);
this.addClassName(value);
this.setPropStoreFieldValue('outlookStyleClassNames', value);
}
});
*/
this.defineProp('visible', {'dataType': DataType.BOOL, 'serializable': false,
'getter': function()
{
return Kekule.StyleUtils.isVisible(this.getElement());
},
'setter': function(value, byPassShowStateChange)
{
Kekule.StyleUtils.setVisibility(this.getElement(), value);
if (!byPassShowStateChange)
this.widgetShowStateChanged(this.isShown());
}
});
this.defineProp('displayed', {'dataType': DataType.BOOL, 'serializable': false,
'getter': function()
{
return Kekule.StyleUtils.isDisplayed(this.getElement());
},
'setter': function(value, byPassShowStateChange)
{
//console.log('set displayed', value, byPassShowStateChange);
Kekule.StyleUtils.setDisplay(this.getElement(), value);
if (!byPassShowStateChange)
this.widgetShowStateChanged(this.isShown());
}
});
// stores show/hide information
this.defineProp('showHideType', {'dataType': DataType.INT, 'serializable': false, 'scope': Class.PropertyScope.PRIVATE}); // private
this.defineProp('showHideCaller', {'dataType': 'Kekule.Widget.BaseWidget', 'serializable': false, 'scope': Class.PropertyScope.PRIVATE}); // private
this.defineProp('showHideCallerPageRect', {'dataType': DataType.HASH, 'serializable': false, 'scope': Class.PropertyScope.PRIVATE}); // private
this.defineProp('finalizeAfterHiding', {'dataType': DataType.BOOL, 'scope': Class.PropertyScope.PUBLIC});
this.defineProp('layout', {'dataType': DataType.INT,
'setter': function(value)
{
if (this.getPropStoreFieldValue('layout') !== value)
{
this.setPropStoreFieldValue('layout', value);
this.layoutChanged();
}
}
});
this.defineProp('useCornerDecoration', {'dataType': DataType.BOOL,
'setter': function(value)
{
this.setPropStoreFieldValue('useCornerDecoration', value);
if (!value)
{
//console.log('setNonROund');
this.removeClassName(CNS.CORNER_ALL);
}
else
{
//console.log('setROund');
this.addClassName(CNS.CORNER_ALL);
}
}
});
this.defineProp('useNormalBackground', {'dataType': DataType.BOOL,
'setter': function(value)
{
this.setPropStoreFieldValue('useNormalBackground', value);
if (!value)
{
this.removeClassName(CNS.NORMAL_BACKGROUND);
}
else
{
this.addClassName(CNS.NORMAL_BACKGROUND);
}
}
});
this.defineProp('showText', {'dataType': DataType.BOOL,
'setter': function(value)
{
this.setPropStoreFieldValue('showText', value);
//this._elemTextPart.style.display = value? '': 'none';
if (value)
{
this.removeClassName(CNS.HIDE_TEXT);
this.addClassName(CNS.SHOW_TEXT);
}
else
{
this.addClassName(CNS.HIDE_TEXT);
this.removeClassName(CNS.SHOW_TEXT);
}
}
});
this.defineProp('showGlyph', {'dataType': DataType.BOOL,
'setter': function(value)
{
this.setPropStoreFieldValue('showGlyph', value);
if (value)
{
this.removeClassName(CNS.HIDE_GLYPH);
this.addClassName(CNS.SHOW_GLYPH);
}
else
{
this.addClassName(CNS.HIDE_GLYPH);
this.removeClassName(CNS.SHOW_GLYPH);
}
}
});
this.defineProp('allowTextWrap', {'dataType': DataType.BOOL, 'serialzable': false,
'setter': function(value)
{
if (value)
this.removeClassName(CNS.TEXT_NO_WRAP);
else
this.addClassName(CNS.TEXT_NO_WRAP);
}
});
this.defineProp('selfEnabled', {'dataType': DataType.BOOL, 'scope': Class.PropertyScope.PRIVATE}); // private properties
this.defineProp('inheritEnabled', {'dataType': DataType.BOOL,
'setter': function(value)
{
this.setPropStoreFieldValue('inheritEnabled', value);
this.stateChanged();
}
});
this.defineProp('enabled', {'dataType': DataType.BOOL, 'serializable': false,
'getter': function()
{
var result = this.getPropStoreFieldValue('selfEnabled');
if (this.getInheritEnabled())
{
var p = this.getParent();
if (p)
result = result && p.getEnabled();
}
return result;
},
'setter': function(value)
{
//this.getCoreElement().disabled = !value;
//console.log('set disabled: ' + this.getClassName() + ' ' + !value);
var elem = this.getElement();
if (!value)
elem.setAttribute('disabled', 'true');
else
elem.removeAttribute('disabled');
var elem = this.getCoreElement();
if (elem != this.getElement())
{
if (!value)
elem.setAttribute('disabled', 'true');
else
elem.removeAttribute('disabled');
}
this.setPropStoreFieldValue('selfEnabled', value);
this.stateChanged();
}
});
this.defineProp('selfStatic', {'dataType': DataType.BOOL, 'scope': Class.PropertyScope.PRIVATE}); // private properties
this.defineProp('inheritStatic', {'dataType': DataType.BOOL,
'setter': function(value)
{
this.setPropStoreFieldValue('inheritStatic', value);
this.stateChanged();
}
});
this.defineProp('static', {'dataType': DataType.BOOL, 'serializable': false,
'getter': function()
{
var result = this.getPropStoreFieldValue('selfStatic');
if (this.getInheritStatic())
{
var p = this.getParent();
if (p)
result = result || p.getStatic();
}
return result;
},
'setter': function(value)
{
this.setPropStoreFieldValue('selfStatic', value);
this.stateChanged();
}
});
this.defineProp('inheritState', {'dataType': DataType.BOOL,
'setter': function(value)
{
this.setPropStoreFieldValue('inheritState', value);
this.stateChanged();
}
});
this.defineProp('state', {'dataType': DataType.INT, 'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'setter': null,
'getter': function()
{
var result;
if (this.getInheritState())
{
var p = this.getParent();
if (p)
result = p.getState();
}
else
{
if (!this.getEnabled())
result = WS.DISABLED;
else
result =
//(!this.getEnabled())? WS.DISABLED:
this.getIsActive()? WS.ACTIVE:
this.getIsHover()? WS.HOVER:
this.getIsFocused()? WS.FOCUSED:
WS.NORMAL;
}
return result;
}
});
//this.defineElemStyleMappingProp('cursor', 'cursor');
this.defineProp('cursor', {
'dataType': DataType.VARIANT,
'serializable': false,
'getter': function() { return this.getStyleProperty('cursor'); },
'setter': function(value) {
if (DataType.isArrayValue(value)) // try each cursor keywords
Kekule.StyleUtils.setCursor(this.getElement(), value);
else // normal string value
this.setStyleProperty('cursor', value);
}
});
this.defineProp('isHover', {'dataType': DataType.BOOL, 'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'setter': function(value)
{
this.setPropStoreFieldValue('isHover', value);
// if not hover, the active state should also be turned off
if (!value && !this.isCaptureMouse())
this.setPropStoreFieldValue('isActive', false);
this.stateChanged();
}
});
this.defineProp('isActive', {'dataType': DataType.BOOL, 'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'setter': function(value)
{
this.setPropStoreFieldValue('isActive', value);
if (value) // active widget should always be focused
{
this.focus();
}
var m = this.getGlobalManager();
if (m)
m.notifyWidgetActiveChanged(this, value);
this.stateChanged();
}
});
this.defineProp('isFocused', {'dataType': DataType.BOOL, 'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'getter': function()
{
var doc = this.getDocument();
var elem = this.getCoreElement();
if (doc && elem && doc.activeElement)
return (doc.activeElement === elem);
},
'setter': function(value)
{
this.setPropStoreFieldValue('isFocused', value);
var m = this.getGlobalManager();
if (m)
m.notifyWidgetFocusChanged(this, value);
var elem = this.getCoreElement();
if (elem)
{
// TODO: currently restrict focus element to form controls, avoid normal element IE focused on auto scrolling to top-left
if (elem.focus && value && Kekule.HtmlElementUtils.isFormCtrlElement(elem))
elem.focus();
if (elem.blur && (!value))
elem.blur();
}
this.stateChanged();
}
});
this.defineProp('minDimension', {'dataType': DataType.HASH});
this.defineProp('enableDimensionTransform', {'dataType': DataType.BOOL});
this.defineProp('autoResizeConstraints', {'dataType': DataType.HASH,
'setter': function(value){
this.setPropStoreFieldValue('autoResizeConstraints', value);
var gm = this.getGlobalManager() || Kekule.Widget.globalManager;
if (value)
{
this.autoResizeToClient();
gm.registerAutoResizeWidget(this);
}
else
gm.unregisterAutoResizeWidget(this);
}
});
this.defineProp('autoAdjustSizeOnPopup', {'dataType': DataType.BOOL, 'scope': Class.PropertyScope.PUBLIC});
this.defineProp('isDialog', {'dataType': DataType.BOOL, 'scope': Class.PropertyScope.PUBLIC});
this.defineProp('isPopup', {'dataType': DataType.BOOL, 'scope': Class.PropertyScope.PUBLIC});
this.defineProp('popupCaller', {'dataType': DataType.BOOL, 'scope': Class.PropertyScope.PRIVATE}); // private, record who calls this popup
this.defineProp('modalInfo', {'dataType': DataType.HASH, 'scope': Class.PropertyScope.PUBLIC}); // for dialog only
this.defineProp('enablePeriodicalExec', {'dataType': DataType.BOOL});
this.defineProp('periodicalExecDelay', {'dataType': DataType.INT});
this.defineProp('periodicalExecInterval', {'dataType': DataType.INT});
this.defineElemAttribMappingProp('hint', 'title');
this.defineProp('action', {'dataType': 'Kekule.Action', 'serializable': false,
'setter': function(value)
{
var old = this.getAction();
if (old !== value)
{
if (old && old.unlinkWidget)
{
old.unlinkWidget(this);
this.unlinkAction(old);
}
this.setPropStoreFieldValue('action', value);
if (value && value.linkWidget)
{
value.linkWidget(this);
this.linkAction(value);
}
}
}
});
this.defineProp('iaControllerMap', {'dataType': DataType.OBJECT, 'serializable': false, 'setter': null,
'scope': Class.PropertyScope.PUBLIC,
'getter': function()
{
var result = this.getPropStoreFieldValue('iaControllerMap');
if (!result)
{
result = new Kekule.HashEx();
this.setPropStoreFieldValue('iaControllerMap', result);
}
return result;
}
});
this.defineProp('defIaControllerId', {'dataType': DataType.STRING, 'serializable': false, 'scope': Class.PropertyScope.PUBLIC});
this.defineProp('defIaController', {'dataType': DataType.STRING, 'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'setter': null,
'getter': function() { return this.getIaControllerMap().get(this.getDefIaControllerId()); } });
this.defineProp('activeIaControllerId', {'dataType': DataType.STRING, 'serializable': false, 'scope': Class.PropertyScope.PUBLIC,
'setter': function(value)
{
if (value !== this.getActiveIaControllerId())
{
this.setPropStoreFieldValue('activeIaControllerId', value);
var currController = this.getActiveIaController();
if (currController && currController.activated) // call some init method of controller
{
currController.activated(this);
}
}
}
});
this.defineProp('activeIaController', {'dataType': DataType.OBJECT, 'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'setter': null,
'getter': function() { return this.getIaControllerMap().get(this.getActiveIaControllerId()); }});
this.defineProp('observingGestureEvents', {'dataType': DataType.ARRAY, 'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'setter': null
});
},
/** @private */
doFinalize: function($super)
{
this.setAction(null);
this.setParent(null);
this.releaseChildWidgets();
var elem = this.getElement();
this.setElement(null);
this.destroyElement(elem);
if (this.getGlobalManager())
this.getGlobalManager().notifyWidgetFinalized(this);
$super();
},
/** @ignore */
initPropValues: function($super)
{
$super();
this.setEnableObjectChangeEvent(true);
},
/** @ignore */
invokeEvent: function($super, eventName, event)
{
if (!event)
event = {};
// add a 'widget' param
if (!event.widget)
event.widget = this;
$super(eventName, event);
// notify global manager when a widget event occurs
var m = this.getGlobalManager(); // Kekule.Widget.globalManager;
if (m)
{
m.notifyWidgetEventFired(this, eventName, event);
}
},
/**
* Returns global widget manager in current document.
* @returns {Object}
*/
getGlobalManager: function()
{
//return Kekule.Widget.globalManager;
var doc = this.getDocument();
var win = doc && Kekule.DocumentUtils.getDefaultView(doc);
var kekuleRoot = win && win.Kekule;
if (!kekuleRoot)
kekuleRoot = Kekule;
return kekuleRoot.Widget.globalManager;
},
/**
* Returns core element of widget.
* Usually core element is the element widget binded to, but in some
* cases, core element may be a child of widget element. Descendants
* can override this method to reflect that situation.
* @returns {HTMLElement}
*/
getCoreElement: function()
{
return this.getElement();
},
/**
* Returns the element that be used as root to insert child widgets.
* Descendants can override this method to reflect that situation.
* @returns {HTMLElement}
*/
getChildrenHolderElement: function()
{
return this.getCoreElement();
},
/**
* Whether the text content inside widget element can be user selected.
* Most widget (like tree, button) should return false, but form controls (like textbox) should return true.
* Descendants may override this method.
* @returns {Bool}
*/
getTextSelectable: function()
{
return false;
},
/**
* Returns whether a placeholder widget can be bind to element to represent this widget.
* This method is used when auto-launching widget on HTML element. Descendants can override this method.
* @param {HTMLElement} elem
* @returns {Bool}
*/
canUsePlaceHolderOnElem: function(elem)
{
return false;
},
/** @private */
doPropChanged: function(propName, newValue)
{
if ((propName === 'width') || (propName === 'height'))
{
this.resized();
}
},
/** @private */
getActualBubbleUiEvents: function()
{
var parent = this.getParent();
if (this.getBubbleUiEvents())
return true
else if (this.getInheritBubbleUiEvents() && parent && parent.getActualBubbleUiEvents)
return parent.getActualBubbleUiEvents();
else
false;
},
/*
* Report an exception (error/warning and so on) occurs related to widget.
* @param {Variant} e Error object or message.
*/
/*
reportException: function(e)
{
if (!this.doReportException(e))
Kekule.error(e);
},
*/
/*
* Do actual job of reportError. If error is handled (and should not raise to browser),
* this method should return true. Descendant can override this method.
* @param {Variant} e Error object or message.
*/
/*
doReportException: function(e)
{
return false;
},
*/
/** @private */
releaseChildWidgets: function()
{
var children = this.getChildWidgets();
for (var i = children.length - 1; i >= 0; --i)
children[i].finalize();
},
/**
* Create a property that read/write attribute of HTML element.
* @param {String} propName
* @param {String} elemAttribName Attribute name of HTML element.
* @param {Hash} options Options to define property. If not set, default option will be used.
* @return {Object} Property info object added to property list.
*/
defineElemAttribMappingProp: function(propName, elemAttribName, options)
{
var ops = Object.extend({
'dataType': DataType.STRING,
'serializable': false,
'getter': function() { return this.getElement() && this.getElement().getAttribute(elemAttribName); },
'setter': function(value) { this.getElement() && this.getElement().setAttribute(elemAttribName, value); }
}, options || {});
return this.defineProp(propName, ops);
},
/**
* Create a property that read/write style property of HTML element.
* @param {String} propName
* @param {String} stylePropName Property name of element.style.
* @param {Hash} options Options to define property. If not set, default option will be used.
* @return {Object} Property info object added to property list.
*/
defineElemStyleMappingProp: function(propName, stylePropName, options)
{
var ops = Object.extend({
'dataType': DataType.STRING,
'serializable': false,
'getter': function() { return this.getStyleProperty(stylePropName); },
'setter': function(value) { this.setStyleProperty(stylePropName, value); }
}, options || {});
return this.defineProp(propName, ops);
},
/** @private */
getHigherLevelObj: function()
{
return this.getParent();
},
/**
* Called when action property is set.
* Widget should set it's outlook property (text/hint and so on) according to action here.
* Descendants can override this method to do their own job.
* @private
*/
linkAction: function(action)
{
/*
var text = action.getText();
var hint = action.getHint();
if (hint)
this.setHint(hint);
this.setEnabled(action.getEnabled());
this.setDisplayed(action.getDisplayed());
this.setVisible(action.getVisible());
*/
// do nothing here
},
/**
* Called when action property is set to null
* @private
*/
unlinkAction: function(action)
{
// do nothing here
},
/**
* Returns child action class associated with name for this widget.
* @param {String} actionName
* @param {Bool} checkSupClasses When true, if action is not found in current widget class, super classes will also be checked.
* @returns {Class}
*/
getChildActionClass: function(actionName, checkSupClasses)
{
var result = Kekule.ActionManager.getActionClassOfName(actionName, this, checkSupClasses);
return result;
},
/**
* Apply style resource to self or an element.
* @param {Variant} resOrName An instance of {@link Kekule.Widget.StyleResource} or resource name.
* @param {HTMLElement} element If not set, style will be set to widget element.
*/
linkStyleResource: function(resOrName, element)
{
var res = (resOrName instanceof Kekule.Widget.StyleResource)? resOrName: Kekule.Widget.StyleResourceManager.getResource(resOrName);
if (res)
{
res.linkTo(element || this);
}
return this;
},
/**
* Remove style resource from self or an element.
* @param {Variant} resOrName An instance of {@link Kekule.Widget.StyleResource} or resource name.
* @param {HTMLElement} element If not set, style will be removed from widget element.
*/
unlinkStyleResource: function(resOrName, element)
{
var res = (resOrName instanceof Kekule.Widget.StyleResource)? resOrName: Kekule.Widget.StyleResourceManager.getResource(resOrName);
if (res)
res.unlinkFrom(element || this);
return this;
},
/**
* Get CSS property value or a style resource linked to element.
* @param {String} cssPropName CSS property name in JavaScript form.
* @param {HTMLElement} element If not set, widget element will be used.
* @returns {Variant} A instance of {@link Kekule.Widget.StyleResource} or simply a CSS value.
*/
getStyleProperty: function(cssPropName, element)
{
var elem = element || this.getElement();
var styleRes = elem[this.STYLE_RES_FIELD];
if (!styleRes || !styleRes[cssPropName])
return elem.style[cssPropName];
else
return styleRes[cssPropName];
},
/**
* Set CSS property value to widget or another element.
* @param {String} cssPropName CSS property name in JavaScript form.
* @param {Variant} value A simple css value or an instance of {@link Kekule.Widget.StyleResource}
* or a style resource name.
* @param {HTMLElement} element If not set, style will be set to widget element.
* UNFINISHED yet
*/
setStyleProperty: function(cssPropName, value, element)
{
var elem = element || this.getElement();
if (elem)
{
var res;
if (value instanceof Kekule.Widget.StyleResource)
{
res = value;
}
else if ((DataType.getType(value) === DataType.STRING) && (value.startsWith(Kekule.Widget.StyleResourceNames.PREFIX)))
{
res = Kekule.Widget.StyleResourceManager.getResource(value);
}
if (res) // style resource
{
var styleRes = elem[this.STYLE_RES_FIELD];
if (!styleRes)
{
styleRes = {};
elem[this.STYLE_RES_FIELD] = styleRes;
}
var old = styleRes[cssPropName];
if (old) // already has old value
{
this.unlinkStyleResource(old, elem);
}
if (res)
{
this.linkStyleResource(res, elem);
}
styleRes[cssPropName] = res;
}
else // simple value
{
elem.style[cssPropName] = value;
var styleRes = elem[this.STYLE_RES_FIELD];
if (styleRes && styleRes[cssPropName])
{
this.unlinkStyleResource(styleRes[cssPropName], elem);
}
}
}
},
/**
* Clear CSS property to widget or another element.
* @param {String} cssPropName CSS property name in JavaScript form.
* @param {HTMLElement} element If not set, style will be set to widget element.
*/
removeStyleProperty: function(cssPropName, value, element)
{
var elem = element || this.getElement();
Kekule.StyleUtils.removeStyleProperty(elem.style, cssPropName);
},
/**
* Add a child widget.
* User should not call this method directly, instead, child.setParent should be used.
* @param {Kekule.Widget.BaseWidget} child
* @private
*/
_addChild: function(child)
{
if (!child)
return;
var ws = this.getChildWidgets();
if (ws.indexOf(child) < 0)
{
ws.push(child);
/*
// append to element
var parentElem = this.getChildrenHolderElement();
child.appendToElem(parentElem);
*/
this.childWidgetAdded(child);
this.childrenModified();
}
},
/**
* Insert a child widget before refChild.
* @param {Kekule.Widget.BaseWidget} child
* @param {Kekule.Widget.BaseWidget} refChild
* @private
*/
_insertChild: function(child, refChild)
{
if (!child)
return;
var ws = this.getChildWidgets();
var refIndex = refChild? ws.indexOf(refChild): ws.length;
if (ws.indexOf(child) >= 0) // already in, adjust pos
{
if (refIndex >= 0)
this._moveChild(child, refIndex);
}
else // new one
{
if (refIndex < 0)
refIndex = ws.length;
ws.splice(refIndex, 0, child);
/*
var refWidget = ws[refIndex];
var refElem = refWidget? refWidget.getElement(): null;
this.getChildrenHolderElement().insertBefore(child.getElement(), refElem);
*/
this.childWidgetAdded(child);
this.childrenModified();
}
},
/**
* Remove a child widget.
* User should not call this method directly, instead, child.setParent(null) should be used.
* @param {Kekule.Widget.BaseWidget} child
* @private
*/
_removeChild: function(child)
{
if (!child)
return;
var ws = this.getChildWidgets();
var index = ws.indexOf(child);
if (index >= 0)
{
ws.splice(index, 1);
// remove from element if possible
var parentElem = this.getChildrenHolderElement();
var childElem = child.getElement();
if (childElem && Kekule.DomUtils.isDescendantOf(childElem, parentElem))
{
childElem.parentNode.removeChild(childElem);
}
this.childWidgetRemoved(child);
this.childrenModified();
}
//Kekule.ArrayUtils.remove(this.getChildWidgets(), child);
},
/**
* Move existed child to an new position of child widget array.
* @param {Kekule.Widget.BaseWidget} child
* @param {Int} newIndex
* @private
*/
_moveChild: function(child, newIndex)
{
if (!child)
return;
if (Kekule.ArrayUtils.changeItemIndex(this.getChildWidgets(), child, newIndex))
{
/*
// change index of element
var parentElem = this.getChildrenHolderElement();
var refWidget = this.getChildWidgets()[newIndex + 1];
var refElem = refWidget? refWidget.getElement(): null;
parentElem.insertBefore(child.getElement(), refElem);
*/
this.childWidgetMoved(child, newIndex);
this.childrenModified();
}
},
/**
* Called when child widget array has some modification (child added, removed or moved).
* @private
*/
childrenModified: function()
{
// do nothing here
},
/**
* This method will be called after widget is added to childWidgets array.
* @param {Kekule.Widget.BaseWidget} widget
* @private
*/
childWidgetAdded: function(widget)
{
// do nothing here
//console.log('widget added', this.getClassName(), widget.getClassName());
},
/**
* This method will be called after widget is removed from childWidgets array.
* @param {Kekule.Widget.BaseWidget} widget
* @private
*/
childWidgetRemoved: function(widget)
{
// do nothing here
},
/**
* This method will be called after widget position is moved in childWidgets array.
* @param {Kekule.Widget.BaseWidget} widget
* @param {Int} newIndex
* @private
*/
childWidgetMoved: function(widget, newIndex)
{
// do nothing here
},
/**
* Returns index of child widget. If widget is not a child, -1 will be returned.
* @param {Kekule.Widget.BaseWidget} widget
* @returns {Int}
*/
indexOfChild: function(widget)
{
return this.getChildWidgets().indexOf(widget);
},
/**
* Check if widget is a child of current widget.
* @param {Kekule.Widget.BaseWidget} widget
* @returns {Bool}
*/
hasChild: function(widget)
{
var index = this.indexOfChild(widget);
//console.log('Child index: ', index, this.getChildWidgets());
return (index >= 0);
},
/**
* Returns child widget at index
* @param {Int} index
* @return {Kekule.Widget.BaseWidget}
*/
getChildAtIndex: function(index)
{
return this.getChildWidgets()[index];
},
/**
* Returns previous sibling widget under the same parent widget.
*/
getPrevSibling: function()
{
var parent = this.getParent();
if (parent)
{
var index = parent.indexOfChild(this);
return this.getChildAtIndex(--index);
}
else
return null;
},
/**
* Returns next sibling widget under the same parent widget.
*/
getNextSibling: function()
{
var parent = this.getParent();
if (parent)
{
var index = parent.indexOfChild(this);
return this.getChildAtIndex(++index);
}
else
return null;
},
/** @private */
_haltPrevShowHideProcess: function()
{
if (Kekule.Widget.showHideManager)
{
if (this.__$showHideTransInfo) // has prev transition not finished yet
{
this.__$showHideTransInfo.halt();
}
}
},
/** @private */
_setEnableShowHideEvents: function(enabled)
{
this._enableShowHideEvents = enabled;
},
/**
* Make widget visible.
* @param {Kekule.Widget.BaseWidget} caller Who calls the show method and make this widget visible.
* @param {Func} callback This callback function will be called when the widget is totally shown.
* @param {Int} showType, value from {@link Kekule.Widget.ShowHideType}.
* @param {Hash} extraOptions Extra transition options.
* It may contains a field "instantly". If this field is set to true, the showing process will be executed without transition.
*/
show: function(caller, callback, showType, extraOptions)
{
if (!this.getElement())
return;
if (this.__$isShowing) // avoid duplicate execute
{
return;
}
this._haltPrevShowHideProcess();
//console.log('call show', this.getClassName());
/*
var self = this;
var showProc = function()
{
self.doShow(caller, callback, showType);
}
setTimeout(showProc, 0);
*/
this.doShow(caller, callback, showType, extraOptions);
return this;
},
/** @private */
doShow: function(caller, callback, showType, extraOptions)
{
//console.log('do show', this.getClassName());
this.__$isShowing = true;
//this.__$isHiding = false;
var self = this;
var done = function()
{
//self.__$isShowHiding = false;
self.__$showHideTransInfo = null;
//console.log('show done', self.__$isShowHiding);
self.widgetShowStateDone(true);
self.__$isShowing = false;
//self.__$isHiding = false;
if (callback)
callback();
};
this.setShowHideType(showType);
if (Kekule.ObjUtils.notUnset(showType))
{
this.setIsPopup((showType === Kekule.Widget.ShowHideType.DROPDOWN)
|| (showType === Kekule.Widget.ShowHideType.POPUP));
if (showType === Kekule.Widget.ShowHideType.DIALOG)
{
this.setIsDialog(true);
showType = Kekule.Widget.ShowHideType.POPUP;
}
else
this.setIsDialog(false);
}
this.widgetShowStateBeforeChanging(true);
var gm = this.getGlobalManager();
if (showType === Kekule.Widget.ShowHideType.DROPDOWN || showType === Kekule.Widget.ShowHideType.POPUP) // prepare
{
gm.preparePopupWidget(this, caller, showType);
}
//console.log('show', this.getClassName(), this.getElement(), this.getElement().parentNode);
if (Kekule.Widget.showHideManager && !(extraOptions && extraOptions.instantly))
{
/*
if (this.__$showHideTransInfo) // has prev transition not finished yet
{
this.__$showHideTransInfo.halt();
}
*/
//if (!this.__$isShowHiding) // avoid call show in show transition process
if (!this.__$showHideTransInfo)
{
//console.log('show', this.__$isShowHiding);
//this.__$isShowHiding = true;
this.__$showHideTransInfo = Kekule.Widget.showHideManager.show(this, caller, done, showType, extraOptions);
}
}
else
{
// here call setDisplayed and setVisible with second param, avoid call widgetShowStateChanged multiple times
this.setDisplayed(true, true);
this.setVisible(true, true);
done();
}
//this.setShowHideType(showType);
this.setShowHideCaller(caller);
if (caller) // also save the page rect of caller, avoid caller to be hidden afterward
{
//this.setShowHideCallerPageRect(EU.getElemPageRect((caller.getElement && caller.getElement()) || caller));
this.setShowHideCallerPageRect(EU.getElemBoundingClientRect((caller.getElement && caller.getElement()) || caller));
}
this.widgetShowStateChanged(true);
},
/**
* Show widget then hide it after a period of time.
* @param {Int} time In milliseconds.
* @param {Kekule.Widget.BaseWidget} caller Who calls the show method and make this widget visible.
* @param {Func} callback This callback function will be called when the widget is totally shown.
* @param {Int} showType, value from {@link Kekule.Widget.ShowHideType}.
*/
flash: function(time, caller, callback, showType)
{
var self = this;
var done = function()
{
if (callback)
callback();
setTimeout(self.hide.bind(self), time);
};
this.show(caller, done, showType);
return this;
},
/**
* Hide widget.
* @param {Kekule.Widget.BaseWidget} caller Who calls the hide method and make this widget invisible.
* @param {Func} callback This callback function will be called when the widget is totally hidden.
* @param {Int} hideType, value from {@link Kekule.Widget.ShowHideType}.
* @param {Hash} extraOptions Extra transition options.
* It may contains two special fields. One is "instantly". If this field is set to true, the showing process will be executed without transition.
* The other is "useVisible", if true, when hiding the widget, visible property will be setted to false, otherwise the displayed property will be setted to false.
*/
hide: function(caller, callback, hideType, extraOptions)
{
if (!this.getElement())
return;
if (this.__$isHiding) // avoid duplicate execute
return;
this.__$isHiding = true;
//this.__$isShowing = false;
if (!caller)
{
caller = this.getShowHideCaller();
}
if (!hideType)
hideType = this.getShowHideType();
this._haltPrevShowHideProcess();
//console.log('call hide', this.getClassName());
/*
var self = this;
var hideProc = function()
{
self.doHide(caller, callback, hideType, useVisible);
};
setTimeout(hideProc, 0);
*/
this.doHide(caller, callback, hideType, extraOptions);
return this;
},
/** @private */
doHide: function(caller, callback, hideType, extraOptions)
{
var hideOptions = Object.extend({'callerPageRect': this.getShowHideCallerPageRect()}, extraOptions);
var useVisible = hideOptions.useVisible;
var self = this;
var finalizeAfterHiding = this.getFinalizeAfterHiding();
var done = function()
{
//console.log('do Hide', self.getClassName());
//self.__$isShowHiding = false;
self.__$showHideTransInfo = null;
self.widgetShowStateDone(false);
var gm = self.getGlobalManager();
if (hideType === Kekule.Widget.ShowHideType.DROPDOWN || hideType === Kekule.Widget.ShowHideType.POPUP) // unprepare
{
//console.log('unprepare');
//Kekule.Widget.globalManager.unpreparePopupWidget(self);
gm.unpreparePopupWidget(self);
}
self.__$isHiding = false;
//self.__$isShowing = false;
if (callback)
callback();
if (finalizeAfterHiding)
self.finalize();
};
this.widgetShowStateBeforeChanging(false);
if (Kekule.Widget.showHideManager && !hideOptions.instantly)
{
if (this.__$showHideTransInfo)
this.__$showHideTransInfo.halt();
//if (!this.__$isShowHiding) // avoid call hide() in hide transition process
if (!this.__$showHideTransInfo)
{
//console.log('hide', this.__$isShowHiding);
//this.__$isShowHiding = true;
//console.log('Hide by manager');
this.__$showHideTransInfo = Kekule.Widget.showHideManager.hide(this, caller, done, hideType,
false, hideOptions);
}
}
else
{
if (useVisible)
this.setVisible(false, true);
else
this.setDisplayed(false, true);
done();
}
this.widgetShowStateChanged(false);
},
/*
* Popup the widget.
* @param {Kekule.Widget.BaseWidget} caller Who calls the show method and make this widget visible.
* @param {Func} callback This callback function will be called when the widget is totally shown.
*/
/*
popup: function(caller, callback)
{
return this.show(caller, callback, Kekule.Widget.ShowHideType.POPUP);
},
*/
/**
* Dismiss a widget and cancel its modified value.
* Here we simply hide the widget.Descendant may override this method to do more complex job.
* @param {Kekule.Widget.BaseWidget} caller Who calls the hide method and make this widget invisible.
* @param {Func} callback This callback function will be called when the widget is totally hidden.
* @param {Int} hideType, value from {@link Kekule.Widget.ShowHideType}.
* * @param {Hash} extraOptions Extra transition options.
* It may contains two special fields. One is "instantly". If this field is set to true, the showing process will be executed without transition.
* The other is "useVisible", if true, when hiding the widget, visible property will be setted to false, otherwise the displayed property will be setted to false.
*/
dismiss: function(caller, callback, hideType, extraOptions)
{
this._isDismissed = true;
return this.hide(caller, callback, hideType, extraOptions);
},
/**
* Check if widget element is visible to user.
* @param {Bool} ignoreDom If true, this method will only check CSS visibility and display property.
* @returns {Bool}
*/
isShown: function(ignoreDom)
{
//var result = !!this.getElement().parentNode && this.getVisible() && this.getDisplayed();
var result = (this.isInDomTree() || ignoreDom) && this.getElement() && this.getVisible() && this.getDisplayed();
return result;
},
/**
* Called before show or hide.
* @param {Bool} isShown
* @private
*/
widgetShowStateBeforeChanging: function(isShown)
{
if (isShown)
this.autoResizeToClient(); // if set autosize, recalculate size before showing
},
/**
* Called immediately after show or hide, even if transition is still underway.
* @param {Bool} isShown
* @param {Bool} byDomChange Whether the show state change is caused by inserting to or removing widget from DOM.
* @private
*/
widgetShowStateChanged: function(isShown, byDomChange)
{
if (Kekule.ObjUtils.isUnset(isShown))
isShown = this.isShown();
if (Kekule.ObjUtils.notUnset(this._lastShown) && (this._lastShown === isShown)) // show state not changed
return; // do nothing
this._lastShown = isShown;
if (isShown)
this._isDismissed = false;
var gm = this.getGlobalManager();
if (this.getIsPopup())
{
if (isShown)
{
//console.log('register');
//Kekule.Widget.globalManager.registerPopupWidget(this);
gm.registerPopupWidget(this);
}
else
{
//Kekule.Widget.globalManager.unregisterPopupWidget(this);
gm.unregisterPopupWidget(this);
}
}
if (this.getIsDialog())
{
if (isShown)
//Kekule.Widget.globalManager.registerDialogWidget(this);
gm.registerDialogWidget(this);
else
//Kekule.Widget.globalManager.unregisterDialogWidget(this);
gm.unregisterDialogWidget(this);
}
this.doWidgetShowStateChanged(isShown);
if (this._enableShowHideEvents)
{
this.invokeEvent('showStateChange', {
'widget': this,
'isShown': isShown,
'isDismissed': this._isDismissed,
'byDomChange': byDomChange
});
}
},
/**
* Descendant can override this method.
* @param {Bool} isShown
* @private
*/
doWidgetShowStateChanged: function(isShown)
{
// do nothing here
},
/**
* Called after show or hide transition is totally done.
* @param {Bool} isShown
* @private
*/
widgetShowStateDone: function(isShown)
{
// do nothing here
},
/**
* Check if widget is in document DOM tree.
* @returns {Bool}
*/
isInDomTree: function()
{
var elem = this.getElement();
return Kekule.DomUtils.isInDomTree(elem);
},
/**
* Focus on widget.
*/
focus: function()
{
this.setIsFocused(true);
return this;
},
/**
* Move focus out of widget.
*/
blur: function()
{
this.setIsFocused(false);
return this;
},
/**
* Returns bounding client rectangle of widget.
* @param {HTMLElement} elem
* @param {Bool} includeScroll If this value is true, scrollTop/Left of documentElement will be added to result.
* @returns {Hash} {top, left, bottom, right, width, height}
*/
getBoundingClientRect: function(includeScroll)
{
// if widget is not displayed, display it first, otherwise width and height may returns 0
if (!this.getDisplayed())
{
var d = Kekule.StyleUtils.getDisplayed(this.getElement());
var v = Kekule.StyleUtils.getVisibility(this.getElement());
try
{
this.setVisible('hidden');
this.setDisplayed('');
var result = Kekule.HtmlElementUtils.getElemBoundingClientRect(this.getElement(), includeScroll);
//var result = Kekule.HtmlElementUtils.getElemPageRect(this.getElement(), !includeScroll);
}
finally
{
this.setDisplayed(d);
this.setVisible(v);
}
return result;
}
else
return Kekule.HtmlElementUtils.getElemBoundingClientRect(this.getElement(), includeScroll);
//return Kekule.HtmlElementUtils.getElemPageRect(this.getElement(), !includeScroll);
},
/**
* Returns rectangle of widget in HTML page.
* @param {HTMLElement} elem
* @param {Bool} relToViewport If this value is true, scrollTop/Left of documentElement will be substracted from result.
* @returns {Hash} {top, left, bottom, right, width, height}
*/
getPageRect: function(relToViewport)
{
// if widget is not displayed, display it first, otherwise width and height may returns 0
if (!this.getDisplayed())
{
var d = Kekule.StyleUtils.getDisplayed(this.getElement());
var v = Kekule.StyleUtils.getVisibility(this.getElement());
try
{
this.setVisible('hidden');
this.setDisplayed('');
//var result = Kekule.HtmlElementUtils.getElemBoundingClientRect(this.getElement(), includeScroll);
var result = Kekule.HtmlElementUtils.getElemPageRect(this.getElement(), relToViewport);
}
finally
{
this.setDisplayed(d);
this.setVisible(v);
}
return result;
}
else
//return Kekule.HtmlElementUtils.getElemBoundingClientRect(this.getElement(), includeScroll);
return Kekule.HtmlElementUtils.getElemPageRect(this.getElement(), relToViewport);
},
/**
* Returns dimension in px of this widget.
* @returns {Hash} {width, height}.
*/
getDimension: function()
{
//return this.getBoundingClientRect(false);
return this.getPageRect();
},
/**
* Set width and height of current widget. Width and height value can be number (how many pixels)
* or a CSS string value directly.
* @param {Variant} width
* @param {Variant} height
* @param {Bool} suppressResize If this value is true, resized method will not be called.
*/
setDimension: function(width, height, suppressResize)
{
var notUnset = Kekule.ObjUtils.notUnset;
var minDim = this.getMinDimension();
var minWidth = minDim && minDim.width;
var minHeight = minDim && minDim.height;
var handled = false;
if (this.getEnableDimensionTransform()) // may scale
{
var ratioWidth = (notUnset(width) && minWidth) ? width / minWidth : null;
var ratioHeight = (notUnset(height) && minHeight) ? height / minHeight : null;
var actualRatio;
if (!ratioWidth || !ratioHeight)
actualRatio = ratioWidth || ratioHeight;
else
actualRatio = Math.min(ratioWidth, ratioHeight);
if (!actualRatio)
{
handled = false; // do not scale transform
}
else if (actualRatio >= 1)
{
this._setTransformScale(1);
handled = true;
return this.doSetDimension(width, height, suppressResize);
}
else
{
var actualWidth, actualHeight;
if (!ratioHeight || ratioWidth <= ratioHeight)
{
actualWidth = minWidth;
actualHeight = height && (height / actualRatio);
}
else // ratioHeight < ratioWidth
{
actualHeight = minHeight;
actualWidth = width && (width / actualRatio);
}
this._setTransformScale(actualRatio);
handled = true;
return this.doSetDimension(actualWidth, actualHeight, suppressResize);
}
}
if (!handled)
{
var actualWidth = notUnset(width)?
(minWidth? Math.max(width, minWidth): width): null;
var actualHeight = notUnset(height)?
(minHeight? Math.max(height, minHeight): height): null;
this._setTransformScale(1);
return this.doSetDimension(actualWidth, actualHeight, suppressResize);
}
},
/** @private */
doSetDimension: function(width, height, suppressResize)
{
var doResize = false;
var notUnset = Kekule.ObjUtils.notUnset;
if (notUnset(width))
{
this.getStyle().width = (typeof(width) === 'number')? width + 'px': width;
doResize = true;
}
if (notUnset(height))
{
this.getStyle().height = (typeof(height) === 'number')? height + 'px': height;
doResize = true;
}
if (doResize && (!suppressResize))
this.resized();
this.objectChange(['width', 'height']);
return this;
},
/** @private */
_setTransformScale: function(scale)
{
var elem = this.getElement();
if (scale !== 1)
{
elem.style.transformOrigin = '0 0';
elem.style.transform = 'scale(' + scale + ')';
}
else
{
Kekule.StyleUtils.removeStyleProperty(elem.style, 'transform');
}
},
/**
* Update widget transform based on current dimension.
*/
updateDimensionTransform: function()
{
var dim = this.getDimension();
this.setDimension(dim.width, dim.height, true);
},
/**
* Auto resize the widget itself when the window client size changes.
* @private
*/
autoResizeToClient: function()
{
//if (this.isShown())
{
var constraints = this.getAutoResizeConstraints();
if (constraints)
{
var clientDim = Kekule.DocumentUtils.getClientDimension(this.getDocument());
var newWidth = constraints.width? clientDim.width * constraints.width: null;
var newHeight = constraints.height? clientDim.height * constraints.height: null;
this.setDimension(newWidth, newHeight);
}
}
},
/**
* Called when width or height of widget changed.
* @private
*/
resized: function()
{
this.doResize();
this.invokeEvent('resize', {'widget': this});
},
/**
* Called when width or height of widget changed.
* Descendants may override this method to do some actual work (for instance, change size of child widgets).
* @private
*/
doResize: function()
{
},
/**
* Notify the layout property of widget has changed.
* @ignore
*/
layoutChanged: function()
{
var layout = this.getLayout();
if (this._layoutClassName)
this.removeClassName(this._layoutClassName);
this._layoutClassName = this.getLayoutClassName(layout);
if (this._layoutClassName)
this.addClassName(this._layoutClassName);
},
/**
* Returns suitable class name to reflect current layout.
* @param {Int} layout
* @returns {String}
* @private
*/
getLayoutClassName: function(layout)
{
var WL = Kekule.Widget.Layout;
return (layout === WL.VERTICAL)? CNS.LAYOUT_V:
(layout === WL.HORIZONTAL)? CNS.LAYOUT_H:
null;
},
/**
* Called after isHover, isActive, isFocused changed.
* @private
*/
stateChanged: function()
{
//console.log('old state class', this.getElement(), this._stateClassName);
if (this._stateClassName)
this.removeClassName(this._stateClassName);
this._stateClassName = this.getStateClassName(this.getState());
//console.log('new state class', this._stateClassName);
if (this._stateClassName)
this.addClassName(this._stateClassName);
// notify children
var children = this.getChildWidgets();
for (var i = 0, l = children.length; i < l; ++i)
children[i].stateChanged();
},
/**
* Get class name to set the outlook of current state.
* Descendants can override this method.
* @param {Int} state
* @returns {String}
*/
getStateClassName: function(state)
{
var WS = Kekule.Widget.State;
var result =
(state === WS.ACTIVE)? CNS.STATE_ACTIVE:
(state === WS.HOVER)? CNS.STATE_HOVER:
(state === WS.FOCUSED)? CNS.STATE_FOCUSED:
(state === WS.DISABLED)? CNS.STATE_DISABLED:
CNS.STATE_NORMAL;
return result;
},
/**
* Create an HTML element to represent the widget.
* //@param {HTMLElement} parentElement
* @returns {HTMLElement}
*/
createElement: function()
{
var doc = this.getDocument();
var result = this.doCreateRootElement(doc);
//this.doCreateSubElements(doc, result);
this.setElement(result);
/*
// append element to parent
var p = this.getParent();
var elem = p? p.getElement(): null;
if (elem)
elem.appendChild(result);
*/
return result;
},
/**
* Do actual work of create root element of widget.
* Descendants may override this method.
* @param {HTMLDocument} doc
* @returns {HTMLElement}
* @private
*/
doCreateRootElement: function(doc)
{
// do nothing here
},
/**
* Create element inside root element.
* This method is used by bindElement when create a widget based on an existing root element.
* Descendants may override this method.
* @param {HTMLDocument} doc
* @param {HTMLDocumentFragment} docFragment
* @returns {Array} Created sub elements.
* @private
*/
doCreateSubElements: function(doc, docFragment)
{
// do nothing here
return [];
},
/**
* Remove the binding element from DOM tree.
* @param {HTMLElement} elem
*/
destroyElement: function(elem)
{
var elem = elem || this.getElement();
if (elem && elem.parentNode)
{
elem.parentNode.removeChild(elem);
}
},
/**
* Append current widget to parentElem.
* @param {HTMLElement} parentElem
*/
appendToElem: function(parentElem)
{
if (parentElem)
parentElem.appendChild(this.getElement());
//this.insertedToDom();
return this;
},
/**
* Insert current widget to parentElem, before refElem.
* @param {HTMLElement} parentElem
* @param {HTMLElement} refElem
*/
insertToElem: function(parentElem, refElem)
{
parentElem.insertBefore(this.getElement(), refElem);
//this.insertedToDom();
return this;
},
/**
* Remove current widget from DOM temporarily.
*/
removeFromDom: function()
{
var elem = this.getElement();
var parent = elem.parentNode;
if (parent)
parent.removeChild(elem);
},
/**
* Append widget as a child to parentWidget.
* @param {Kekule.Widget.BaseWidget} parentWidget
*/
appendToWidget: function(parentWidget)
{
this.setParent(parentWidget);
this.appendToElem(parentWidget.getChildrenHolderElement());
return this;
},
/**
* Insert this widget as child to parentWidget, before refWidget. If refWidget not set, widget will be appended to parent.
* @param {Kekule.Widget.BaseWidget} parentWidget
* @param {Kekule.Widget.BaseWidget} refWidget
*/
insertToWidget: function(parentWidget, refWidget)
{
this.setParent(parentWidget);
//this.setPropStoreFieldValue('parent', parentWidget);
//parentWidget._insertChild(this, refWidget);
if (refWidget)
this.insertToElem(parentWidget.getChildrenHolderElement(), refWidget.getElement());
else
this.appendToElem(parentWidget.getChildrenHolderElement());
return this;
},
/**
* Called when widget is inserted into DOM tree.
*/
insertedToDom: function()
{
this.widgetDomStateChanged(true);
this.widgetShowStateChanged(this.isShown(), true);
return this.doInsertedToDom();
},
/** @private */
doInsertedToDom: function()
{
// do nothing here
},
/**
* Called when widget is removed from DOM tree.
*/
removedFromDom: function()
{
this.widgetDomStateChanged(false);
this.widgetShowStateChanged(false, true); // removed from dom, alway a hidden action
return this.doRemovedFromDom();
},
/** @private */
doRemovedFromDom: function()
{
// do nothing here
},
/**
* Called when widget is inserted in or removed from HTML page DOM.
* @param {Bool} isInDom
*/
widgetDomStateChanged: function(isInDom)
{
this.doWidgetDomStateChanged(isInDom)
this.invokeEvent('domStateChange', {'widget': this, 'isInDom': isInDom});
},
/** @private */
doWidgetDomStateChanged: function(isInDom)
{
// do nothing here
},
/**
* Called when additional element inserted inside widget.
* @param {HTMLElement} elem
*/
domElemAdded: function(elem)
{
return this.doDomElemAdded(elem);
},
/** @private */
doDomElemAdded: function(elem)
{
// do nothing here
},
/**
* Called when element removed from widget.
* @param {HTMLElement} elem
*/
domElemRemoved: function(elem)
{
return this.doDomElemRemoved(elem);
},
/** @private */
doDomElemRemoved: function(elem)
{
// do nothing here
},
/**
* Returns widget identity class name(s) need to add to HTML element.
* @returns {string}
*/
getWidgetClassName: function()
{
var result = Kekule.Widget.HtmlClassNames.BASE;
if (this.getElement() && !Kekule.HtmlElementUtils.isFormCtrlElement(this.getCoreElement()) && !!this.getUseNormalBackground())
result += ' ' + Kekule.Widget.HtmlClassNames.NORMAL_BACKGROUND;
result += ' ' + this.doGetWidgetClassName();
return result;
},
/**
* Returns class name need to add to HTML element.
* Descendants should override this method and return concrete names.
* @returns {string}
* @private
*/
doGetWidgetClassName: function()
{
return '';
},
/**
* Check if a class is associate with element of this widget.
* @param {String} className
* @return {Bool}
*/
hasClassName: function(className)
{
return EU.hasClass(this.getElement(), className);
},
/**
* Add class name(s) to widget element. If affectCustomProp is true, this method will change customHtmlClassName property.
* @param {Variant} classNames Can be a simple name, or a series of name separated by space ('name1 name2')
* or an array of strings.
* @param {Bool} affectCustomProp Whether change customHtmlClassName property of widget.
*/
addClassName: function(classNames, affectCustomProp)
{
if (this.getElement())
{
if (affectCustomProp)
{
var cname = this.getCustomHtmlClassName();
cname = Kekule.StrUtils.addTokens(cname, classNames);
this.setCustomHtmlClassName(cname);
}
else
EU.addClass(this.getElement(), classNames);
}
else // pending
{
this._pendingHtmlClassNames = Kekule.StrUtils.addTokens(this._pendingHtmlClassNames, classNames);
}
return this;
},
/**
* remove class(es) from widget element. This method not also change customHtmlClassName property.
* @param {Variant} classNames Can be a simple name, or a series of name separated by space ('name1 name2')
* or an array of strings.
* @param {Bool} affectCustomProp Whether change customHtmlClassName property of widget.
*/
removeClassName: function(classNames, affectCustomProp)
{
if (this.getElement())
{
if (affectCustomProp)
{
var cname = this.getCustomHtmlClassName();
cname = Kekule.StrUtils.removeTokens(cname, classNames);
this.setCustomHtmlClassName(cname);
}
else
EU.removeClass(this.getElement(), classNames);
}
else
{
this._pendingHtmlClassNames = Kekule.StrUtils.removeTokens(this._pendingHtmlClassNames, classNames);
}
return this;
},
/**
* Toggle class(es) from element. This method not also change customHtmlClassName property.
* @param {Variant} className Can be a simple name, or a series of name separated by space ('name1 name2')
* or an array of strings.
* @param {Bool} affectCustomProp Whether change customHtmlClassName property of widget.
*/
toggleClassName: function(classNames, affectCustomProp)
{
if (this.getElement())
{
if (affectCustomProp)
{
var cname = this.getCustomHtmlClassName();
cname = Kekule.StrUtils.toggleTokens(cname, classNames);
this.setCustomHtmlClassName(cname);
}
else
EU.toggleClass(this.getElement(), classNames);
}
return this;
},
/**
* Check if widget can bind to an element.
* Descendants can override this method to do some further check on element.
* @param {HTMLElement} element
* @return {Bool}
*/
isElementBindable: function(element)
{
var allowedTags = this.getBindableElemTagNames();
if (allowedTags)
{
var currTag = element.tagName;
return (allowedTags.indexOf(currTag.toLowerCase()) >= 0);
}
else
return true;
},
/**
* Returns the tag names of element can be binded with widget. Tag names should be all lowercased.
* The return value of null means widget can bind to any element.
* On the contrary, if [](empty array) is returned, the widget will be regarded as unbindable to any element.
* Defaultly, this method will return widget.BINDABLE_TAG_NAMES.
* Descendants can overwrite that variable to meet their own needs.
* @returns {Array}
*/
getBindableElemTagNames: function()
{
if (this.getPrototype().hasOwnProperty('BINDABLE_TAG_NAMES'))
return this.BINDABLE_TAG_NAMES;
else
return null; // can bind any elements
},
/**
* Bind current widget to a HTML element, install event handlers and set styles.
* @param {HTMLElement} element
* @private
*/
bindElement: function(element)
{
if (element)
{
if (!this.isElementBindable(element))
{
Kekule.error(/*Kekule.ErrorMsg.WIDGET_CAN_NOT_BIND_TO_ELEM*/
Kekule.$L('ErrorMsg.WIDGET_CAN_NOT_BIND_TO_ELEM').format(this.getClassName(), element.tagName));
return;
}
// if element already has class name, regard it as custom HTML class name
var originClassName = element.className;
if (originClassName)
this.setCustomHtmlClassName(this.getCustomHtmlClassName() || '' + ' ' + originClassName);
var DU = Kekule.DomUtils;
var HU = Kekule.HtmlElementUtils;
// clear possiblely previously created dynamic elements
var clearDynElements = function(rootElem)
{
var children = DU.getDirectChildElems(rootElem);
for (var i = children.length - 1; i >= 0; --i)
{
var child = children[i];
if (HU.hasClass(child, CNS.DYN_CREATED))
rootElem.removeChild(child);
}
};
clearDynElements(element);
// create essential sub elements
var doc = this.getDocument();
var docFrag = doc.createDocumentFragment();
//var subElems = this.doCreateSubElements(this.getDocument(), element);
var subElems = this.doCreateSubElements(this.getDocument(), docFrag);
if (subElems && subElems.length)
{
for (var i = 0, l = subElems.length; i < l; ++i)
{
var elem = subElems[i];
HU.addClass(elem, CNS.DYN_CREATED);
}
}
if ((subElems && subElems.length)
|| (docFrag.children && docFrag.children.length)
|| (docFrag.childNodes && docFrag.childNodes.length))
element.appendChild(docFrag);
this.doBindElement(element);
if (Kekule.DomUtils.hasAttribute(element, 'disabled'))
this.setEnabled(false);
// check dataset properties of element, and use them to set self's properties
// width/height attribute should also be regarded as property settings
var w = element.getAttribute('width');
var h = element.getAttribute('height');
if (Kekule.ObjUtils.notUnset(w) || Kekule.ObjUtils.notUnset(h))
{
w = parseFloat((w || '').toString()) || 0;
h = parseFloat((h || '').toString()) || 0;
this.setDimension(w, h);
}
var dataset = Kekule.DomUtils.getDataset(element);
if (dataset)
{
for (var attribName in dataset)
{
var value = dataset[attribName];
try
{
Kekule.Widget.Utils.setWidgetPropFromElemAttrib(this, attribName, value);
}
catch(e)
{
//console.warn(e);
Kekule.warn(e);
//throw e;
}
}
}
var cname = this.getWidgetClassName();
EU.addClass(element, cname);
cname = this.getCustomHtmlClassName();
if (cname)
EU.addClass(element, cname);
if (!this.getTextSelectable())
EU.addClass(element, CNS.NONSELECTABLE);
if (this._pendingHtmlClassNames)
{
EU.addClass(element, this._pendingHtmlClassNames);
this._pendingHtmlClassNames = '';
}
// ensure touch action value applied to element
var touchAction = this.getTouchAction();
if (Kekule.ObjUtils.notUnset(touchAction))
this.setTouchAction(touchAction);
if (!this.getIsDumb())
this.installUiEventHandlers(element);
// add a field to element to quick access widget from element itself
element[widgetBindingField] = this;
this.invokeEvent('bind', {'widget': this, 'element': element});
}
},
/**
* Do actual work of bindElement for descendents' overriding.
* @param {HTMLElement} element
*/
doBindElement: function(element)
{
// do nothing here
},
/**
* Unbind current widget from a HTML element, uninstall event handlers.
* @param {HTMLElement} element
* @private
*/
unbindElement: function(element)
{
if (element)
{
if (!this.getIsDumb())
this.uninstallUiEventHandlers(element);
var cname = this.getWidgetClassName();
EU.removeClass(element, cname);
this.doUnbindElement(element);
// remove the link field in element
element[widgetBindingField] = undefined;
try
{
delete element[widgetBindingField];
}
catch(e)
{
}
this.invokeEvent('unbind', {'widget': this, 'element': element});
}
},
/**
* Do actual work of unbindElement for descendents' overriding.
* @param {HTMLElement} element
*/
doUnbindElement: function(element)
{
// do nothing here
},
/** @private */
elementChanged: function(newElem, oldElem)
{
if (oldElem) // uninstall event handlers
this.unbindElement(oldElem);
if (newElem)
this.bindElement(newElem);
},
/**
* Called when property observeElementAttribChanges changes.
* This method is used to install/uninstall mutation observes.
* @private
*/
observeElementAttribChangesChanged: function(value)
{
var elem = this.getElement();
if (value)
this._installAttribMutationObserver(elem);
else
this._uninstallAttribMutationObserver(elem);
},
/** @private */
_installAttribMutationObserver: function(elem)
{
if (Kekule.X.MutationObserver)
{
if (!this._attribMutationObserver)
{
var ob = new Kekule.X.MutationObserver(this._reactElemAttribMutationBind);
this._attribMutationObserver = ob;
}
this._attribMutationObserver.observe(elem, {attributes: true});
}
},
/** @private */
_uninstallAttribMutationObserver: function(elem)
{
if (this._attribMutationObserver)
this._attribMutationObserver.disconnect();
},
/** @private */
_reactElemAttribMutation: function(mutations)
{
var elem = this.getElement();
for (var i = 0, l = mutations.length; i < l; ++i)
{
var m = mutations[i];
if (m.type !== 'attributes')
continue;
if (m.target !== elem)
continue;
var attribName = m.attributeName;
if (attribName && Kekule.DomUtils.isDataAttribName(attribName))
{
var coreName = Kekule.DomUtils.getDataAttribCoreName(attribName);
if (coreName)
{
var attribValue = elem.getAttribute(attribName);
//console.log('set prop', attribName, attribValue);
Kekule.Widget.Utils.setWidgetPropFromElemAttrib(this, coreName, attribValue);
}
}
}
},
/**
* Create a decoration content element and insert it to parentElem before refElem.
* @param {HTMLElement} parentElem
* @param {HTMLElement} refElem
* @returns {HTMLElement} Element created.
*/
createDecorationContent: function(parentElem, refElem)
{
var doc = parentElem.ownerDocument;
var result = doc.createElement('span');
EU.addClass(result, [CNS.PART_CONTENT, CNS.PART_DECORATION_CONTENT, CNS.DYN_CREATED]);
if (refElem)
parentElem.insertBefore(result, refElem);
else
parentElem.appendChild(result);
return result;
},
/**
* Create an text content element and insert it to parentElem before refElem.
* @param {String} text
* @param {HTMLElement} parentElem
* @param {HTMLElement} refElem
* @returns {HTMLElement} Element created.
*/
createTextContent: function(text, parentElem, refElem)
{
var doc = parentElem.ownerDocument;
var result = doc.createElement('span');
EU.addClass(result, [CNS.PART_CONTENT, CNS.PART_TEXT_CONTENT, CNS.DYN_CREATED]);
result.innerHTML = text;
if (refElem)
parentElem.insertBefore(result, refElem);
else
parentElem.appendChild(result);
return result;
},
/**
* Create a glyph content container and insert it to parentElem before refElem.
* @param {HTMLElement} parentElem
* @param {HTMLElement} refElem
* @param {String} htmlClassName Class name added to content element.
* @returns {HTMLElement} Element created.
*/
createGlyphContent: function(parentElem, refElem, htmlClassName)
{
var doc = parentElem.ownerDocument;
var result = doc.createElement('span');
EU.addClass(result, [CNS.PART_CONTENT, CNS.PART_GLYPH_CONTENT, CNS.DYN_CREATED]);
if (htmlClassName)
EU.addClass(result, htmlClassName);
if (refElem)
parentElem.insertBefore(result, refElem);
else
parentElem.appendChild(result);
return result;
},
/*
reportFatalError: function(errMsg)
{
console.
},
*/
/** @private */
_touchActionNoneTouchStartHandler: function(e)
{
e.preventDefault();
},
/**
* Install UI event (mousemove, click...) handlers to element.
* @param {HTMLElement} element
* @private
*/
installUiEventHandlers: function(element)
{
var events = Kekule.Widget.UiLocalEvents; //Kekule.Widget.UiEvents;
for (var i = 0, l = events.length; i < l; ++i)
{
//console.log('install events', events[i], this.reactUiEventBind, element);
Kekule.X.Event.addListener(element, events[i], this.reactUiEventBind);
}
},
/**
* Uninstall UI event (mousemove, click...) handlers from old mainContextElement.
* @param {HTMLElement} element
* @private
*/
uninstallUiEventHandlers: function(element)
{
var events = Kekule.Widget.UiLocalEvents; // Kekule.Widget.UiEvents;
for (var i = 0, l = events.length; i < l; ++i)
{
Kekule.X.Event.removeListener(element, events[i], this.reactUiEventBind);
}
},
/** @private */
reactUiEvent: function(e)
{
//if ((!this.getEnabled()) || this.getStatic())
if (this.getStatic()) // static, do nothing
{
}
else if (!this.getEnabled()) // disabled, eat all event on self
{
e.stopPropagation();
}
else // normal handling
{
//console.log('here', this.getEnabled(), this.getElement().tagName);
//var target = e.getTarget();
//if (target === this.getElement())
//var CNS = Kekule.Widget.ClassNames;
/*
if (!!e.getCustomPropValue('__kekule_widget__')) // event rises by child widget
{
return;
}
else
*/
{
var handled = false;
var evType = e.getType();
/*
if (!e.widget)
{
e.widget = this;
e._type = evType;
}
*/
/*
/// TODO: In IE7/8, property of event params can not be set
//e.__kekule_widget__ = this; // mark that this widget rises this event
e.setCustomPropValue('__kekule_widget__', this);
*/
var targetElem = e.getTarget();
var targetWidget = Kekule.Widget.Utils.getBelongedResponsiveWidget(targetElem);
var eventOnSelf = targetWidget === this;
var KC = Kekule.X.Event.KeyCode;
var keyCode;
if (evType === 'mousemove' || evType === 'pointermove') // test mouse cursor
{
this.reactPointerMoving(e);
var coord = this.getEventMouseRelCoord(e);
var cursor = this.testMouseCursor(coord, e);
if (Kekule.ObjUtils.notUnset(cursor) && this.getElement())
{
//this.getElement().style.cursor = cursor;
this.setCursor(cursor);
handled = true;
}
}
else if (evType === 'touchmove')
{
this.reactPointerMoving(e);
if (this.getIsActive() && !this.isCaptureMouse())
{
var touchPosition = e.touches[0];
if (touchPosition)
{
var doc = this.getDocument();
var currElement = doc.elementFromPoint(touchPosition.clientX, touchPosition.clientY);
if (!Kekule.DomUtils.isOrIsDescendantOf(currElement, this.getElement())) // move out of this widget, deactivate
{
//this.setIsFocused(false);
this.setIsHover(false);
this.setIsActive(false); // do not use reactDeactivating, otherwise an execute event may be invoked
}
}
else
{
//this.setIsFocused(false);
this.setIsHover(false);
this.setIsActive(false);
}
}
handled = true;
}
else if (evType === 'focus')
{
if (eventOnSelf && e.getTarget() === this.getCoreElement()) // important, only react to focus on the very element
{
this.setIsFocused(true);
handled = true;
}
}
else if (evType === 'blur')
{
if (eventOnSelf && e.getTarget() == this.getCoreElement()) // important, only react to blur off the very element
{
// check if focus changed to another child element
var currFocusElem = this.getDocument().activeElement;
var elem = this.getElement();
if (currFocusElem && (Kekule.DomUtils.isDescendantOf(currFocusElem, elem) || currFocusElem === elem))
;//console.log('focus to child'); // do nothing
else
{
//console.log('blur', this.getElement());
this.setIsFocused(false);
}
handled = true;
}
}
//else if (evType === 'mouseover')
else if (evType === 'pointerover')
{
//console.log('OVER', this.getElement(), e);
if (e.pointerType !== 'touch' && !e.ghostMouseEvent)
this.setIsHover(true);
handled = true;
}
else if (evType === 'mouseout' || evType === 'touchleave')
{
if (!e.ghostMouseEvent)
{
var relatedTarget = e.getRelatedTarget();
if (relatedTarget && Kekule.DomUtils.isOrIsDescendantOf(relatedTarget, this.getCoreElement())) // still move inside widget
{
// do nothing
}
else
{
//console.log('OUT');
this.setIsHover(false);
if (!this.isCaptureMouse())
{
this.reactDeactiviting(e);
handled = true;
}
}
}
}
//else if ((evType === 'mousedown' && e.getButton() === Kekule.X.Event.MouseButton.LEFT) || (evType === 'touchstart'))
else if (evType === 'pointerdown' && e.getButton() === Kekule.X.Event.MouseButton.LEFT)
{
if (eventOnSelf && !e.ghostMouseEvent)
{
//console.log('activating', this.getElement(), e);
this.reactActiviting(e);
}
handled = true;
}
/*
else if (evType === 'mouseleave')
{
//console.log('MOUSE LEAVE');
}
else if (evType === 'mouseenter')
{
//console.log('MOUSE ENTER');
}
*/
//else if ((evType === 'mouseup' && e.getButton() === Kekule.X.Event.MouseButton.LEFT)
// || (evType === 'touchend') || (evType === 'touchcancel'))
else if ((evType === 'pointerup' && e.getButton() === Kekule.X.Event.MouseButton.LEFT) || (evType === 'touchcancel'))
{
if (eventOnSelf && !e.ghostMouseEvent)
{
this.reactDeactiviting(e);
/*
if (evType === 'touchend' || evType === 'touchCancel')
this.setIsHover(false);
*/
}
handled = true;
}
else if (evType === 'keydown')
{
keyCode = e.getKeyCode();
if ((keyCode === KC.ENTER) || (keyCode === KC.SPACE))
{
if (eventOnSelf && !this.getIsActive())
{
this.reactActiviting(e);
}
handled = true;
}
}
else if (evType === 'keyup')
{
keyCode = e.getKeyCode();
if ((keyCode === KC.ENTER) || (keyCode === KC.SPACE))
{
if (eventOnSelf && this.getIsActive())
this.reactDeactiviting(e);
handled = true;
}
}
this.doBeforeDispatchUiEvent(e);
// check first if the component has event handler itself
var funcName = Kekule.Widget.getEventHandleFuncName(e.getType());
if (this[funcName]) // has own handler
{
//handled = handled || this[funcName](e);
handled = this[funcName](e); // avoid shortcircuit
}
else // check for controller
{
// dispatch event to interaction controllers
//handled = handled || this.dispatchEventToIaControllers(e);
handled = this.dispatchEventToIaControllers(e) || handled; // avoid shortcircuit
}
}
this.doReactUiEvent(e);
// map HTML event to object event system
this.invokeEvent(evType, {'htmlEvent': e});
/*
if (evType === 'mouseleave')
console.log('invokeLeave', this, e, e.widget);
*/
/*
if (evType === 'mouseout')
console.log('mouseout', this);
*/
//if (handled)
//if (['mouseleave', 'mouseenter'].indexOf(evType) < 0)
// e.stopPropagation(); // IMPORTANT! eat the event, prevent it from bubble to parent widget and elements to disturb the event handle process
}
//if (Kekule.Widget.UiLocalEvents.indexOf(evType) < 0) // not a local (not bubblable) event
{
// HINT: local event such as blur or focus must be bubble and handle carefully,
// otherwise cause problems (even recursion) in browser
if (this.getActualBubbleUiEvents())
{
var parent = this.getParent();
if (parent && parent.reactUiEvent)
parent.reactUiEvent(e);
}
}
},
/**
* For descendants override.
* Called after event being dispatched to IA controllers.
* @private
*/
doReactUiEvent: function(e)
{
// do nothing here
},
/**
* For descendants override.
* Called before event being dispatched to IA controllers.
* @private
*/
doBeforeDispatchUiEvent: function(e)
{
// do nothing here
},
/**
* Check if gesture event observing is usable.
* @private
*/
_supportGestureEvent: function()
{
return (typeof(Kekule.$jsRoot.Hammer) !== 'undefined') && (document && document.addEventListener); // hammer need addEventListener to install event handlers
},
/**
* Start observing gesture events.
* @param {Array} eventNames Events need to be observed.
*/
startObservingGestureEvents: function(eventNames)
{
if (this._supportGestureEvent())
{
if (!eventNames)
eventNames = Kekule.Widget.TouchGestures;
//console.log('observe gesture events', eventNames);
var newEvents = AU.exclude(eventNames, this.getObservingGestureEvents() || []);
if (newEvents.length)
this.installHammerTouchHandlers(newEvents);
}
},
/**
* Stop observing gesture events.
* @param {Array} eventNames Events need to be stopped.
*/
stopObservingGestureEvents: function(eventNames)
{
if (this._supportGestureEvent() && this.getObservingGestureEvents())
{
if (!eventNames)
eventNames = Kekule.Widget.TouchGestures;
var events = AU.intersect(eventNames, this.getObservingGestureEvents());
if (events.length)
this.uninstallHammerTouchHandlers(events);
}
},
/**
* Install touch gesture event (touch, swipe, pinch...) handlers to element.
* Currently hammer.js is used.
* @param {Array} observingEvents An array of event names that need to be observed.
* @private
*/
installHammerTouchHandlers: function(observingEvents)
{
if (this._supportGestureEvent())
{
if (!observingEvents)
observingEvents = Kekule.Widget.TouchGestures;
var elem = this.getCoreElement();
var hammertime = new Hammer(elem); // Hammer(target).on(Kekule.Widget.TouchGestures.join(' '), this.reactTouchGestureBind);
if (observingEvents.indexOf('pinch') >= 0)
hammertime.get('pinch').set({ enable: true });
if (observingEvents.indexOf('rotate') >= 0)
hammertime.get('rotate').set({ enable: true });
hammertime.on(observingEvents.join(' '), this.reactTouchGestureBind);
this._hammertime = hammertime;
this.setPropStoreFieldValue('observingGestureEvents', observingEvents);
//console.log('observe', observingEvents);
return hammertime;
}
},
/**
* Uninstall gesture event (touch, swipe, pinch...) handlers to element.
* Currently hammer.js is used.
* @param {Array} observingEvents An array of event names that need to be uninstalled.
* @private
*/
uninstallHammerTouchHandlers: function(observingEvents)
{
if (this._hammertime)
{
if (!observingEvents)
observingEvents = this.getObservingGestureEvents(); //Kekule.Widget.TouchGestures;
this._hammertime.off(observingEvents.join(' '), this.reactTouchGestureBind);
}
},
/** @private */
reactTouchGesture: function(e)
{
var funcName = Kekule.Widget.getTouchGestureHandleFuncName((e.getType && e.getType()) || e.type);
if (this[funcName]) // has own handler
{
this[funcName](e);
}
else // check for controller
{
// dispatch event to interaction controllers
this.dispatchEventToIaControllers(e, 'hammer');
}
},
/** @private */
observingGestureEventsChanged: function(eventNames)
{
if (enabled)
this.installHammerTouchHandlers();
},
/**
* React to active event (mouse down, enter key down and so on).
* @param {Event} e
* @ignore
*/
reactActiviting: function(e)
{
if (!this.getIsActive())
{
this.setIsActive(true);
this.doReactActiviting(e);
this.invokeEvent('activate', {'widget': this});
}
//console.log('active on', this.getIsActive(), e.getType());
},
/**
* Do concrete job of reactActiviting method. Descendants may override this method.
* @param {Event} e
* @ignore
*/
doReactActiviting: function(e)
{
// do nothing here
//console.log('active on', this.getIsActive(), e.getType());
},
/**
* React to deactive event (mouse up, enter key up and so on).
* @param {Event} e
* @ignore
*/
reactDeactiviting: function(e)
{
//console.log('deactive on', this.getIsActive(), e.getType());
if (this.getIsActive())
{
this.doReactDeactiviting(e);
this.invokeEvent('deactivate', {'widget': this});
this.setIsActive(false);
}
},
/**
* Do concrete job of reactDeactiviting method. Descendants may override this method.
* @param {Event} e
* @ignore
*/
doReactDeactiviting: function(e)
{
// do nothing here
},
/**
* React to mousemove or touchmove event.
* @param {Event} e
* @ignore
*/
reactPointerMoving: function(e)
{
this.doReactPointerMoving(e);
},
/**
* Do concrete job of reactPointerMoving method. Descendants may override this method.
* @param {Event} e
* @ignore
*/
doReactPointerMoving: function(e)
{
// do nothing here
},
/** @private */
dispatchEventToIaControllers: function(e, eventCategory)
{
var handled = false;
var controller = this.getActiveIaController();
if (controller)
{
if (eventCategory === 'hammer')
handled = controller.handleGestureEvent(e);
else
handled = controller.handleUiEvent(e);
}
if (!handled)
{
controller = this.getDefIaController();
if (controller)
{
if (eventCategory === 'hammer')
handled = controller.handerGestureEvent(e);
else
handled = controller.handleUiEvent(e);
}
}
return handled;
},
/** @private */
getEventMouseRelCoord: function(e, relElement)
{
if (!relElement)
relElement = this.getCoreElement(); // defaultly base on client element, not widget element
var coord = {'x': e.getClientX(), 'y': e.getClientY()};
//var offset = {'x': relElement.getBoundingClientRect().left - relElement.scrollLeft, 'y': relElement.getBoundingClientRect().top - relElement.scrollTop};
var rect = Kekule.HtmlElementUtils.getElemPageRect(relElement, true);
var offset = {
'x': rect.left - relElement.scrollLeft,
'y': rect.top - relElement.scrollTop};
var result = Kekule.CoordUtils.substract(coord, offset);
//console.log(result, elem.tagName);
return result;
//return {x: e.getRelXToCurrTarget(), y: e.getRelYToCurrTarget()};
},
/**
* Get mouse cursor at a certain coord.
* Component can implement this function, or dispatch it to controllers.
* @param {Hash} coord 2D mouse coord
* @param {Object} e event arg passed from mouse move event
* @return {Variant} CSS cursor property value. Return '' to use default one.
* The return value can also be a array of cursor key words, the first legal one in current browser will be used.
*/
testMouseCursor: function(coord, e)
{
var result = this.doTestMouseCursor(coord, e);
if (!result)
{
var controller = this.getActiveIaController();
if (controller)
{
result = controller.testMouseCursor(coord, e);
}
if (!result)
{
controller = this.getDefIaController();
if (controller)
result = controller.testMouseCursor(coord, e);
}
}
return result;
},
/** @private */
doTestMouseCursor: function(coord, e)
{
return null;
},
/**
* Set or unset mouse capture feature of current widget.
* @param {Bool} capture
*/
setMouseCapture: function(capture)
{
var gm = this.getGlobalManager();
if (capture)
gm.setMouseCaptureWidget(this);
else if (this.isCaptureMouse())
gm.setMouseCaptureWidget(null);
},
/**
* Returns whether this widget is currently capturing mouse/touch event.
* @returns {Bool}
*/
isCaptureMouse: function()
{
var gm = this.getGlobalManager();
return gm.getMouseCaptureWidget() === this;
},
/**
* Link a controller with this component.
* @param {String} id Unique id of controller
* @param {Kekule.Widget.InteractionController} controller
* @param {Bool} asDefault Whether set this controller as the default one to handle events.
*/
addIaController: function(id, controller, asDefault)
{
this.getIaControllerMap().set(id, controller);
if (asDefault)
this.setDefIaControllerId(id);
if (controller)
controller.setWidget(this);
},
/**
* Unlink a controller with this component.
* @param {String} id Unique id of controller
*/
removeIaController: function(id)
{
var controller = this.getIaControllerMap().get(id);
if (controller)
{
this.getIaControllerMap().remove(id);
if (id === this.getDefIaControllerId())
this.setDefIaControllerId(null);
if (id === this.getActiveIaControllerId())
this.setActiveIaControllerId(null);
controller.setWidget(null);
}
},
/**
* Returns controller by id.
* @param {String} id
* @returns {Kekule.Widget.InteractionController}
*/
getIaController: function(id)
{
return this.getIaControllerMap().get(id);
},
/**
* This method should be called when the primary action is taken on widge
* (such as click on button, select on menu and so on).
* @param {Object} invokerHtmlEvent HTML event object that invokes executing process.
*/
execute: function(invokerHtmlEvent)
{
this.doExecute(invokerHtmlEvent);
this.invokeEvent('execute', {'widget': this, 'htmlEvent': invokerHtmlEvent});
},
/** @private */
doExecute: function(invokerHtmlEvent)
{
// do nothing here
},
/**
* Check if periodical executing is on process.
* @returns {Bool}
*/
isPeriodicalExecuting: function()
{
return this._periodicalExecBind;
},
/**
* Begin periodical execution.
* @param {Object} htmlEvent HTML event that starts periodical execution.
*/
startPeriodicalExec: function(htmlEvent)
{
var delay = this.getPeriodicalExecDelay() || 0;
this._periodicalExecuting = true;
this._periodicalExecHtmlEvent = htmlEvent;
setTimeout(this._periodicalExecBind, delay);
},
/**
* Stop periodical execution.
*/
stopPeriodicalExec: function()
{
this._periodicalExecuting = false;
this._periodicalExecHtmlEvent = null;
},
/** @private */
_periodicalExec: function(interval)
{
if (this.isPeriodicalExecuting())
{
if (!this._waitPeriodicalProcess)
{
this._waitPeriodicalProcess = true; // flag
this.execute(this._periodicalExecHtmlEvent);
this._waitPeriodicalProcess = false;
}
/*
else
console.log('wait exec...');
*/
setTimeout(this._periodicalExecBind, this.getPeriodicalExecInterval() || 20);
}
}
});
/**
* Tool object that manage the interaction (mouse, key event) of a UI widget.
* A component may have multiple controllers, for example, in Ctab based editor, select tool is used to select nodes and connectors,
* bond tools is used to draw new bond, charge tool is used to assign charge... Different tools have different function and must
* response differently when a event is occured. So different tool object is used to cooperate with editor.
* Tool object has a set of methods to handle events (reactMouseMove, reactMouseClick, etc). If the event is handled, the method must
* return true, otherwise the event will be handled by default tool.
* @class
* @augments ObjectEx
*
* @param {Kekule.Widget.BaseWidget} widget Widget of current object being installed to.
*
* @property {Kekule.Widget.BaseWidget} widget Widget of current object being installed to.
*/
Kekule.Widget.InteractionController = Class.create(ObjectEx,
/** @lends Kekule.Widget.InteractionController# */
{
/** @private */
CLASS_NAME: 'Kekule.Widget.InteractionController',
/** @constructs */
initialize: function($super, widget)
{
$super();
if (widget)
this.setWidget(widget);
},
doFinalize: function($super)
{
this.setWidget(null);
$super();
},
/** @private */
initProperties: function()
{
this.defineProp('widget', {'dataType': 'Kekule.Widget.BaseWidget', 'serializable': false});
},
/**
* This util method will be called when this ia controller is set to be the active one in widget.
* Descendants may override this method to do some initialization jobs.
* @param {Kekule.Widget.BaseWidget} widget
* @private
*/
activated: function(widget)
{
// do nothing here
},
/**
* Handle and dispatch event.
* @param {Object} e Event object.
*/
handleUiEvent: function(e)
{
var eventName = e.getType();
var funcName = Kekule.Widget.getEventHandleFuncName(eventName);
if (this[funcName])
{
return this[funcName](e);
}
else
{
return false;
}
},
/**
* Handle and dispatch gesture (hammer) event.
* @param {Object} e Hammer event object.
*/
handleGestureEvent: function(e)
{
var eventName = e.type;
var funcName = Kekule.Widget.getEventHandleFuncName(eventName);
if (this[funcName])
{
return this[funcName](e);
}
else
{
return false;
}
},
/** @private */
_defEventHandler: function(e)
{
return false; // do not handle by default
},
/**
* Get mouse cursor at a certain coord.
* @param {Hash} coord 2D mouse coord
* @param {Object} e Event arg passed from mouse move event.
* @return {String} CSS cursor property value.
*/
testMouseCursor: function(coord, e)
{
return this.doTestMouseCursor(coord, e);
},
/** @private */
doTestMouseCursor: function(coord, e)
{
return null;
},
/** @private */
_getEventMouseCoord: function(e, clientElem)
{
var elem = clientElem || this.getWidget().getElement();
var targetElem = e.getTarget();
//var coord = {'x': e.getOffsetX(), 'y': e.getOffsetY()};
var coord = e.getOffsetCoord(true); // consider CSS transform
if (targetElem === elem)
return coord;
else
{
var elemPos = Kekule.HtmlElementUtils.getElemPagePos(elem);
var targetPos = Kekule.HtmlElementUtils.getElemPagePos(targetElem);
var offset = {'x': targetPos.x - elemPos.x, 'y': targetPos.y - elemPos.y};
coord = Kekule.CoordUtils.substract(coord, offset);
//console.log('mouse coord', e.getOffsetX(), e.getOffsetY(), e.layerX, e.layerY, offset, coord);
return coord;
}
/*
//return {x: e.getRelXToCurrTarget(), y: e.getRelYToCurrTarget()};
var coord = {'x': e.getClientX(), 'y': e.getClientY()};
//var offset = {'x': elem.getBoundingClientRect().left - elem.scrollLeft, 'y': elem.getBoundingClientRect().top - elem.scrollTop};
var rect = Kekule.HtmlElementUtils.getElemPageRect(elem, true);
var offset = {
'x': rect.left - elem.scrollLeft,
'y': rect.top - elem.scrollTop
};
var result = Kekule.CoordUtils.substract(coord, offset);
return result;
*/
}
});
/**
* An dumb widget that will not react to event.
* @class
* @augments Kekule.Widget.BaseWidget
*/
Kekule.Widget.DumbWidget = Class.create(Kekule.Widget.BaseWidget,
/** @lends Kekule.Widget.DumbWidget# */
{
/** @private */
CLASS_NAME: 'Kekule.Widget.DumbWidget',
/** @constructs */
initialize: function($super, parentOrElementOrDocument)
{
$super(parentOrElementOrDocument, true);
},
/** @ignore */
doGetWidgetClassName: function($super)
{
return $super() + ' ' + CNS.DUMB_WIDGET;
},
/** @ignore */
doCreateRootElement: function(doc)
{
var result = doc.createElement('span');
return result;
}
});
/**
* Placeholder is a special lightweight widget, helping to create real heavy weight widget on demand on an HTML element.
* This type of widget should not be created alone, but only can create on an existing element.
* @class
* @augments Kekule.Widget.BaseWidget
*/
Kekule.Widget.PlaceHolder = Class.create(Kekule.Widget.BaseWidget,
/** @lends Kekule.Widget.PlaceHolder# */
{
/** @private */
CLASS_NAME: 'Kekule.Widget.PlaceHolder',
/** @constructs */
initialize: function($super, parentOrElementOrDocument, targetWidgetClass)
{
this.setPropStoreFieldValue('targetWidgetClass', targetWidgetClass);
$super(parentOrElementOrDocument, true);
},
/** @private */
initProperties: function()
{
this.defineProp('targetWidgetClass', {'dataType': DataType.CLASS, 'serializable': false,
'getter': function()
{
var result = this.getPropStoreFieldValue('targetWidgetClass');
if (!result)
{
var name = this.getPropStoreFieldValue('targetWidgetClassName');
if (name)
result = ClassEx.findClass(name);
}
return result;
}
});
this.defineProp('targetWidgetClassName', {
'dataType': DataType.STRING,
'getter': function()
{
var c = this.getTargetWidgetClass();
var result = c? ClassEx.getClassName(C): this.getPropStoreFieldValue('targetWidgetClassName');
return result;
},
'setter': function(value)
{
this.setPropStoreFieldValue('targetWidgetClassName', value);
this.setTargetWidgetClass(ClassEx.findClass(value));
}
});
// alias for targetWidgetClassName
this.defineProp('target', {
'dataType': DataType.STRING,
'getter': function()
{
return this.getTargetWidgetClassName();
},
'setter': function(value)
{
this.setTargetWidgetClassName(value);
}
});
this.defineProp('targetWidget', {
'dataType': 'Kekule.Widget.BaseWidget', serializable: 'false', 'setter': null,
'getter': function()
{
var result = this.getPropStoreFieldValue('targetWidget');
if (!result)
{
result = this.createTargetWidget();
}
return result;
}
});
},
/** @ignore */
doGetWidgetClassName: function($super)
{
var result = $super() + ' ' + CNS.PLACEHOLDER;
var targetClass = this.getTargetWidgetClass();
if (targetClass)
{
var targetHtmlClassName = ClassEx.getPrototype(targetClass).getWidgetClassName();
result = Kekule.StrUtils.addTokens(result, targetHtmlClassName);
//console.log('HTML class name', this.getId(), targetHtmlClassName, result);
}
return result;
},
/** @ignore */
doCreateRootElement: function(doc)
{
var result = doc.createElement('img');
return result;
},
/**
* For descendants override.
* @private
*/
doReactUiEvent: function(e)
{
// when UI event occurs, create real widget
var widget = this.createTargetWidget();
//widget.reactUiEvent(e);
},
/**
* Create real widget, replace this placeholder.
* @returns {Kekule.Widget.BaseWidget}
*/
createTargetWidget: function()
{
var widgetClass = this.getTargetWidgetClass();
if (widgetClass)
{
try
{
var elem = this.getElement();
var parentWidget = this.getParent();
if (parentWidget && parentWidget instanceof Kekule.Widget.PlaceHolder)
{
// concrete parent first
parentWidget = parentWidget.createTargetWidget();
}
var children = AU.clone(this.getChildWidgets());
this.unbindElement(this.getElement());
this.setPropStoreFieldValue('element', null); // avoid delete element when finalize self
var result = new widgetClass(elem);
if (result)
{
//result.setParent(parentWidget);
if (parentWidget)
{
var refChild = this.getNextSibling();
this.setParent(null);
result.setPropStoreFieldValue('parent', parentWidget);
parentWidget._insertChild(result, refChild);
//parentWidget._removeChild(this);
}
// move all children of self to new created widget
if (children)
{
for (var i = 0, l = children.length; i < l; ++i)
{
children[i].setParent(result);
}
}
}
}
finally
{
this.finalize();
}
return result;
}
else
{
Kekule.error(Kekule.$L('ErrorMsg.WIDGET_UNAVAILABLE_FOR_PLACEHOLDER'));
}
}
});
/**
* Helper methods about widget.
* @class
*/
Kekule.Widget.Utils = {
/**
* Returns widget binding on element.
* @param {HTMLElement} element
* @returns {Kekule.Widget.BaseWidget}
*/
getWidgetOnElem: function(element, retainPlaceholder)
{
var result = element[widgetBindingField];
if (!retainPlaceholder && (result instanceof Kekule.Widget.PlaceHolder))
{
result = result.getTargetWidget();
}
return result;
},
/**
* Returns all widgets in element and its child elements.
* @param {HTMLElement} element
* @param {Bool} checkElemInsideWidget
* @returns {Array}
*/
getWidgetsInsideElem: function(element, checkElemInsideWidget)
{
var result;
var widget = Kekule.Widget.Utils.getWidgetOnElem(element);
if (widget)
result = [widget];
else
result = [];
if (!checkElemInsideWidget)
return result;
var childElems = Kekule.DomUtils.getDirectChildElems(element);
for (var i = 0, l = childElems.length; i < l; ++i)
{
var elem = childElems[i];
var widgets = Kekule.Widget.Utils.getWidgetsInsideElem(elem, checkElemInsideWidget);
result = result.concat(widgets);
}
return result;
},
/**
* Returns an ID specified widget in document.
* @param {String} id
* @param {HTMLDocument} doc
* @returns {Kekule.Widget.BaseWidget}
*/
getWidgetById: function(id, doc)
{
if (!doc)
doc = document;
var elem = doc.getElementById(id);
if (elem)
return Kekule.Widget.Utils.getWidgetOnElem(elem);
else
return undefined;
},
/**
* Returns widget the element belonged to.
* @param {HTMLElement} element
* @returns {Kekule.Widget.BaseWidget}
*/
getBelongedWidget: function(element)
{
var result = null; //Kekule.Widget.Utils.getWidgetOnElem(element);
while (element && (!result))
{
result = Kekule.Widget.Utils.getWidgetOnElem(element);
element = element.parentNode;
}
return result;
},
/**
* Returns widget the element belonged to. The widget must be a non-static one.
* @param {HTMLElement} element
* @returns {Kekule.Widget.BaseWidget}
*/
getBelongedResponsiveWidget: function(element)
{
var result = null;
while (element && (!result))
{
result = Kekule.Widget.Utils.getWidgetOnElem(element);
if (result && result.getStatic())
result = null;
element = element.parentNode;
}
return result;
},
/**
* Create widget from a definition hash object. The hash object may include the following fields:
* {
* 'widgetClass' or 'widget': widget class, class object or string, must have,
* 'htmlClass': string, HTML class name should be added to widget,
* 'children': array of child widget definition hash
* }
* Other fields will be set to properties of widget with the same names. If the field name starts with
* '#' and the value is a function, then the function will be set as an event handler.
* @param {Variant} parentOrElementOrDocument
* @param {Hash} defineObj
* @returns {Kekule.Widget.BaseWidget}
*/
createFromHash: function(parentOrElementOrDocument, defineObj)
{
var specialFields = ['widget', 'widgetClass', 'htmlClass', 'children'];
var wclass = defineObj.widgetClass || defineObj.widget;
if (typeof(wclass) === 'string')
wclass = ClassEx.findClass(wclass);
if (!wclass)
{
Kekule.error(Kekule.$L('ErrorMsg.WIDGET_CLASS_NOT_FOUND'));
return null;
}
var result = new wclass(parentOrElementOrDocument);
var fields = Kekule.ObjUtils.getOwnedFieldNames(defineObj, true);
fields = Kekule.ArrayUtils.exclude(fields, specialFields);
for (var i = 0, l = fields.length; i < l; ++i)
{
var field = fields[i];
var value = defineObj[field];
if (field.startsWith('#') && DataType.isFunctionValue(value))
{
var eventName = field.substr(1);
result.addEventListener(eventName, value);
}
else if (result.hasProperty(field))
{
//console.log('set prop value', field, value);
result.setPropValue(field, value);
}
}
if (defineObj.htmlClass)
{
result.addClassName(defineObj.htmlClass, true);
}
if (defineObj.children)
{
var childDefs = defineObj.children;
if (DataType.isArrayValue(childDefs))
{
for (var i = 0, l = childDefs.length; i < l; ++i)
{
var def = childDefs[i];
var child = Kekule.Widget.Utils.createFromHash(result, def);
if (child)
child.appendToWidget(result);
}
}
}
return result;
},
/**
* When binding to element, properties of widget can be set by element attribute values.
* This method helps to turn string type attribute values to proper type and set it to widget.
* @param {Kekule.Widget.BaseWidget} widget
* @param {String} attribName
* @param {String} attribValue
*/
setWidgetPropFromElemAttrib: function(widget, attribName, attribValue)
{
var propName = attribName.camelize();
// get widget property type first
var dtype = widget.getPropertyDataType(propName);
if (!dtype) // can not find property, exit
return;
if (dtype === DataType.STRING)
widget.setPropValue(propName, attribValue);
else // need to convert type
{
if (Kekule.PredefinedResReferer.isResValue(attribValue))
{
Kekule.PredefinedResReferer.loadResource(attribValue, function(resInfo, success)
{
//if (success)
{
// Check if widget has a special method to handle predefined resource
//console.log(resInfo);
if (widget.loadPredefinedResDataToProp)
widget.loadPredefinedResDataToProp(propName, resInfo, success);
}
}, null, widget.getDocument());
}
else if (attribValue.startsWith('#') && (ClassEx.isOrIsDescendantOf(ClassEx.findClass(dtype), Kekule.Widget.BaseWidget))) // start with '#', e.g. #id, means a id of another widget
{
var id = attribValue.substr(1).trim();
Kekule.Widget.Utils._setWidgetRefPropFromId(widget, propName, id);
}
else
{
var value = JSON.parse(attribValue);
if (Kekule.ObjUtils.notUnset(value))
{
var obj = ObjSerializerFactory.getSerializer('json').load(null, value);
widget.setPropValue(propName, obj);
}
}
}
},
/** @private */
_setWidgetRefPropFromId: function(widget, propName, id)
{
if (id)
{
var refWidget = Kekule.Widget.getWidgetById(id, widget.getDocument());
if (refWidget)
widget.setPropValue(propName, refWidget);
}
}
};
// Alias of important Kekule.Widget.Utils methods
/**
* Returns widget binding on element.
* @param {HTMLElement} element
* @returns {Kekule.Widget.BaseWidget}
* @function
*/
Kekule.Widget.getWidgetOnElem = Kekule.Widget.Utils.getWidgetOnElem;
/**
* Returns an ID specified widget in document.
* @param {HTMLDocument} doc
* @param {String} id
* @returns {Kekule.Widget.BaseWidget}
* @function
*/
Kekule.Widget.getWidgetById = Kekule.Widget.Utils.getWidgetById;
Kekule.$W = Kekule.Widget.Utils.getWidgetById;
/**
* Returns widget the element belonged to.
* @param {HTMLElement} element
* @returns {Kekule.Widget.BaseWidget}
* @function
*/
Kekule.Widget.getBelongedWidget = Kekule.Widget.Utils.getBelongedWidget;
/**
* Create widget from a definition hash object. The hash object may include the following fields:
* {
* 'widgetClass' or 'widget': widget class, class object or string, must have,
* 'htmlClass': string, HTML class name should be added to widget,
* 'children': array of child widget definition hash
* }
* Other fields will be set to properties of widget with the same names. If the field name starts with
* '#' and the value is a function, then the function will be set as an event handler.
* @param {Variant} parentOrElementOrDocument
* @param {Hash} defineObj
* @returns {Kekule.Widget.BaseWidget}
*/
Kekule.Widget.createFromHash = Kekule.Widget.Utils.createFromHash;
/**
* A singleton class to manage some global settings of widgets on HTML document.
* User should not use this class directly.
* @class
* @augments ObjectEx
*
* @param {HTMLDocument} doc
*
* @property {Kekule.Widget.BaseWidget} mouseCaptureWidget Widget to capture all mouse/touch events.
* @property {Array} popupWidgets Current popup widgets.
* @property {Array} dialogWidgets Current opened dialogs.
* @property {Bool} preserveWidgetList Whether the manager keep a list of all widgets on document.
* @property {Array} widgets An array of all widgets on document.
* This property is only available when property preserveWidgetList is true.
* @property {Bool} enableMouseEventToPointerPolyfill If true, mouseXXXX/touchXXXX event will also evoke react_pointerXXXX handlers on browsers that do not support pointer events directly.
* Currently this property should always be set to true.
* @private
*/
/**
* Invoked when a widget is created on document.
* event param of it has field: {widget}.
* @name Kekule.Widget.GlobalManager#widgetCreate
* @event
*/
/**
* Invoked when a widget is finalized on document.
* event param of it has field: {widget}.
* @name Kekule.Widget.GlobalManager#widgetFinalize
* @event
*/
Kekule.Widget.GlobalManager = Class.create(ObjectEx,
/** @lends Kekule.Widget.GlobalManager# */
{
/** @private */
CLASS_NAME: 'Kekule.Widget.GlobalManager',
/** @private */
INFO_FIELD: '__$info__',
/** @constructs */
initialize: function($super, doc)
{
$super();
this._document = doc || document;
this._touchEventSeq = []; // internal, for detecting ghost mouse event
this._hammertime = null; // private
this.setPropStoreFieldValue('popupWidgets', []);
this.setPropStoreFieldValue('dialogWidgets', []);
this.setPropStoreFieldValue('modalWidgets', []);
this.setPropStoreFieldValue('autoResizeWidgets', []);
this.setPropStoreFieldValue('widgets', []);
this.setPropStoreFieldValue('preserveWidgetList', true);
this.setPropStoreFieldValue('enableMouseEventToPointerPolyfill', true);
this.setPropStoreFieldValue('enableHammerGesture', !true);
/*
this.react_pointerdown_binding = this.react_pointerdown.bind(this);
this.react_keydown_binding = this.react_keydown.bind(this);
this.react_touchstart_binding = this.react_touchstart.bind(this);
*/
this.reactUiEventBind = this.reactUiEvent.bind(this);
this.reactTouchGestureBind = this.reactTouchGesture.bind(this);
this.reactWindowResizeBind = this.reactWindowResize.bind(this);
/*
this.reactPageShowBind = this.reactPageShow.bind(this);
if (window)
this.installWindowEventHandlers(window);
*/
Kekule.X.domReady(this.domReadyInit.bind(this));
},
/** @ignore */
finalize: function($super)
{
this.uninstallWindowEventHandlers(Kekule.DocumentUtils.getDefaultView(this._document));
this.uninstallGlobalDomMutationHandlers(this._document.documentElement/*.body*/);
//this.uninstallGlobalHammerTouchHandlers(this._document.documentElement/*.body*/);
//this.uninstallGlobalEventHandlers(this._document.documentElement/*.body*/);
this.uninstallGlobalEventHandlers(this._document.body);
this._hammertime = null;
this.setPropStoreFieldValue('popupWidgets', null);
this.setPropStoreFieldValue('widgets', null);
$super();
},
/** @private */
initProperties: function()
{
this.defineProp('mouseCaptureWidget', {'dataType': DataType.OBJECT, 'serializable': false});
this.defineProp('popupWidgets', {'dataType': DataType.ARRAY, 'serializable': false});
this.defineProp('dialogWidgets', {'dataType': DataType.ARRAY, 'serializable': false});
this.defineProp('modalWidgets', {'dataType': DataType.ARRAY, 'serializable': false});
this.defineProp('autoResizeWidgets', {'dataType': DataType.ARRAY, 'serializable': false}); // widgets resize itself when client size changing
this.defineProp('preserveWidgetList', {'dataType': DataType.BOOL, 'serializable': false,
'setter': function(value)
{
this.setPropStoreFieldValue('preserveWidgetList', value);
if (!value)
this.setPropStoreFieldValue('widgets', []);
}
});
this.defineProp('widgets', {'dataType': DataType.ARRAY, 'serializable': false, 'setter': null});
// private, record current active and focused widget
// at one time, only one widget can be in those states
this.defineProp('currActiveWidget', {'dataType': 'Kekule.Widget.BaseWidget', 'serializable': false});
this.defineProp('currFocusedWidget', {'dataType': 'Kekule.Widget.BaseWidget', 'serializable': false});
//this.defineProp('currHoverWidget', {'dataType': 'Kekule.Widget.BaseWidget', 'serializable': false});
this.defineProp('modalBackgroundElem', {'dataType': DataType.OBJECT, 'serializable': false, 'setter': null});
this.defineProp('enableHammerGesture', {'dataType': DataType.BOOL, 'serializable': false});
// should always set to be true
this.defineProp('enableMouseEventToPointerPolyfill', {'dataType': DataType.BOOL, 'serializable': false});
},
/** @private */
domReadyInit: function()
{
this.installGlobalEventHandlers(this._document.documentElement/*.body*/);
//this.installGlobalEventHandlers(this._document.body);
if (this.getEnableHammerGesture())
this._hammertime = this.installGlobalHammerTouchHandlers(this._document.body);
this.installGlobalDomMutationHandlers(this._document.documentElement/*.body*/);
this.installWindowEventHandlers(Kekule.DocumentUtils.getDefaultView(this._document));
},
/**
* Notify that a widget is created on page.
* @param {Kekule.Widget.BaseWidget} widget
* @private
*/
notifyWidgetCreated: function(widget)
{
if (this.getPreserveWidgetList())
this.getWidgets().push(widget);
this.invokeEvent('widgetCreate', {'widget': widget});
},
/**
* Notify that a widget is finalized on page.
* @param {Kekule.Widget.BaseWidget} widget
* @private
*/
notifyWidgetFinalized: function(widget)
{
if (this.getWidgets())
Kekule.ArrayUtils.remove(this.getWidgets(), widget);
if (this.getPopupWidgets())
Kekule.ArrayUtils.remove(this.getPopupWidgets(), widget);
if (this.getDialogWidgets())
Kekule.ArrayUtils.remove(this.getDialogWidgets(), widget);
if (this.getAutoResizeWidgets())
Kekule.ArrayUtils.remove(this.getAutoResizeWidgets(), widget);
if (widget === this.getCurrActiveWidget())
this.setCurrActiveWidget(null);
if (widget === this.getCurrFocusedWidget())
this.setCurrFocusedWidget(null);
this.invokeEvent('widgetFinalize', {'widget': widget});
},
/**
* Notify that active state of a widget is changed.
* @param {Kekule.Widget.BaseWidget} widget
* @param {Bool} active
* @private
*/
notifyWidgetActiveChanged: function(widget, active)
{
var oldWidget = this.getCurrActiveWidget();
if (active)
{
if (widget !== oldWidget)
{
if (oldWidget)
oldWidget.setIsActive(false);
this.setCurrActiveWidget(widget);
}
}
else
{
if (oldWidget === widget)
this.setCurrActiveWidget(null);
}
},
/**
* Notify that focus state of a widget is changed.
* @param {Kekule.Widget.BaseWidget} widget
* @param {Bool} focused
* @private
*/
notifyWidgetFocusChanged: function(widget, focused)
{
var oldWidget = this.getCurrFocusedWidget();
if (focused)
{
if (widget !== oldWidget)
{
// do not need to blur old widget, this will be done automatically by browser
if (oldWidget)
oldWidget.setIsFocused(false);
this.setCurrFocusedWidget(widget);
}
}
else
{
if (oldWidget === widget)
this.setCurrFocusedWidget(null);
}
},
/**
* Return if there is popup widget in current document.
*/
hasPopupWidgets: function()
{
return !!this.getPopupWidgets().length;
},
/**
* Return if there is dialog widget in current document.
*/
hasDialogWidgets: function()
{
return !!this.getDialogWidgets().length;
},
/**
* Returns widget the element belonged to. The widget must be a non-static one.
* @param {HTMLElement} element
* @returns {Kekule.Widget.BaseWidget}
*/
getBelongedResponsiveWidget: function(element)
{
/*
var result = null;
while (element && (!result))
{
result = Kekule.Widget.Utils.getWidgetOnElem(element);
if (result && result.getStatic())
result = null;
element = element.parentNode;
}
return result;
*/
return Kekule.Widget.Utils.getBelongedResponsiveWidget(element);
},
/**
* Notify that a widget on document has fired an event.
* @param {Kekule.Widget.BaseWidget} widget
* @param {String} eventName
* @param {Hash} event
*/
notifyWidgetEventFired: function(widget, eventName, event)
{
var e = Object.extend({}, event);
e.widget = widget;
this.invokeEvent(eventName, e);
},
// global event handlers
/*
* Install event handlers binding to current window.
* @param {Window} target
* @private
*/
/*
installWindowEventHandlers: function(target)
{
console.log('install window event');
if (!this._windowEventInstalled)
{
Kekule.X.Event.addListener(target, 'pageshow', this.reactPageShowBind);
this._windowEventInstalled = true;
}
},
*/
/*
* Called when current window is shown. Notifies widgets in page that their show state may be changed.
* @param {Object} e
* @private
*/
/*
reactPageShow: function(e)
{
//console.log('widget page show');
var widgets = this.getWidgets();
for (var i = 0, l = widgets.length; i < l; ++i)
{
var w = widgets[i];
w.widgetShowStateChanged();
}
},
*/
/**
* Install UI event (mousemove, click...) handlers to element.
* @param {HTMLElement} target
* @private
*/
installGlobalEventHandlers: function(target)
{
if (!this._globalEventInstalled)
{
var events = Kekule.Widget.UiEvents;
for (var i = 0, l = events.length; i < l; ++i)
{
if (events[i] === 'touchstart' || events[i] === 'touchmove' || events[i] === 'touchend') // explicit set passive to true for scroll performance on mobile devices
Kekule.X.Event.addListener(target, events[i], this.reactUiEventBind, {passive: true});
else
Kekule.X.Event.addListener(target, events[i], this.reactUiEventBind);
}
this._globalEventInstalled = true;
}
},
/**
* Uninstall UI event (mousemove, click...) handlers from old mainContextElement.
* @param {HTMLElement} element
* @private
*/
uninstallGlobalEventHandlers: function(target)
{
var events = Kekule.Widget.UiEvents;
for (var i = 0, l = events.length; i < l; ++i)
{
Kekule.X.Event.removeListener(target, events[i], this.reactUiEventBind);
}
},
/**
* Install event handlers on window object.
* @param {Window} win
*/
installWindowEventHandlers: function(win)
{
Kekule.X.Event.addListener(win, 'resize', this.reactWindowResizeBind);
},
/**
* Uninstall event handlers on window object.
* @param {Window} win
*/
uninstallWindowEventHandlers: function(win)
{
Kekule.X.Event.removeListener(win, 'resize', this.reactWindowResizeBind);
},
/**
* Install touch event (touch, swipe, pinch...) handlers to element.
* Currently hammer.js is used.
* @param {HTMLElement} element
* @private
*/
installGlobalHammerTouchHandlers: function(target)
{
if (typeof(Kekule.$jsRoot.Hammer) !== 'undefined')
{
var hammertime = new Hammer(target); // Hammer(target).on(Kekule.Widget.TouchGestures.join(' '), this.reactTouchGestureBind);
hammertime.get('pinch').set({ enable: true });
hammertime.get('rotate').set({ enable: true });
hammertime.on(Kekule.Widget.TouchGestures.join(' '), this.reactTouchGestureBind);
this._hammertime = hammertime;
//console.log(result);
//result.stop_browser_behavior.touchAction = 'none';
return hammertime;
}
},
/**
* Uninstall touch event (touch, swipe, pinch...) handlers to element.
* Currently hammer.js is used.
* @param {HTMLElement} element
* @private
*/
uninstallGlobalHammerTouchHandlers: function(target)
{
if (this._hammertime)
this._hammertime.off(Kekule.Widget.TouchGestures.join(' '), this.reactTouchGestureBind);
},
/**
* Install handlers to react to DOM node changes.
* @param {HTMLElement} target
* @private
*/
installGlobalDomMutationHandlers: function(target)
{
var self = this;
if (Kekule.X.MutationObserver)
{
var observer = new Kekule.X.MutationObserver(
function(mutations)
{
for (var i = 0, l = mutations.length; i < l; ++i)
{
var m = mutations[i];
if (m.type === 'childList') // dom tree changes
{
var nodes = m.addedNodes;
for (var j = 0, k = nodes.length; j < k; ++j)
{
var node = nodes[j];
if (node.nodeType === Node.ELEMENT_NODE)
{
self._handleDomAddedElem(node);
}
}
var nodes = m.removedNodes;
for (var j = 0, k = nodes.length; j < k; ++j)
{
var node = nodes[j];
if (node.nodeType === Node.ELEMENT_NODE)
{
self._handleDomRemovedElem(node);
}
}
}
}
});
observer.observe(target, {
childList: true,
subtree: true
});
this._domMutationObserver = observer;
}
else // traditional DOM event method
{
this._reactDomNodeInserted = function(e)
{
var target = e.getTarget();
if (target.nodeType === (Node.ELEMENT_NODE)) // is element
{
self._handleDomAddedElem(target);
}
};
this._reactDomNodeRemoved = function(e)
{
var target = e.getTarget();
if (target.nodeType === (Node.ELEMENT_NODE)) // is element
{
self._handleDomRemovedElem(target);
}
};
Kekule.X.Event.addListener(target, 'DOMNodeInserted', this._reactDomNodeInserted);
Kekule.X.Event.addListener(target, 'DOMNodeRemoved', this._reactDomNodeRemoved);
}
},
/**
* Uninstall handlers to react to DOM node changes.
* @param {HTMLElement} target
* @private
*/
uninstallGlobalDomMutationHandlers: function(target)
{
if (this._domMutationObserver)
this._domMutationObserver.disconnect();
if (this._reactDomNodeInserted)
Kekule.X.Event.removeListener(target, 'DOMNodeInserted', this._reactDomNodeInserted);
if (this._reactDomNodeRemoved)
Kekule.X.Event.removeListener(target, 'DOMNodeRemoved', this._reactDomNodeRemoved);
},
/** @private */
_handleDomAddedElem: function(elem)
{
var widgets = Kekule.Widget.Utils.getWidgetsInsideElem(elem, true);
for (var i = 0, l = widgets.length; i < l; ++i)
{
var w = widgets[i];
w.insertedToDom();
//console.log('dom inserted', w.getClassName(), elem);
}
var w = Kekule.Widget.Utils.getBelongedWidget(elem);
if (w)
{
if (w.getElement() === elem)
w.insertedToDom();
else
w.domElemAdded(elem);
//console.log('dom add', w.getClassName(), elem);
}
},
/** @private */
_handleDomRemovedElem: function(elem)
{
var widgets = Kekule.Widget.Utils.getWidgetsInsideElem(elem, true);
for (var i = 0, l = widgets.length; i < l; ++i)
{
var w = widgets[i];
w.removedFromDom();
}
var w = Kekule.Widget.Utils.getBelongedWidget(elem);
if (w)
w.domElemRemoved(elem);
},
/** @private */
isMouseEvent: function(eventName)
{
return eventName.startsWith('mouse') || (eventName === 'click');
},
/** @private */
isTouchEvent: function(eventName)
{
return eventName.startsWith('touch');
},
/** @private */
isPointerEvent: function(eventName)
{
return eventName.startsWith('pointer');
},
/**
* Convert a touch event to corresponding pointer event, useful for browsers that does not support pointer events.
* @param {Object} e
*/
mapTouchToPointerEvent: function(e)
{
var touchEvents = ['touchstart', 'touchmove', 'touchleave', 'touchend', 'touchcancel'];
var pointerEvents = ['pointerdown', 'pointermove', 'pointerout', 'pointerover', 'pointerup'];
var evType = e.getType();
var newEventObj = e; //Object.create(e);
newEventObj.pointerType = 'touch';
var newEventType;
if (evType === 'touchstart') // map to pointerdown
newEventType = 'pointerdown';
else if (evType === 'touchmove')
newEventType = 'pointermove';
else if (evType === 'touchleave')
newEventType = 'pointerout';
else if (evType === 'touchend')
newEventType = 'pointerup';
else if (evType === 'touchcancel') // has no corresponding pointer event
; // do nothing
if (newEventType)
{
newEventObj.setType(newEventType);
newEventObj.button = Kekule.X.Event.MouseButton.LEFT; // simulate mouse button
// map touch coordinate to clientX/Y, offsetX/Y, pageX/Y, screenX/Y
var touchPosition = e.touches[0];
var positionFieldNames = [
'clientX', 'clientY',
'pageX', 'pageY',
'screenX', 'screenY'
];
if (touchPosition)
{
var positionCache = {};
for (var i = 0, l = positionFieldNames.length; i < l; ++i)
{
var fname = positionFieldNames[i];
newEventObj[fname] = touchPosition[fname];
positionCache[fname] = touchPosition[fname];
}
// all event type (except touchstart), currentTarget is always the touch evoker,
// should be transformed to element under touch pos
if (evType !== 'touchstart')
{
var doc = this._document;
var currElement = doc.elementFromPoint(newEventObj.clientX, newEventObj.clientY);
newEventObj.setTarget(currElement);
//console.log('save touch data 1', currElement, newEventObj.getTarget());
}
this._touchPointerMapData = {'positionCache': positionCache, 'targetCache': newEventObj.getTarget()};
//console.log('save touch data 2', currElement, newEventObj.getTarget());
}
else // touch end event, may has no position info, use the cache of last touch event to fulfill it
{
if (this._touchPointerMapData)
{
for (var i = 0, l = positionFieldNames.length; i < l; ++i)
{
var fname = positionFieldNames[i];
newEventObj[fname] = this._touchPointerMapData.positionCache[fname];
}
}
newEventObj.setTarget(this._touchPointerMapData.targetCache);
//console.log('fetch touch cache', newEventObj.getTarget());
}
//console.log('map', evType, newEventObj.getType());
return newEventObj;
}
return null; // no mapping event, returns null
},
/** @private */
reactUiEvent: function(e)
{
var evType = e.getType();
// get target widget to dispatch event
var targetWidget;
var mouseCaptured;
if (this.getMouseCaptureWidget() && (this.isMouseEvent(evType) || this.isTouchEvent(evType) || this.isPointerEvent(evType))) // may be captured
{
targetWidget = this.getMouseCaptureWidget();
mouseCaptured = true;
}
else
{
var elem = e.getTarget();
targetWidget = this.getBelongedResponsiveWidget(elem);
}
// detect and mark ghost mouse event
/*
if (this.isTouchEvent(evType) && evType !== 'touchmove')
{
console.log('touch event', evType);
}
*/
if (evType === 'touchstart') // begin the sequence check
{
this._touchEventSeq = ['touchstart'];
this._touchDoneTimeStamp = null;
}
else if (evType === 'touchcancel') // touch cancelled, should not evoke mouse events
this._touchEventSeq = [];
else if (evType === 'touchend')
{
if (this._touchEventSeq[0] === 'touchstart') // a normal sequence, may cause mouse simulation
{
this._touchEventSeq.push('touchend');
this._touchDoneTimeStamp = Date.now();
/*
if (this._ghostMouseCheckId)
clearTimeout(this._ghostMouseCheckId);
var self = this;
this._ghostMouseCheckId = setTimeout(function(){ self._touchDoneTimeStamp = false; }, 5000);
*/
}
}
/*
if (evType === 'touchstart' || evType === 'touchend')
{
console.log('[Global touch event]', evType);
//if (evType === 'touchstart')
// e.preventDefault(); // prevent ghost mouse events, but also prevent page scroll
this._touchDoneTimeStamp = true; // a flag to avoid "ghost mouse event" after touch
if (this._ghostMouseCheckId)
clearTimeout(this._ghostMouseCheckId);
var self = this;
this._ghostMouseCheckId = setTimeout(function(){ self._touchDoneTimeStamp = false; }, 1000);
}
*/
else if (['mousedown', 'mouseup', 'mouseover', 'mouseout', 'click'].indexOf(evType) >= 0)
{
if (this._touchEventSeq[0] === 'touchstart' && this._touchEventSeq[1] === 'touchend') // match the touch seq
{
e.ghostMouseEvent = true;
if (evType === 'click') // the last mouse simulation event, release the ghost check
{
this._touchEventSeq = [];
this._touchDoneTimeStamp = Date.now(); // some times mouse simulation will be evoked twice, so preserve a time check, eliminate the second round
}
}
else if (this._touchDoneTimeStamp) // mark ghost mouse event
{
var timeStamp = Date.now();
if (timeStamp - this._touchDoneTimeStamp < 1000) // event fires less in 1 sec, should be a ghost one
e.ghostMouseEvent = true;
}
/*
if (e.ghostMouseEvent)
{
console.log('receice mouse event', evType, e.ghostMouseEvent, this._touchEventSeq);
}
*/
}
/*
if (['mouseup', 'click', 'touchend'].indexOf(evType) >= 0) // dismiss mouse capture
{
if (mouseCaptured)
this.setMouseCaptureWidget(null);
}
*/
// check first if the component has event handler itself
var funcName = Kekule.Widget.getEventHandleFuncName(e.getType());
if (this[funcName]) // has own handler
this[funcName](e);
// dispatch to widget
if (targetWidget)
{
//console.log('event', e.getTarget().tagName, widget.getClassName());
targetWidget.reactUiEvent(e);
}
if (!e.ghostMouseEvent && !Kekule.BrowserFeature.pointerEvent && this.getEnableMouseEventToPointerPolyfill())
{
var mouseEvents = ['mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup'];
var touchEvents = ['touchstart', 'touchmove', 'touchleave', 'touchend', 'touchcancel'];
var pointerEvents = ['pointerdown', 'pointermove', 'pointerout', 'pointerover', 'pointerup'];
var index = mouseEvents.indexOf(evType);
if (index >= 0)
{
e.setType(pointerEvents[index]);
e.pointerType = 'mouse';
this.reactUiEvent(e);
}
else if (touchEvents.indexOf(evType) >= 0) // touch events, need further polyfill
{
//console.log('prepare map', evType);
var newEvent = this.mapTouchToPointerEvent(e);
if (newEvent)
this.reactUiEvent(newEvent);
}
}
},
/** @private */
reactTouchGesture: function(e)
{
var funcName = Kekule.Widget.getTouchGestureHandleFuncName((e.getType && e.getType()) || e.type);
//console.log('gesture', e.type, funcName, e.target);
if (this[funcName])
this[funcName](e);
// as touch gesture handling function is installed in global handler only, need to dispatch to corresponding widgets
var widget = Kekule.Widget.getBelongedWidget(e.target);
if (widget)
{
widget.reactTouchGesture(e);
}
else
{
//console.log('not captured ', e.target, e);
}
},
/** @private */
reactWindowResize: function(e)
{
var widgets = this.getAutoResizeWidgets();
for (var i = 0, l = widgets.length; i < l; ++i)
{
if (widgets[i].isShown())
widgets[i].autoResizeToClient();
}
},
/** @private */
_startPointerHolderTimer: function(e)
{
var self = this;
this._pointerHolderTimer = {
pointerType: e.pointerType,
button: e.getButton(),
coord: {'x': e.getClientX(), 'y': e.getClientY()},
target: e.getTarget(),
_timeoutId: setTimeout(function(){
var newEvent = e;
newEvent.setType('pointerhold');
self._pointerHolderTimer = null;
self.reactUiEvent(newEvent);
}, Kekule.Widget._PointerHoldParams.DURATION_THRESHOLD),
cancel: function()
{
clearTimeout(self._pointerHolderTimer._timeoutId);
self._pointerHolderTimer = null;
}
};
return this._pointerHolderTimer;
},
/** @private */
react_pointerdown: function(e)
{
if (this.hasPopupWidgets() && !e.ghostMouseEvent)
{
if (e.getButton() === Kekule.X.Event.MouseButton.LEFT)
{
var elem = e.getTarget();
this.hidePopupWidgets(elem);
}
}
if (!this._pointerHolderTimer)
this._startPointerHolderTimer(e);
else
{
if (e.pointerType !== this._pointerHolderTimer.pointerType || e.getButton() !== this._pointerHolderTimer.button)
this._pointerHolderTimer.cancel();
}
},
/** @private */
react_pointerup: function(e)
{
if (this._pointerHolderTimer)
this._pointerHolderTimer.cancel();
},
/** @private */
react_pointermove: function(e)
{
if (this._pointerHolderTimer)
{
if (e.getTarget() !== this._pointerHolderTimer.target)
this._pointerHolderTimer.cancel();
else
{
var oldCoord = this._pointerHolderTimer.coord;
var newCoord = {x: e.getClientX(), y: e.getClientY()};
if (Kekule.CoordUtils.getDistance(oldCoord, newCoord) > Kekule.Widget._PointerHoldParams.MOVEMENT_THRESHOLD)
this._pointerHolderTimer.cancel();
}
}
},
/** @private */
react_keydown: function(e)
{
var keyCode = e.getKeyCode();
if (keyCode === Kekule.X.Event.KeyCode.ESC)
{
if (this.hasPopupWidgets())
{
//var elem = e.getTarget();
this.hidePopupWidgets(null, true); // dismiss even if focus on popup widget
}
else if (this.hasDialogWidgets())
{
this.hideTopmostDialogWidget(null, true); // dismiss dialog
}
}
},
/** @private */
react_touchstart: function(e)
{
if (this.hasPopupWidgets())
{
var elem = e.getTarget();
this.hidePopupWidgets(elem);
}
},
// methods about popups
/**
* Called after popup widget is hidden.
* @param {Kekule.Widget.BaseWidget} widget
*/
unpreparePopupWidget: function(widget)
{
widget.setPopupCaller(null);
this.restoreElem(widget.getElement());
},
/**
* Prepare before showing popup widget.
* In this method, popupWidget element will be moved to top most layer and its position will
* be recalculated.
* @param {Kekule.Widget.BaseWidget} popupWidget Widget to be popped up.
* @param {Kekule.Widget.BaseWidget} invokerWidget Who popped that widget.
* @param {Int} showType Value from {@link Kekule.Widget.ShowHideType}. If showType is DROPDOWN,
* the new position will be calculated based on invokerWidget.
*/
preparePopupWidget: function(popupWidget, invokerWidget, showType)
{
//console.log('prepare popup', popupWidget.getClassName(), 'called by', invokerWidget && invokerWidget.getClassName());
popupWidget.setPopupCaller(invokerWidget);
var popupElem = popupWidget.getElement();
//this.restoreElem(popupElem); // restore possible changed styles in last popping
var doc = popupElem.ownerDocument;
// check if already in top most layer
var topmostLayer = this.getTopmostLayer(doc, true);
var isOnTopLayer = popupElem.parentNode === topmostLayer;
// calc widget position
var ST = Kekule.Widget.ShowHideType;
// DONE: currently disable position recalculation of popup widget, since some popup widgets position is directly set
// (e.g., atom setter in composer).
/*
if (showType !== ST.DROPDOWN)
return;
*/
var autoAdjustSize = popupWidget.getAutoAdjustSizeOnPopup();
var posInfo;
/*
if ((showType === ST.DROPDOWN) && invokerWidget) // need to calc position on invokerWidget
{
posInfo = this._calcDropDownWidgetPosInfo(popupWidget, invokerWidget);
}
else if (!isOnTopLayer)
{
posInfo = this._calcPopupWidgetPosInfo(popupWidget);
}
*/
var parentFixedPosition;
if (showType === ST.DROPDOWN) // need to calc position on invokerWidget
{
parentFixedPosition = Kekule.StyleUtils.isSelfOrAncestorPositionFixed(invokerWidget.getElement());
posInfo = this._calcDropDownWidgetPosInfo(popupWidget, invokerWidget, parentFixedPosition);
}
else // if (showType === ST.POPUP)
{
posInfo = this._calcPopupWidgetPosInfo(popupWidget, isOnTopLayer);
}
if (autoAdjustSize && posInfo) // check if need to adjust size of widget
{
var viewPortVisibleBox = Kekule.DocumentUtils.getClientVisibleBox((invokerWidget || popupWidget).getDocument());
var visibleWidth = viewPortVisibleBox.right - viewPortVisibleBox.left;
var visibleHeight = viewPortVisibleBox.bottom - viewPortVisibleBox.top;
var widgetBox = posInfo.rect;
if (widgetBox.left + widgetBox.width > visibleWidth + viewPortVisibleBox.left) // need to shrink
{
posInfo.width = (viewPortVisibleBox.right - widgetBox.left) + 'px';
posInfo.widthChanged = true;
//posInfo.right = '0px';
}
if (widgetBox.top + widgetBox.height > visibleHeight + viewPortVisibleBox.top) // need to shrink
{
//posInfo.bottom = '0px';
posInfo.height = (viewPortVisibleBox.bottom - widgetBox.top) + 'px';
posInfo.heightChanged = true;
}
}
if (!isOnTopLayer)
this.moveElemToTopmostLayer(popupElem);
else // even is elem is on topmost layer, still append it to tail
this.moveElemToTopmostLayer(popupElem, true);
if (posInfo)
{
// set style
var stylePropNames = ['left', 'top', 'right', 'bottom']; //, 'width', 'height'];
if (posInfo.widthChanged)
stylePropNames.push('width');
if (posInfo.heightChanged)
stylePropNames.push('height');
var oldStyle = {};
var style = popupElem.style;
if (showType === ST.DROPDOWN && parentFixedPosition)
style.position = 'fixed'; // drop down widget should use the same position style to parent
else if (!Kekule.StyleUtils.isAbsOrFixPositioned(popupElem))
{
style.position = 'absolute';
}
for (var i = 0, l = stylePropNames.length; i < l; ++i)
{
var name = stylePropNames[i];
var value = posInfo[name];
if (value)
{
oldStyle[name] = style[name] || '';
style[name] = value;
}
}
var info = this._getElemStoredInfo(popupElem);
info.styles = oldStyle;
}
},
/** @private */
_calcPopupWidgetPosInfo: function(widget, isOnTopLayer)
{
var result;
var EU = Kekule.HtmlElementUtils;
var elem = widget.getElement();
var isShown = widget.isShown();
if (!isShown)
{
elem.style.visible = 'hidden';
elem.style.display = '';
}
var clientRect = EU.getElemBoundingClientRect(elem, true); // include scroll offset
//var clientRect = EU.getElemPageRect(elem, false); // include scroll offset
result = {
'rect': clientRect
};
if (!isOnTopLayer) // if not on top layer, need to adjust element position
{
if (Kekule.StyleUtils.getComputedStyle(elem, 'position') !== 'fixed')
{
result.top = clientRect.top + 'px';
result.left = clientRect.left + 'px';
}
}
return result;
},
/** @private */
_calcDropDownWidgetPosInfo: function(dropDownWidget, invokerWidget, parentFixedPosition)
{
var EU = Kekule.HtmlElementUtils;
var D = Kekule.Widget.Position;
var pos = invokerWidget.getDropPosition? invokerWidget.getDropPosition(): null;
var p = invokerWidget.getParent();
var layout = p && p.getLayout() || Kekule.Widget.Layout.HORIZONTAL;
// check which direction can display all part of widget and drop dropdown widget to that direction
var invokerElem = invokerWidget.getElement();
var invokerClientRect = EU.getElemBoundingClientRect(invokerElem, true);
//var invokerClientRect = EU.getElemPageRect(invokerElem, false);
//var viewPortDim = EU.getViewportDimension(invokerElem);
var viewPortBox = Kekule.DocumentUtils.getClientVisibleBox(invokerWidget.getDocument());
var dropElem = dropDownWidget.getElement();
// add the dropdown element to DOM tree first, else the offsetDimension will always return 0
dropElem.style.visible = 'hidden';
dropElem.style.display = '';
var manualAppended = false;
var topmostLayer = this.getTopmostLayer(dropElem.ownerDocument);
if (!dropElem.parentNode)
{
//invokerElem.appendChild(dropElem); // IMPORTANT: must append to invoker, otherwise style may be different
topmostLayer.appendChild(dropElem);
manualAppended = true;
}
dropElem.style.position = 'relative'; // absolute may cause size problem in Firefox
var dropOffsetDim = EU.getElemOffsetDimension(dropElem);
var dropScrollDim = EU.getElemScrollDimension(dropElem);
//var dropClientRect = EU.getElemBoundingClientRect(dropElem);
var dropClientRect = EU.getElemPageRect(dropElem, true);
dropElem.style.position = 'absolute'; // restore
// then remove from DOM tree
if (manualAppended)
//invokerElem.removeChild(dropElem);
topmostLayer.removeChild(dropElem);
//if (layout)
if (!pos || pos === D.AUTO) // decide drop pos
{
pos = 0;
if (layout === Kekule.Widget.Layout.VERTICAL) // check left or right
{
//var left = invokerClientRect.x;
var left = invokerClientRect.x - viewPortBox.left;
//var right = viewPortDim.width - left - invokerClientRect.width;
var right = viewPortBox.right - invokerClientRect.x - invokerClientRect.width;
// we prefer right, check if right can display drop down widget
if (right >= dropOffsetDim.width)
pos |= D.RIGHT;
else
pos |= (left > right)? D.LEFT: D.RIGHT;
}
else // check top or bottom
{
//var top = invokerClientRect.y;
var top = invokerClientRect.y - viewPortBox.top;
//var bottom = viewPortDim.height - top - invokerClientRect.height;
var bottom = viewPortBox.bottom - invokerClientRect.y - invokerClientRect.height;
// we prefer bottom
if (bottom >= dropOffsetDim.height)
pos |= D.BOTTOM;
else
pos |= (top > bottom)? D.TOP: D.BOTTOM;
}
}
// at last returns top/left/width/height
/*
var xprop = (pos & D.LEFT)? 'right': 'left';
var yprop = (pos & D.TOP)? 'bottom': 'top';
*/
//console.log(invokerClientRect);
var SU = Kekule.StyleUtils;
//var invokerClientRect = EU.getElemBoundingClientRect(invokerElem, true); // refetch, with document scroll considered
var invokerClientRect = EU.getElemBoundingClientRect(invokerElem, !parentFixedPosition); // refetch, with document scroll considered
//var invokerClientRect = EU.getElemPageRect(invokerElem, !!parentFixedPosition); // refetch, with document scroll considered
var w = /*SU.getComputedStyle(dropElem, 'width') ||*/ dropScrollDim.width;
var h = /*SU.getComputedStyle(dropElem, 'height') ||*/ dropScrollDim.height;
/*
var x = (pos & D.LEFT) || (pos & D.RIGHT)? invokerClientRect.left - w: invokerClientRect.left;
var y = (pos & D.BOTTOM) || (pos & D.TOP)? invokerClientRect.top - h: invokerClientRect.top;
*/
/*
var x = (pos & D.LEFT)? invokerClientRect.left - dropClientRect.width:
(pos & D.RIGHT)? invokerClientRect.right:
invokerClientRect.left; // not appointed, decide automatically
*/
var x;
if (pos & D.LEFT)
x = invokerClientRect.left - dropClientRect.width;
else if (pos & D.RIGHT)
x = invokerClientRect.right;
else // not appointed, decide automatically
{
/*
var leftDistance = viewPortDim.width - invokerClientRect.right;
var rightDistance = viewPortDim.width - invokerClientRect.left;
*/
var leftDistance = invokerClientRect.right - viewPortBox.left;
var rightDistance = viewPortBox.right - /* viewPortBox.left - */ invokerClientRect.left;
if (rightDistance >= dropClientRect.width) // default, can drop left align to left edge of invoker
x = invokerClientRect.left;
else if (leftDistance >= dropClientRect.width) // must drop right align to right edge of invoker
x = invokerClientRect.right - dropClientRect.width;
else // left or right size are all not suffient
{
x = Math.max(viewPortBox.left, viewPortBox.right - dropClientRect.width);
/*
var preferredX = viewPortBox.right - dropClientRect.width;
if (preferredX < viewPortBox.left) // width larger than viewPortBox, show widget at the left edge of view box
{
x = viewPortBox.left;
if (autoAdjustSize)
w = viewPortBox.right - viewPortBox.left;
}
else
x = preferredX;
*/
}
}
/*
var y = (pos & D.TOP)? invokerClientRect.top - dropClientRect.height:
(pos & D.BOTTOM)? invokerClientRect.bottom:
invokerClientRect.top;
*/
var y;
if (pos & D.TOP)
y = invokerClientRect.top - dropClientRect.height;
else if (pos & D.BOTTOM)
y = invokerClientRect.bottom;
else // not appointed, calc
{
/*
var topDistance = viewPortDim.height - invokerClientRect.bottom;
var bottomDistance = viewPortDim.height - invokerClientRect.top;
*/
var topDistance = invokerClientRect.bottom - viewPortBox.top;
var bottomDistance = viewPortBox.bottom - /*viewPortBox.top - */ invokerClientRect.top;
if (bottomDistance >= dropClientRect.height)
y = invokerClientRect.bottom;
else if (topDistance >= dropClientRect.height) // must drop right align to right edge of invoker
y = invokerClientRect.bottom - dropClientRect.height;
else // top or bottom size are all not suffient
{
y = Math.max(viewPortBox.top, viewPortBox.bottom - dropClientRect.height);
/*
var preferredY = viewPortBox.bottom - dropClientRect.height;
if (preferredY < viewPortBox.top) // width larger than viewPortBox, show widget at the left edge of view box
{
y = viewPortBox.top;
if (autoAdjustSize)
h = viewPortBox.bottom - viewPortBox.top;
}
else
y = preferredY;
*/
}
}
//console.log(pos, invokerClientRect, y, h, dropClientRect.height);
var result = {};
result.rect = {'left': x, 'top': y, 'width': w, 'height': h};
x += 'px';
y += 'px';
w += 'px';
h += 'px';
//console.log(xprop, x, yprop, y);
result.left = x;
result.top = y;
result.width = w;
result.height = h;
return result;
},
/** @private */
_fillPersistentPopupWidgetsInHidden: function(activateWidget, allPopupWidgets, persistPopups, checkedWidgets)
{
checkedWidgets.push(activateWidget);
var activateElem, parentWidget, callerWidget;
try
{
if (activateWidget && activateWidget instanceof Kekule.Widget.BaseWidget)
{
activateElem = activateWidget.getElement();
parentWidget = activateWidget.getParent();
callerWidget = activateWidget.getPopupCaller();
}
else if (activateWidget instanceof HTMLElement) // maybe invoke directly by an element
activateElem = activateWidget;
else
activateElem = null;
}
catch(e)
{
// do nothing
activateElem = null;
}
if (!activateElem)
return;
for (var i = 0, l = allPopupWidgets.length; i < l; ++i)
{
var w = allPopupWidgets[i];
if (w === activateWidget)
{
persistPopups.push(w);
}
else
{
var elem = w.getElement();
if (Kekule.DomUtils.isDescendantOf(activateElem, elem))
persistPopups.push(w);
}
}
if (parentWidget && checkedWidgets.indexOf(parentWidget) <= 0)
this._fillPersistentPopupWidgetsInHidden(parentWidget, allPopupWidgets, persistPopups, checkedWidgets);
if (callerWidget && checkedWidgets.indexOf(callerWidget) <= 0)
this._fillPersistentPopupWidgetsInHidden(callerWidget, allPopupWidgets, persistPopups, checkedWidgets);
},
/**
* When use activate an element outside the popups, all popped widget should be hidden.
* @param {HTMLElement} activateElement
* @private
*/
hidePopupWidgets: function(activateElement, isDismissed)
{
var widgets = this.getPopupWidgets();
var activateWidget = Kekule.Widget.getBelongedWidget(activateElement);
/*
var activateWidgetCaller = activateWidget? activateWidget.getPopupCaller(): null;
var activateWidgetCallerElem = activateWidgetCaller? activateWidgetCaller.getElement(): null;
*/
var persistPopups = [];
if (activateWidget)
this._fillPersistentPopupWidgetsInHidden(activateWidget, widgets, persistPopups, []);
for (var i = widgets.length - 1; i >= 0; --i)
{
var widget = widgets[i];
var elem = widget.getElement();
if (elem)
{
/*
if (elem === activateWidgetCallerElem || Kekule.DomUtils.isDescendantOf(activateWidgetCallerElem, elem))
continue;
*/
if (persistPopups.indexOf(widget) >= 0)
continue;
if ((!activateElement) ||
((elem !== activateElement) && (!Kekule.DomUtils.isDescendantOf(activateElement, elem)))) // active outside this widget, this widget need to hide
{
if (!isDismissed)
{
widget.hide();
}
else
widget.dismiss();
}
}
else // element of widgets is missing, may be removed or finalized already
{
Kekule.ArrayUtils.remove(this.getPopupWidgets(), widget); // remove from popup array
}
}
},
/**
* When press ESC key, topmost dialog widget should be hidden.
* @param {HTMLElement} activateElement
* @private
*/
hideTopmostDialogWidget: function(activateElement, isDismissed)
{
var widgets = this.getDialogWidgets() || [];
var w = widgets[widgets.length - 1];
if (w)
{
var elem = w.getElement();
if (elem)
{
if (isDismissed)
w.dismiss();
else
w.hide();
}
else // element of widgets is missing, may be removed or finalized already
Kekule.ArrayUtils.remove(this.geDialogWidgets(), w);
}
},
/**
* Notify the manager that an popup widget is shown.
* @param {Kekule.Widget.BaseWidget} widget
* //@param {HTMLElement} element Base element of widget. If not set, widget.getElement() will be used.
*/
registerPopupWidget: function(widget /*, element*/)
{
/*
var elem = element || widget.getElement();
this.getPopupWidgetMapping().set(elem, widget);
*/
widget.addClassName(CNS.SHOW_POPUP);
Kekule.ArrayUtils.pushUnique(this.getPopupWidgets(), widget);
},
/**
* Notify the manager that an popup widget is hidden.
* @param {Kekule.Widget.BaseWidget} widget
* //@param {HTMLElement} element Base element of widget. If not set, widget.getElement() will be used.
*/
unregisterPopupWidget: function(widget/*, element*/)
{
/*
var elem = element || widget.getElement();
this.getPopupWidgetMapping().remove(elem);
*/
widget.removeClassName(CNS.SHOW_POPUP);
Kekule.ArrayUtils.remove(this.getPopupWidgets(), widget);
},
/**
* Notify the manager that an dialog widget is shown.
* @param {Kekule.Widget.BaseWidget} widget
* //@param {HTMLElement} element Base element of widget. If not set, widget.getElement() will be used.
*/
registerDialogWidget: function(widget /*, element*/)
{
widget.addClassName(CNS.SHOW_DIALOG);
Kekule.ArrayUtils.pushUnique(this.getDialogWidgets(), widget);
},
/**
* Notify the manager that an dialog widget is hidden.
* @param {Kekule.Widget.BaseWidget} widget
* //@param {HTMLElement} element Base element of widget. If not set, widget.getElement() will be used.
*/
unregisterDialogWidget: function(widget/*, element*/)
{
widget.removeClassName(CNS.SHOW_DIALOG);
Kekule.ArrayUtils.remove(this.getDialogWidgets(), widget);
},
/**
* Notify the manager that an dialog widget is shown.
* @param {Kekule.Widget.BaseWidget} widget
*/
registerModalWidget: function(widget)
{
var prevModal = this.getCurrModalWidget();
if (prevModal)
prevModal.removeClassName(CNS.SHOW_ACTIVE_MODAL);
Kekule.ArrayUtils.pushUnique(this.getModalWidgets(), widget);
widget.addClassName(CNS.SHOW_ACTIVE_MODAL);
},
/**
* Notify the manager that an dialog widget is hidden.
* @param {Kekule.Widget.BaseWidget} widget
*/
unregisterModalWidget: function(widget)
{
widget.removeClassName(CNS.SHOW_ACTIVE_MODAL);
Kekule.ArrayUtils.remove(this.getModalWidgets(), widget);
var prevModal = this.getCurrModalWidget();
if (prevModal)
prevModal.addClassName(CNS.SHOW_ACTIVE_MODAL);
},
/**
* Returns current (topmost) modal widget.
* @returns {Kekule.Widget.BaseWidget}
*/
getCurrModalWidget: function()
{
var remainingModalWidgets = this.getModalWidgets();
if (!remainingModalWidgets || !remainingModalWidgets.length)
return null;
else
return remainingModalWidgets[0];
},
/** @private */
prepareModalWidget: function(widget)
{
// create a modal background and then relocate dialog element on it
var doc = widget.getDocument();
var bgElem = this.getModalBackgroundElem();
if (!bgElem)
{
//console.log('create new background');
bgElem = doc.createElement('div');
bgElem.className = CNS.MODAL_BACKGROUND;
this.setPropStoreFieldValue('modalBackgroundElem', bgElem);
}
var elem = widget.getElement();
widget.setModalInfo({
'oldParent': elem.parentNode,
'oldSibling': elem.nextSibling
});
//alert('hi');
if (elem.parentNode)
elem.parentNode.removeChild(elem);
//div.appendChild(elem);
if (bgElem.parentNode)
bgElem.parentNode.removeChild(bgElem);
doc.body.appendChild(bgElem);
doc.body.appendChild(elem); // append widget elem on background
this.registerModalWidget(widget);
},
/** @private */
unprepareModalWidget: function(widget)
{
var doc = widget.getDocument();
this.unregisterModalWidget(widget);
//console.log('unprepareModal');
var parentElem = widget.getElement().parentNode;
if (parentElem)
parentElem.removeChild(widget.getElement());
var modalInfo = widget.getModalInfo();
if (modalInfo)
{
if (modalInfo.oldParent)
modalInfo.oldParent.insertBefore(widget.getElement(), modalInfo.oldSibling);
widget.setModalInfo(null);
}
var remainActiveModalWidget = this.getCurrModalWidget();
var bgElem = this.getModalBackgroundElem();
if (bgElem)
{
if (remainActiveModalWidget) // still has modal widget, move background element under it
{
if (bgElem.parentNode)
bgElem.parentNode.removeChild(bgElem);
var parentNode = remainActiveModalWidget.getElement().parentNode;
parentNode.insertBefore(bgElem, remainActiveModalWidget.getElement());
}
else // no modal widget, remove background element
{
if (bgElem.parentNode)
{
bgElem.parentNode.removeChild(bgElem);
}
}
}
},
/**
* Register an auto-resize widget.
* @param {Kekule.Widget.BaseWidget} widget
*/
registerAutoResizeWidget: function(widget)
{
Kekule.ArrayUtils.pushUnique(this.getAutoResizeWidgets(), widget);
},
/**
* Unregister an auto-resize widget.
* @param {Kekule.Widget.BaseWidget} widget
*/
unregisterAutoResizeWidget: function(widget)
{
Kekule.ArrayUtils.remove(this.getAutoResizeWidgets(), widget);
},
/**
* Get top most layer previous created in document.
* @param {HTMLDocument} doc
* @returns {HTMLElement}
*/
getTopmostLayer: function(doc, canCreate)
{
var body = doc.body;
return body;
/*
var child = body.lastChild;
while (child && (child.className !== CNS.TOP_LAYER))
{
child = child.previousSibling;
}
if (!child && canCreate)
child = this.createTopmostLayer(doc);
return child;
*/
},
/*
* Create a topmost transparent element in document to put drop down and popup widgets.
* @param {HTMLDocument} doc
* @returns {HTMLElement}
* @deprecated
*/
/*
createTopmostLayer: function(doc)
{
var div = doc.createElement('div');
div.className = CNS.TOP_LAYER;
doc.body.appendChild(div);
return div;
},
*/
/** @private */
_getElemStoredInfo: function(elem)
{
var result = null;
if (elem)
{
result = elem[this.INFO_FIELD];
if (!result)
{
result = {};
elem[this.INFO_FIELD] = result;
}
}
return result;
},
_clearElemStoredInfo: function(elem)
{
if (elem[this.INFO_FIELD])
elem[this.INFO_FIELD] = undefined;
},
/**
* Move an element to top most layer for popup or dropdown.
* @param {HTMLElement} elem
* @private
*/
moveElemToTopmostLayer: function(elem, doNotStoreOldInfo)
{
// store elem's old position info first
/*
var oldInfo = {
'parentElem': elem.parentNode,
'nextSibling': elem.nextSibling
};
elem[this.INFO_FIELD] = oldInfo;
*/
if (!doNotStoreOldInfo)
{
var info = this._getElemStoredInfo(elem);
info.parentElem = elem.parentNode;
info.nextSibling = elem.nextSibling;
}
var layer = this.getTopmostLayer(elem.ownerDocument);
layer.appendChild(elem);
},
/**
* Restore element's style and position in DOM tree by stored info
* @param {HTMLElement} elem
* @private
*/
restoreElem: function(elem)
{
if (!elem)
return;
var info = this._getElemStoredInfo(elem);
if (!info)
return;
// restore style
var oldStyles = info.styles;
var names = Kekule.ObjUtils.getOwnedFieldNames(oldStyles);
var style = elem.style;
for (var i = 0, l = names.length; i < l; ++i)
{
var name = names[i];
var value = oldStyles[name];
if (value)
style[name] = value;
else
Kekule.StyleUtils.removeStyleProperty(style, name);
}
// restore DOM tree
if (info.parentElem)
{
if (info.nextSibling)
info.parentElem.insertBefore(elem, info.nextSibling);
else
info.parentElem.appendChild(elem);
}
// clear info
elem[this.INFO_FIELD] = null;
}
});
Kekule.ClassUtils.makeSingleton(Kekule.Widget.GlobalManager);
Kekule.Widget.globalManager = Kekule.Widget.GlobalManager.getInstance();
})();