devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
610 lines (511 loc) • 20.2 kB
JavaScript
var noop = require("../../core/utils/common").noop,
windowUtils = require("../../core/utils/window"),
domAdapter = require("../../core/dom_adapter"),
typeUtils = require("../../core/utils/type"),
each = require("../../core/utils/iterator").each,
version = require("../../core/version"),
_windowResizeCallbacks = require("../../core/utils/resize_callbacks"),
_stringFormat = require("../../core/utils/string").format,
_isObject = require("../../core/utils/type").isObject,
extend = require("../../core/utils/extend").extend,
_floor = Math.floor,
DOMComponent = require("../../core/dom_component"),
helpers = require("./helpers"),
_parseScalar = require("./utils").parseScalar,
errors = require("./errors_warnings"),
_log = errors.log,
rendererModule = require("./renderers/renderer"),
_Layout = require("./layout"),
OPTION_RTL_ENABLED = "rtlEnabled",
SIZED_ELEMENT_CLASS = "dx-sized-element",
_option = DOMComponent.prototype.option;
function getTrue() {
return true;
}
function getFalse() {
return false;
}
function areCanvasesDifferent(canvas1, canvas2) {
return !(canvas1.width === canvas2.width && canvas1.height === canvas2.height && canvas1.left === canvas2.left && canvas1.top === canvas2.top && canvas1.right === canvas2.right && canvas1.bottom === canvas2.bottom);
}
function createResizeHandler(callback) {
var timeout,
handler = function handler() {
clearTimeout(timeout);
timeout = setTimeout(callback, 100);
};
handler.dispose = function () {
clearTimeout(timeout);
return this;
};
return handler;
}
function defaultOnIncidentOccurred(e) {
_log.apply(null, [e.target.id].concat(e.target.args || []));
}
// TODO: Such ugly declaration is because of jshint
var createIncidentOccurred = function createIncidentOccurred(widgetName, eventTrigger) {
return incidentOccurred;
function incidentOccurred(id, args) {
eventTrigger("incidentOccurred", {
target: {
id: id,
type: id[0] === "E" ? "error" : "warning",
args: args,
text: _stringFormat.apply(null, [errors.ERROR_MESSAGES[id]].concat(args || [])),
widget: widgetName,
version: version
}
});
}
};
function pickPositiveValue(values) {
return values.reduce(function (result, value) {
return value > 0 && !result ? value : result;
}, 0);
}
// TODO - Changes handling
// * Provide more validation - something like
// _changes: [{
// code: "THEME",
// options: ["theme"],
// type: "option",
// handler: function () {
// this._setThemeAndRtl();
// }
// }, {
// code: "CONTAINER_SIZE",
// options: ["size", "option"],
// type: "layout",
// handler: function () {
// this._updateSize();
// }
// }]
var getEmptyComponent = function getEmptyComponent() {
var emptyComponentConfig = {};
emptyComponentConfig.ctor = function (element, options) {
this.callBase(element, options);
var sizedElement = domAdapter.createElement("div");
var width = options && typeUtils.isNumeric(options.width) ? options.width + "px" : "100%";
var height = options && typeUtils.isNumeric(options.height) ? options.height + "px" : this._getDefaultSize().height + "px";
domAdapter.setStyle(sizedElement, "width", width);
domAdapter.setStyle(sizedElement, "height", height);
domAdapter.setClass(sizedElement, SIZED_ELEMENT_CLASS);
domAdapter.insertElement(element, sizedElement);
};
var EmptyComponent = DOMComponent.inherit(emptyComponentConfig);
var originalInherit = EmptyComponent.inherit;
EmptyComponent.inherit = function (config) {
for (var field in config) {
if (typeUtils.isFunction(config[field]) && field.substr(0, 1) !== "_" || field === "_dispose" || field === "_optionChanged") {
config[field] = noop;
}
}
return originalInherit.call(this, config);
};
return EmptyComponent;
};
var isServerSide = !windowUtils.hasWindow();
module.exports = isServerSide ? getEmptyComponent() : DOMComponent.inherit({
_eventsMap: {
"onIncidentOccurred": { name: "incidentOccurred" },
"onDrawn": { name: "drawn" }
},
_getDefaultOptions: function _getDefaultOptions() {
return extend(this.callBase(), {
onIncidentOccurred: defaultOnIncidentOccurred
});
},
_useLinks: true,
_init: function _init() {
var that = this,
linkTarget;
that._$element.children("." + SIZED_ELEMENT_CLASS).remove();
that.callBase.apply(that, arguments);
that._changesLocker = 0;
that._changes = helpers.changes();
that._suspendChanges();
that._themeManager = that._createThemeManager();
that._themeManager.setCallback(function () {
that._requestChange(that._themeDependentChanges);
});
that._renderElementAttributes();
that._initRenderer();
// Shouldn't "_useLinks" be passed to the renderer instead of doing 3 checks here?
linkTarget = that._useLinks && that._renderer.root;
// There is an implicit relation between `_useLinks` and `loading indicator` - it uses links
// Though this relation is not ensured in code we will immediately know when it is broken - `loading indicator` will break on construction
linkTarget && linkTarget.enableLinks().virtualLink("core").virtualLink("peripheral");
that._renderVisibilityChange();
that._attachVisibilityChangeHandlers();
that._initEventTrigger();
that._incidentOccurred = createIncidentOccurred(that.NAME, that._eventTrigger);
that._layout = new _Layout();
// Such solution is used only to avoid writing lots of "after" for all core elements in all widgets
// May be later a proper solution would be found
linkTarget && linkTarget.linkAfter("core");
that._initPlugins();
that._initCore();
linkTarget && linkTarget.linkAfter();
that._change(that._initialChanges);
},
_initialChanges: ["LAYOUT", "RESIZE_HANDLER", "THEME"],
_initPlugins: function _initPlugins() {
var that = this;
each(that._plugins, function (_, plugin) {
plugin.init.call(that);
});
},
_disposePlugins: function _disposePlugins() {
var that = this;
each(that._plugins.slice().reverse(), function (_, plugin) {
plugin.dispose.call(that);
});
},
_change: function _change(codes) {
this._changes.add(codes);
},
_suspendChanges: function _suspendChanges() {
++this._changesLocker;
},
_resumeChanges: function _resumeChanges() {
var that = this;
if (--that._changesLocker === 0 && that._changes.count() > 0 && !that._applyingChanges) {
that._renderer.lock();
that._applyingChanges = true;
that._applyChanges();
that._changes.reset();
that._applyingChanges = false;
that._renderer.unlock();
if (that._optionsQueue) {
that._applyQueuedOptions();
}
}
},
_applyQueuedOptions: function _applyQueuedOptions() {
var that = this,
queue = that._optionsQueue;
that._optionsQueue = null;
that.beginUpdate();
each(queue, function (_, action) {
action();
});
that.endUpdate();
},
_requestChange: function _requestChange(codes) {
this._suspendChanges();
this._change(codes);
this._resumeChanges();
},
_applyChanges: function _applyChanges() {
var that = this,
changes = that._changes,
order = that._totalChangesOrder,
i,
ii = order.length;
for (i = 0; i < ii; ++i) {
if (changes.has(order[i])) {
that["_change_" + order[i]]();
}
}
},
_optionChangesOrder: ["EVENTS", "THEME", "RENDERER", "RESIZE_HANDLER"],
_layoutChangesOrder: ["ELEMENT_ATTR", "CONTAINER_SIZE", "LAYOUT"],
_customChangesOrder: [],
_change_EVENTS: function _change_EVENTS() {
this._eventTrigger.applyChanges();
},
_change_THEME: function _change_THEME() {
this._setThemeAndRtl();
},
_change_RENDERER: function _change_RENDERER() {
this._setRendererOptions();
},
_change_RESIZE_HANDLER: function _change_RESIZE_HANDLER() {
this._setupResizeHandler();
},
_change_ELEMENT_ATTR: function _change_ELEMENT_ATTR() {
this._renderElementAttributes();
this._change(["CONTAINER_SIZE"]);
},
_change_CONTAINER_SIZE: function _change_CONTAINER_SIZE() {
this._updateSize();
},
_change_LAYOUT: function _change_LAYOUT() {
this._setContentSize();
},
_themeDependentChanges: ["RENDERER"],
_initRenderer: function _initRenderer() {
var that = this;
// Canvas is calculated before the renderer is created in order to capture actual size of the container
that._canvas = that._calculateCanvas();
that._renderer = new rendererModule.Renderer({ cssClass: that._rootClassPrefix + " " + that._rootClass, pathModified: that.option("pathModified"), container: that._$element[0] });
that._renderer.resize(that._canvas.width, that._canvas.height);
},
_disposeRenderer: function _disposeRenderer() {
///#DEBUG
// NOTE: This is temporary - until links mechanism is stabilized
this._useLinks && this._renderer.root.checkLinks();
///#ENDDEBUG
this._renderer.dispose();
},
_getAnimationOptions: noop,
render: function render() {
this._requestChange(["CONTAINER_SIZE"]);
this._onRender();
},
// This is actually added only to make tooltip pluggable. This is bad but much better than entire tooltip in BaseWidget.
_onRender: noop,
_dispose: function _dispose() {
var that = this;
that.callBase.apply(that, arguments);
that._removeResizeHandler();
that._layout.dispose();
that._eventTrigger.dispose();
that._disposeCore();
that._disposePlugins();
that._disposeRenderer();
that._themeManager.dispose();
that._themeManager = that._renderer = that._eventTrigger = null;
},
_initEventTrigger: function _initEventTrigger() {
var that = this;
that._eventTrigger = createEventTrigger(that._eventsMap, function (name) {
return that._createActionByOption(name);
});
},
_calculateCanvas: function _calculateCanvas() {
var that = this,
size = that.option("size") || {},
margin = that.option("margin") || {},
defaultCanvas = that._getDefaultSize() || {},
elementWidth = windowUtils.hasWindow() ? that._$element.width() : 0,
elementHeight = windowUtils.hasWindow() ? that._$element.height() : 0,
canvas = {
width: size.width <= 0 ? 0 : _floor(pickPositiveValue([size.width, elementWidth, defaultCanvas.width])),
height: size.height <= 0 ? 0 : _floor(pickPositiveValue([size.height, elementHeight, defaultCanvas.height])),
left: pickPositiveValue([margin.left, defaultCanvas.left]),
top: pickPositiveValue([margin.top, defaultCanvas.top]),
right: pickPositiveValue([margin.right, defaultCanvas.right]),
bottom: pickPositiveValue([margin.bottom, defaultCanvas.bottom])
};
// This for backward compatibility - widget was not rendered when canvas is empty.
// Now it will be rendered but because of "width" and "height" of the root both set to 0 it will not be visible.
if (canvas.width - canvas.left - canvas.right <= 0 || canvas.height - canvas.top - canvas.bottom <= 0) {
canvas = { width: 0, height: 0 };
}
return canvas;
},
_updateSize: function _updateSize() {
var that = this,
canvas = that._calculateCanvas();
that._renderer.fixPlacement();
if (areCanvasesDifferent(that._canvas, canvas) || that.__forceRender /* for charts */) {
that._canvas = canvas;
that._recreateSizeDependentObjects(true);
that._renderer.resize(canvas.width, canvas.height);
that._change(["LAYOUT"]);
}
},
_recreateSizeDependentObjects: noop,
_getMinSize: function _getMinSize() {
return [0, 0];
},
_getAlignmentRect: noop,
_setContentSize: function _setContentSize() {
var canvas = this._canvas,
layout = this._layout,
rect = canvas.width > 0 && canvas.height > 0 ? [canvas.left, canvas.top, canvas.width - canvas.right, canvas.height - canvas.bottom] : [0, 0, 0, 0],
nextRect;
rect = layout.forward(rect, this._getMinSize());
nextRect = this._applySize(rect) || rect;
layout.backward(nextRect, this._getAlignmentRect() || nextRect);
},
///#DEBUG
DEBUG_getCanvas: function DEBUG_getCanvas() {
return this._canvas;
},
DEBUG_getEventTrigger: function DEBUG_getEventTrigger() {
return this._eventTrigger;
},
///#ENDDEBUG
_getOption: function _getOption(name, isScalar) {
var theme = this._themeManager.theme(name),
option = this.option(name);
return isScalar ? option !== undefined ? option : theme : extend(true, {}, theme, option);
},
_setupResizeHandler: function _setupResizeHandler() {
var that = this,
redrawOnResize = _parseScalar(this._getOption("redrawOnResize", true), true);
if (that._resizeHandler) {
that._removeResizeHandler();
}
that._resizeHandler = createResizeHandler(function () {
if (redrawOnResize) {
that._requestChange(["CONTAINER_SIZE"]);
} else {
that._renderer.fixPlacement();
}
});
_windowResizeCallbacks.add(that._resizeHandler);
},
_removeResizeHandler: function _removeResizeHandler() {
if (this._resizeHandler) {
_windowResizeCallbacks.remove(this._resizeHandler);
this._resizeHandler.dispose();
this._resizeHandler = null;
}
},
// This is actually added only to make loading indicator pluggable. This is bad but much better than entire loading indicator in BaseWidget.
_onBeginUpdate: noop,
beginUpdate: function beginUpdate() {
var that = this;
// The "_initialized" flag is checked because first time "beginUpdate" is called in the constructor.
if (that._initialized && that._updateLockCount === 0) {
that._onBeginUpdate();
that._suspendChanges();
}
that.callBase.apply(that, arguments);
return that;
},
endUpdate: function endUpdate() {
var that = this;
that.callBase.apply(that, arguments);
if (that._updateLockCount === 0) {
that._resumeChanges();
}
return that;
},
option: function option(name) {
var that = this;
// NOTE: `undefined` has to be returned because base option setter returns `undefined`.
// `argument.length` and `isObject` checks are copypaste from Component.
if (that._initialized && that._applyingChanges && (arguments.length > 1 || _isObject(name))) {
that._optionsQueue = that._optionsQueue || [];
that._optionsQueue.push(that._getActionForUpdating(arguments));
} else {
return _option.apply(that, arguments);
}
},
_getActionForUpdating: function _getActionForUpdating(args) {
var that = this;
return that._deprecatedOptionsSuppressed ? function () {
// T479911
that._suppressDeprecatedWarnings();
_option.apply(that, args);
that._resumeDeprecatedWarnings();
} : function () {
_option.apply(that, args);
};
},
// For quite a long time the following method were abstract (from the Component perspective).
// Now they are not but that basic functionality is not required here.
_clean: noop,
_render: noop,
_optionChanged: function _optionChanged(arg) {
var that = this;
if (that._eventTrigger.change(arg.name)) {
that._change(["EVENTS"]);
} else if (that._optionChangesMap[arg.name]) {
that._change([that._optionChangesMap[arg.name]]);
} else {
that.callBase.apply(that, arguments);
}
},
_optionChangesMap: {
size: "CONTAINER_SIZE",
margin: "CONTAINER_SIZE",
redrawOnResize: "RESIZE_HANDLER",
theme: "THEME",
rtlEnabled: "THEME",
encodeHtml: "THEME",
elementAttr: "ELEMENT_ATTR"
},
_visibilityChanged: function _visibilityChanged() {
this.render();
},
_setThemeAndRtl: function _setThemeAndRtl() {
this._themeManager.setTheme(this.option("theme"), this.option(OPTION_RTL_ENABLED));
},
_getRendererOptions: function _getRendererOptions() {
return {
rtl: this.option(OPTION_RTL_ENABLED),
encodeHtml: this.option("encodeHtml"),
animation: this._getAnimationOptions()
};
},
_setRendererOptions: function _setRendererOptions() {
this._renderer.setOptions(this._getRendererOptions());
},
svg: function svg() {
return this._renderer.svg();
},
getSize: function getSize() {
var canvas = this._canvas || {};
return { width: canvas.width, height: canvas.height };
},
isReady: getFalse,
_dataIsReady: getTrue,
_resetIsReady: function _resetIsReady() {
this.isReady = getFalse;
},
_drawn: function _drawn() {
var that = this;
that.isReady = getFalse;
if (that._dataIsReady()) {
that._renderer.onEndAnimation(function () {
that.isReady = getTrue;
});
}
that._eventTrigger("drawn", {});
}
});
helpers.replaceInherit(module.exports);
function createEventTrigger(eventsMap, callbackGetter) {
var triggers = {};
each(eventsMap, function (name, info) {
if (info.name) {
createEvent(name);
}
});
var changes;
triggerEvent.change = function (name) {
var eventInfo = eventsMap[name];
if (eventInfo) {
(changes = changes || {})[name] = eventInfo;
}
return !!eventInfo;
};
triggerEvent.applyChanges = function () {
if (changes) {
each(changes, function (name, eventInfo) {
createEvent(eventInfo.newName || name);
});
changes = null;
}
};
triggerEvent.dispose = function () {
eventsMap = callbackGetter = triggers = null;
};
return triggerEvent;
function createEvent(name) {
var eventInfo = eventsMap[name];
triggers[eventInfo.name] = callbackGetter(name);
}
function triggerEvent(name, arg, complete) {
triggers[name](arg);
complete && complete();
}
}
///#DEBUG
module.exports.DEBUG_createEventTrigger = createEventTrigger;
module.exports.DEBUG_createIncidentOccurred = createIncidentOccurred;
module.exports.DEBUG_stub_createIncidentOccurred = function (stub) {
createIncidentOccurred = stub;
};
module.exports.DEBUG_restore_createIncidentOccurred = function () {
createIncidentOccurred = module.exports.DEBUG_createIncidentOccurred;
};
module.exports.DEBUG_createResizeHandler = createResizeHandler;
///#ENDDEBUG
;