[Modern Media Controls] Controls bar may disappear while captions menu is visible
[WebKit-https.git] / Source / WebCore / Modules / modern-media-controls / controls / layout-node.js
1
2 const dirtyNodes = new Set;
3 const nodesRequiringChildrenUpdate = new Set;
4
5 class LayoutNode
6 {
7
8     constructor(stringOrElement)
9     {
10
11         if (!stringOrElement)
12             this.element = document.createElement("div");
13         else if (stringOrElement instanceof Element)
14             this.element = stringOrElement;
15         else if (typeof stringOrElement === "string" || stringOrElement instanceof String)
16             this.element = elementFromString(stringOrElement);
17
18         this._parent = null;
19         this._children = [];
20
21         this._x = 0;
22         this._y = 0;
23         this._width = 0;
24         this._height = 0;
25         this._visible = true;
26
27         this._needsLayout = false;
28         this._dirtyProperties = new Set;
29
30         this._pendingDOMManipulation = LayoutNode.DOMManipulation.None;
31     }
32
33     get x()
34     {
35         return this._x;
36     }
37
38     set x(x)
39     {
40         if (x === this._x)
41             return;
42
43         this._x = x;
44         this.markDirtyProperty("x");
45     }
46
47     get y()
48     {
49         return this._y;
50     }
51
52     set y(y)
53     {
54         if (y === this._y)
55             return;
56
57         this._y = y;
58         this.markDirtyProperty("y");
59     }
60
61     get width()
62     {
63         return this._width;
64     }
65
66     set width(width)
67     {
68         if (width === this._width)
69             return;
70
71         this._width = width;
72         this.markDirtyProperty("width");
73     }
74
75     get height()
76     {
77         return this._height;
78     }
79
80     set height(height)
81     {
82         if (height === this._height)
83             return;
84
85         this._height = height;
86         this.markDirtyProperty("height");
87     }
88
89     get visible()
90     {
91         return this._visible;
92     }
93
94     set visible(flag)
95     {
96         if (flag === this._visible)
97             return;
98
99         this._visible = flag;
100         this.markDirtyProperty("visible");
101     }
102
103     get needsLayout()
104     {
105         return this._needsLayout || this._pendingDOMManipulation !== LayoutNode.DOMManipulation.None || this._dirtyProperties.size > 0;
106     }
107
108     set needsLayout(flag)
109     {
110         if (this.needsLayout === flag)
111             return;
112
113         this._needsLayout = flag;
114         this._updateDirtyState();
115     }
116
117     get parent()
118     {
119         return this._parent;
120     }
121
122     get children()
123     {
124         return this._children;
125     }
126
127     set children(children)
128     {
129         while (this._children.length)
130             this.removeChild(this._children[0]);
131
132         for (let child of children)
133             this.addChild(child);
134     }
135
136     parentOfType(type)
137     {
138         let node = this;
139         while (node = node._parent) {
140             if (node instanceof type)
141                 return node;
142         }
143         return null;
144     }
145
146     addChild(child, index)
147     {
148         child.remove();
149
150         if (index === undefined || index < 0 || index > this._children.length)
151             index = this._children.length;
152
153         this._children.splice(index, 0, child);
154         child._parent = this;
155
156         child._markNodeManipulation(LayoutNode.DOMManipulation.Addition);
157
158         return child;
159     }
160
161     insertBefore(newSibling, referenceSibling)
162     {
163         return this.addChild(newSibling, this._children.indexOf(referenceSibling));
164     }
165
166     insertAfter(newSibling, referenceSibling)
167     {
168         const index = this._children.indexOf(referenceSibling);
169         return this.addChild(newSibling, index + 1);
170     }
171
172     removeChild(child)
173     {
174         if (child._parent !== this)
175             return;
176
177         const index = this._children.indexOf(child);
178         if (index === -1)
179             return;
180
181         this._children.splice(index, 1);
182         child._parent = null;
183
184         child._markNodeManipulation(LayoutNode.DOMManipulation.Removal);
185
186         return child;
187     }
188
189     remove()
190     {
191         if (this._parent instanceof LayoutNode)
192             return this._parent.removeChild(this);
193     }
194
195     markDirtyProperty(propertyName)
196     {
197         const hadProperty = this._dirtyProperties.has(propertyName);
198         this._dirtyProperties.add(propertyName);
199
200         if (!hadProperty)
201             this._updateDirtyState();
202     }
203
204     commitProperty(propertyName)
205     {
206         const style = this.element.style;
207
208         switch (propertyName) {
209         case "x":
210             style.left = `${this._x}px`;
211             break;
212         case "y":
213             style.top = `${this._y}px`;
214             break;
215         case "width":
216             style.width = `${this._width}px`;
217             break;
218         case "height":
219             style.height = `${this._height}px`;
220             break;
221         case "visible":
222             style.display = this._visible ? "inherit" : "none";
223             break;
224         }
225     }
226
227     layout()
228     {
229         if (this._pendingDOMManipulation === LayoutNode.DOMManipulation.Removal) {
230             const parent = this.element.parentNode;
231             if (parent)
232                 parent.removeChild(this.element);
233         }
234     
235         for (let propertyName of this._dirtyProperties)
236             this.commitProperty(propertyName);
237
238         this._dirtyProperties.clear();
239
240         if (this._pendingDOMManipulation === LayoutNode.DOMManipulation.Addition)
241             nodesRequiringChildrenUpdate.add(this.parent);
242     }
243
244     // Private
245
246     _markNodeManipulation(manipulation)
247     {
248         this._pendingDOMManipulation = manipulation;
249         this._updateDirtyState();
250     }
251
252     _updateDirtyState()
253     {
254         if (this.needsLayout) {
255             dirtyNodes.add(this);
256             scheduler.scheduleLayout(performScheduledLayout);
257         } else {
258             dirtyNodes.delete(this);
259             if (dirtyNodes.size === 0)
260                 scheduler.unscheduleLayout(performScheduledLayout);
261         }
262     }
263
264     _updateChildren()
265     {
266         let nextChildElement = null;
267         const element = this.element;
268         for (let i = this.children.length - 1; i >= 0; --i) {
269             let child = this.children[i];
270             let childElement = child.element;
271
272             if (child._pendingDOMManipulation === LayoutNode.DOMManipulation.Addition) {
273                 element.insertBefore(childElement, nextChildElement);
274                 child._pendingDOMManipulation = LayoutNode.DOMManipulation.None;
275             }
276
277             nextChildElement = childElement;
278         }
279     }
280
281 }
282
283 LayoutNode.DOMManipulation = {
284     None:     0,
285     Removal:  1,
286     Addition: 2
287 };
288
289 function performScheduledLayout()
290 {
291     const previousDirtyNodes = Array.from(dirtyNodes);
292     dirtyNodes.clear();
293     previousDirtyNodes.forEach(node => {
294         node._needsLayout = false;
295         node.layout();
296     });
297
298     nodesRequiringChildrenUpdate.forEach(node => node._updateChildren());
299     nodesRequiringChildrenUpdate.clear();
300 }
301
302 function elementFromString(elementString)
303 {
304     const element = document.createElement("div");
305     element.innerHTML = elementString;
306     return element.firstElementChild;
307 }