1// Copyright (c) 2011 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
5/**
6 * @fileoverview New tab page
7 * This is the main code for the new tab page used by touch-enabled Chrome
8 * browsers.  For now this is still a prototype.
9 */
10
11// Use an anonymous function to enable strict mode just for this file (which
12// will be concatenated with other files when embedded in Chrome
13cr.define('ntp4', function() {
14  'use strict';
15
16  /**
17   * The CardSlider object to use for changing app pages.
18   * @type {CardSlider|undefined}
19   */
20  var cardSlider;
21
22  /**
23   * Template to use for creating new 'dot' elements
24   * @type {!Element|undefined}
25   */
26  var dotTemplate;
27
28  /**
29   * The 'page-list' element.
30   * @type {!Element|undefined}
31   */
32  var pageList;
33
34  /**
35   * A list of all 'tile-page' elements.
36   * @type {!NodeList|undefined}
37   */
38  var tilePages;
39
40  /**
41   * The Most Visited page.
42   * @type {!Element|undefined}
43   */
44  var mostVisitedPage;
45
46  /**
47   * A list of all 'apps-page' elements.
48   * @type {!NodeList|undefined}
49   */
50  var appsPages;
51
52  /**
53   * The 'dots-list' element.
54   * @type {!Element|undefined}
55   */
56  var dotList;
57
58  /**
59   * A list of all 'dots' elements.
60   * @type {!NodeList|undefined}
61   */
62  var dots;
63
64  /**
65   * The 'trash' element.  Note that technically this is unnecessary,
66   * JavaScript creates the object for us based on the id.  But I don't want
67   * to rely on the ID being the same, and JSCompiler doesn't know about it.
68   * @type {!Element|undefined}
69   */
70  var trash;
71
72  /**
73   * The time in milliseconds for most transitions.  This should match what's
74   * in new_tab.css.  Unfortunately there's no better way to try to time
75   * something to occur until after a transition has completed.
76   * @type {number}
77   * @const
78   */
79  var DEFAULT_TRANSITION_TIME = 500;
80
81  /**
82   * All the Grabber objects currently in use on the page
83   * @type {Array.<Grabber>}
84   */
85  var grabbers = [];
86
87  /**
88   * Invoked at startup once the DOM is available to initialize the app.
89   */
90  function initialize() {
91    // Load the current theme colors.
92    themeChanged(false);
93
94    dotList = getRequiredElement('dot-list');
95    pageList = getRequiredElement('page-list');
96    trash = getRequiredElement('trash');
97    trash.hidden = true;
98
99    // Request data on the apps so we can fill them in.
100    // Note that this is kicked off asynchronously.  'getAppsCallback' will be
101    // invoked at some point after this function returns.
102    chrome.send('getApps');
103
104    // Prevent touch events from triggering any sort of native scrolling
105    document.addEventListener('touchmove', function(e) {
106      e.preventDefault();
107    }, true);
108
109    // Get the template elements and remove them from the DOM.  Things are
110    // simpler if we start with 0 pages and 0 apps and don't leave hidden
111    // template elements behind in the DOM.
112    dots = dotList.getElementsByClassName('dot');
113    assert(dots.length == 1,
114           'Expected exactly one dot in the dots-list.');
115    dotTemplate = dots[0];
116    dotList.removeChild(dots[0]);
117
118    tilePages = pageList.getElementsByClassName('tile-page');
119    appsPages = pageList.getElementsByClassName('apps-page');
120
121    // Initialize the cardSlider without any cards at the moment
122    var sliderFrame = getRequiredElement('card-slider-frame');
123    cardSlider = new CardSlider(sliderFrame, pageList, [], 0,
124                                sliderFrame.offsetWidth);
125    cardSlider.initialize();
126
127    // Ensure the slider is resized appropriately with the window
128    window.addEventListener('resize', function() {
129      cardSlider.resize(sliderFrame.offsetWidth);
130    });
131
132    // Handle the page being changed
133    pageList.addEventListener(
134        CardSlider.EventType.CARD_CHANGED,
135        function(e) {
136          // Update the active dot
137          var curDot = dotList.getElementsByClassName('selected')[0];
138          if (curDot)
139            curDot.classList.remove('selected');
140          var newPageIndex = e.cardSlider.currentCard;
141          dots[newPageIndex].classList.add('selected');
142          // If an app was being dragged, move it to the end of the new page
143          if (draggingAppContainer)
144            appsPages[newPageIndex].appendChild(draggingAppContainer);
145        });
146
147    // Add a drag handler to the body (for drags that don't land on an existing
148    // app)
149    document.addEventListener(Grabber.EventType.DRAG_ENTER, appDragEnter);
150
151    // Handle dropping an app anywhere other than on the trash
152    document.addEventListener(Grabber.EventType.DROP, appDrop);
153
154    // Add handles to manage the transition into/out-of rearrange mode
155    // Note that we assume here that we only use a Grabber for moving apps,
156    // so ANY GRAB event means we're enterring rearrange mode.
157    sliderFrame.addEventListener(Grabber.EventType.GRAB, enterRearrangeMode);
158    sliderFrame.addEventListener(Grabber.EventType.RELEASE, leaveRearrangeMode);
159
160    // Add handlers for the tash can
161    trash.addEventListener(Grabber.EventType.DRAG_ENTER, function(e) {
162      trash.classList.add('hover');
163      e.grabbedElement.classList.add('trashing');
164      e.stopPropagation();
165    });
166    trash.addEventListener(Grabber.EventType.DRAG_LEAVE, function(e) {
167      e.grabbedElement.classList.remove('trashing');
168      trash.classList.remove('hover');
169    });
170    trash.addEventListener(Grabber.EventType.DROP, appTrash);
171
172    cr.ui.decorate($('recently-closed-menu-button'), ntp4.RecentMenuButton);
173    chrome.send('getRecentlyClosedTabs');
174
175    mostVisitedPage = new ntp4.MostVisitedPage('Most Visited');
176    appendTilePage(mostVisitedPage);
177    chrome.send('getMostVisited');
178  }
179
180  /**
181   * Simple common assertion API
182   * @param {*} condition The condition to test.  Note that this may be used to
183   *     test whether a value is defined or not, and we don't want to force a
184   *     cast to Boolean.
185   * @param {string=} opt_message A message to use in any error.
186   */
187  function assert(condition, opt_message) {
188    'use strict';
189    if (!condition) {
190      var msg = 'Assertion failed';
191      if (opt_message)
192        msg = msg + ': ' + opt_message;
193      throw new Error(msg);
194    }
195  }
196
197  /**
198   * Get an element that's known to exist by its ID. We use this instead of just
199   * calling getElementById and not checking the result because this lets us
200   * satisfy the JSCompiler type system.
201   * @param {string} id The identifier name.
202   * @return {!Element} the Element.
203   */
204  function getRequiredElement(id) {
205    var element = document.getElementById(id);
206    assert(element, 'Missing required element: ' + id);
207    return element;
208  }
209
210  /**
211   * Callback invoked by chrome with the apps available.
212   *
213   * Note that calls to this function can occur at any time, not just in
214   * response to a getApps request. For example, when a user installs/uninstalls
215   * an app on another synchronized devices.
216   * @param {Object} data An object with all the data on available
217   *        applications.
218   */
219  function getAppsCallback(data) {
220    // Clean up any existing grabber objects - cancelling any outstanding drag.
221    // Ideally an async app update wouldn't disrupt an active drag but
222    // that would require us to re-use existing elements and detect how the apps
223    // have changed, which would be a lot of work.
224    // Note that we have to explicitly clean up the grabber objects so they stop
225    // listening to events and break the DOM<->JS cycles necessary to enable
226    // collection of all these objects.
227    grabbers.forEach(function(g) {
228      // Note that this may raise DRAG_END/RELEASE events to clean up an
229      // oustanding drag.
230      g.dispose();
231    });
232    assert(!draggingAppContainer && !draggingAppOriginalPosition &&
233           !draggingAppOriginalPage);
234    grabbers = [];
235
236    // Clear any existing apps pages and dots.
237    // TODO(rbyers): It might be nice to preserve animation of dots after an
238    // uninstall. Could we re-use the existing page and dot elements?  It seems
239    // unfortunate to have Chrome send us the entire apps list after an
240    // uninstall.
241    for (var i = 0; i < appsPages.length; i++) {
242      var page = appsPages[i];
243      var dot = page.navigationDot;
244
245      page.tearDown();
246      page.parentNode.removeChild(page);
247      dot.parentNode.removeChild(dot);
248    }
249
250    // Get the array of apps and add any special synthesized entries
251    var apps = data.apps;
252
253    // Sort by launch index
254    apps.sort(function(a, b) {
255      return a.app_launch_index - b.app_launch_index;
256    });
257
258    // Add the apps, creating pages as necessary
259    for (var i = 0; i < apps.length; i++) {
260      var app = apps[i];
261      var pageIndex = (app.page_index || 0);
262      while (pageIndex >= appsPages.length) {
263        var origPageCount = appsPages.length;
264        appendTilePage(new ntp4.AppsPage('Apps'));
265        // Confirm that appsPages is a live object, updated when a new page is
266        // added (otherwise we'd have an infinite loop)
267        assert(appsPages.length == origPageCount + 1, 'expected new page');
268      }
269
270      appsPages[pageIndex].appendApp(app);
271    }
272
273    // Add a couple blank apps pages for testing. TODO(estade): remove this.
274    appendTilePage(new ntp4.AppsPage('Foo'));
275    appendTilePage(new ntp4.AppsPage('Bar'));
276
277    // Tell the slider about the pages
278    updateSliderCards();
279
280    // Mark the current page
281    dots[cardSlider.currentCard].classList.add('selected');
282  }
283
284  /**
285   * Make a synthesized app object representing the chrome web store.  It seems
286   * like this could just as easily come from the back-end, and then would
287   * support being rearranged, etc.
288   * @return {Object} The app object as would be sent from the webui back-end.
289   */
290  function makeWebstoreApp() {
291    return {
292      id: '',   // Empty ID signifies this is a special synthesized app
293      page_index: 0,
294      app_launch_index: -1,   // always first
295      name: templateData.web_store_title,
296      launch_url: templateData.web_store_url,
297      icon_big: getThemeUrl('IDR_WEBSTORE_ICON')
298    };
299  }
300
301  /**
302   * Given a theme resource name, construct a URL for it.
303   * @param {string} resourceName The name of the resource.
304   * @return {string} A url which can be used to load the resource.
305   */
306  function getThemeUrl(resourceName) {
307    return 'chrome://theme/' + resourceName;
308  }
309
310  /**
311   * Callback invoked by chrome whenever an app preference changes.
312   * The normal NTP uses this to keep track of the current launch-type of an
313   * app, updating the choices in the context menu.  We don't have such a menu
314   * so don't use this at all (but it still needs to be here for chrome to
315   * call).
316   * @param {Object} data An object with all the data on available
317   *        applications.
318   */
319  function appsPrefChangeCallback(data) {
320  }
321
322  /**
323   * Invoked whenever the pages in apps-page-list have changed so that
324   * the Slider knows about the new elements.
325   */
326  function updateSliderCards() {
327    var pageNo = cardSlider.currentCard;
328    if (pageNo >= tilePages.length)
329      pageNo = tilePages.length - 1;
330    var pageArray = [];
331    for (var i = 0; i < tilePages.length; i++)
332      pageArray[i] = tilePages[i];
333    cardSlider.setCards(pageArray, pageNo);
334  }
335
336  /**
337   * Appends a tile page (for apps or most visited).
338   *
339   * @param {TilePage} page The page element.
340   * @param {boolean=} opt_animate If true, add the class 'new' to the created
341   *        dot.
342   */
343  function appendTilePage(page, opt_animate) {
344    pageList.appendChild(page);
345
346    // Make a deep copy of the dot template to add a new one.
347    var dotCount = dots.length;
348    var newDot = dotTemplate.cloneNode(true);
349    newDot.querySelector('span').textContent = page.pageName;
350    if (opt_animate)
351      newDot.classList.add('new');
352    dotList.appendChild(newDot);
353    page.navigationDot = newDot;
354
355    // Add click handler to the dot to change the page.
356    // TODO(rbyers): Perhaps this should be TouchHandler.START_EVENT_ (so we
357    // don't rely on synthesized click events, and the change takes effect
358    // before releasing). However, click events seems to be synthesized for a
359    // region outside the border, and a 10px box is too small to require touch
360    // events to fall inside of. We could get around this by adding a box around
361    // the dot for accepting the touch events.
362    function switchPage(e) {
363      cardSlider.selectCard(dotCount, true);
364      e.stopPropagation();
365    }
366    newDot.addEventListener('click', switchPage);
367
368    // Change pages whenever an app is dragged over a dot.
369    newDot.addEventListener(Grabber.EventType.DRAG_ENTER, switchPage);
370  }
371  /**
372   * Search an elements ancestor chain for the nearest element that is a member
373   * of the specified class.
374   * @param {!Element} element The element to start searching from.
375   * @param {string} className The name of the class to locate.
376   * @return {Element} The first ancestor of the specified class or null.
377   */
378  function getParentByClassName(element, className) {
379    for (var e = element; e; e = e.parentElement) {
380      if (e.classList.contains(className))
381        return e;
382    }
383    return null;
384  }
385
386  /**
387   * The container where the app currently being dragged came from.
388   * @type {!Element|undefined}
389   */
390  var draggingAppContainer;
391
392  /**
393   * The apps-page that the app currently being dragged camed from.
394   * @type {!Element|undefined}
395   */
396  var draggingAppOriginalPage;
397
398  /**
399   * The element that was originally after the app currently being dragged (or
400   * null if it was the last on the page).
401   * @type {!Element|undefined}
402   */
403  var draggingAppOriginalPosition;
404
405  /**
406   * Invoked when app dragging begins.
407   * @param {Grabber.Event} e The event from the Grabber indicating the drag.
408   */
409  function appDragStart(e) {
410    // Pull the element out to the sliderFrame using fixed positioning. This
411    // ensures that the app is not affected (remains under the finger) if the
412    // slider changes cards and is translated.  An alternate approach would be
413    // to use fixed positioning for the slider (so that changes to its position
414    // don't affect children that aren't positioned relative to it), but we
415    // don't yet have GPU acceleration for this.
416    var element = e.grabbedElement;
417
418    var pos = element.getBoundingClientRect();
419    element.style.webkitTransform = '';
420
421    element.style.position = 'fixed';
422    // Don't want to zoom around the middle since the left/top co-ordinates
423    // are post-transform values.
424    element.style.webkitTransformOrigin = 'left top';
425    element.style.left = pos.left + 'px';
426    element.style.top = pos.top + 'px';
427
428    // Keep track of what app is being dragged and where it came from
429    assert(!draggingAppContainer, 'got DRAG_START without DRAG_END');
430    draggingAppContainer = element.parentNode;
431    assert(draggingAppContainer.classList.contains('app-container'));
432    draggingAppOriginalPosition = draggingAppContainer.nextSibling;
433    draggingAppOriginalPage = draggingAppContainer.parentNode;
434
435    // Move the app out of the container
436    // Note that appendChild also removes the element from its current parent.
437    sliderFrame.appendChild(element);
438  }
439
440  /**
441   * Invoked when app dragging terminates (either successfully or not)
442   * @param {Grabber.Event} e The event from the Grabber.
443   */
444  function appDragEnd(e) {
445    // Stop floating the app
446    var appBeingDragged = e.grabbedElement;
447    assert(appBeingDragged.classList.contains('app'));
448    appBeingDragged.style.position = '';
449    appBeingDragged.style.webkitTransformOrigin = '';
450    appBeingDragged.style.left = '';
451    appBeingDragged.style.top = '';
452
453    // Ensure the trash can is not active (we won't necessarily get a DRAG_LEAVE
454    // for it - eg. if we drop on it, or the drag is cancelled)
455    trash.classList.remove('hover');
456    appBeingDragged.classList.remove('trashing');
457
458    // If we have an active drag (i.e. it wasn't aborted by an app update)
459    if (draggingAppContainer) {
460      // Put the app back into it's container
461      if (appBeingDragged.parentNode != draggingAppContainer)
462        draggingAppContainer.appendChild(appBeingDragged);
463
464      // If we care about the container's original position
465      if (draggingAppOriginalPage)
466      {
467        // Then put the container back where it came from
468        if (draggingAppOriginalPosition) {
469          draggingAppOriginalPage.insertBefore(draggingAppContainer,
470                                               draggingAppOriginalPosition);
471        } else {
472          draggingAppOriginalPage.appendChild(draggingAppContainer);
473        }
474      }
475    }
476
477    draggingAppContainer = undefined;
478    draggingAppOriginalPage = undefined;
479    draggingAppOriginalPosition = undefined;
480  }
481
482  /**
483   * Invoked when an app is dragged over another app.  Updates the DOM to affect
484   * the rearrangement (but doesn't commit the change until the app is dropped).
485   * @param {Grabber.Event} e The event from the Grabber indicating the drag.
486   */
487  function appDragEnter(e)
488  {
489    assert(draggingAppContainer, 'expected stored container');
490    var sourceContainer = draggingAppContainer;
491
492    // Ensure enter events delivered to an app-container don't also get
493    // delivered to the document.
494    e.stopPropagation();
495
496    var curPage = appsPages[cardSlider.currentCard];
497    var followingContainer = null;
498
499    // If we dragged over a specific app, determine which one to insert before
500    if (e.currentTarget != document) {
501
502      // Start by assuming we'll insert the app before the one dragged over
503      followingContainer = e.currentTarget;
504      assert(followingContainer.classList.contains('app-container'),
505             'expected drag over container');
506      assert(followingContainer.parentNode == curPage);
507      if (followingContainer == draggingAppContainer)
508        return;
509
510      // But if it's after the current container position then we'll need to
511      // move ahead by one to account for the container being removed.
512      if (curPage == draggingAppContainer.parentNode) {
513        for (var c = draggingAppContainer; c; c = c.nextElementSibling) {
514          if (c == followingContainer) {
515            followingContainer = followingContainer.nextElementSibling;
516            break;
517          }
518        }
519      }
520    }
521
522    // Move the container to the appropriate place on the page
523    curPage.insertBefore(draggingAppContainer, followingContainer);
524  }
525
526  /**
527   * Invoked when an app is dropped on the trash
528   * @param {Grabber.Event} e The event from the Grabber indicating the drop.
529   */
530  function appTrash(e) {
531    var appElement = e.grabbedElement;
532    assert(appElement.classList.contains('app'));
533    var appId = appElement.getAttribute('app-id');
534    assert(appId);
535
536    // Mark this drop as handled so that the catch-all drop handler
537    // on the document doesn't see this event.
538    e.stopPropagation();
539
540    // Tell chrome to uninstall the app (prompting the user)
541    chrome.send('uninstallApp', [appId]);
542  }
543
544  /**
545   * Called when an app is dropped anywhere other than the trash can.  Commits
546   * any movement that has occurred.
547   * @param {Grabber.Event} e The event from the Grabber indicating the drop.
548   */
549  function appDrop(e) {
550    if (!draggingAppContainer)
551      // Drag was aborted (eg. due to an app update) - do nothing
552      return;
553
554    // If the app is dropped back into it's original position then do nothing
555    assert(draggingAppOriginalPage);
556    if (draggingAppContainer.parentNode == draggingAppOriginalPage &&
557        draggingAppContainer.nextSibling == draggingAppOriginalPosition)
558      return;
559
560    // Determine which app was being dragged
561    var appElement = e.grabbedElement;
562    assert(appElement.classList.contains('app'));
563    var appId = appElement.getAttribute('app-id');
564    assert(appId);
565
566    // Update the page index for the app if it's changed.  This doesn't trigger
567    // a call to getAppsCallback so we want to do it before reorderApps
568    var pageIndex = cardSlider.currentCard;
569    assert(pageIndex >= 0 && pageIndex < appsPages.length,
570           'page number out of range');
571    if (appsPages[pageIndex] != draggingAppOriginalPage)
572      chrome.send('setPageIndex', [appId, pageIndex]);
573
574    // Put the app being dragged back into it's container
575    draggingAppContainer.appendChild(appElement);
576
577    // Create a list of all appIds in the order now present in the DOM
578    var appIds = [];
579    for (var page = 0; page < appsPages.length; page++) {
580      var appsOnPage = appsPages[page].getElementsByClassName('app');
581      for (var i = 0; i < appsOnPage.length; i++) {
582        var id = appsOnPage[i].getAttribute('app-id');
583        if (id)
584          appIds.push(id);
585      }
586    }
587
588    // We are going to commit this repositioning - clear the original position
589    draggingAppOriginalPage = undefined;
590    draggingAppOriginalPosition = undefined;
591
592    // Tell chrome to update its database to persist this new order of apps This
593    // will cause getAppsCallback to be invoked and the apps to be redrawn.
594    chrome.send('reorderApps', [appId, appIds]);
595    appMoved = true;
596  }
597
598  /**
599   * Set to true if we're currently in rearrange mode and an app has
600   * been successfully dropped to a new location.  This indicates that
601   * a getAppsCallback call is pending and we can rely on the DOM being
602   * updated by that.
603   * @type {boolean}
604   */
605  var appMoved = false;
606
607  /**
608   * Invoked whenever some app is grabbed
609   * @param {Grabber.Event} e The Grabber Grab event.
610   */
611  function enterRearrangeMode(e)
612  {
613    // Stop the slider from sliding for this touch
614    cardSlider.cancelTouch();
615
616    // Add an extra blank page in case the user wants to create a new page
617    appendTilePage(new ntp4.AppsPage(''), true);
618    var pageAdded = appsPages.length - 1;
619    window.setTimeout(function() {
620      dots[pageAdded].classList.remove('new');
621    }, 0);
622
623    updateSliderCards();
624
625    // Cause the dot-list to grow
626    getRequiredElement('footer').classList.add('rearrange-mode');
627
628    assert(!appMoved, 'appMoved should not be set yet');
629  }
630
631  /**
632   * Invoked whenever some app is released
633   * @param {Grabber.Event} e The Grabber RELEASE event.
634   */
635  function leaveRearrangeMode(e)
636  {
637    // Return the dot-list to normal
638    getRequiredElement('footer').classList.remove('rearrange-mode');
639
640    // If we didn't successfully re-arrange an app, then we won't be
641    // refreshing the app view in getAppCallback and need to explicitly remove
642    // the extra empty page we added.  We don't want to do this in the normal
643    // case because if we did actually drop an app there, we want to retain that
644    // page as our current page number.
645    if (!appMoved) {
646      assert(appsPages[appsPages.length - 1].
647             getElementsByClassName('app-container').length == 0,
648             'Last app page should be empty');
649      removePage(appsPages.length - 1);
650    }
651    appMoved = false;
652  }
653
654  /**
655   * Remove the page with the specified index and update the slider.
656   * @param {number} pageNo The index of the page to remove.
657   */
658  function removePage(pageNo) {
659    pageList.removeChild(tilePages[pageNo]);
660
661    // Remove the corresponding dot
662    // Need to give it a chance to animate though
663    var dot = dots[pageNo];
664    dot.classList.add('new');
665    window.setTimeout(function() {
666      // If we've re-created the apps (eg. because an app was uninstalled) then
667      // we will have removed the old dots from the document already, so skip.
668      if (dot.parentNode)
669        dot.parentNode.removeChild(dot);
670    }, DEFAULT_TRANSITION_TIME);
671
672    updateSliderCards();
673  }
674
675  // TODO(estade): remove |hasAttribution|.
676  // TODO(estade): rename newtab.css to new_tab_theme.css
677  function themeChanged(hasAttribution) {
678    $('themecss').href = 'chrome://theme/css/newtab.css?' + Date.now();
679  }
680
681  function setRecentlyClosedTabs(dataItems) {
682    $('recently-closed-menu-button').dataItems = dataItems;
683  }
684
685  function setMostVisitedPages(data, firstRun, hasBlacklistedUrls) {
686    mostVisitedPage.data = data;
687  }
688
689  // Return an object with all the exports
690  return {
691    assert: assert,
692    appsPrefChangeCallback: appsPrefChangeCallback,
693    getAppsCallback: getAppsCallback,
694    initialize: initialize,
695    themeChanged: themeChanged,
696    setRecentlyClosedTabs: setRecentlyClosedTabs,
697    setMostVisitedPages: setMostVisitedPages,
698  };
699});
700
701// publish ntp globals
702// TODO(estade): update the content handlers to use ntp namespace instead of
703// making these global.
704var assert = ntp4.assert;
705var getAppsCallback = ntp4.getAppsCallback;
706var appsPrefChangeCallback = ntp4.appsPrefChangeCallback;
707var themeChanged = ntp4.themeChanged;
708var recentlyClosedTabs = ntp4.setRecentlyClosedTabs;
709var mostVisitedPages = ntp4.setMostVisitedPages;
710
711document.addEventListener('DOMContentLoaded', ntp4.initialize);
712