Update the design of the WebKit Feature Status page
[WebKit-https.git] / Websites / webkit.org / wp-content / themes / webkit / status.php
1 <?php
2 /**
3  * Template Name: Status Page
4  **/
5 ?>
6 <?php get_header(); ?>
7 <script>
8 function xhrPromise(url) {
9     return new Promise(function(resolve, reject) {
10         var xhrRequest = new XMLHttpRequest();
11         xhrRequest.open('GET', url, true);
12         xhrRequest.responseType = "json";
13
14         xhrRequest.onload = function() {
15             if (xhrRequest.status == 200) {
16                 if (xhrRequest.response) {
17                     resolve(xhrRequest.response);
18                 } else {
19                     reject({ request: xhrRequest, url:url});
20                 }
21             } else {
22                 reject({ request: xhrRequest, url:url});
23             }
24         };
25         xhrRequest.onerror = function() {
26             reject({ request: xhrRequest, url:url});
27         };
28         xhrRequest.send();
29     });
30 }
31 var origin = new URL("https://<?php echo strpos(WP_HOST, "webkit.org") !== false ? "svn.webkit.org" : WP_HOST; ?>/");
32 var loadJavaScriptCoreFeatures = xhrPromise(new URL("/repository/webkit/trunk/Source/JavaScriptCore/features.json", origin));
33 var loadWebCoreFeatures = xhrPromise(new URL("/repository/webkit/trunk/Source/WebCore/features.json", origin));
34 </script>
35
36 <style>
37
38 .page h1 {
39     font-size: 4.2rem;
40     font-weight: 200;
41     line-height: 6rem;
42     color: black;
43     margin: 3rem auto;
44     width: 100%;
45     text-align: center;
46 }
47
48 .page h1 a {
49     color: #444444;
50 }    
51
52 .feature-filters {
53     background-color: #ffffff;
54     width: 100vw;
55     left: 50%;
56     position: relative;
57     transform: translate(-50vw, 0);
58     box-sizing: border-box;
59     margin-bottom: 3rem;
60     border: 1px solid #DDDDDD;
61     border-left: none;
62     border-right: none;
63 }
64
65 .feature-filters form {
66     padding-top: 3rem;
67     padding-bottom: 3rem;
68     display: flex;
69     flex-wrap: wrap;
70 }
71
72 input[type=text].search-input {
73     margin-bottom: 1rem;
74     width: 100%;
75     flex: 1;
76 }
77
78 .feature-filters li {
79     display: inline-block;
80 }
81
82 .feature-status label,
83 .feature-filters label {
84     display: table-cell;
85     padding: 0.5rem 1rem;
86     border-style: solid;
87     border-width: 1px;
88     border-radius: 3px;
89     cursor: pointer;
90     float: right;
91     line-height: 1;
92     font-size: 1.6rem;
93 }
94
95 .status-filters label {
96     margin-left: 1rem;
97 }
98
99 .status-filters {
100     list-style: none;
101     display: inline-block;
102     text-align: right;
103     flex: 2;
104     flex-grow: 1;
105 }
106
107 .filter-toggle:checked + .filter-status {
108     color: #ffffff;
109 }
110
111 .filter-status,
112 .feature-status {
113     color: #999999;
114     border-color: #999999;
115 }
116
117 .feature-status a {
118     color: inherit;
119 }
120
121 .filter-status,
122 .status-marker {
123     border-color: #999999;
124 }
125 .filter-toggle:checked + .filter-status {
126     background-color: #999999;
127 }
128
129 .supported {
130     color: #339900;
131     border-color: #339900;
132 }
133
134 .filter-toggle:checked + .supported {
135     background-color: #339900;
136 }
137
138 .supported-in-preview {
139     color: #66149f;
140     border-color: #66149f;
141 }
142
143 .filter-toggle:checked + .supported-in-preview {
144     background-color: #66149f;
145 }
146
147 .in-development {
148     color: #f46c0e;
149     border-color: #f46c0e;
150 }
151 .filter-toggle:checked + .in-development {
152     background-color: #f46c0e;
153 }
154
155 .no-active-development {
156     color: #5858D6;
157     border-color: #5858D6;
158 }
159
160 .filter-toggle:checked + .no-active-development {
161     background-color: #5858D6;
162 }
163
164 .partially-supported  {
165     color: #548c8c;
166     border-color: #548c8c;
167 }
168
169 .filter-toggle:checked + .partially-supported {
170     background-color: #548c8c;
171 }
172
173 .prototyping {
174     color: #007AFF;
175     border-color: #007AFF;
176 }
177
178 .filter-toggle:checked + .prototyping {
179     background-color: #007AFF;
180 }
181
182 .under-consideration {
183     color: #c27870;
184     border-color: #c27870;
185 }
186
187 .filter-toggle:checked + .under-consideration {
188     background-color: #c27870;
189 }
190
191 .feature.is-hidden {
192     display: none;
193 }
194
195 .features {
196     max-width: 920px;
197     margin: 0 auto 3rem;
198     border-bottom: 1px solid #e4e4e4;
199     
200
201
202 .feature-count {
203     max-width: 920px;
204     margin: 0 auto 3rem;
205     
206     text-align: right;
207     color: #999;
208 }
209
210 .feature-count p {
211     margin: 0;
212 }
213
214 .feature {
215     border-color: transparent;
216     border-width: 1px;
217     border-style: solid;
218     border-top-color: #e4e4e4;
219     padding: 0.5rem;
220     line-height: 1.618;
221     -webkit-transition: background-color 0.3s ease-out;
222     transition: background-color 0.3s ease-out;
223 }
224
225 .feature-header {
226     font-weight: 400;
227     font-size: 2.5rem;
228     display: flex;
229 }
230
231 .feature-header h3 {
232     flex: 1;
233     flex-grow: 2;
234     padding-right: 1rem;
235     box-sizing: border-box;
236 }
237
238 .feature-header h3 a { 
239     padding-right: 1rem;
240 }
241
242 .feature-header .feature-status {
243     flex: 2;
244     text-align: right;
245     font-size: 2rem;
246 }
247
248 .feature-container.status-marker {
249     border-left-width: 3px;
250     border-left-style: solid;
251     padding: 0.5rem 0 0.5rem 1rem;
252 }
253
254 .feature-header a[name] {
255     color: #444444;
256 }
257
258 .feature-header .internal-reference {
259     display: inline-block;
260     font-size: 1.6rem;
261     font-weight: 600;
262     white-space: nowrap;
263 }
264
265 .feature-header .internal-reference a {
266     color: #999999;
267 }
268
269 .feature-header:after {
270     position: relative;
271     width: 2rem;
272     height: 2rem;
273     right: 0;
274     top: 0.5rem;
275     margin-left: 1rem;
276     -webkit-transition: transform 0.3s ease-out;
277     -moz-transition: transform 0.3s ease-out;
278     transition: transform 0.3s ease-out;
279 }
280
281 .feature-details {
282     display: none;
283     width: 50%;
284 }
285
286 .feature.opened {
287     background: #ffffff;
288     border-left-color: #e4e4e4;
289     border-right-color: #e4e4e4;
290 }
291
292 .feature.opened .feature-details {
293     display: block;
294 }
295
296 .feature h4 {
297     color: #999999;
298     font-weight: 600;
299     margin-top: 1rem;
300     margin-bottom: 0;
301 }
302
303 .feature .moreinfo {
304     list-style: none;
305     display: flex;
306     width: 100%;
307 }
308
309 .feature .moreinfo li {
310     flex-grow: 1;
311 }
312
313 .feature .moreinfo .contact {
314     text-align: right;
315 }
316
317 .feature .feature-desc {
318     color: #444444;
319 }
320
321 .feature .comment {
322     color: #666666;
323     font-style: italic;
324 }
325
326 .sub-features {
327     font-size: 1.5rem;
328     color: #555;
329 }
330
331 .sub-features ul {
332     list-style: none;
333     padding: 0;
334     margin: 0;
335 }
336
337 .sub-features li {
338     display: inline-block;
339     white-space: nowrap;
340 }
341
342 .sub-features li:after {
343     content: ", ";
344     white-space: pre;
345 }
346
347 .sub-features li:last-child:after {
348     content: "";
349 }
350
351 @media only screen and (max-width: 1000px) {
352     .feature-details {
353         width: 100%;
354     }
355 }
356
357 @media only screen and (max-width: 508px) {
358     #feature-filters,
359     #feature-list {
360         width: 100%;
361     }
362     
363     #feature-filters {
364         padding-left: 2rem;
365         padding-right: 2rem;
366     }
367
368     .feature-header h3 {
369         font-size: 2rem;
370         padding-right: 0.5rem;
371     }
372
373     .feature-status {
374         font-size: 1.6rem;
375         margin-top: 0.4rem;
376         float: left;
377     }
378     
379     .feature-header:after {
380         width: 1rem;
381         height: 1rem;
382         background-size: 1rem;
383         top: 1rem;
384     }
385     
386     .feature h3 {
387         font-size: 2rem;
388         padding-top: 4rem;
389     }
390     
391     .feature-header .feature-status {
392         font-size: 1.6rem;
393         position: absolute;
394         text-align: left;
395     }
396     
397     .feature .moreinfo {
398         flex-wrap: wrap;
399     }
400     
401     .feature .moreinfo .contact {
402         text-align: left;
403     }
404     
405     .status-filters {
406         text-align: left;
407         flex-basis: 100%;
408     }
409     
410     .status-filters label {
411         margin-left: 0;
412         margin-right: 1rem;
413     }
414 }
415
416 h3 a[name], .admin-bar h3 a[name] {
417     top: initial;
418     width: auto;
419     display: inline-block;
420     visibility: visible;
421 }
422
423
424 </style>
425         <?php if (have_posts()) : while (have_posts()) : the_post(); ?>
426         
427         <div id="content">
428         <div class="page feature-status-page" id="post-<?php the_ID(); ?>">
429                         <h1><a href="<?php echo get_permalink() ?>" rel="bookmark" title="Permanent Link: <?php the_title(); ?>"><?php the_title(); ?></a></h1>
430             
431             <div class="feature-filters">
432                 <form id="feature-filters" class="page-width">
433                     <input type="text" id="search" class="search-input" placeholder="Search features&hellip;" title="Filter the feature list." required>
434                     <ul id="status-filters" class="status-filters"></ul>
435                 </form>
436             </div>
437
438             <div id="feature-list">
439                 <div class="feature-count">
440                     <p><span id="feature-count"></span> <span id="feature-pluralize">features</span></p>
441                 </div>
442                 
443             </div>
444
445             <template id="success-template">
446                 <ul class="features" id="features-container"></ul>
447                 <p>Cannot find something? You can contact <a href="https://twitter.com/webkit">@webkit</a> on Twitter or contact the <a href="https://lists.webkit.org/mailman/listinfo/webkit-help">webkit-help</a> mailing list for questions.</p>
448                 <p>You can also <a href="/contributing-code/">contribute to features</a> directly, the entire project is Open Source. To report bugs on existing features or check existing bug reports, see <a href="https://bugs.webkit.org">https://bugs.webkit.org</a>.</p>
449             </template>
450             <template id="error-template">
451                 <p>Error: unable to load the features list (<span id="error-message"></span>).</p>
452                 <p>If this is not resolved soon, please contact <a href="https://twitter.com/webkit">@webkit</a> on Twitter or the <a href="https://lists.webkit.org/mailman/listinfo/webkit-help">webkit-help</a> mailing list.</p>
453             </template>
454         </div>
455         </div>
456
457         <?php //comments_template(); ?>
458
459         <?php endwhile; else: ?>
460
461                 <p>No posts.</p>
462
463         <?php endif; ?>
464
465
466 <script>
467 function initializeStatusPage() {
468
469     const statusOrder = [
470         'under consideration',
471         'prototyping',
472         'in development',
473         'supported in preview',
474         'partially supported',
475         'supported',
476         'removed',
477         'not considering'
478     ];
479
480     function sortAlphabetically(array)
481     {
482         array.sort(function(a, b){
483             var aName = a.name.toLowerCase();
484             var bName = b.name.toLowerCase();
485
486             var nameCompareResult = aName.localeCompare(bName);
487
488             if ( nameCompareResult )
489                 return nameCompareResult;
490
491             // Status sort
492             var aStatus = a.status != undefined ? a.status.status.toLowerCase() : '';
493             var bStatus = b.status != undefined ? b.status.status.toLowerCase() : '';
494
495             return aStatus.localeCompare(bStatus);
496         });
497     }
498
499     function createFeatureView(featureObject)
500     {
501                 
502         function createLinkWithHeading(elementName, heading, linkText, linkUrl) {
503             var container = document.createElement(elementName);
504             if (heading) {
505                 var h4 = document.createElement('h4');
506                 h4.textContent = heading;
507                 container.appendChild(h4);
508             }
509             var link = document.createElement("a");
510             link.textContent = linkText;
511             link.href = linkUrl;
512             if (linkText == linkUrl)
513                 link.textContent = link.hostname + "…";
514             container.appendChild(link);
515             return container;
516         }
517
518         function makeTwitterLink(twitterHandle) {
519             if (twitterHandle[0] == "@")
520                 twitterHandle = twitterHandle.substring(1);
521             return "https://twitter.com/" + twitterHandle;
522         }
523
524         var container = document.createElement('li');
525         var hasDocumentationLink = "documentation-url" in featureObject;
526         var hasReferenceLink = "url" in featureObject;
527         var hasContactObject = "contact" in featureObject;
528         var hasSpecificationObject = "specification" in featureObject;
529
530         container.addEventListener('click', function (e) {
531             if ( container.className.indexOf('opened') !== -1 ) {
532                 container.className = container.className.replace(' opened','');
533             } else container.className += " opened";
534         });
535
536         container.className = "feature";
537
538         var slug = canonicalizeIdentifier(featureObject.name);
539         if ("features" in featureObject) {
540             container.setAttribute("id", "specification-" + slug);
541         } else {
542             container.setAttribute("id", "feature-" + slug);
543         }
544
545         if (window.location.hash && window.location.hash == "#" + container.getAttribute('id')) {
546             container.className += " opened";
547         }
548
549         var featureContainer = document.createElement('div');
550         featureContainer.className = "feature-container status-marker";
551
552         var featureHeaderContainer = document.createElement('div');
553         featureHeaderContainer.className = "feature-header";
554         featureContainer.appendChild(featureHeaderContainer);
555
556         var titleElement = document.createElement("h3");
557         var anchorLinkElement = document.createElement("a");
558         anchorLinkElement.href = "#" + container.getAttribute("id");
559         anchorLinkElement.name = container.getAttribute("id");
560         anchorLinkElement.textContent = featureObject.name;
561         titleElement.appendChild(anchorLinkElement);
562
563         // Add sub-feature here
564         if (hasSpecificationObject) {
565             var specification = featureObject.specification;
566             var specSpan = createLinkWithHeading("h4", null, specification.name, "#specification-" + specification.name.toLowerCase().replace(/ /g, '-'));
567             specSpan.className = "internal-reference";
568             titleElement.appendChild(specSpan);
569         }
570
571         featureHeaderContainer.appendChild(titleElement);
572
573         if ("status" in featureObject) {
574             var statusContainer = document.createElement("div");
575             var statusClassName = canonicalizeIdentifier(featureObject.status.status);
576             featureContainer.className += " " + statusClassName;
577             statusContainer.className = "feature-status " + statusClassName;
578             var statusLabel = document.createElement("label");
579
580             if ("webkit-url" in featureObject) {
581                 var statusLink = document.createElement("a");
582                 statusLink.href = featureObject["webkit-url"];
583                 statusLink.textContent = featureObject.status.status;
584                 statusLabel.appendChild(statusLink);
585             } else {
586                 statusLabel.textContent = featureObject.status.status;
587             }
588
589             statusContainer.appendChild(statusLabel);
590             featureHeaderContainer.appendChild(statusContainer);
591         }
592         
593         var featureDetails = document.createElement('div');
594         featureDetails.className = 'feature-details';
595
596         if ("description" in featureObject) {
597             var textDescription = document.createElement('p');
598             textDescription.className = "feature-desc";
599             textDescription.innerHTML = featureObject.description;
600             featureDetails.appendChild(textDescription);
601         }
602
603         if ("comment" in featureObject) {
604             var comment = document.createElement('p');
605             comment.className = 'comment';
606             comment.innerHTML = featureObject.comment;
607             featureDetails.appendChild(comment);
608         }
609
610         if ("features" in featureObject && featureObject.features.length) {
611             var internalLinkContainer = document.createElement("div");
612             internalLinkContainer.className = "internal-reference sub-features";
613             var internalHeading = document.createElement("h4");
614             internalHeading.textContent = "Includes";
615             internalLinkContainer.appendChild(internalHeading);
616
617             var list = document.createElement("ul");
618             for (var feature of featureObject.features) {
619                 var link = document.createElement("a");
620                 link.textContent = feature.name;
621                 link.href = "#feature-" + canonicalizeIdentifier(feature.name);
622
623                 var li = document.createElement("li");
624                 li.appendChild(link);
625                 list.appendChild(li);
626             }
627             internalLinkContainer.appendChild(list);
628             featureDetails.appendChild(internalLinkContainer);
629         }
630
631         if (hasDocumentationLink || hasReferenceLink || hasContactObject) {
632             var moreInfoList = document.createElement("ul");
633             moreInfoList.className = 'moreinfo';
634             if (hasDocumentationLink) {
635                 var url = featureObject["documentation-url"];
636                 moreInfoList.appendChild(createLinkWithHeading("li", "Documentation", url, url));
637             }
638
639             if (hasReferenceLink) {
640                 var url = featureObject.url;
641                 moreInfoList.appendChild(createLinkWithHeading("li", "Reference", url, url));
642             }
643
644             if (hasContactObject) {
645                 var li = document.createElement("li");
646                 li.className = "contact";
647                 var contactHeading = document.createElement("h4");
648                 contactHeading.textContent = "Contact";
649                 li.appendChild(contactHeading);
650
651                 if (featureObject.contact.twitter) {
652                     li.appendChild(createLinkWithHeading("span", null, featureObject.contact.twitter, makeTwitterLink(featureObject.contact.twitter)));
653                 }
654                 if (featureObject.contact.email) {
655                     if (featureObject.contact.twitter) {
656                         li.appendChild(document.createTextNode(" - "));
657                     }
658                     var emailText = featureObject.contact.email;
659                     if (featureObject.contact.name) {
660                         emailText = featureObject.contact.name;
661                     }
662                     li.appendChild(createLinkWithHeading("span", null, emailText, "mailto:" + featureObject.contact.email));
663                 }
664                 moreInfoList.appendChild(li);
665             }
666
667             featureDetails.appendChild(moreInfoList);
668         }
669
670         featureContainer.appendChild(featureDetails);
671         container.appendChild(featureContainer);
672
673         return container;
674     }
675     
676     function canonicalizeIdentifier(identifier)
677     {
678         return identifier.toLocaleLowerCase().replace(/ /g, '-');
679     }
680     
681
682     function renderFeaturesAndSpecifications(featureLikeObjects) 
683     {
684         var featureContainer = document.getElementById('features-container');
685         for (var featureLikeObject of featureLikeObjects) {
686             featureContainer.appendChild(createFeatureView(featureLikeObject));
687         }
688     }
689
690     function initSearch(featuresArray) 
691     {
692         var filtersForm = document.getElementById('feature-filters');
693         var statusContainer = document.getElementById('status-filters');
694         var inputField = document.getElementById('search');
695         var featuresEls = document.querySelectorAll('.features > li');
696         var statusFilters = {};
697
698         featuresArray.forEach(function(feature, i) {
699             feature.el = featuresEls[i];
700             feature.visible = true;
701
702             if (feature.status != undefined) {
703                 featureStatusKey = feature.status.status.toLocaleLowerCase();
704
705                 if (!statusFilters[featureStatusKey])
706                     statusFilters[featureStatusKey] = feature.status.status;
707                 
708                 if (statusOrder.indexOf(featureStatusKey) == -1)
709                     window.console.log('Status ' + featureStatusKey + ' is not one of the predefined status keys ', statusOrder);
710
711             }
712         });
713
714         for (var key of statusOrder) {
715             if (statusFilters[key] == undefined)
716                 continue;
717
718             var statusLabel = statusFilters[key];
719             var statusId = canonicalizeIdentifier(statusLabel);
720             var entry = document.createElement("li");
721             var label = document.createElement("label");
722             var input = document.createElement("input");
723             
724             input.setAttribute('type','checkbox');
725             input.setAttribute('value', key);
726             input.setAttribute('id', 'toggle-' + statusId);
727             input.className = 'filter-toggle';
728             input.addEventListener('change', function() { updateSearch(featuresArray); });
729             
730             
731             label.className = "filter-status " + statusId;
732             label.setAttribute('for', 'toggle-' + statusId);
733             label.appendChild(input);
734             label.appendChild(document.createTextNode(" " + statusLabel));
735
736             entry.appendChild(label);
737
738             statusContainer.appendChild(entry);
739         }
740         
741         filtersForm.addEventListener('click', function (e) {
742             if ( filtersForm.className.indexOf('opened') !== -1 ) {
743                 filtersForm.className = filtersForm.className.replace(' opened','');
744             } else filtersForm.className += " opened";
745         });
746
747         inputField.addEventListener('input', function() { updateSearch(featuresArray); });
748         
749
750         var inputs = [].slice.call(filtersForm.getElementsByTagName('input'));
751         inputs.forEach(function (input,i) {
752             input.addEventListener('click', function (e) {
753                 e.stopPropagation();
754             });
755         });
756
757         function search(ev)
758         {
759             var searchTerm = inputField.value.trim().toLowerCase();
760             var activeStatusFilters = [];
761             var checkboxes = [].slice.call(statusContainer.getElementsByTagName('input'));
762             checkboxes.forEach(function(checkbox,i) {
763                 if ( checkbox.checked )
764                     activeStatusFilters.push(checkbox.value);
765             });
766
767             searchFeatures(featuresArray, searchTerm, activeStatusFilters);
768         }
769     }
770
771     function getValuesOfCheckedItems(items)
772     {
773         var checkedValues = [];
774         items.forEach(function(item,i) {
775             if (item.checked)
776                 checkedValues.push(item.value);
777         });
778         
779         return checkedValues;
780     }
781
782     function updateSearch(properties)
783     {
784         var inputField = document.getElementById('search');
785         var statusContainer = document.getElementById('status-filters');
786
787         var searchTerm = inputField.value.trim().toLowerCase();
788         var activeStatusFilters = getValuesOfCheckedItems([].slice.call(statusContainer.querySelectorAll('.filter-toggle')));
789
790         var numVisible = searchFeatures(properties, searchTerm, activeStatusFilters);
791         document.getElementById('feature-pluralize').textContent = numVisible == 1 ? 'feature' : 'features';
792         document.getElementById('feature-count').textContent = numVisible;
793         
794         updateURL(searchTerm, activeStatusFilters);
795     }
796     
797     function searchFeatures(features, searchTerm, statusFilters)
798     {
799         var visibleCount = 0;
800         features.forEach(function(featureObject) {
801             var matchesStatusSearch = isStatusFiltered(featureObject, statusFilters);
802             
803             var visible = isSearchMatch(featureObject, searchTerm) && matchesStatusSearch;
804             if (visible && !featureObject.visible)
805                 featureObject.el.className = 'feature';
806             else if (!visible && featureObject.visible)
807                 featureObject.el.className = 'feature is-hidden';
808             
809             if (visible) {
810                 // filterValues(featureObject, searchTerm);
811                 ++visibleCount;
812             }
813
814             featureObject.visible = visible;
815         });
816         
817         return visibleCount;
818     }
819
820     function isSearchMatch(feature, searchTerm)
821     {
822         if (feature.name.toLowerCase().indexOf(searchTerm) !== -1)
823             return true;
824         if ("keywords" in feature) {
825             for (var keyword of feature.keywords) {
826                 if (keyword.toLowerCase().indexOf(searchTerm) !== -1)
827                     return true;
828             }
829         }
830         return false;
831     }
832
833     function isStatusFiltered(feature, activeFilters)
834     {
835         if (activeFilters.length == 0)
836             return true;
837         if (feature.status === undefined)
838             return false;
839         if (activeFilters.indexOf(feature.status.status.toLowerCase()) != -1)
840             return true;
841
842         return false;
843     }
844     
845     function filterValues(featureObject, searchTerm, statusFilters)
846     {
847         for (var valueObj of featureObject.values) {
848             if (!valueObj.el)
849                 continue;
850
851             var visible = false;
852             visible = valueObj.value.toLowerCase().indexOf(searchTerm) !== -1;
853
854             if (visible)
855                 valueObj.el.classList.remove('hidden');
856             else
857                 valueObj.el.classList.add('hidden');
858         }
859     }
860
861     function displayFeatures(results)
862     {
863         var mainContent = document.getElementById("feature-list");
864         var successSubtree = document.importNode(document.getElementById("success-template").content, true);
865         mainContent.appendChild(successSubtree);
866
867         var allSpecifications = [];
868         for (var i in results) {
869             allSpecifications = allSpecifications.concat(results[i].specification);
870         }
871         var specificationsByName = {}
872         for (var specification of allSpecifications) {
873             specification.features = [];
874             specification.isSpecification = true;
875             specificationsByName[specification.name] = specification;
876         }
877
878         var allFeatures = [];
879         for (var i in results) {
880             allFeatures = allFeatures.concat(results[i].features);
881         }
882         var featuresByName = {};
883         for (var feature of allFeatures) {
884             if ('specification' in feature) {
885                 var featureSpecification = feature.specification;
886                 var specificationObject = specificationsByName[featureSpecification];
887                 if (specificationObject != undefined) {
888                     specificationObject.features.push(feature);
889                     feature.specification = specificationObject;
890                 } else {
891                     feature.specification = {
892                         name: featureSpecification
893                     };
894                 }
895             }
896             feature.isSpecification = false;
897             featuresByName[feature.name] = feature;
898         }
899
900         var everythingToShow = allFeatures.concat(allSpecifications);
901
902         sortAlphabetically(everythingToShow);
903         
904         renderFeaturesAndSpecifications(everythingToShow);
905         
906         initSearch(everythingToShow);
907         
908         updateSearch(everythingToShow);
909
910         if (window.location.hash) {
911             var hash = window.location.hash;
912             window.location.hash = ""; // Change hash so navigation takes place
913             window.location.hash = hash;
914         }
915     }
916
917     function displayError(error)
918     {
919         var mainContent = document.getElementById("feature-list");
920         var successSubtree = document.importNode(document.getElementById("error-template").content, true);
921
922         var errorMessage = "Unable to load " + error.url;
923
924         if (error.request.status !== 200) {
925             errorMessage += ", status: " + error.request.status + " - " + error.request.statusText;
926         } else if (!error.response) {
927             errorMessage += ", the JSON file cannot be processed.";
928         }
929
930         successSubtree.querySelector("#error-message").textContent = errorMessage;
931
932         mainContent.appendChild(successSubtree);
933     }
934     
935     function updateURL(searchTerm, activeStatusFilters)
936     {
937         var searchString = '';
938         
939         function appendDelimiter()
940         {
941             searchString += searchString.length ? '&' : '?';
942         }
943         
944         if (searchTerm.length > 0) {
945             appendDelimiter();
946             searchString += 'search=' + encodeURIComponent(searchTerm);
947         }
948         
949         if (activeStatusFilters.length) {
950             appendDelimiter();
951             searchString += 'status=' + activeStatusFilters.join(',');
952         }
953
954         var current = window.location.href;
955         window.location.href = current.replace(/#(.*)$/, '') + '#' + searchString;
956     }
957     
958
959     Promise.all([loadJavaScriptCoreFeatures, loadWebCoreFeatures]).then(displayFeatures).catch(displayError);
960 }
961
962 document.addEventListener("DOMContentLoaded", initializeStatusPage);
963 </script>
964
965 <?php get_footer(); ?>