UIUtils.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086
  1. /*
  2. * Copyright (C) 2011 Google Inc. All rights reserved.
  3. * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
  4. * Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com).
  5. * Copyright (C) 2009 Joseph Pecoraro
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions
  9. * are met:
  10. *
  11. * 1. Redistributions of source code must retain the above copyright
  12. * notice, this list of conditions and the following disclaimer.
  13. * 2. Redistributions in binary form must reproduce the above copyright
  14. * notice, this list of conditions and the following disclaimer in the
  15. * documentation and/or other materials provided with the distribution.
  16. * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
  17. * its contributors may be used to endorse or promote products derived
  18. * from this software without specific prior written permission.
  19. *
  20. * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
  21. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  22. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  23. * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
  24. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  25. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  26. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  27. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  28. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  29. * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  30. */
  31. /**
  32. * @param {Element} element
  33. * @param {?function(Event): boolean} elementDragStart
  34. * @param {function(Event)} elementDrag
  35. * @param {?function(Event)} elementDragEnd
  36. * @param {string} cursor
  37. */
  38. WebInspector.installDragHandle = function(element, elementDragStart, elementDrag, elementDragEnd, cursor)
  39. {
  40. element.addEventListener("mousedown", WebInspector._elementDragStart.bind(WebInspector, elementDragStart, elementDrag, elementDragEnd, cursor), false);
  41. }
  42. /**
  43. * @param {?function(Event)} elementDragStart
  44. * @param {function(Event)} elementDrag
  45. * @param {?function(Event)} elementDragEnd
  46. * @param {string} cursor
  47. * @param {Event} event
  48. */
  49. WebInspector._elementDragStart = function(elementDragStart, elementDrag, elementDragEnd, cursor, event)
  50. {
  51. // Only drag upon left button. Right will likely cause a context menu. So will ctrl-click on mac.
  52. if (event.button || (WebInspector.isMac() && event.ctrlKey))
  53. return;
  54. if (WebInspector._elementDraggingEventListener)
  55. return;
  56. if (elementDragStart && !elementDragStart(event))
  57. return;
  58. if (WebInspector._elementDraggingGlassPane) {
  59. WebInspector._elementDraggingGlassPane.dispose();
  60. delete WebInspector._elementDraggingGlassPane;
  61. }
  62. var targetDocument = event.target.ownerDocument;
  63. WebInspector._elementDraggingEventListener = elementDrag;
  64. WebInspector._elementEndDraggingEventListener = elementDragEnd;
  65. WebInspector._mouseOutWhileDraggingTargetDocument = targetDocument;
  66. targetDocument.addEventListener("mousemove", WebInspector._elementDragMove, true);
  67. targetDocument.addEventListener("mouseup", WebInspector._elementDragEnd, true);
  68. targetDocument.addEventListener("mouseout", WebInspector._mouseOutWhileDragging, true);
  69. targetDocument.body.style.cursor = cursor;
  70. event.preventDefault();
  71. }
  72. WebInspector._mouseOutWhileDragging = function()
  73. {
  74. WebInspector._unregisterMouseOutWhileDragging();
  75. WebInspector._elementDraggingGlassPane = new WebInspector.GlassPane();
  76. }
  77. WebInspector._unregisterMouseOutWhileDragging = function()
  78. {
  79. if (!WebInspector._mouseOutWhileDraggingTargetDocument)
  80. return;
  81. WebInspector._mouseOutWhileDraggingTargetDocument.removeEventListener("mouseout", WebInspector._mouseOutWhileDragging, true);
  82. delete WebInspector._mouseOutWhileDraggingTargetDocument;
  83. }
  84. WebInspector._elementDragMove = function(event)
  85. {
  86. if (WebInspector._elementDraggingEventListener(event))
  87. WebInspector._cancelDragEvents(event);
  88. }
  89. WebInspector._cancelDragEvents = function(event)
  90. {
  91. var targetDocument = event.target.ownerDocument;
  92. targetDocument.removeEventListener("mousemove", WebInspector._elementDragMove, true);
  93. targetDocument.removeEventListener("mouseup", WebInspector._elementDragEnd, true);
  94. WebInspector._unregisterMouseOutWhileDragging();
  95. targetDocument.body.style.removeProperty("cursor");
  96. if (WebInspector._elementDraggingGlassPane)
  97. WebInspector._elementDraggingGlassPane.dispose();
  98. delete WebInspector._elementDraggingGlassPane;
  99. delete WebInspector._elementDraggingEventListener;
  100. delete WebInspector._elementEndDraggingEventListener;
  101. }
  102. WebInspector._elementDragEnd = function(event)
  103. {
  104. var elementDragEnd = WebInspector._elementEndDraggingEventListener;
  105. WebInspector._cancelDragEvents(event);
  106. event.preventDefault();
  107. if (elementDragEnd)
  108. elementDragEnd(event);
  109. }
  110. /**
  111. * @constructor
  112. */
  113. WebInspector.GlassPane = function()
  114. {
  115. this.element = document.createElement("div");
  116. this.element.style.cssText = "position:absolute;top:0;bottom:0;left:0;right:0;background-color:transparent;z-index:1000;";
  117. this.element.id = "glass-pane-for-drag";
  118. document.body.appendChild(this.element);
  119. WebInspector._glassPane = this;
  120. }
  121. WebInspector.GlassPane.prototype = {
  122. dispose: function()
  123. {
  124. delete WebInspector._glassPane;
  125. WebInspector.inspectorView.focus();
  126. if (this.element.parentElement)
  127. this.element.parentElement.removeChild(this.element);
  128. }
  129. }
  130. WebInspector.isBeingEdited = function(element)
  131. {
  132. if (element.hasStyleClass("text-prompt") || element.nodeName === "INPUT")
  133. return true;
  134. if (!WebInspector.__editingCount)
  135. return false;
  136. while (element) {
  137. if (element.__editing)
  138. return true;
  139. element = element.parentElement;
  140. }
  141. return false;
  142. }
  143. WebInspector.markBeingEdited = function(element, value)
  144. {
  145. if (value) {
  146. if (element.__editing)
  147. return false;
  148. element.__editing = true;
  149. WebInspector.__editingCount = (WebInspector.__editingCount || 0) + 1;
  150. } else {
  151. if (!element.__editing)
  152. return false;
  153. delete element.__editing;
  154. --WebInspector.__editingCount;
  155. }
  156. return true;
  157. }
  158. /**
  159. * @constructor
  160. * @param {function(Element,string,string,*,string)} commitHandler
  161. * @param {function(Element,*)} cancelHandler
  162. * @param {*=} context
  163. */
  164. WebInspector.EditingConfig = function(commitHandler, cancelHandler, context)
  165. {
  166. this.commitHandler = commitHandler;
  167. this.cancelHandler = cancelHandler
  168. this.context = context;
  169. /**
  170. * Handles the "paste" event, return values are the same as those for customFinishHandler
  171. * @type {function(Element)|undefined}
  172. */
  173. this.pasteHandler;
  174. /**
  175. * Whether the edited element is multiline
  176. * @type {boolean|undefined}
  177. */
  178. this.multiline;
  179. /**
  180. * Custom finish handler for the editing session (invoked on keydown)
  181. * @type {function(Element,*)|undefined}
  182. */
  183. this.customFinishHandler;
  184. }
  185. WebInspector.EditingConfig.prototype = {
  186. setPasteHandler: function(pasteHandler)
  187. {
  188. this.pasteHandler = pasteHandler;
  189. },
  190. /**
  191. * @param {string} initialValue
  192. * @param {Object} mode
  193. * @param {string} theme
  194. * @param {boolean=} lineWrapping
  195. * @param {boolean=} smartIndent
  196. */
  197. setMultilineOptions: function(initialValue, mode, theme, lineWrapping, smartIndent)
  198. {
  199. this.multiline = true;
  200. this.initialValue = initialValue;
  201. this.mode = mode;
  202. this.theme = theme;
  203. this.lineWrapping = lineWrapping;
  204. this.smartIndent = smartIndent;
  205. },
  206. setCustomFinishHandler: function(customFinishHandler)
  207. {
  208. this.customFinishHandler = customFinishHandler;
  209. }
  210. }
  211. WebInspector.CSSNumberRegex = /^(-?(?:\d+(?:\.\d+)?|\.\d+))$/;
  212. WebInspector.StyleValueDelimiters = " \xA0\t\n\"':;,/()";
  213. /**
  214. * @param {Event} event
  215. * @return {?string}
  216. */
  217. WebInspector._valueModificationDirection = function(event)
  218. {
  219. var direction = null;
  220. if (event.type === "mousewheel") {
  221. if (event.wheelDeltaY > 0)
  222. direction = "Up";
  223. else if (event.wheelDeltaY < 0)
  224. direction = "Down";
  225. } else {
  226. if (event.keyIdentifier === "Up" || event.keyIdentifier === "PageUp")
  227. direction = "Up";
  228. else if (event.keyIdentifier === "Down" || event.keyIdentifier === "PageDown")
  229. direction = "Down";
  230. }
  231. return direction;
  232. }
  233. /**
  234. * @param {string} hexString
  235. * @param {Event} event
  236. */
  237. WebInspector._modifiedHexValue = function(hexString, event)
  238. {
  239. var direction = WebInspector._valueModificationDirection(event);
  240. if (!direction)
  241. return hexString;
  242. var number = parseInt(hexString, 16);
  243. if (isNaN(number) || !isFinite(number))
  244. return hexString;
  245. var maxValue = Math.pow(16, hexString.length) - 1;
  246. var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
  247. var delta;
  248. if (arrowKeyOrMouseWheelEvent)
  249. delta = (direction === "Up") ? 1 : -1;
  250. else
  251. delta = (event.keyIdentifier === "PageUp") ? 16 : -16;
  252. if (event.shiftKey)
  253. delta *= 16;
  254. var result = number + delta;
  255. if (result < 0)
  256. result = 0; // Color hex values are never negative, so clamp to 0.
  257. else if (result > maxValue)
  258. return hexString;
  259. // Ensure the result length is the same as the original hex value.
  260. var resultString = result.toString(16).toUpperCase();
  261. for (var i = 0, lengthDelta = hexString.length - resultString.length; i < lengthDelta; ++i)
  262. resultString = "0" + resultString;
  263. return resultString;
  264. }
  265. /**
  266. * @param {number} number
  267. * @param {Event} event
  268. */
  269. WebInspector._modifiedFloatNumber = function(number, event)
  270. {
  271. var direction = WebInspector._valueModificationDirection(event);
  272. if (!direction)
  273. return number;
  274. var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
  275. // Jump by 10 when shift is down or jump by 0.1 when Alt/Option is down.
  276. // Also jump by 10 for page up and down, or by 100 if shift is held with a page key.
  277. var changeAmount = 1;
  278. if (event.shiftKey && !arrowKeyOrMouseWheelEvent)
  279. changeAmount = 100;
  280. else if (event.shiftKey || !arrowKeyOrMouseWheelEvent)
  281. changeAmount = 10;
  282. else if (event.altKey)
  283. changeAmount = 0.1;
  284. if (direction === "Down")
  285. changeAmount *= -1;
  286. // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns.
  287. // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1.
  288. var result = Number((number + changeAmount).toFixed(6));
  289. if (!String(result).match(WebInspector.CSSNumberRegex))
  290. return null;
  291. return result;
  292. }
  293. /**
  294. * @param {Event} event
  295. * @param {Element} element
  296. * @param {function(string,string)=} finishHandler
  297. * @param {function(string)=} suggestionHandler
  298. * @param {function(number):number=} customNumberHandler
  299. */
  300. WebInspector.handleElementValueModifications = function(event, element, finishHandler, suggestionHandler, customNumberHandler)
  301. {
  302. var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
  303. var pageKeyPressed = (event.keyIdentifier === "PageUp" || event.keyIdentifier === "PageDown");
  304. if (!arrowKeyOrMouseWheelEvent && !pageKeyPressed)
  305. return false;
  306. var selection = window.getSelection();
  307. if (!selection.rangeCount)
  308. return false;
  309. var selectionRange = selection.getRangeAt(0);
  310. if (!selectionRange.commonAncestorContainer.isSelfOrDescendant(element))
  311. return false;
  312. var originalValue = element.textContent;
  313. var wordRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, WebInspector.StyleValueDelimiters, element);
  314. var wordString = wordRange.toString();
  315. if (suggestionHandler && suggestionHandler(wordString))
  316. return false;
  317. var replacementString;
  318. var prefix, suffix, number;
  319. var matches;
  320. matches = /(.*#)([\da-fA-F]+)(.*)/.exec(wordString);
  321. if (matches && matches.length) {
  322. prefix = matches[1];
  323. suffix = matches[3];
  324. number = WebInspector._modifiedHexValue(matches[2], event);
  325. if (customNumberHandler)
  326. number = customNumberHandler(number);
  327. replacementString = prefix + number + suffix;
  328. } else {
  329. matches = /(.*?)(-?(?:\d+(?:\.\d+)?|\.\d+))(.*)/.exec(wordString);
  330. if (matches && matches.length) {
  331. prefix = matches[1];
  332. suffix = matches[3];
  333. number = WebInspector._modifiedFloatNumber(parseFloat(matches[2]), event);
  334. // Need to check for null explicitly.
  335. if (number === null)
  336. return false;
  337. if (customNumberHandler)
  338. number = customNumberHandler(number);
  339. replacementString = prefix + number + suffix;
  340. }
  341. }
  342. if (replacementString) {
  343. var replacementTextNode = document.createTextNode(replacementString);
  344. wordRange.deleteContents();
  345. wordRange.insertNode(replacementTextNode);
  346. var finalSelectionRange = document.createRange();
  347. finalSelectionRange.setStart(replacementTextNode, 0);
  348. finalSelectionRange.setEnd(replacementTextNode, replacementString.length);
  349. selection.removeAllRanges();
  350. selection.addRange(finalSelectionRange);
  351. event.handled = true;
  352. event.preventDefault();
  353. if (finishHandler)
  354. finishHandler(originalValue, replacementString);
  355. return true;
  356. }
  357. return false;
  358. }
  359. /**
  360. * @param {Element} element
  361. * @param {WebInspector.EditingConfig=} config
  362. */
  363. WebInspector.startEditing = function(element, config)
  364. {
  365. if (!WebInspector.markBeingEdited(element, true))
  366. return null;
  367. config = config || new WebInspector.EditingConfig(function() {}, function() {});
  368. var committedCallback = config.commitHandler;
  369. var cancelledCallback = config.cancelHandler;
  370. var pasteCallback = config.pasteHandler;
  371. var context = config.context;
  372. var isMultiline = config.multiline || false;
  373. var oldText = isMultiline ? config.initialValue : getContent(element);
  374. var moveDirection = "";
  375. var oldTabIndex;
  376. var codeMirror;
  377. var cssLoadView;
  378. function consumeCopy(e)
  379. {
  380. e.consume();
  381. }
  382. if (isMultiline) {
  383. loadScript("CodeMirrorTextEditor.js");
  384. cssLoadView = new WebInspector.CodeMirrorCSSLoadView();
  385. cssLoadView.show(element);
  386. WebInspector.setCurrentFocusElement(element);
  387. element.addEventListener("copy", consumeCopy, true);
  388. codeMirror = window.CodeMirror(element, {
  389. mode: config.mode,
  390. lineWrapping: config.lineWrapping,
  391. smartIndent: config.smartIndent,
  392. autofocus: true,
  393. theme: config.theme,
  394. value: oldText
  395. });
  396. } else {
  397. element.addStyleClass("editing");
  398. oldTabIndex = element.getAttribute("tabIndex");
  399. if (typeof oldTabIndex !== "number" || oldTabIndex < 0)
  400. element.tabIndex = 0;
  401. WebInspector.setCurrentFocusElement(element);
  402. }
  403. /**
  404. * @param {Event=} e
  405. */
  406. function blurEventListener(e) {
  407. if (!isMultiline || !e || !e.relatedTarget || !e.relatedTarget.isSelfOrDescendant(element))
  408. editingCommitted.call(element);
  409. }
  410. function getContent(element) {
  411. if (isMultiline)
  412. return codeMirror.getValue();
  413. if (element.tagName === "INPUT" && element.type === "text")
  414. return element.value;
  415. return element.textContent;
  416. }
  417. /** @this {Element} */
  418. function cleanUpAfterEditing()
  419. {
  420. WebInspector.markBeingEdited(element, false);
  421. element.removeEventListener("blur", blurEventListener, isMultiline);
  422. element.removeEventListener("keydown", keyDownEventListener, true);
  423. if (pasteCallback)
  424. element.removeEventListener("paste", pasteEventListener, true);
  425. WebInspector.restoreFocusFromElement(element);
  426. if (isMultiline) {
  427. element.removeEventListener("copy", consumeCopy, true);
  428. cssLoadView.detach();
  429. return;
  430. }
  431. this.removeStyleClass("editing");
  432. if (typeof oldTabIndex !== "number")
  433. element.removeAttribute("tabIndex");
  434. else
  435. this.tabIndex = oldTabIndex;
  436. this.scrollTop = 0;
  437. this.scrollLeft = 0;
  438. }
  439. /** @this {Element} */
  440. function editingCancelled()
  441. {
  442. if (isMultiline)
  443. codeMirror.setValue(oldText);
  444. else {
  445. if (this.tagName === "INPUT" && this.type === "text")
  446. this.value = oldText;
  447. else
  448. this.textContent = oldText;
  449. }
  450. cleanUpAfterEditing.call(this);
  451. cancelledCallback(this, context);
  452. }
  453. /** @this {Element} */
  454. function editingCommitted()
  455. {
  456. cleanUpAfterEditing.call(this);
  457. committedCallback(this, getContent(this), oldText, context, moveDirection);
  458. }
  459. function defaultFinishHandler(event)
  460. {
  461. var isMetaOrCtrl = WebInspector.isMac() ?
  462. event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey :
  463. event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey;
  464. if (isEnterKey(event) && (event.isMetaOrCtrlForTest || !isMultiline || isMetaOrCtrl))
  465. return "commit";
  466. else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code || event.keyIdentifier === "U+001B")
  467. return "cancel";
  468. else if (!isMultiline && event.keyIdentifier === "U+0009") // Tab key
  469. return "move-" + (event.shiftKey ? "backward" : "forward");
  470. }
  471. function handleEditingResult(result, event)
  472. {
  473. if (result === "commit") {
  474. editingCommitted.call(element);
  475. event.consume(true);
  476. } else if (result === "cancel") {
  477. editingCancelled.call(element);
  478. event.consume(true);
  479. } else if (result && result.startsWith("move-")) {
  480. moveDirection = result.substring(5);
  481. if (event.keyIdentifier !== "U+0009")
  482. blurEventListener();
  483. }
  484. }
  485. function pasteEventListener(event)
  486. {
  487. var result = pasteCallback(event);
  488. handleEditingResult(result, event);
  489. }
  490. function keyDownEventListener(event)
  491. {
  492. var handler = config.customFinishHandler || defaultFinishHandler;
  493. var result = handler(event);
  494. handleEditingResult(result, event);
  495. }
  496. element.addEventListener("blur", blurEventListener, isMultiline);
  497. element.addEventListener("keydown", keyDownEventListener, true);
  498. if (pasteCallback)
  499. element.addEventListener("paste", pasteEventListener, true);
  500. return {
  501. cancel: editingCancelled.bind(element),
  502. commit: editingCommitted.bind(element),
  503. codeMirror: codeMirror // For testing.
  504. };
  505. }
  506. /**
  507. * @param {number} seconds
  508. * @param {boolean=} higherResolution
  509. * @return {string}
  510. */
  511. Number.secondsToString = function(seconds, higherResolution)
  512. {
  513. if (!isFinite(seconds))
  514. return "-";
  515. if (seconds === 0)
  516. return "0";
  517. var ms = seconds * 1000;
  518. if (higherResolution && ms < 1000)
  519. return WebInspector.UIString("%.3f\u2009ms", ms);
  520. else if (ms < 1000)
  521. return WebInspector.UIString("%.0f\u2009ms", ms);
  522. if (seconds < 60)
  523. return WebInspector.UIString("%.2f\u2009s", seconds);
  524. var minutes = seconds / 60;
  525. if (minutes < 60)
  526. return WebInspector.UIString("%.1f\u2009min", minutes);
  527. var hours = minutes / 60;
  528. if (hours < 24)
  529. return WebInspector.UIString("%.1f\u2009hrs", hours);
  530. var days = hours / 24;
  531. return WebInspector.UIString("%.1f\u2009days", days);
  532. }
  533. /**
  534. * @param {number} bytes
  535. * @return {string}
  536. */
  537. Number.bytesToString = function(bytes)
  538. {
  539. if (bytes < 1024)
  540. return WebInspector.UIString("%.0f\u2009B", bytes);
  541. var kilobytes = bytes / 1024;
  542. if (kilobytes < 100)
  543. return WebInspector.UIString("%.1f\u2009KB", kilobytes);
  544. if (kilobytes < 1024)
  545. return WebInspector.UIString("%.0f\u2009KB", kilobytes);
  546. var megabytes = kilobytes / 1024;
  547. if (megabytes < 100)
  548. return WebInspector.UIString("%.1f\u2009MB", megabytes);
  549. else
  550. return WebInspector.UIString("%.0f\u2009MB", megabytes);
  551. }
  552. Number.withThousandsSeparator = function(num)
  553. {
  554. var str = num + "";
  555. var re = /(\d+)(\d{3})/;
  556. while (str.match(re))
  557. str = str.replace(re, "$1\u2009$2"); // \u2009 is a thin space.
  558. return str;
  559. }
  560. WebInspector.useLowerCaseMenuTitles = function()
  561. {
  562. return WebInspector.platform() === "windows" && Preferences.useLowerCaseMenuTitlesOnWindows;
  563. }
  564. WebInspector.formatLocalized = function(format, substitutions, formatters, initialValue, append)
  565. {
  566. return String.format(WebInspector.UIString(format), substitutions, formatters, initialValue, append);
  567. }
  568. WebInspector.openLinkExternallyLabel = function()
  569. {
  570. return WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Open link in new tab" : "Open Link in New Tab");
  571. }
  572. WebInspector.copyLinkAddressLabel = function()
  573. {
  574. return WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Copy link address" : "Copy Link Address");
  575. }
  576. WebInspector.platform = function()
  577. {
  578. if (!WebInspector._platform)
  579. WebInspector._platform = InspectorFrontendHost.platform();
  580. return WebInspector._platform;
  581. }
  582. WebInspector.isMac = function()
  583. {
  584. if (typeof WebInspector._isMac === "undefined")
  585. WebInspector._isMac = WebInspector.platform() === "mac";
  586. return WebInspector._isMac;
  587. }
  588. WebInspector.isWin = function()
  589. {
  590. if (typeof WebInspector._isWin === "undefined")
  591. WebInspector._isWin = WebInspector.platform() === "windows";
  592. return WebInspector._isWin;
  593. }
  594. WebInspector.PlatformFlavor = {
  595. WindowsVista: "windows-vista",
  596. MacTiger: "mac-tiger",
  597. MacLeopard: "mac-leopard",
  598. MacSnowLeopard: "mac-snowleopard",
  599. MacLion: "mac-lion",
  600. MacMountainLion: "mac-mountain-lion"
  601. }
  602. WebInspector.platformFlavor = function()
  603. {
  604. function detectFlavor()
  605. {
  606. const userAgent = navigator.userAgent;
  607. if (WebInspector.platform() === "windows") {
  608. var match = userAgent.match(/Windows NT (\d+)\.(?:\d+)/);
  609. if (match && match[1] >= 6)
  610. return WebInspector.PlatformFlavor.WindowsVista;
  611. return null;
  612. } else if (WebInspector.platform() === "mac") {
  613. var match = userAgent.match(/Mac OS X\s*(?:(\d+)_(\d+))?/);
  614. if (!match || match[1] != 10)
  615. return WebInspector.PlatformFlavor.MacSnowLeopard;
  616. switch (Number(match[2])) {
  617. case 4:
  618. return WebInspector.PlatformFlavor.MacTiger;
  619. case 5:
  620. return WebInspector.PlatformFlavor.MacLeopard;
  621. case 6:
  622. return WebInspector.PlatformFlavor.MacSnowLeopard;
  623. case 7:
  624. return WebInspector.PlatformFlavor.MacLion;
  625. case 8:
  626. return WebInspector.PlatformFlavor.MacMountainLion;
  627. default:
  628. return "";
  629. }
  630. }
  631. }
  632. if (!WebInspector._platformFlavor)
  633. WebInspector._platformFlavor = detectFlavor();
  634. return WebInspector._platformFlavor;
  635. }
  636. WebInspector.port = function()
  637. {
  638. if (!WebInspector._port)
  639. WebInspector._port = InspectorFrontendHost.port();
  640. return WebInspector._port;
  641. }
  642. WebInspector.installPortStyles = function()
  643. {
  644. var platform = WebInspector.platform();
  645. document.body.addStyleClass("platform-" + platform);
  646. var flavor = WebInspector.platformFlavor();
  647. if (flavor)
  648. document.body.addStyleClass("platform-" + flavor);
  649. var port = WebInspector.port();
  650. document.body.addStyleClass("port-" + port);
  651. }
  652. WebInspector._windowFocused = function(event)
  653. {
  654. if (event.target.document.nodeType === Node.DOCUMENT_NODE)
  655. document.body.removeStyleClass("inactive");
  656. }
  657. WebInspector._windowBlurred = function(event)
  658. {
  659. if (event.target.document.nodeType === Node.DOCUMENT_NODE)
  660. document.body.addStyleClass("inactive");
  661. }
  662. WebInspector.previousFocusElement = function()
  663. {
  664. return WebInspector._previousFocusElement;
  665. }
  666. WebInspector.currentFocusElement = function()
  667. {
  668. return WebInspector._currentFocusElement;
  669. }
  670. WebInspector._focusChanged = function(event)
  671. {
  672. WebInspector.setCurrentFocusElement(event.target);
  673. }
  674. WebInspector._textInputTypes = ["text", "search", "tel", "url", "email", "password"].keySet();
  675. WebInspector._isTextEditingElement = function(element)
  676. {
  677. if (element instanceof HTMLInputElement)
  678. return element.type in WebInspector._textInputTypes;
  679. if (element instanceof HTMLTextAreaElement)
  680. return true;
  681. return false;
  682. }
  683. WebInspector.setCurrentFocusElement = function(x)
  684. {
  685. if (WebInspector._glassPane && x && !WebInspector._glassPane.element.isAncestor(x))
  686. return;
  687. if (WebInspector._currentFocusElement !== x)
  688. WebInspector._previousFocusElement = WebInspector._currentFocusElement;
  689. WebInspector._currentFocusElement = x;
  690. if (WebInspector._currentFocusElement) {
  691. WebInspector._currentFocusElement.focus();
  692. // Make a caret selection inside the new element if there isn't a range selection and there isn't already a caret selection inside.
  693. // This is needed (at least) to remove caret from console when focus is moved to some element in the panel.
  694. // The code below should not be applied to text fields and text areas, hence _isTextEditingElement check.
  695. var selection = window.getSelection();
  696. if (!WebInspector._isTextEditingElement(WebInspector._currentFocusElement) && selection.isCollapsed && !WebInspector._currentFocusElement.isInsertionCaretInside()) {
  697. var selectionRange = WebInspector._currentFocusElement.ownerDocument.createRange();
  698. selectionRange.setStart(WebInspector._currentFocusElement, 0);
  699. selectionRange.setEnd(WebInspector._currentFocusElement, 0);
  700. selection.removeAllRanges();
  701. selection.addRange(selectionRange);
  702. }
  703. } else if (WebInspector._previousFocusElement)
  704. WebInspector._previousFocusElement.blur();
  705. }
  706. WebInspector.restoreFocusFromElement = function(element)
  707. {
  708. if (element && element.isSelfOrAncestor(WebInspector.currentFocusElement()))
  709. WebInspector.setCurrentFocusElement(WebInspector.previousFocusElement());
  710. }
  711. WebInspector.setToolbarColors = function(backgroundColor, color)
  712. {
  713. if (!WebInspector._themeStyleElement) {
  714. WebInspector._themeStyleElement = document.createElement("style");
  715. document.head.appendChild(WebInspector._themeStyleElement);
  716. }
  717. WebInspector._themeStyleElement.textContent =
  718. "#toolbar {\
  719. background-image: none !important;\
  720. background-color: " + backgroundColor + " !important;\
  721. }\
  722. \
  723. .toolbar-label {\
  724. color: " + color + " !important;\
  725. text-shadow: none;\
  726. }";
  727. }
  728. WebInspector.resetToolbarColors = function()
  729. {
  730. if (WebInspector._themeStyleElement)
  731. WebInspector._themeStyleElement.textContent = "";
  732. }
  733. /**
  734. * @param {Element} element
  735. * @param {number} offset
  736. * @param {number} length
  737. * @param {Array.<Object>=} domChanges
  738. */
  739. WebInspector.highlightSearchResult = function(element, offset, length, domChanges)
  740. {
  741. var result = WebInspector.highlightSearchResults(element, [{offset: offset, length: length }], domChanges);
  742. return result.length ? result[0] : null;
  743. }
  744. /**
  745. * @param {Element} element
  746. * @param {Array.<Object>} resultRanges
  747. * @param {Array.<Object>=} changes
  748. */
  749. WebInspector.highlightSearchResults = function(element, resultRanges, changes)
  750. {
  751. return WebInspector.highlightRangesWithStyleClass(element, resultRanges, "webkit-search-result", changes);
  752. }
  753. /**
  754. * @param {Element} element
  755. * @param {Array.<Object>} resultRanges
  756. * @param {string} styleClass
  757. * @param {Array.<Object>=} changes
  758. */
  759. WebInspector.highlightRangesWithStyleClass = function(element, resultRanges, styleClass, changes)
  760. {
  761. changes = changes || [];
  762. var highlightNodes = [];
  763. var lineText = element.textContent;
  764. var ownerDocument = element.ownerDocument;
  765. var textNodeSnapshot = ownerDocument.evaluate(".//text()", element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
  766. var snapshotLength = textNodeSnapshot.snapshotLength;
  767. if (snapshotLength === 0)
  768. return highlightNodes;
  769. var nodeRanges = [];
  770. var rangeEndOffset = 0;
  771. for (var i = 0; i < snapshotLength; ++i) {
  772. var range = {};
  773. range.offset = rangeEndOffset;
  774. range.length = textNodeSnapshot.snapshotItem(i).textContent.length;
  775. rangeEndOffset = range.offset + range.length;
  776. nodeRanges.push(range);
  777. }
  778. var startIndex = 0;
  779. for (var i = 0; i < resultRanges.length; ++i) {
  780. var startOffset = resultRanges[i].offset;
  781. var endOffset = startOffset + resultRanges[i].length;
  782. while (startIndex < snapshotLength && nodeRanges[startIndex].offset + nodeRanges[startIndex].length <= startOffset)
  783. startIndex++;
  784. var endIndex = startIndex;
  785. while (endIndex < snapshotLength && nodeRanges[endIndex].offset + nodeRanges[endIndex].length < endOffset)
  786. endIndex++;
  787. if (endIndex === snapshotLength)
  788. break;
  789. var highlightNode = ownerDocument.createElement("span");
  790. highlightNode.className = styleClass;
  791. highlightNode.textContent = lineText.substring(startOffset, endOffset);
  792. var lastTextNode = textNodeSnapshot.snapshotItem(endIndex);
  793. var lastText = lastTextNode.textContent;
  794. lastTextNode.textContent = lastText.substring(endOffset - nodeRanges[endIndex].offset);
  795. changes.push({ node: lastTextNode, type: "changed", oldText: lastText, newText: lastTextNode.textContent });
  796. if (startIndex === endIndex) {
  797. lastTextNode.parentElement.insertBefore(highlightNode, lastTextNode);
  798. changes.push({ node: highlightNode, type: "added", nextSibling: lastTextNode, parent: lastTextNode.parentElement });
  799. highlightNodes.push(highlightNode);
  800. var prefixNode = ownerDocument.createTextNode(lastText.substring(0, startOffset - nodeRanges[startIndex].offset));
  801. lastTextNode.parentElement.insertBefore(prefixNode, highlightNode);
  802. changes.push({ node: prefixNode, type: "added", nextSibling: highlightNode, parent: lastTextNode.parentElement });
  803. } else {
  804. var firstTextNode = textNodeSnapshot.snapshotItem(startIndex);
  805. var firstText = firstTextNode.textContent;
  806. var anchorElement = firstTextNode.nextSibling;
  807. firstTextNode.parentElement.insertBefore(highlightNode, anchorElement);
  808. changes.push({ node: highlightNode, type: "added", nextSibling: anchorElement, parent: firstTextNode.parentElement });
  809. highlightNodes.push(highlightNode);
  810. firstTextNode.textContent = firstText.substring(0, startOffset - nodeRanges[startIndex].offset);
  811. changes.push({ node: firstTextNode, type: "changed", oldText: firstText, newText: firstTextNode.textContent });
  812. for (var j = startIndex + 1; j < endIndex; j++) {
  813. var textNode = textNodeSnapshot.snapshotItem(j);
  814. var text = textNode.textContent;
  815. textNode.textContent = "";
  816. changes.push({ node: textNode, type: "changed", oldText: text, newText: textNode.textContent });
  817. }
  818. }
  819. startIndex = endIndex;
  820. nodeRanges[startIndex].offset = endOffset;
  821. nodeRanges[startIndex].length = lastTextNode.textContent.length;
  822. }
  823. return highlightNodes;
  824. }
  825. WebInspector.applyDomChanges = function(domChanges)
  826. {
  827. for (var i = 0, size = domChanges.length; i < size; ++i) {
  828. var entry = domChanges[i];
  829. switch (entry.type) {
  830. case "added":
  831. entry.parent.insertBefore(entry.node, entry.nextSibling);
  832. break;
  833. case "changed":
  834. entry.node.textContent = entry.newText;
  835. break;
  836. }
  837. }
  838. }
  839. WebInspector.revertDomChanges = function(domChanges)
  840. {
  841. for (var i = domChanges.length - 1; i >= 0; --i) {
  842. var entry = domChanges[i];
  843. switch (entry.type) {
  844. case "added":
  845. if (entry.node.parentElement)
  846. entry.node.parentElement.removeChild(entry.node);
  847. break;
  848. case "changed":
  849. entry.node.textContent = entry.oldText;
  850. break;
  851. }
  852. }
  853. }
  854. WebInspector._coalescingLevel = 0;
  855. WebInspector.startBatchUpdate = function()
  856. {
  857. if (!WebInspector._coalescingLevel)
  858. WebInspector._postUpdateHandlers = new Map();
  859. WebInspector._coalescingLevel++;
  860. }
  861. WebInspector.endBatchUpdate = function()
  862. {
  863. if (--WebInspector._coalescingLevel)
  864. return;
  865. var handlers = WebInspector._postUpdateHandlers;
  866. delete WebInspector._postUpdateHandlers;
  867. var keys = handlers.keys();
  868. for (var i = 0; i < keys.length; ++i) {
  869. var object = keys[i];
  870. var methods = handlers.get(object).keys();
  871. for (var j = 0; j < methods.length; ++j)
  872. methods[j].call(object);
  873. }
  874. }
  875. /**
  876. * @param {Object} object
  877. * @param {function()} method
  878. */
  879. WebInspector.invokeOnceAfterBatchUpdate = function(object, method)
  880. {
  881. if (!WebInspector._coalescingLevel) {
  882. method.call(object);
  883. return;
  884. }
  885. var methods = WebInspector._postUpdateHandlers.get(object);
  886. if (!methods) {
  887. methods = new Map();
  888. WebInspector._postUpdateHandlers.put(object, methods);
  889. }
  890. methods.put(method);
  891. }
  892. /**
  893. * This bogus view is needed to load/unload CodeMirror-related CSS on demand.
  894. *
  895. * @constructor
  896. * @extends {WebInspector.View}
  897. */
  898. WebInspector.CodeMirrorCSSLoadView = function()
  899. {
  900. WebInspector.View.call(this);
  901. this.element.addStyleClass("hidden");
  902. this.registerRequiredCSS("cm/codemirror.css");
  903. this.registerRequiredCSS("cm/cmdevtools.css");
  904. }
  905. WebInspector.CodeMirrorCSSLoadView.prototype = {
  906. __proto__: WebInspector.View.prototype
  907. }
  908. ;(function() {
  909. function windowLoaded()
  910. {
  911. window.addEventListener("focus", WebInspector._windowFocused, false);
  912. window.addEventListener("blur", WebInspector._windowBlurred, false);
  913. document.addEventListener("focus", WebInspector._focusChanged.bind(this), true);
  914. window.removeEventListener("DOMContentLoaded", windowLoaded, false);
  915. }
  916. window.addEventListener("DOMContentLoaded", windowLoaded, false);
  917. })();