1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5cr.define('print_preview', function() {
6  'use strict';
7
8  /**
9   * Class that represents a UI component.
10   * @constructor
11   * @extends {cr.EventTarget}
12   */
13  function Component() {
14    cr.EventTarget.call(this);
15
16    /**
17     * Component's HTML element.
18     * @type {Element}
19     * @private
20     */
21    this.element_ = null;
22
23    this.isInDocument_ = false;
24
25    /**
26     * Component's event tracker.
27     * @type {EventTracker}
28     * @private
29     */
30     this.tracker_ = new EventTracker();
31
32    /**
33     * Child components of the component.
34     * @type {Array.<print_preview.Component>}
35     * @private
36     */
37    this.children_ = [];
38  };
39
40  Component.prototype = {
41    __proto__: cr.EventTarget.prototype,
42
43    /** Gets the component's element. */
44    getElement: function() {
45      return this.element_;
46    },
47
48    /** @return {EventTracker} Component's event tracker. */
49    get tracker() {
50      return this.tracker_;
51    },
52
53    /**
54     * @return {boolean} Whether the element of the component is already in the
55     *     HTML document.
56     */
57    get isInDocument() {
58      return this.isInDocument_;
59    },
60
61    /**
62     * Creates the root element of the component. Sub-classes should override
63     * this method.
64     */
65    createDom: function() {
66      this.element_ = cr.doc.createElement('div');
67    },
68
69    /**
70     * Called when the component's element is known to be in the document.
71     * Anything using document.getElementById etc. should be done at this stage.
72     * Sub-classes should extend this method and attach listeners.
73     */
74    enterDocument: function() {
75      this.isInDocument_ = true;
76      this.children_.forEach(function(child) {
77        if (!child.isInDocument && child.getElement()) {
78          child.enterDocument();
79        }
80      });
81    },
82
83    /** Removes all event listeners. */
84    exitDocument: function() {
85      this.children_.forEach(function(child) {
86        if (child.isInDocument) {
87          child.exitDocument();
88        }
89      });
90      this.tracker_.removeAll();
91      this.isInDocument_ = false;
92    },
93
94    /**
95     * Renders this UI component and appends the element to the given parent
96     * element.
97     * @param {!Element} parentElement Element to render the component's
98     *     element into.
99     */
100    render: function(parentElement) {
101      assert(!this.isInDocument, 'Component is already in the document');
102      if (!this.element_) {
103        this.createDom();
104      }
105      parentElement.appendChild(this.element_);
106      this.enterDocument();
107    },
108
109    /**
110     * Decorates an existing DOM element. Sub-classes should override the
111     * override the decorateInternal method.
112     * @param {Element} element Element to decorate.
113     */
114    decorate: function(element) {
115      assert(!this.isInDocument, 'Component is already in the document');
116      this.setElementInternal(element);
117      this.decorateInternal();
118      this.enterDocument();
119    },
120
121    /**
122     * @param {print_preview.Component} child Component to add as a child of
123     *     this component.
124     */
125    addChild: function(child) {
126      this.children_.push(child);
127    },
128
129    /**
130     * @param {!print_preview.Component} child Component to remove from this
131     *     component's children.
132     */
133    removeChild: function(child) {
134      var childIdx = this.children_.indexOf(child);
135      if (childIdx != -1) {
136        this.children_.splice(childIdx, 1);
137      }
138      if (child.isInDocument) {
139        child.exitDocument();
140        if (child.getElement()) {
141          child.getElement().parentNode.removeChild(child.getElement());
142        }
143      }
144    },
145
146    /** Removes all of the component's children. */
147    removeChildren: function() {
148      while (this.children_.length > 0) {
149        this.removeChild(this.children_[0]);
150      }
151    },
152
153    /**
154     * @param {string} query Selector query to select an element starting from
155     *     the component's root element using a depth first search for the first
156     *     element that matches the query.
157     * @return {HTMLElement} Element selected by the given query.
158     */
159    getChildElement: function(query) {
160      return this.element_.querySelector(query);
161    },
162
163    /**
164     * Sets the component's element.
165     * @param {Element} element HTML element to set as the component's element.
166     * @protected
167     */
168    setElementInternal: function(element) {
169      this.element_ = element;
170    },
171
172    /**
173     * Decorates the given element for use as the element of the component.
174     * @protected
175     */
176    decorateInternal: function() { /*abstract*/ },
177
178    /**
179     * Clones a template HTML DOM tree.
180     * @param {string} templateId Template element ID.
181     * @param {boolean=} opt_keepHidden Whether to leave the cloned template
182     *     hidden after cloning.
183     * @return {Element} Cloned element with its 'id' attribute stripped.
184     * @protected
185     */
186    cloneTemplateInternal: function(templateId, opt_keepHidden) {
187      var templateEl = $(templateId);
188      assert(templateEl != null,
189             'Could not find element with ID: ' + templateId);
190      var el = templateEl.cloneNode(true);
191      el.id = '';
192      if (!opt_keepHidden) {
193        setIsVisible(el, true);
194      }
195      return el;
196    }
197  };
198
199  return {
200    Component: Component
201  };
202});
203