devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
525 lines (524 loc) • 23.4 kB
JavaScript
/**
* DevExtreme (esm/integration/angular/component_registrator.js)
* Version: 21.1.4
* Build date: Mon Jun 21 2021
*
* Copyright (c) 2012 - 2021 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
import $ from "../../core/renderer";
import angular from "angular";
import eventsEngine from "../../events/core/events_engine";
import Config from "../../core/config";
import registerComponentCallbacks from "../../core/component_registrator_callbacks";
import Class from "../../core/class";
import Callbacks from "../../core/utils/callbacks";
import {
type,
isDefined,
isNumeric
} from "../../core/utils/type";
import {
each
} from "../../core/utils/iterator";
import {
inArray
} from "../../core/utils/array";
import Locker from "../../core/utils/locker";
import Editor from "../../ui/editor/editor";
import {
NgTemplate
} from "./template";
import ngModule from "./module";
import CollectionWidget from "../../ui/collection/ui.collection_widget.edit";
import {
compileGetter,
compileSetter
} from "../../core/utils/data";
import {
extendFromObject
} from "../../core/utils/extend";
import {
equals
} from "../../core/utils/comparator";
import {
dasherize
} from "../../core/utils/inflector";
var ITEM_ALIAS_ATTRIBUTE_NAME = "dxItemAlias";
var SKIP_APPLY_ACTION_CATEGORIES = ["rendering"];
var NG_MODEL_OPTION = "value";
if (angular) {
var safeApply = (func, scope) => {
if (scope.$root.$$phase) {
return func(scope)
} else {
return scope.$apply(() => func(scope))
}
};
var getClassMethod = (initClass, methodName) => {
var hasParentProperty = Object.prototype.hasOwnProperty.bind(initClass)("parent");
var isES6Class = !hasParentProperty && initClass.parent;
if (isES6Class) {
var baseClass = Object.getPrototypeOf(initClass);
return baseClass.prototype[methodName] ? () => baseClass.prototype[methodName]() : getClassMethod(baseClass, methodName)
} else {
var method = initClass.parent.prototype[methodName];
if (method) {
return () => method()
}
if (!method || !initClass.parent.subclassOf) {
return () => {}
}
return getClassMethod(initClass.parent, methodName)
}
};
var ComponentBuilder = Class.inherit({
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(optionsString) {
var clearOptionsStringWatcher = this._scope.$watch(optionsString, newOptions => {
if (!newOptions) {
return
}
clearOptionsStringWatcher();
this._normalizeOptions(newOptions);
this._initComponentBindings();
this._component.option(this._evalOptions(this._scope))
});
this._componentDisposing.add(clearOptionsStringWatcher)
},
_normalizeOptions(options) {
this._ngOptions = extendFromObject({}, options);
if (!options) {
return
}
if (!Object.prototype.hasOwnProperty.call(options, "bindingOptions") && options.bindingOptions) {
this._ngOptions.bindingOptions = options.bindingOptions
}
if (options.bindingOptions) {
each(options.bindingOptions, (key, value) => {
if ("string" === type(value)) {
this._ngOptions.bindingOptions[key] = {
dataPath: value
}
}
})
}
},
_initComponent(scope) {
this._component = new this._componentClass(this._$element, this._evalOptions(scope));
this._component._isHidden = true;
this._handleDigestPhase()
},
_handleDigestPhase() {
var beginUpdate = () => {
this._component.beginUpdate()
};
var endUpdate = () => {
this._component.endUpdate()
};
this._digestCallbacks.begin.add(beginUpdate);
this._digestCallbacks.end.add(endUpdate);
this._componentDisposing.add(() => {
this._digestCallbacks.begin.remove(beginUpdate);
this._digestCallbacks.end.remove(endUpdate)
})
},
_initComponentBindings() {
var optionDependencies = {};
if (!this._ngOptions.bindingOptions) {
return
}
each(this._ngOptions.bindingOptions, (optionPath, value) => {
var separatorIndex = optionPath.search(/\[|\./);
var optionForSubscribe = separatorIndex > -1 ? optionPath.substring(0, separatorIndex) : optionPath;
var prevWatchMethod;
var clearWatcher;
var valuePath = value.dataPath;
var deepWatch = true;
var forcePlainWatchMethod = false;
if (void 0 !== value.deep) {
forcePlainWatchMethod = deepWatch = !!value.deep
}
if (!optionDependencies[optionForSubscribe]) {
optionDependencies[optionForSubscribe] = {}
}
optionDependencies[optionForSubscribe][optionPath] = valuePath;
var updateWatcher = () => {
var watchMethod = Array.isArray(this._scope.$eval(valuePath)) && !forcePlainWatchMethod ? "$watchCollection" : "$watch";
if (prevWatchMethod !== watchMethod) {
if (clearWatcher) {
clearWatcher()
}
clearWatcher = this._scope[watchMethod](valuePath, (newValue, oldValue) => {
if (this._ngLocker.locked(optionPath)) {
return
}
this._ngLocker.obtain(optionPath);
this._component.option(optionPath, newValue);
updateWatcher();
if (equals(oldValue, newValue) && this._ngLocker.locked(optionPath)) {
this._ngLocker.release(optionPath)
}
}, deepWatch);
prevWatchMethod = watchMethod
}
};
updateWatcher();
this._componentDisposing.add(clearWatcher)
});
this._optionChangedCallbacks.add(args => {
var optionName = args.name;
var fullName = args.fullName;
var component = args.component;
if (this._ngLocker.locked(fullName)) {
this._ngLocker.release(fullName);
return
}
if (!optionDependencies || !optionDependencies[optionName]) {
return
}
var isActivePhase = this._scope.$root.$$phase;
var obtainOption = () => {
this._ngLocker.obtain(fullName)
};
if (isActivePhase) {
this._digestCallbacks.begin.add(obtainOption)
} else {
obtainOption()
}
safeApply(() => {
each(optionDependencies[optionName], (optionPath, valuePath) => {
if (!this._optionsAreLinked(fullName, optionPath)) {
return
}
var value = component.option(optionPath);
this._parse(valuePath).assign(this._scope, value);
var scopeValue = this._parse(valuePath)(this._scope);
if (scopeValue !== value) {
args.component.option(optionPath, scopeValue)
}
})
}, this._scope);
var releaseOption = () => {
if (this._ngLocker.locked(fullName)) {
this._ngLocker.release(fullName)
}
this._digestCallbacks.begin.remove(obtainOption);
this._digestCallbacks.end.remove(releaseOption)
};
if (isActivePhase) {
this._digestCallbacks.end.addPrioritized(releaseOption)
} else {
releaseOption()
}
})
},
_optionsAreNested(optionPath1, optionPath2) {
var parentSeparator = optionPath1[optionPath2.length];
return 0 === optionPath1.indexOf(optionPath2) && ("." === parentSeparator || "[" === parentSeparator)
},
_optionsAreLinked(optionPath1, optionPath2) {
if (optionPath1 === optionPath2) {
return true
}
return optionPath1.length > optionPath2.length ? this._optionsAreNested(optionPath1, optionPath2) : this._optionsAreNested(optionPath2, optionPath1)
},
_compilerByTemplate(template) {
var scopeItemsPath = this._getScopeItemsPath();
return options => {
var $resultMarkup = $(template).clone();
var dataIsScope = options.model && options.model.constructor === this._scope.$root.constructor;
var templateScope = dataIsScope ? options.model : options.noModel ? this._scope : this._createScopeWithData(options);
if (scopeItemsPath) {
this._synchronizeScopes(templateScope, scopeItemsPath, options.index)
}
$resultMarkup.appendTo(options.container);
if (!options.noModel) {
eventsEngine.on($resultMarkup, "$destroy", () => {
var destroyAlreadyCalled = !templateScope.$parent;
if (destroyAlreadyCalled) {
return
}
templateScope.$destroy()
})
}
var ngTemplate = this._compile($resultMarkup, this._transcludeFn);
this._applyAsync(scope => {
ngTemplate(scope, null, {
parentBoundTranscludeFn: this._transcludeFn
})
}, templateScope);
return $resultMarkup
}
},
_applyAsync(func, scope) {
func(scope);
if (!scope.$root.$$phase) {
if (!this._renderingTimer) {
var clearRenderingTimer = () => {
clearTimeout(this._renderingTimer)
};
this._renderingTimer = setTimeout(() => {
scope.$apply();
this._renderingTimer = null;
this._componentDisposing.remove(clearRenderingTimer)
});
this._componentDisposing.add(clearRenderingTimer)
}
}
},
_getScopeItemsPath() {
if (this._componentClass.subclassOf(CollectionWidget) && this._ngOptions.bindingOptions && this._ngOptions.bindingOptions.items) {
return this._ngOptions.bindingOptions.items.dataPath
}
},
_createScopeWithData(options) {
var newScope = this._scope.$new();
if (this._itemAlias) {
newScope[this._itemAlias] = options.model
}
if (isDefined(options.index)) {
newScope.$index = options.index
}
return newScope
},
_synchronizeScopes(itemScope, parentPrefix, itemIndex) {
if (this._itemAlias && "object" !== typeof itemScope[this._itemAlias]) {
this._synchronizeScopeField({
parentScope: this._scope,
childScope: itemScope,
fieldPath: this._itemAlias,
parentPrefix: parentPrefix,
itemIndex: itemIndex
})
}
},
_synchronizeScopeField(args) {
var parentScope = args.parentScope;
var childScope = args.childScope;
var fieldPath = args.fieldPath;
var parentPrefix = args.parentPrefix;
var itemIndex = args.itemIndex;
var innerPathSuffix = fieldPath === this._itemAlias ? "" : "." + fieldPath;
var collectionField = void 0 !== itemIndex;
var optionOuterBag = [parentPrefix];
if (collectionField) {
if (!isNumeric(itemIndex)) {
return
}
optionOuterBag.push("[", itemIndex, "]")
}
optionOuterBag.push(innerPathSuffix);
var optionOuterPath = optionOuterBag.join("");
var clearParentWatcher = parentScope.$watch(optionOuterPath, (newValue, oldValue) => {
if (newValue !== oldValue) {
compileSetter(fieldPath)(childScope, newValue)
}
});
var clearItemWatcher = childScope.$watch(fieldPath, (newValue, oldValue) => {
if (newValue !== oldValue) {
if (collectionField && !compileGetter(parentPrefix)(parentScope)[itemIndex]) {
clearItemWatcher();
return
}
compileSetter(optionOuterPath)(parentScope, newValue)
}
});
this._componentDisposing.add([clearParentWatcher, clearItemWatcher])
},
_evalOptions(scope) {
var result = extendFromObject({}, this._ngOptions);
delete result.bindingOptions;
if (this._ngOptions.bindingOptions) {
each(this._ngOptions.bindingOptions, (key, value) => {
result[key] = scope.$eval(value.dataPath)
})
}
result._optionChangedCallbacks = this._optionChangedCallbacks;
result._disposingCallbacks = this._componentDisposing;
result.onActionCreated = (component, action, config) => {
if (config && inArray(config.category, SKIP_APPLY_ACTION_CATEGORIES) > -1) {
return action
}
return function() {
var args = arguments;
if (!scope || !scope.$root || scope.$root.$$phase) {
return action.apply(this, args)
}
return safeApply(() => action.apply(this, args), scope)
}
};
result.beforeActionExecute = result.onActionCreated;
result.nestedComponentOptions = component => ({
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 = () => {
safeApply(() => {}, scope)
}
}
result.integrationOptions = {
createTemplate: element => new NgTemplate(element, this._compilerByTemplate.bind(this)),
watchMethod: (fn, callback, options) => {
options = options || {};
var immediateValue;
var skipCallback = options.skipImmediate;
var disposeWatcher = scope.$watch(() => {
var value = fn();
if (value instanceof Date) {
value = value.valueOf()
}
return value
}, 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(() => {}, scope)
}
return disposeWatcher
},
templates: {
"dx-polymorph-widget": {
render: options => {
var widgetName = options.model.widget;
if (!widgetName) {
return
}
var markup = $("<div>").attr(dasherize(widgetName), "options").get(0);
var newScope = this._scope.$new();
newScope.options = options.model.options;
options.container.append(markup);
this._compile(markup)(newScope)
}
}
}
};
result.modelByElement = () => scope;
return result
}
});
ComponentBuilder = ComponentBuilder.inherit({
ctor(options) {
this._componentName = options.componentName;
this._ngModel = options.ngModel;
this._ngModelController = options.ngModelController;
this.callBase(...arguments)
},
_isNgModelRequired() {
return Editor.isEditor(this._componentClass.prototype) && this._ngModel
},
_initComponentBindings() {
this.callBase(...arguments);
this._initNgModelBinding()
},
_initNgModelBinding() {
if (!this._isNgModelRequired()) {
return
}
var clearNgModelWatcher = this._scope.$watch(this._ngModel, (newValue, oldValue) => {
if (this._ngLocker.locked(NG_MODEL_OPTION)) {
return
}
if (newValue === oldValue) {
return
}
this._component.option(NG_MODEL_OPTION, newValue)
});
this._optionChangedCallbacks.add(args => {
this._ngLocker.obtain(NG_MODEL_OPTION);
try {
if (args.name !== NG_MODEL_OPTION) {
return
}
this._ngModelController.$setViewValue(args.value)
} finally {
if (this._ngLocker.locked(NG_MODEL_OPTION)) {
this._ngLocker.release(NG_MODEL_OPTION)
}
}
});
this._componentDisposing.add(clearNgModelWatcher)
},
_evalOptions() {
if (!this._isNgModelRequired()) {
return this.callBase(...arguments)
}
var result = this.callBase(...arguments);
result[NG_MODEL_OPTION] = this._parse(this._ngModel)(this._scope);
return result
}
});
var registeredComponents = {};
var registerComponentDirective = name => {
var priority = "dxValidator" !== name ? 1 : 10;
ngModule.directive(name, ["$compile", "$parse", "dxDigestCallbacks", ($compile, $parse, dxDigestCallbacks) => ({
restrict: "A",
require: "^?ngModel",
priority: priority,
compile($element) {
var componentClass = registeredComponents[name];
var useTemplates = componentClass.prototype._useTemplates ? componentClass.prototype._useTemplates() : getClassMethod(componentClass, "_useTemplates")();
var $content = useTemplates ? $element.contents().detach() : null;
return (scope, $element, attrs, ngModelController, transcludeFn) => {
$element.append($content);
safeApply(() => {
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((name, componentClass) => {
if (!registeredComponents[name]) {
registerComponentDirective(name)
}
registeredComponents[name] = componentClass
})
}