UNPKG

13.5 kBPlain TextView Raw
1import {NumberSequence, Utils as _} from "../utils";
2import {Context} from "../context/context";
3import {BeanStub} from "../context/beanStub";
4import {IComponent} from "../interfaces/iComponent";
5import {AgEvent} from "../events";
6
7let compIdSequence = new NumberSequence();
8
9export interface VisibleChangedEvent extends AgEvent {
10 visible: boolean;
11}
12
13interface AttrLists {
14 normal: NameValue [];
15 events: NameValue [];
16 bindings: NameValue [];
17}
18
19interface NameValue {
20 name: string;
21 value: string;
22}
23
24export class Component extends BeanStub implements IComponent<any> {
25
26 public static EVENT_VISIBLE_CHANGED = 'visibleChanged';
27
28 private eGui: HTMLElement;
29
30 private childComponents: IComponent<any>[] = [];
31
32 private annotatedEventListeners: any[] = [];
33
34 private visible = true;
35
36 // unique id for this row component. this is used for getting a reference to the HTML dom.
37 // we cannot use the RowNode id as this is not unique (due to animation, old rows can be lying
38 // around as we create a new rowComp instance for the same row node).
39 private compId = compIdSequence.next();
40
41 constructor(template?: string) {
42 super();
43 if (template) {
44 this.setTemplate(template);
45 }
46 }
47
48 public getCompId(): number {
49 return this.compId;
50 }
51
52 public instantiate(context: Context): void {
53 this.instantiateRecurse(this.getGui(), context);
54 }
55
56 private instantiateRecurse(parentNode: Element, context: Context): void {
57
58 // we MUST take a copy of the list first, as the 'swapComponentForNode' adds comments into the DOM
59 // which messes up the traversal order of the children.
60 let childNodeList: Node[] = _.copyNodeList(parentNode.childNodes);
61
62 childNodeList.forEach(childNode => {
63 let childComp = context.createComponent(<Element>childNode, (childComp)=> {
64 let attrList = this.getAttrLists(<Element>childNode);
65 this.copyAttributesFromNode(attrList, childComp.getGui());
66 this.createChildAttributes(attrList, childComp);
67 this.addEventListenersToComponent(attrList, childComp);
68 });
69 if (childComp) {
70 this.swapComponentForNode(childComp, parentNode, childNode);
71 } else {
72 if (childNode.childNodes) {
73 this.instantiateRecurse(<Element>childNode, context);
74 }
75 if (childNode instanceof HTMLElement) {
76 let attrList = this.getAttrLists(<Element>childNode);
77 this.addEventListenersToElement(attrList, <HTMLElement>childNode);
78 }
79 }
80 });
81 }
82
83 private getAttrLists(child: Element): AttrLists {
84 let res: AttrLists = {
85 bindings: [],
86 events: [],
87 normal: []
88 };
89 _.iterateNamedNodeMap(child.attributes,
90 (name: string, value: string) => {
91 let firstCharacter = name.substr(0,1);
92 if (firstCharacter==='(') {
93 let eventName = name.replace('(', '').replace(')', '');
94 res.events.push({
95 name: eventName,
96 value: value
97 });
98 } else if (firstCharacter==='[') {
99 let bindingName = name.replace('[', '').replace(']', '');
100 res.bindings.push({
101 name: bindingName,
102 value: value
103 });
104 } else {
105 res.normal.push({
106 name: name,
107 value: value
108 });
109 }
110 }
111 );
112 return res;
113 }
114
115 private addEventListenersToElement(attrLists: AttrLists, element: HTMLElement): void {
116 this.addEventListenerCommon(attrLists, (eventName: string, listener: (event?: any)=>void )=> {
117 this.addDestroyableEventListener(element, eventName, listener);
118 });
119 }
120
121 private addEventListenersToComponent(attrLists: AttrLists, component: Component): void {
122 this.addEventListenerCommon(attrLists, (eventName: string, listener: (event?: any)=>void )=> {
123 this.addDestroyableEventListener(component, eventName, listener);
124 });
125 }
126
127 private addEventListenerCommon(attrLists: AttrLists,
128 callback: (eventName: string, listener: (event?: any)=>void)=>void): void {
129 let methodAliases = this.getAgComponentMetaData('methods');
130
131 attrLists.events.forEach( nameValue => {
132 let methodName = nameValue.value;
133 let methodAlias = _.find(methodAliases, 'alias', methodName);
134
135 let methodNameToUse = _.exists(methodAlias) ? methodAlias.methodName : methodName;
136
137 let listener = (<any>this)[methodNameToUse];
138 if (typeof listener !== 'function') {
139 console.warn('ag-Grid: count not find callback ' + methodName);
140 return;
141 }
142
143 let eventCamelCase = _.hyphenToCamelCase(nameValue.name);
144
145 callback(eventCamelCase, listener.bind(this));
146 });
147 }
148
149 private createChildAttributes(attrLists: AttrLists, child: any): void {
150
151 let childAttributes: any = {};
152
153 attrLists.normal.forEach( nameValue => {
154 let nameCamelCase = _.hyphenToCamelCase(nameValue.name);
155 childAttributes[nameCamelCase] = nameValue.value;
156 });
157
158 attrLists.bindings.forEach( nameValue => {
159 let nameCamelCase = _.hyphenToCamelCase(nameValue.name);
160 childAttributes[nameCamelCase] = (<any>this)[nameValue.value];
161 });
162
163 child.props = childAttributes;
164 }
165
166 private copyAttributesFromNode(attrLists: AttrLists, childNode: Element): void {
167 attrLists.normal.forEach( nameValue => {
168 childNode.setAttribute(nameValue.name, nameValue.value);
169 });
170 }
171
172 private swapComponentForNode(newComponent: Component, parentNode: Element, childNode: Node): void {
173 let eComponent = newComponent.getGui();
174 parentNode.replaceChild(eComponent, childNode);
175 parentNode.insertBefore(document.createComment(childNode.nodeName), eComponent);
176 this.childComponents.push(newComponent);
177 this.swapInComponentForQuerySelectors(newComponent, childNode);
178 }
179
180 private swapInComponentForQuerySelectors(newComponent: Component, childNode: Node): void {
181
182 let thisProto: any = Object.getPrototypeOf(this);
183
184 let thisNoType = <any> this;
185 while (thisProto != null) {
186 let metaData = thisProto.__agComponentMetaData;
187 let currentProtoName = (thisProto.constructor).name;
188
189 if (metaData && metaData[currentProtoName] && metaData[currentProtoName].querySelectors) {
190 metaData[currentProtoName].querySelectors.forEach((querySelector: any) => {
191 if (thisNoType[querySelector.attributeName] === childNode) {
192 thisNoType[querySelector.attributeName] = newComponent;
193 }
194 });
195 }
196
197 thisProto = Object.getPrototypeOf(thisProto);
198 }
199 }
200
201 public setTemplate(template: string): void {
202 let eGui = _.loadTemplate(<string>template);
203 this.setTemplateFromElement(eGui);
204 }
205
206 public setTemplateFromElement(element: HTMLElement): void {
207 this.eGui = element;
208 (<any>this.eGui).__agComponent = this;
209 this.addAnnotatedEventListeners();
210 this.wireQuerySelectors();
211 }
212
213 protected wireQuerySelectors(): void {
214 if (!this.eGui) {
215 return;
216 }
217
218 let thisProto: any = Object.getPrototypeOf(this);
219
220 while (thisProto != null) {
221 let metaData = thisProto.__agComponentMetaData;
222 let currentProtoName = (thisProto.constructor).name;
223
224 if (metaData && metaData[currentProtoName] && metaData[currentProtoName].querySelectors) {
225 let thisNoType = <any> this;
226 metaData[currentProtoName].querySelectors.forEach((querySelector: any) => {
227 let resultOfQuery = this.eGui.querySelector(querySelector.querySelector);
228 if (resultOfQuery) {
229 let backingComponent = (<any>resultOfQuery).__agComponent;
230 if (backingComponent) {
231 thisNoType[querySelector.attributeName] = backingComponent;
232 } else {
233 thisNoType[querySelector.attributeName] = resultOfQuery;
234 }
235 } else {
236 // put debug msg in here if query selector fails???
237 }
238 });
239 }
240
241 thisProto = Object.getPrototypeOf(thisProto);
242 }
243 }
244
245 private addAnnotatedEventListeners(): void {
246 this.removeAnnotatedEventListeners();
247 if (!this.eGui) {
248 return;
249 }
250
251 let listenerMethods = this.getAgComponentMetaData('listenerMethods');
252
253 if (_.missingOrEmpty(listenerMethods)) { return; }
254
255 if (!this.annotatedEventListeners) {
256 this.annotatedEventListeners = [];
257 }
258
259 listenerMethods.forEach((eventListener: any) => {
260 let listener = (<any>this)[eventListener.methodName].bind(this);
261 this.eGui.addEventListener(eventListener.eventName, listener);
262 this.annotatedEventListeners.push({eventName: eventListener.eventName, listener: listener});
263 });
264 }
265
266 private getAgComponentMetaData(key: string): any[] {
267 let res: any[] = [];
268
269 let thisProto: any = Object.getPrototypeOf(this);
270
271 while (thisProto != null) {
272 let metaData = thisProto.__agComponentMetaData;
273 let currentProtoName = (thisProto.constructor).name;
274
275 if (metaData && metaData[currentProtoName] && metaData[currentProtoName][key]) {
276 res = res.concat(metaData[currentProtoName][key]);
277 }
278
279 thisProto = Object.getPrototypeOf(thisProto);
280 }
281
282 return res;
283 }
284
285 private removeAnnotatedEventListeners(): void {
286 if (!this.annotatedEventListeners) {
287 return;
288 }
289 if (!this.eGui) {
290 return;
291 }
292 this.annotatedEventListeners.forEach((eventListener: any) => {
293 this.eGui.removeEventListener(eventListener.eventName, eventListener.listener);
294 });
295 this.annotatedEventListeners = null;
296 }
297
298 public getGui(): HTMLElement {
299 return this.eGui;
300 }
301
302 // this method is for older code, that wants to provide the gui element,
303 // it is not intended for this to be in ag-Stack
304 protected setGui(eGui: HTMLElement): void {
305 this.eGui = eGui;
306 }
307
308 protected queryForHtmlElement(cssSelector: string): HTMLElement {
309 return <HTMLElement> this.eGui.querySelector(cssSelector);
310 }
311
312 protected queryForHtmlInputElement(cssSelector: string): HTMLInputElement {
313 return <HTMLInputElement> this.eGui.querySelector(cssSelector);
314 }
315
316 public appendChild(newChild: Node | IComponent<any>): void {
317 if (_.isNodeOrElement(newChild)) {
318 this.eGui.appendChild(<Node>newChild);
319 } else {
320 let childComponent = <IComponent<any>>newChild;
321 this.eGui.appendChild(childComponent.getGui());
322 this.childComponents.push(childComponent);
323 }
324 }
325
326 public addFeature(context: Context, feature: BeanStub): void {
327 context.wireBean(feature);
328 if (feature.destroy) {
329 this.addDestroyFunc(feature.destroy.bind(feature));
330 }
331 }
332
333 public isVisible(): boolean {
334 return this.visible;
335 }
336
337 public setVisible(visible: boolean): void {
338 if (visible !== this.visible) {
339 this.visible = visible;
340 _.addOrRemoveCssClass(this.eGui, 'ag-hidden', !visible);
341 let event: VisibleChangedEvent = {
342 type: Component.EVENT_VISIBLE_CHANGED,
343 visible: this.visible
344 };
345 this.dispatchEvent(event);
346 }
347 }
348
349 public addOrRemoveCssClass(className: string, addOrRemove: boolean): void {
350 _.addOrRemoveCssClass(this.eGui, className, addOrRemove);
351 }
352
353 public destroy(): void {
354 super.destroy();
355 this.childComponents.forEach(childComponent => childComponent.destroy());
356 this.childComponents.length = 0;
357
358 this.removeAnnotatedEventListeners();
359 }
360
361 public addGuiEventListener(event: string, listener: (event: any) => void): void {
362 this.getGui().addEventListener(event, listener);
363 this.addDestroyFunc(() => this.getGui().removeEventListener(event, listener));
364 }
365
366 public addCssClass(className: string): void {
367 _.addCssClass(this.getGui(), className);
368 }
369
370 public removeCssClass(className: string): void {
371 _.removeCssClass(this.getGui(), className);
372 }
373
374 public getAttribute(key: string): string {
375 let eGui = this.getGui();
376 if (eGui) {
377 return eGui.getAttribute(key);
378 } else {
379 return null;
380 }
381 }
382
383 public getRefElement(refName: string): HTMLElement {
384 return this.queryForHtmlElement('[ref="' + refName + '"]');
385 }
386
387}
\No newline at end of file