Stack sampling profiler: add fire-and-forget interface
[chromium-blink-merge.git] / components / dom_distiller / core / javascript / dom_distiller_viewer.js
blobf953414f028f055b849012a2330f92f1f680a242
1 // Copyright 2014 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.
5 // This variable will be changed by iOS scripts.
6 var distiller_on_ios = false;
8 function addToPage(html) {
9 var div = document.createElement('div');
10 div.innerHTML = html;
11 document.getElementById('content').appendChild(div);
12 fillYouTubePlaceholders();
15 function fillYouTubePlaceholders() {
16 var placeholders = document.getElementsByClassName('embed-placeholder');
17 for (var i = 0; i < placeholders.length; i++) {
18 if (!placeholders[i].hasAttribute('data-type') ||
19 placeholders[i].getAttribute('data-type') != 'youtube' ||
20 !placeholders[i].hasAttribute('data-id')) {
21 continue;
23 var embed = document.createElement('iframe');
24 var url = 'http://www.youtube.com/embed/' +
25 placeholders[i].getAttribute('data-id');
26 embed.setAttribute('class', 'youtubeIframe');
27 embed.setAttribute('src', url);
28 embed.setAttribute('type', 'text/html');
29 embed.setAttribute('frameborder', '0');
31 var parent = placeholders[i].parentElement;
32 var container = document.createElement('div');
33 container.setAttribute('class', 'youtubeContainer');
34 container.appendChild(embed);
36 parent.replaceChild(container, placeholders[i]);
40 function showLoadingIndicator(isLastPage) {
41 document.getElementById('loadingIndicator').className =
42 isLastPage ? 'hidden' : 'visible';
43 updateLoadingIndicator(isLastPage);
46 // Sets the title.
47 function setTitle(title) {
48 var holder = document.getElementById('titleHolder');
50 holder.textContent = title;
51 document.title = title;
54 // Set the text direction of the document ('ltr', 'rtl', or 'auto').
55 function setTextDirection(direction) {
56 document.body.setAttribute('dir', direction);
59 // Maps JS Font Family to CSS class and then changes body class name.
60 // CSS classes must agree with distilledpage.css.
61 function useFontFamily(fontFamily) {
62 var cssClass;
63 if (fontFamily == "serif") {
64 cssClass = "serif";
65 } else if (fontFamily == "monospace") {
66 cssClass = "monospace";
67 } else {
68 cssClass = "sans-serif";
70 // Relies on the classname order of the body being Theme class, then Font
71 // Family class.
72 var themeClass = document.body.className.split(" ")[0];
73 document.body.className = themeClass + " " + cssClass;
76 // Maps JS theme to CSS class and then changes body class name.
77 // CSS classes must agree with distilledpage.css.
78 function useTheme(theme) {
79 var cssClass;
80 if (theme == "sepia") {
81 cssClass = "sepia";
82 } else if (theme == "dark") {
83 cssClass = "dark";
84 } else {
85 cssClass = "light";
87 // Relies on the classname order of the body being Theme class, then Font
88 // Family class.
89 var fontFamilyClass = document.body.className.split(" ")[1];
90 document.body.className = cssClass + " " + fontFamilyClass;
93 var updateLoadingIndicator = function() {
94 var colors = ["red", "yellow", "green", "blue"];
95 return function(isLastPage) {
96 if (!isLastPage && typeof this.colorShuffle == "undefined") {
97 var loader = document.getElementById("loader");
98 if (loader) {
99 var colorIndex = -1;
100 this.colorShuffle = setInterval(function() {
101 colorIndex = (colorIndex + 1) % colors.length;
102 loader.className = colors[colorIndex];
103 }, 600);
105 } else if (isLastPage && typeof this.colorShuffle != "undefined") {
106 clearInterval(this.colorShuffle);
109 }();
112 * Show the distiller feedback form.
113 * @param questionText The i18n text for the feedback question.
114 * @param yesText The i18n text for the feedback answer 'YES'.
115 * @param noText The i18n text for the feedback answer 'NO'.
117 function showFeedbackForm(questionText, yesText, noText) {
118 // If the distiller is running on iOS, do not show the feedback form. This
119 // variable is set in distiller_viewer.cc before this function is run.
120 if (distiller_on_ios) return;
122 document.getElementById('feedbackYes').innerText = yesText;
123 document.getElementById('feedbackNo').innerText = noText;
124 document.getElementById('feedbackQuestion').innerText = questionText;
126 document.getElementById('contentWrap').style.paddingBottom = '120px';
127 document.getElementById('feedbackContainer').style.display = 'block';
131 * Send feedback about this distilled article.
132 * @param good True if the feedback was positive, false if negative.
134 function sendFeedback(good) {
135 var img = document.createElement('img');
136 if (good) {
137 img.src = '/feedbackgood';
138 } else {
139 img.src = '/feedbackbad';
141 img.style.display = "none";
142 document.body.appendChild(img);
145 // Add a listener to the "View Original" link to report opt-outs.
146 document.getElementById('closeReaderView').addEventListener('click',
147 function(e) {
148 var img = document.createElement('img');
149 img.src = "/vieworiginal";
150 img.style.display = "none";
151 document.body.appendChild(img);
152 }, true);
154 document.getElementById('feedbackYes').addEventListener('click', function(e) {
155 sendFeedback(true);
156 document.getElementById('feedbackContainer').className += " fadeOut";
157 }, true);
159 document.getElementById('feedbackNo').addEventListener('click', function(e) {
160 sendFeedback(false);
161 document.getElementById('feedbackContainer').className += " fadeOut";
162 }, true);
164 document.getElementById('feedbackContainer').addEventListener('animationend',
165 function(e) {
166 document.getElementById('feedbackContainer').style.display = 'none';
167 // Close the gap where the feedback form was.
168 var contentWrap = document.getElementById('contentWrap');
169 contentWrap.style.transition = '0.5s';
170 contentWrap.style.paddingBottom = '0px';
171 }, true);
173 document.getElementById('contentWrap').addEventListener('transitionend',
174 function(e) {
175 var contentWrap = document.getElementById('contentWrap');
176 contentWrap.style.transition = '';
177 }, true);
179 var pincher = (function() {
180 'use strict';
181 // When users pinch in Reader Mode, the page would zoom in or out as if it
182 // is a normal web page allowing user-zoom. At the end of pinch gesture, the
183 // page would do text reflow. These pinch-to-zoom and text reflow effects
184 // are not native, but are emulated using CSS and JavaScript.
186 // In order to achieve near-native zooming and panning frame rate, fake 3D
187 // transform is used so that the layer doesn't repaint for each frame.
189 // After the text reflow, the web content shown in the viewport should
190 // roughly be the same paragraph before zooming.
192 // The control point of font size is the html element, so that both "em" and
193 // "rem" are adjusted.
195 // TODO(wychen): Improve scroll position when elementFromPoint is body.
197 var pinching = false;
198 var fontSizeAnchor = 1.0;
200 var focusElement = null;
201 var focusPos = 0;
202 var initClientMid;
204 var clampedScale = 1;
206 var lastSpan;
207 var lastClientMid;
209 var scale = 1;
210 var shiftX;
211 var shiftY;
213 // The zooming speed relative to pinching speed.
214 var FONT_SCALE_MULTIPLIER = 0.5;
215 var MIN_SPAN_LENGTH = 20;
217 // The font size is guaranteed to be in px.
218 var baseSize =
219 parseFloat(getComputedStyle(document.documentElement).fontSize);
221 var refreshTransform = function() {
222 var slowedScale = Math.exp(Math.log(scale) * FONT_SCALE_MULTIPLIER);
223 clampedScale = Math.max(0.4, Math.min(2.5, fontSizeAnchor * slowedScale));
225 // Use "fake" 3D transform so that the layer is not repainted.
226 // With 2D transform, the frame rate would be much lower.
227 document.body.style.transform =
228 'translate3d(' + shiftX + 'px,' +
229 shiftY + 'px, 0px)' +
230 'scale(' + clampedScale/fontSizeAnchor + ')';
233 function endPinch() {
234 pinching = false;
236 document.body.style.transformOrigin = '';
237 document.body.style.transform = '';
238 document.documentElement.style.fontSize = clampedScale * baseSize + "px";
240 var rect = focusElement.getBoundingClientRect();
241 var targetTop = focusPos * (rect.bottom - rect.top) + rect.top +
242 document.body.scrollTop - (initClientMid.y + shiftY);
243 document.body.scrollTop = targetTop;
246 function touchSpan(e) {
247 var count = e.touches.length;
248 var mid = touchClientMid(e);
249 var sum = 0;
250 for (var i = 0; i < count; i++) {
251 var dx = (e.touches[i].clientX - mid.x);
252 var dy = (e.touches[i].clientY - mid.y);
253 sum += Math.hypot(dx, dy);
255 // Avoid very small span.
256 return Math.max(MIN_SPAN_LENGTH, sum/count);
259 function touchClientMid(e) {
260 var count = e.touches.length;
261 var sumX = 0;
262 var sumY = 0;
263 for (var i = 0; i < count; i++) {
264 sumX += e.touches[i].clientX;
265 sumY += e.touches[i].clientY;
267 return {x: sumX/count, y: sumY/count};
270 function touchPageMid(e) {
271 var clientMid = touchClientMid(e);
272 return {x: clientMid.x - e.touches[0].clientX + e.touches[0].pageX,
273 y: clientMid.y - e.touches[0].clientY + e.touches[0].pageY};
276 return {
277 handleTouchStart: function(e) {
278 if (e.touches.length < 2) return;
279 e.preventDefault();
281 var span = touchSpan(e);
282 var clientMid = touchClientMid(e);
284 if (e.touches.length > 2) {
285 lastSpan = span;
286 lastClientMid = clientMid;
287 refreshTransform();
288 return;
291 scale = 1;
292 shiftX = 0;
293 shiftY = 0;
295 pinching = true;
296 fontSizeAnchor =
297 parseFloat(getComputedStyle(document.documentElement).fontSize) /
298 baseSize;
300 var pinchOrigin = touchPageMid(e);
301 document.body.style.transformOrigin =
302 pinchOrigin.x + 'px ' + pinchOrigin.y + 'px';
304 // Try to preserve the pinching center after text reflow.
305 // This is accurate to the HTML element level.
306 focusElement = document.elementFromPoint(clientMid.x, clientMid.y);
307 var rect = focusElement.getBoundingClientRect();
308 initClientMid = clientMid;
309 focusPos = (initClientMid.y - rect.top) / (rect.bottom - rect.top);
311 lastSpan = span;
312 lastClientMid = clientMid;
314 refreshTransform();
317 handleTouchMove: function(e) {
318 if (!pinching) return;
319 if (e.touches.length < 2) return;
320 e.preventDefault();
322 var span = touchSpan(e);
323 var clientMid = touchClientMid(e);
325 scale *= touchSpan(e) / lastSpan;
326 shiftX += clientMid.x - lastClientMid.x;
327 shiftY += clientMid.y - lastClientMid.y;
329 refreshTransform();
331 lastSpan = span;
332 lastClientMid = clientMid;
335 handleTouchEnd: function(e) {
336 if (!pinching) return;
337 e.preventDefault();
339 var span = touchSpan(e);
340 var clientMid = touchClientMid(e);
342 if (e.touches.length >= 2) {
343 lastSpan = span;
344 lastClientMid = clientMid;
345 refreshTransform();
346 return;
349 endPinch();
352 handleTouchCancel: function(e) {
353 endPinch();
356 reset: function() {
357 scale = 1;
358 shiftX = 0;
359 shiftY = 0;
360 clampedScale = 1;
361 document.documentElement.style.fontSize = clampedScale * baseSize + "px";
364 status: function() {
365 return {
366 scale: scale,
367 clampedScale: clampedScale,
368 shiftX: shiftX,
369 shiftY: shiftY
373 }());
375 window.addEventListener('touchstart', pincher.handleTouchStart, false);
376 window.addEventListener('touchmove', pincher.handleTouchMove, false);
377 window.addEventListener('touchend', pincher.handleTouchEnd, false);
378 window.addEventListener('touchcancel', pincher.handleTouchCancel, false);