[Modern Media Controls] Scrubber stops moving while scrubbing on macOS
[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         if (children.length === this._children.length) {
130             let arraysDiffer = false;
131             for (let i = children.length - 1; i >= 0; --i) {
132                 if (children[i] !== this._children[i]) {
133                     arraysDiffer = true;
134                     break;
135                 }
136             }
137             if (!arraysDiffer)
138                 return;
139         }
140
141         while (this._children.length)
142             this.removeChild(this._children[0]);
143
144         for (let child of children)
145             this.addChild(child);
146     }
147
148     parentOfType(type)
149     {
150         let node = this;
151         while (node = node._parent) {
152             if (node instanceof type)
153                 return node;
154         }
155         return null;
156     }
157
158     addChild(child, index)
159     {
160         child.remove();
161
162         if (index === undefined || index < 0 || index > this._children.length)
163             index = this._children.length;
164
165         this._children.splice(index, 0, child);
166         child._parent = this;
167
168         child._markNodeManipulation(LayoutNode.DOMManipulation.Addition);
169
170         return child;
171     }
172
173     insertBefore(newSibling, referenceSibling)
174     {
175         return this.addChild(newSibling, this._children.indexOf(referenceSibling));
176     }
177
178     insertAfter(newSibling, referenceSibling)
179     {
180         const index = this._children.indexOf(referenceSibling);
181         return this.addChild(newSibling, index + 1);
182     }
183
184     removeChild(child)
185     {
186         if (child._parent !== this)
187             return;
188
189         const index = this._children.indexOf(child);
190         if (index === -1)
191             return;
192
193         this._children.splice(index, 1);
194         child._parent = null;
195
196         child._markNodeManipulation(LayoutNode.DOMManipulation.Removal);
197
198         return child;
199     }
200
201     remove()
202     {
203         if (this._parent instanceof LayoutNode)
204             return this._parent.removeChild(this);
205     }
206
207     markDirtyProperty(propertyName)
208     {
209         const hadProperty = this._dirtyProperties.has(propertyName);
210         this._dirtyProperties.add(propertyName);
211
212         if (!hadProperty)
213             this._updateDirtyState();
214     }
215
216     commitProperty(propertyName)
217     {
218         const style = this.element.style;
219
220         switch (propertyName) {
221         case "x":
222             style.left = `${this._x}px`;
223             break;
224         case "y":
225             style.top = `${this._y}px`;
226             break;
227         case "width":
228             style.width = `${this._width}px`;
229             break;
230         case "height":
231             style.height = `${this._height}px`;
232             break;
233         case "visible":
234             style.display = this._visible ? "inherit" : "none";
235             break;
236         }
237     }
238
239     layout()
240     {
241         if (this._pendingDOMManipulation === LayoutNode.DOMManipulation.Removal) {
242             const parent = this.element.parentNode;
243             if (parent)
244                 parent.removeChild(this.element);
245         }
246     
247         for (let propertyName of this._dirtyProperties)
248             this.commitProperty(propertyName);
249
250         this._dirtyProperties.clear();
251
252         if (this._pendingDOMManipulation === LayoutNode.DOMManipulation.Addition)
253             nodesRequiringChildrenUpdate.add(this.parent);
254     }
255
256     // Private
257
258     _markNodeManipulation(manipulation)
259     {
260         this._pendingDOMManipulation = manipulation;
261         this._updateDirtyState();
262     }
263
264     _updateDirtyState()
265     {
266         if (this.needsLayout) {
267             dirtyNodes.add(this);
268             scheduler.scheduleLayout(performScheduledLayout);
269         } else {
270             dirtyNodes.delete(this);
271             if (dirtyNodes.size === 0)
272                 scheduler.unscheduleLayout(performScheduledLayout);
273         }
274     }
275
276     _updateChildren()
277     {
278         let nextChildElement = null;
279         const element = this.element;
280         for (let i = this.children.length - 1; i >= 0; --i) {
281             let child = this.children[i];
282             let childElement = child.element;
283
284             if (child._pendingDOMManipulation === LayoutNode.DOMManipulation.Addition) {
285                 element.insertBefore(childElement, nextChildElement);
286                 child._pendingDOMManipulation = LayoutNode.DOMManipulation.None;
287             }
288
289             nextChildElement = childElement;
290         }
291     }
292
293 }
294
295 LayoutNode.DOMManipulation = {
296     None:     0,
297     Removal:  1,
298     Addition: 2
299 };
300
301 function performScheduledLayout()
302 {
303     const previousDirtyNodes = Array.from(dirtyNodes);
304     dirtyNodes.clear();
305     previousDirtyNodes.forEach(node => {
306         node._needsLayout = false;
307         node.layout();
308     });
309
310     nodesRequiringChildrenUpdate.forEach(node => node._updateChildren());
311     nodesRequiringChildrenUpdate.clear();
312 }
313
314 function elementFromString(elementString)
315 {
316     const element = document.createElement("div");
317     element.innerHTML = elementString;
318     return element.firstElementChild;
319 }