204 lines
5.9 KiB
JavaScript
204 lines
5.9 KiB
JavaScript
import ngModule from '../module';
|
|
import EventEmitter from './event-emitter';
|
|
import {kebabToCamel} from './string';
|
|
|
|
/**
|
|
* Base class for component controllers.
|
|
*/
|
|
export default class Component extends EventEmitter {
|
|
/**
|
|
* Contructor.
|
|
*
|
|
* @param {HTMLElement} $element The main component element
|
|
* @param {$rootScope.Scope} $scope The element scope
|
|
* @param {Function} $transclude The transclusion function
|
|
*/
|
|
constructor($element, $scope, $transclude) {
|
|
super();
|
|
this.$ = $scope;
|
|
|
|
if (!$element) return;
|
|
this.element = $element[0];
|
|
this.element.$ctrl = this;
|
|
this.$element = $element;
|
|
this.$transclude = $transclude;
|
|
this.classList = this.element.classList;
|
|
|
|
const constructor = this.constructor;
|
|
const $options = constructor.$options;
|
|
|
|
if ($options && $options.installClasses)
|
|
this.classList.add(...this.constructor.$classNames);
|
|
|
|
if ($transclude && constructor.slotTemplates) {
|
|
for (let slotTemplate of constructor.slotTemplates)
|
|
this.fillSlots(slotTemplate);
|
|
}
|
|
}
|
|
|
|
$postLink() {
|
|
if (!this.$element) return;
|
|
let attrs = this.$element[0].attributes;
|
|
let $scope = this.$;
|
|
for (let attr of attrs) {
|
|
if (!attr.name.startsWith('on-')) continue;
|
|
let eventName = kebabToCamel(attr.name.substr(3));
|
|
let callback = locals => $scope.$parent.$eval(attr.nodeValue, locals);
|
|
this.on(eventName, callback);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The component owner window.
|
|
*/
|
|
get window() {
|
|
return this.document.defaultView;
|
|
}
|
|
|
|
/**
|
|
* The component owner document.
|
|
*/
|
|
get document() {
|
|
return this.element.ownerDocument;
|
|
}
|
|
|
|
/**
|
|
* Translates an string.
|
|
*
|
|
* @param {String} string String to translate
|
|
* @param {Array} params Translate parameters
|
|
* @return {String} The translated string
|
|
*/
|
|
$t(string, params) {
|
|
return this.$translate.instant(string, params);
|
|
}
|
|
|
|
/**
|
|
* Fills the default transclude slot.
|
|
*
|
|
* @param {JQElement|String} template The slot template
|
|
*/
|
|
fillDefaultSlot(template) {
|
|
const linkFn = this.$compile(template);
|
|
this.$transclude.$$boundTransclude = this.createBoundTranscludeFn(linkFn);
|
|
}
|
|
|
|
/**
|
|
* Fills a named transclude slot.
|
|
*
|
|
* @param {String} slot The trasnclude slot name
|
|
* @param {JQElement|String} template The slot template
|
|
*/
|
|
fillSlot(slot, template) {
|
|
const linkFn = this.$compile(template);
|
|
const slots = this.$transclude.$$boundTransclude.$$slots;
|
|
slots[slot] = this.createBoundTranscludeFn(linkFn);
|
|
}
|
|
|
|
/**
|
|
* Fills component transclude slots using the passed HTML template string
|
|
* as source.
|
|
*
|
|
* @param {String} template The HTML template string
|
|
*/
|
|
fillSlots(template) {
|
|
const name = this.constructor.$options.name;
|
|
const transclude = this.constructor.$options.transclude;
|
|
|
|
if (!transclude)
|
|
throw new Error(`No transclusion option defined in '${name}'`);
|
|
if (!this.$transclude)
|
|
throw new Error(`No $transclude injected in '${name}'`);
|
|
|
|
let slotMap = {};
|
|
for (let slotName in transclude) {
|
|
let slotTag = transclude[slotName].match(/\w+$/)[0];
|
|
slotMap[slotTag] = slotName;
|
|
}
|
|
|
|
const $template = angular.element(template);
|
|
for (let i = 0; i < $template.length; i++) {
|
|
let slotElement = $template[i];
|
|
if (slotElement.nodeType != Node.ELEMENT_NODE) continue;
|
|
let tagName = kebabToCamel(slotElement.tagName.toLowerCase());
|
|
|
|
if (tagName == 'default')
|
|
this.fillDefaultSlot(slotElement.childNodes);
|
|
else {
|
|
let slotName = slotMap[tagName];
|
|
if (!slotName)
|
|
throw new Error(`No slot found for '${tagName}' in '${name}'`);
|
|
this.fillSlot(slotName, slotElement);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a bounded transclude function from a linking function.
|
|
*
|
|
* @param {Function} linkFn The linking function
|
|
* @return {Function} The bounded transclude function
|
|
*/
|
|
createBoundTranscludeFn(linkFn) {
|
|
let scope = this.$;
|
|
let previousBoundTranscludeFn = this.$transclude.$$boundTransclude;
|
|
|
|
function vnBoundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
|
|
if (!transcludedScope) {
|
|
transcludedScope = scope.$new(false, containingScope);
|
|
transcludedScope.$$transcluded = true;
|
|
}
|
|
return linkFn(transcludedScope, cloneFn, {
|
|
parentBoundTranscludeFn: previousBoundTranscludeFn,
|
|
transcludeControllers: controllers,
|
|
futureParentElement: futureParentElement
|
|
});
|
|
}
|
|
vnBoundTranscludeFn.$$slots = previousBoundTranscludeFn.$$slots;
|
|
|
|
return vnBoundTranscludeFn;
|
|
}
|
|
|
|
copySlot(slot, $transclude) {
|
|
this.$transclude.$$boundTransclude.$$slots[slot] =
|
|
$transclude.$$boundTransclude.$$slots[slot];
|
|
}
|
|
}
|
|
Component.$inject = ['$element', '$scope'];
|
|
|
|
/*
|
|
* Automatically adds the most used services to the prototype, so they are
|
|
* available as component properties.
|
|
*/
|
|
function runFn(...args) {
|
|
const proto = Component.prototype;
|
|
|
|
for (let i = 0; i < runFn.$inject.length; i++)
|
|
proto[runFn.$inject[i]] = args[i];
|
|
|
|
Object.assign(proto, {
|
|
$params: proto.$stateParams
|
|
});
|
|
}
|
|
runFn.$inject = [
|
|
'$translate',
|
|
'$q',
|
|
'$http',
|
|
'$httpParamSerializer',
|
|
'$state',
|
|
'$stateParams',
|
|
'$timeout',
|
|
'$transitions',
|
|
'$compile',
|
|
'$filter',
|
|
'$interpolate',
|
|
'$window',
|
|
'vnApp',
|
|
'vnToken',
|
|
'vnConfig',
|
|
'vnModules',
|
|
'aclService'
|
|
];
|
|
|
|
ngModule.run(runFn);
|