devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
622 lines (501 loc) • 22.2 kB
JavaScript
"use strict";
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var $ = require("../../core/renderer"),
eventsEngine = require("../../events/core/events_engine"),
Config = require("../../core/config"),
registerComponentCallbacks = require("../../core/component_registrator_callbacks"),
Class = require("../../core/class"),
Callbacks = require("../../core/utils/callbacks"),
typeUtils = require("../../core/utils/type"),
each = require("../../core/utils/iterator").each,
inArray = require("../../core/utils/array").inArray,
Locker = require("../../core/utils/locker"),
Widget = require("../../ui/widget/ui.widget"),
Editor = require("../../ui/editor/editor"),
NgTemplate = require("./template"),
ngModule = require("./module"),
CollectionWidget = require("../../ui/collection/ui.collection_widget.edit"),
compileSetter = require("../../core/utils/data").compileSetter,
compileGetter = require("../../core/utils/data").compileGetter,
extendFromObject = require("../../core/utils/extend").extendFromObject,
inflector = require("../../core/utils/inflector"),
errors = require("../../core/errors");
var ITEM_ALIAS_ATTRIBUTE_NAME = "dxItemAlias",
SKIP_APPLY_ACTION_CATEGORIES = ["rendering"],
NG_MODEL_OPTION = "value";
var safeApply = function safeApply(func, scope) {
if (scope.$root.$$phase) {
return func(scope);
} else {
return scope.$apply(function () {
return func(scope);
});
}
};
/**
* @name domcomponentoptions.bindingOptions
* @publicName bindingOptions
* @type object
* @default {}
*/
var ComponentBuilder = Class.inherit({
ctor: function ctor(options) {
this._componentDisposing = Callbacks();
this._optionChangedCallbacks = Callbacks();
this._ngLocker = new Locker();
this._scope = options.scope;
this._$element = options.$element;
this._$templates = options.$templates;
this._componentClass = options.componentClass;
this._parse = options.parse;
this._compile = options.compile;
this._itemAlias = options.itemAlias;
this._transcludeFn = options.transcludeFn;
this._digestCallbacks = options.dxDigestCallbacks;
this._normalizeOptions(options.ngOptions);
this._initComponentBindings();
this._initComponent(this._scope);
if (!options.ngOptions) {
this._addOptionsStringWatcher(options.ngOptionsString);
}
},
_addOptionsStringWatcher: function _addOptionsStringWatcher(optionsString) {
var that = this;
var clearOptionsStringWatcher = that._scope.$watch(optionsString, function (newOptions) {
if (!newOptions) {
return;
}
clearOptionsStringWatcher();
that._normalizeOptions(newOptions);
that._initComponentBindings();
that._component.option(that._evalOptions(that._scope));
});
that._componentDisposing.add(clearOptionsStringWatcher);
},
_normalizeOptions: function _normalizeOptions(options) {
var that = this;
that._ngOptions = extendFromObject({}, options);
if (!options) {
return;
}
if (!options.hasOwnProperty('bindingOptions') && options.bindingOptions) {
that._ngOptions.bindingOptions = options.bindingOptions;
}
if (options.bindingOptions) {
each(options.bindingOptions, function (key, value) {
if (typeUtils.type(value) === 'string') {
that._ngOptions.bindingOptions[key] = { dataPath: value };
}
});
}
},
_initComponent: function _initComponent(scope) {
this._component = new this._componentClass(this._$element, this._evalOptions(scope));
this._component._isHidden = true;
this._handleDigestPhase();
},
_handleDigestPhase: function _handleDigestPhase() {
var that = this,
beginUpdate = function beginUpdate() {
that._component.beginUpdate();
},
endUpdate = function endUpdate() {
that._component.endUpdate();
};
that._digestCallbacks.begin.add(beginUpdate);
that._digestCallbacks.end.add(endUpdate);
that._componentDisposing.add(function () {
that._digestCallbacks.begin.remove(beginUpdate);
that._digestCallbacks.end.remove(endUpdate);
});
},
_initComponentBindings: function _initComponentBindings() {
var that = this,
optionDependencies = {};
if (!that._ngOptions.bindingOptions) {
return;
}
each(that._ngOptions.bindingOptions, function (optionPath, value) {
var separatorIndex = optionPath.search(/\[|\./),
optionForSubscribe = separatorIndex > -1 ? optionPath.substring(0, separatorIndex) : optionPath,
prevWatchMethod,
clearWatcher,
valuePath = value.dataPath,
deepWatch = true,
forcePlainWatchMethod = false;
if (value.deep !== undefined) {
forcePlainWatchMethod = deepWatch = !!value.deep;
}
if (!optionDependencies[optionForSubscribe]) {
optionDependencies[optionForSubscribe] = {};
}
optionDependencies[optionForSubscribe][optionPath] = valuePath;
var watchCallback = function watchCallback(newValue, oldValue) {
if (that._ngLocker.locked(optionPath)) {
return;
}
that._ngLocker.obtain(optionPath);
that._component.option(optionPath, newValue);
updateWatcher();
if (that._component._optionValuesEqual(optionPath, oldValue, newValue) && that._ngLocker.locked(optionPath)) {
that._ngLocker.release(optionPath);
}
};
var updateWatcher = function updateWatcher() {
var watchMethod = Array.isArray(that._scope.$eval(valuePath)) && !forcePlainWatchMethod ? "$watchCollection" : "$watch";
if (prevWatchMethod !== watchMethod) {
if (clearWatcher) {
clearWatcher();
}
clearWatcher = that._scope[watchMethod](valuePath, watchCallback, deepWatch);
prevWatchMethod = watchMethod;
}
};
updateWatcher();
that._componentDisposing.add(clearWatcher);
});
that._optionChangedCallbacks.add(function (args) {
var optionName = args.name,
fullName = args.fullName,
component = args.component;
if (that._ngLocker.locked(fullName)) {
that._ngLocker.release(fullName);
return;
}
if (!optionDependencies || !optionDependencies[optionName]) {
return;
}
var isActivePhase = that._scope.$root.$$phase;
var obtainOption = function obtainOption() {
that._ngLocker.obtain(fullName);
};
if (isActivePhase) {
that._digestCallbacks.begin.add(obtainOption);
} else {
obtainOption();
}
safeApply(function () {
each(optionDependencies[optionName], function (optionPath, valuePath) {
if (!that._optionsAreLinked(fullName, optionPath)) {
return;
}
var value = component.option(optionPath);
that._parse(valuePath).assign(that._scope, value);
var scopeValue = that._parse(valuePath)(that._scope);
if (scopeValue !== value) {
args.component.option(optionPath, scopeValue);
}
});
}, that._scope);
var releaseOption = function releaseOption() {
if (that._ngLocker.locked(fullName)) {
that._ngLocker.release(fullName);
}
that._digestCallbacks.begin.remove(obtainOption);
that._digestCallbacks.end.remove(releaseOption);
};
if (isActivePhase) {
that._digestCallbacks.end.addPrioritized(releaseOption);
} else {
releaseOption();
}
});
},
_optionsAreNested: function _optionsAreNested(optionPath1, optionPath2) {
var parentSeparator = optionPath1[optionPath2.length];
return optionPath1.indexOf(optionPath2) === 0 && (parentSeparator === "." || parentSeparator === "[");
},
_optionsAreLinked: function _optionsAreLinked(optionPath1, optionPath2) {
if (optionPath1 === optionPath2) return true;
return optionPath1.length > optionPath2.length ? this._optionsAreNested(optionPath1, optionPath2) : this._optionsAreNested(optionPath2, optionPath1);
},
_compilerByTemplate: function _compilerByTemplate(template) {
var that = this,
scopeItemsPath = this._getScopeItemsPath();
return function (options) {
var $resultMarkup = $(template).clone(),
dataIsScope = options.model && options.model.constructor === that._scope.$root.constructor,
templateScope = dataIsScope ? options.model : options.noModel ? that._scope : that._createScopeWithData(options);
if (scopeItemsPath) {
that._synchronizeScopes(templateScope, scopeItemsPath, options.index);
}
$resultMarkup.appendTo(options.container);
if (!options.noModel) {
eventsEngine.on($resultMarkup, "$destroy", function () {
var destroyAlreadyCalled = !templateScope.$parent;
if (destroyAlreadyCalled) {
return;
}
templateScope.$destroy();
});
}
that._applyAsync(that._compile($resultMarkup, that._transcludeFn), templateScope);
return $resultMarkup;
};
},
_applyAsync: function _applyAsync(func, scope) {
var that = this;
func(scope);
if (!scope.$root.$$phase) {
if (!that._renderingTimer) {
that._renderingTimer = setTimeout(function () {
scope.$apply();
that._renderingTimer = null;
});
}
that._componentDisposing.add(function () {
clearTimeout(that._renderingTimer);
});
}
},
_getScopeItemsPath: function _getScopeItemsPath() {
if (this._componentClass.subclassOf(CollectionWidget) && this._ngOptions.bindingOptions && this._ngOptions.bindingOptions.items) {
return this._ngOptions.bindingOptions.items.dataPath;
}
},
_createScopeWithData: function _createScopeWithData(options) {
var newScope = this._scope.$new();
if (this._itemAlias) {
newScope[this._itemAlias] = options.model;
}
if (typeUtils.isDefined(options.index)) {
newScope.$index = options.index;
}
return newScope;
},
_synchronizeScopes: function _synchronizeScopes(itemScope, parentPrefix, itemIndex) {
if (this._itemAlias && _typeof(itemScope[this._itemAlias]) !== "object") {
this._synchronizeScopeField({
parentScope: this._scope,
childScope: itemScope,
fieldPath: this._itemAlias,
parentPrefix: parentPrefix,
itemIndex: itemIndex
});
}
},
_synchronizeScopeField: function _synchronizeScopeField(args) {
var parentScope = args.parentScope,
childScope = args.childScope,
fieldPath = args.fieldPath,
parentPrefix = args.parentPrefix,
itemIndex = args.itemIndex;
var innerPathSuffix = fieldPath === this._itemAlias ? "" : "." + fieldPath,
collectionField = itemIndex !== undefined,
optionOuterBag = [parentPrefix],
optionOuterPath;
if (collectionField) {
if (!typeUtils.isNumeric(itemIndex)) return;
optionOuterBag.push("[", itemIndex, "]");
}
optionOuterBag.push(innerPathSuffix);
optionOuterPath = optionOuterBag.join("");
var clearParentWatcher = parentScope.$watch(optionOuterPath, function (newValue, oldValue) {
if (newValue !== oldValue) {
compileSetter(fieldPath)(childScope, newValue);
}
});
var clearItemWatcher = childScope.$watch(fieldPath, function (newValue, oldValue) {
if (newValue !== oldValue) {
if (collectionField && !compileGetter(parentPrefix)(parentScope)[itemIndex]) {
clearItemWatcher();
return;
}
compileSetter(optionOuterPath)(parentScope, newValue);
}
});
this._componentDisposing.add([clearParentWatcher, clearItemWatcher]); // TODO: test
},
_evalOptions: function _evalOptions(scope) {
var result = extendFromObject({}, this._ngOptions);
delete result.bindingOptions;
if (this._ngOptions.bindingOptions) {
each(this._ngOptions.bindingOptions, function (key, value) {
result[key] = scope.$eval(value.dataPath);
});
}
result._optionChangedCallbacks = this._optionChangedCallbacks;
result._disposingCallbacks = this._componentDisposing;
result.onActionCreated = function (component, action, config) {
if (config && inArray(config.category, SKIP_APPLY_ACTION_CATEGORIES) > -1) {
return action;
}
var wrappedAction = function wrappedAction() {
var that = this,
args = arguments;
if (!scope || !scope.$root || scope.$root.$$phase) {
return action.apply(that, args);
}
return safeApply(function () {
return action.apply(that, args);
}, scope);
};
return wrappedAction;
};
result.beforeActionExecute = result.onActionCreated;
result.nestedComponentOptions = function (component) {
return {
templatesRenderAsynchronously: component.option("templatesRenderAsynchronously"),
forceApplyBindings: component.option("forceApplyBindings"),
modelByElement: component.option("modelByElement"),
onActionCreated: component.option("onActionCreated"),
beforeActionExecute: component.option("beforeActionExecute"),
nestedComponentOptions: component.option("nestedComponentOptions")
};
};
result.templatesRenderAsynchronously = true;
if (Config().wrapActionsBeforeExecute) {
result.forceApplyBindings = function () {
safeApply(function () {}, scope);
};
}
result.integrationOptions = {
createTemplate: function (element) {
return new NgTemplate(element, this._compilerByTemplate.bind(this));
}.bind(this),
watchMethod: function (fn, callback, options) {
options = options || {};
var immediateValue;
var skipCallback = options.skipImmediate;
var disposeWatcher = scope.$watch(function () {
var value = fn();
if (value instanceof Date) {
value = value.valueOf();
}
return value;
}, function (newValue) {
var isSameValue = immediateValue === newValue;
if (!skipCallback && (!isSameValue || isSameValue && options.deep)) {
callback(newValue);
}
skipCallback = false;
}, options.deep);
if (!skipCallback) {
immediateValue = fn();
callback(immediateValue);
}
if (Config().wrapActionsBeforeExecute) {
this._applyAsync(function () {}, scope);
}
return disposeWatcher;
}.bind(this),
templates: {
"dx-polymorph-widget": {
render: function (options) {
var widgetName = options.model.widget;
if (!widgetName) {
return;
}
if (widgetName === "button" || widgetName === "tabs" || widgetName === "dropDownMenu") {
var deprecatedName = widgetName;
widgetName = inflector.camelize("dx-" + widgetName);
errors.log("W0001", "dxToolbar - 'widget' item field", deprecatedName, "16.1", "Use: '" + widgetName + "' instead");
}
var markup = $("<div>").attr(inflector.dasherize(widgetName), "options").get(0);
var newScope = this._scope.$new();
newScope.options = options.model.options;
options.container.append(markup);
this._compile(markup)(newScope);
}.bind(this)
}
}
};
result.modelByElement = function () {
return scope;
};
return result;
}
});
ComponentBuilder = ComponentBuilder.inherit({
ctor: function ctor(options) {
this._componentName = options.componentName;
this._ngModel = options.ngModel;
this._ngModelController = options.ngModelController;
this.callBase.apply(this, arguments);
},
_isNgModelRequired: function _isNgModelRequired() {
return this._componentClass.subclassOf(Editor) && this._ngModel;
},
_initComponentBindings: function _initComponentBindings() {
this.callBase.apply(this, arguments);
this._initNgModelBinding();
},
_initNgModelBinding: function _initNgModelBinding() {
if (!this._isNgModelRequired()) {
return;
}
var that = this;
var clearNgModelWatcher = this._scope.$watch(this._ngModel, function (newValue, oldValue) {
if (that._ngLocker.locked(NG_MODEL_OPTION)) {
return;
}
if (newValue === oldValue) {
return;
}
that._component.option(NG_MODEL_OPTION, newValue);
});
that._optionChangedCallbacks.add(function (args) {
that._ngLocker.obtain(NG_MODEL_OPTION);
try {
if (args.name !== NG_MODEL_OPTION) {
return;
}
that._ngModelController.$setViewValue(args.value);
} finally {
if (that._ngLocker.locked(NG_MODEL_OPTION)) {
that._ngLocker.release(NG_MODEL_OPTION);
}
}
});
this._componentDisposing.add(clearNgModelWatcher);
},
_evalOptions: function _evalOptions() {
if (!this._isNgModelRequired()) {
return this.callBase.apply(this, arguments);
}
var result = this.callBase.apply(this, arguments);
result[NG_MODEL_OPTION] = this._parse(this._ngModel)(this._scope);
return result;
}
});
var registeredComponents = {};
var registerComponentDirective = function registerComponentDirective(name) {
var priority = name !== "dxValidator" ? 1 : 10;
ngModule.directive(name, ["$compile", "$parse", "dxDigestCallbacks", function ($compile, $parse, dxDigestCallbacks) {
return {
restrict: "A",
require: "^?ngModel",
priority: priority,
compile: function compile($element) {
var componentClass = registeredComponents[name],
$content = componentClass.subclassOf(Widget) ? $element.contents().detach() : null;
return function (scope, $element, attrs, ngModelController, transcludeFn) {
$element.append($content);
safeApply(function () {
new ComponentBuilder({
componentClass: componentClass,
componentName: name,
compile: $compile,
parse: $parse,
$element: $element,
scope: scope,
ngOptionsString: attrs[name],
ngOptions: attrs[name] ? scope.$eval(attrs[name]) : {},
ngModel: attrs.ngModel,
ngModelController: ngModelController,
transcludeFn: transcludeFn,
itemAlias: attrs[ITEM_ALIAS_ATTRIBUTE_NAME],
dxDigestCallbacks: dxDigestCallbacks
});
}, scope);
};
}
};
}]);
};
registerComponentCallbacks.add(function (name, componentClass) {
if (!registeredComponents[name]) {
registerComponentDirective(name);
}
registeredComponents[name] = componentClass;
});