dom.js 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import * as browser from './browser.js';
  6. import { BrowserFeatures } from './canIUse.js';
  7. import { StandardKeyboardEvent } from './keyboardEvent.js';
  8. import { StandardMouseEvent } from './mouseEvent.js';
  9. import { onUnexpectedError } from '../common/errors.js';
  10. import * as event from '../common/event.js';
  11. import * as dompurify from './dompurify/dompurify.js';
  12. import { Disposable, DisposableStore, toDisposable } from '../common/lifecycle.js';
  13. import { FileAccess, RemoteAuthorities } from '../common/network.js';
  14. import * as platform from '../common/platform.js';
  15. export function clearNode(node) {
  16. while (node.firstChild) {
  17. node.firstChild.remove();
  18. }
  19. }
  20. /**
  21. * @deprecated Use node.isConnected directly
  22. */
  23. export function isInDOM(node) {
  24. var _a;
  25. return (_a = node === null || node === void 0 ? void 0 : node.isConnected) !== null && _a !== void 0 ? _a : false;
  26. }
  27. class DomListener {
  28. constructor(node, type, handler, options) {
  29. this._node = node;
  30. this._type = type;
  31. this._handler = handler;
  32. this._options = (options || false);
  33. this._node.addEventListener(this._type, this._handler, this._options);
  34. }
  35. dispose() {
  36. if (!this._handler) {
  37. // Already disposed
  38. return;
  39. }
  40. this._node.removeEventListener(this._type, this._handler, this._options);
  41. // Prevent leakers from holding on to the dom or handler func
  42. this._node = null;
  43. this._handler = null;
  44. }
  45. }
  46. export function addDisposableListener(node, type, handler, useCaptureOrOptions) {
  47. return new DomListener(node, type, handler, useCaptureOrOptions);
  48. }
  49. function _wrapAsStandardMouseEvent(handler) {
  50. return function (e) {
  51. return handler(new StandardMouseEvent(e));
  52. };
  53. }
  54. function _wrapAsStandardKeyboardEvent(handler) {
  55. return function (e) {
  56. return handler(new StandardKeyboardEvent(e));
  57. };
  58. }
  59. export const addStandardDisposableListener = function addStandardDisposableListener(node, type, handler, useCapture) {
  60. let wrapHandler = handler;
  61. if (type === 'click' || type === 'mousedown') {
  62. wrapHandler = _wrapAsStandardMouseEvent(handler);
  63. }
  64. else if (type === 'keydown' || type === 'keypress' || type === 'keyup') {
  65. wrapHandler = _wrapAsStandardKeyboardEvent(handler);
  66. }
  67. return addDisposableListener(node, type, wrapHandler, useCapture);
  68. };
  69. export const addStandardDisposableGenericMouseDownListener = function addStandardDisposableListener(node, handler, useCapture) {
  70. const wrapHandler = _wrapAsStandardMouseEvent(handler);
  71. return addDisposableGenericMouseDownListener(node, wrapHandler, useCapture);
  72. };
  73. export const addStandardDisposableGenericMouseUpListener = function addStandardDisposableListener(node, handler, useCapture) {
  74. const wrapHandler = _wrapAsStandardMouseEvent(handler);
  75. return addDisposableGenericMouseUpListener(node, wrapHandler, useCapture);
  76. };
  77. export function addDisposableGenericMouseDownListener(node, handler, useCapture) {
  78. return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_DOWN : EventType.MOUSE_DOWN, handler, useCapture);
  79. }
  80. export function addDisposableGenericMouseUpListener(node, handler, useCapture) {
  81. return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_UP : EventType.MOUSE_UP, handler, useCapture);
  82. }
  83. /**
  84. * Schedule a callback to be run at the next animation frame.
  85. * This allows multiple parties to register callbacks that should run at the next animation frame.
  86. * If currently in an animation frame, `runner` will be executed immediately.
  87. * @return token that can be used to cancel the scheduled runner (only if `runner` was not executed immediately).
  88. */
  89. export let runAtThisOrScheduleAtNextAnimationFrame;
  90. /**
  91. * Schedule a callback to be run at the next animation frame.
  92. * This allows multiple parties to register callbacks that should run at the next animation frame.
  93. * If currently in an animation frame, `runner` will be executed at the next animation frame.
  94. * @return token that can be used to cancel the scheduled runner.
  95. */
  96. export let scheduleAtNextAnimationFrame;
  97. class AnimationFrameQueueItem {
  98. constructor(runner, priority = 0) {
  99. this._runner = runner;
  100. this.priority = priority;
  101. this._canceled = false;
  102. }
  103. dispose() {
  104. this._canceled = true;
  105. }
  106. execute() {
  107. if (this._canceled) {
  108. return;
  109. }
  110. try {
  111. this._runner();
  112. }
  113. catch (e) {
  114. onUnexpectedError(e);
  115. }
  116. }
  117. // Sort by priority (largest to lowest)
  118. static sort(a, b) {
  119. return b.priority - a.priority;
  120. }
  121. }
  122. (function () {
  123. /**
  124. * The runners scheduled at the next animation frame
  125. */
  126. let NEXT_QUEUE = [];
  127. /**
  128. * The runners scheduled at the current animation frame
  129. */
  130. let CURRENT_QUEUE = null;
  131. /**
  132. * A flag to keep track if the native requestAnimationFrame was already called
  133. */
  134. let animFrameRequested = false;
  135. /**
  136. * A flag to indicate if currently handling a native requestAnimationFrame callback
  137. */
  138. let inAnimationFrameRunner = false;
  139. const animationFrameRunner = () => {
  140. animFrameRequested = false;
  141. CURRENT_QUEUE = NEXT_QUEUE;
  142. NEXT_QUEUE = [];
  143. inAnimationFrameRunner = true;
  144. while (CURRENT_QUEUE.length > 0) {
  145. CURRENT_QUEUE.sort(AnimationFrameQueueItem.sort);
  146. const top = CURRENT_QUEUE.shift();
  147. top.execute();
  148. }
  149. inAnimationFrameRunner = false;
  150. };
  151. scheduleAtNextAnimationFrame = (runner, priority = 0) => {
  152. const item = new AnimationFrameQueueItem(runner, priority);
  153. NEXT_QUEUE.push(item);
  154. if (!animFrameRequested) {
  155. animFrameRequested = true;
  156. requestAnimationFrame(animationFrameRunner);
  157. }
  158. return item;
  159. };
  160. runAtThisOrScheduleAtNextAnimationFrame = (runner, priority) => {
  161. if (inAnimationFrameRunner) {
  162. const item = new AnimationFrameQueueItem(runner, priority);
  163. CURRENT_QUEUE.push(item);
  164. return item;
  165. }
  166. else {
  167. return scheduleAtNextAnimationFrame(runner, priority);
  168. }
  169. };
  170. })();
  171. export function getComputedStyle(el) {
  172. return document.defaultView.getComputedStyle(el, null);
  173. }
  174. export function getClientArea(element) {
  175. // Try with DOM clientWidth / clientHeight
  176. if (element !== document.body) {
  177. return new Dimension(element.clientWidth, element.clientHeight);
  178. }
  179. // If visual view port exits and it's on mobile, it should be used instead of window innerWidth / innerHeight, or document.body.clientWidth / document.body.clientHeight
  180. if (platform.isIOS && window.visualViewport) {
  181. return new Dimension(window.visualViewport.width, window.visualViewport.height);
  182. }
  183. // Try innerWidth / innerHeight
  184. if (window.innerWidth && window.innerHeight) {
  185. return new Dimension(window.innerWidth, window.innerHeight);
  186. }
  187. // Try with document.body.clientWidth / document.body.clientHeight
  188. if (document.body && document.body.clientWidth && document.body.clientHeight) {
  189. return new Dimension(document.body.clientWidth, document.body.clientHeight);
  190. }
  191. // Try with document.documentElement.clientWidth / document.documentElement.clientHeight
  192. if (document.documentElement && document.documentElement.clientWidth && document.documentElement.clientHeight) {
  193. return new Dimension(document.documentElement.clientWidth, document.documentElement.clientHeight);
  194. }
  195. throw new Error('Unable to figure out browser width and height');
  196. }
  197. class SizeUtils {
  198. // Adapted from WinJS
  199. // Converts a CSS positioning string for the specified element to pixels.
  200. static convertToPixels(element, value) {
  201. return parseFloat(value) || 0;
  202. }
  203. static getDimension(element, cssPropertyName, jsPropertyName) {
  204. const computedStyle = getComputedStyle(element);
  205. const value = computedStyle ? computedStyle.getPropertyValue(cssPropertyName) : '0';
  206. return SizeUtils.convertToPixels(element, value);
  207. }
  208. static getBorderLeftWidth(element) {
  209. return SizeUtils.getDimension(element, 'border-left-width', 'borderLeftWidth');
  210. }
  211. static getBorderRightWidth(element) {
  212. return SizeUtils.getDimension(element, 'border-right-width', 'borderRightWidth');
  213. }
  214. static getBorderTopWidth(element) {
  215. return SizeUtils.getDimension(element, 'border-top-width', 'borderTopWidth');
  216. }
  217. static getBorderBottomWidth(element) {
  218. return SizeUtils.getDimension(element, 'border-bottom-width', 'borderBottomWidth');
  219. }
  220. static getPaddingLeft(element) {
  221. return SizeUtils.getDimension(element, 'padding-left', 'paddingLeft');
  222. }
  223. static getPaddingRight(element) {
  224. return SizeUtils.getDimension(element, 'padding-right', 'paddingRight');
  225. }
  226. static getPaddingTop(element) {
  227. return SizeUtils.getDimension(element, 'padding-top', 'paddingTop');
  228. }
  229. static getPaddingBottom(element) {
  230. return SizeUtils.getDimension(element, 'padding-bottom', 'paddingBottom');
  231. }
  232. static getMarginLeft(element) {
  233. return SizeUtils.getDimension(element, 'margin-left', 'marginLeft');
  234. }
  235. static getMarginTop(element) {
  236. return SizeUtils.getDimension(element, 'margin-top', 'marginTop');
  237. }
  238. static getMarginRight(element) {
  239. return SizeUtils.getDimension(element, 'margin-right', 'marginRight');
  240. }
  241. static getMarginBottom(element) {
  242. return SizeUtils.getDimension(element, 'margin-bottom', 'marginBottom');
  243. }
  244. }
  245. export class Dimension {
  246. constructor(width, height) {
  247. this.width = width;
  248. this.height = height;
  249. }
  250. with(width = this.width, height = this.height) {
  251. if (width !== this.width || height !== this.height) {
  252. return new Dimension(width, height);
  253. }
  254. else {
  255. return this;
  256. }
  257. }
  258. static is(obj) {
  259. return typeof obj === 'object' && typeof obj.height === 'number' && typeof obj.width === 'number';
  260. }
  261. static lift(obj) {
  262. if (obj instanceof Dimension) {
  263. return obj;
  264. }
  265. else {
  266. return new Dimension(obj.width, obj.height);
  267. }
  268. }
  269. static equals(a, b) {
  270. if (a === b) {
  271. return true;
  272. }
  273. if (!a || !b) {
  274. return false;
  275. }
  276. return a.width === b.width && a.height === b.height;
  277. }
  278. }
  279. Dimension.None = new Dimension(0, 0);
  280. export function getTopLeftOffset(element) {
  281. // Adapted from WinJS.Utilities.getPosition
  282. // and added borders to the mix
  283. let offsetParent = element.offsetParent;
  284. let top = element.offsetTop;
  285. let left = element.offsetLeft;
  286. while ((element = element.parentNode) !== null
  287. && element !== document.body
  288. && element !== document.documentElement) {
  289. top -= element.scrollTop;
  290. const c = isShadowRoot(element) ? null : getComputedStyle(element);
  291. if (c) {
  292. left -= c.direction !== 'rtl' ? element.scrollLeft : -element.scrollLeft;
  293. }
  294. if (element === offsetParent) {
  295. left += SizeUtils.getBorderLeftWidth(element);
  296. top += SizeUtils.getBorderTopWidth(element);
  297. top += element.offsetTop;
  298. left += element.offsetLeft;
  299. offsetParent = element.offsetParent;
  300. }
  301. }
  302. return {
  303. left: left,
  304. top: top
  305. };
  306. }
  307. export function size(element, width, height) {
  308. if (typeof width === 'number') {
  309. element.style.width = `${width}px`;
  310. }
  311. if (typeof height === 'number') {
  312. element.style.height = `${height}px`;
  313. }
  314. }
  315. /**
  316. * Returns the position of a dom node relative to the entire page.
  317. */
  318. export function getDomNodePagePosition(domNode) {
  319. const bb = domNode.getBoundingClientRect();
  320. return {
  321. left: bb.left + window.scrollX,
  322. top: bb.top + window.scrollY,
  323. width: bb.width,
  324. height: bb.height
  325. };
  326. }
  327. /**
  328. * Returns the effective zoom on a given element before window zoom level is applied
  329. */
  330. export function getDomNodeZoomLevel(domNode) {
  331. let testElement = domNode;
  332. let zoom = 1.0;
  333. do {
  334. const elementZoomLevel = getComputedStyle(testElement).zoom;
  335. if (elementZoomLevel !== null && elementZoomLevel !== undefined && elementZoomLevel !== '1') {
  336. zoom *= elementZoomLevel;
  337. }
  338. testElement = testElement.parentElement;
  339. } while (testElement !== null && testElement !== document.documentElement);
  340. return zoom;
  341. }
  342. // Adapted from WinJS
  343. // Gets the width of the element, including margins.
  344. export function getTotalWidth(element) {
  345. const margin = SizeUtils.getMarginLeft(element) + SizeUtils.getMarginRight(element);
  346. return element.offsetWidth + margin;
  347. }
  348. export function getContentWidth(element) {
  349. const border = SizeUtils.getBorderLeftWidth(element) + SizeUtils.getBorderRightWidth(element);
  350. const padding = SizeUtils.getPaddingLeft(element) + SizeUtils.getPaddingRight(element);
  351. return element.offsetWidth - border - padding;
  352. }
  353. // Adapted from WinJS
  354. // Gets the height of the content of the specified element. The content height does not include borders or padding.
  355. export function getContentHeight(element) {
  356. const border = SizeUtils.getBorderTopWidth(element) + SizeUtils.getBorderBottomWidth(element);
  357. const padding = SizeUtils.getPaddingTop(element) + SizeUtils.getPaddingBottom(element);
  358. return element.offsetHeight - border - padding;
  359. }
  360. // Adapted from WinJS
  361. // Gets the height of the element, including its margins.
  362. export function getTotalHeight(element) {
  363. const margin = SizeUtils.getMarginTop(element) + SizeUtils.getMarginBottom(element);
  364. return element.offsetHeight + margin;
  365. }
  366. // ----------------------------------------------------------------------------------------
  367. export function isAncestor(testChild, testAncestor) {
  368. while (testChild) {
  369. if (testChild === testAncestor) {
  370. return true;
  371. }
  372. testChild = testChild.parentNode;
  373. }
  374. return false;
  375. }
  376. export function findParentWithClass(node, clazz, stopAtClazzOrNode) {
  377. while (node && node.nodeType === node.ELEMENT_NODE) {
  378. if (node.classList.contains(clazz)) {
  379. return node;
  380. }
  381. if (stopAtClazzOrNode) {
  382. if (typeof stopAtClazzOrNode === 'string') {
  383. if (node.classList.contains(stopAtClazzOrNode)) {
  384. return null;
  385. }
  386. }
  387. else {
  388. if (node === stopAtClazzOrNode) {
  389. return null;
  390. }
  391. }
  392. }
  393. node = node.parentNode;
  394. }
  395. return null;
  396. }
  397. export function hasParentWithClass(node, clazz, stopAtClazzOrNode) {
  398. return !!findParentWithClass(node, clazz, stopAtClazzOrNode);
  399. }
  400. export function isShadowRoot(node) {
  401. return (node && !!node.host && !!node.mode);
  402. }
  403. export function isInShadowDOM(domNode) {
  404. return !!getShadowRoot(domNode);
  405. }
  406. export function getShadowRoot(domNode) {
  407. while (domNode.parentNode) {
  408. if (domNode === document.body) {
  409. // reached the body
  410. return null;
  411. }
  412. domNode = domNode.parentNode;
  413. }
  414. return isShadowRoot(domNode) ? domNode : null;
  415. }
  416. export function getActiveElement() {
  417. let result = document.activeElement;
  418. while (result === null || result === void 0 ? void 0 : result.shadowRoot) {
  419. result = result.shadowRoot.activeElement;
  420. }
  421. return result;
  422. }
  423. export function createStyleSheet(container = document.getElementsByTagName('head')[0], beforeAppend) {
  424. const style = document.createElement('style');
  425. style.type = 'text/css';
  426. style.media = 'screen';
  427. beforeAppend === null || beforeAppend === void 0 ? void 0 : beforeAppend(style);
  428. container.appendChild(style);
  429. return style;
  430. }
  431. let _sharedStyleSheet = null;
  432. function getSharedStyleSheet() {
  433. if (!_sharedStyleSheet) {
  434. _sharedStyleSheet = createStyleSheet();
  435. }
  436. return _sharedStyleSheet;
  437. }
  438. function getDynamicStyleSheetRules(style) {
  439. var _a, _b;
  440. if ((_a = style === null || style === void 0 ? void 0 : style.sheet) === null || _a === void 0 ? void 0 : _a.rules) {
  441. // Chrome, IE
  442. return style.sheet.rules;
  443. }
  444. if ((_b = style === null || style === void 0 ? void 0 : style.sheet) === null || _b === void 0 ? void 0 : _b.cssRules) {
  445. // FF
  446. return style.sheet.cssRules;
  447. }
  448. return [];
  449. }
  450. export function createCSSRule(selector, cssText, style = getSharedStyleSheet()) {
  451. if (!style || !cssText) {
  452. return;
  453. }
  454. style.sheet.insertRule(selector + '{' + cssText + '}', 0);
  455. }
  456. export function removeCSSRulesContainingSelector(ruleName, style = getSharedStyleSheet()) {
  457. if (!style) {
  458. return;
  459. }
  460. const rules = getDynamicStyleSheetRules(style);
  461. const toDelete = [];
  462. for (let i = 0; i < rules.length; i++) {
  463. const rule = rules[i];
  464. if (rule.selectorText.indexOf(ruleName) !== -1) {
  465. toDelete.push(i);
  466. }
  467. }
  468. for (let i = toDelete.length - 1; i >= 0; i--) {
  469. style.sheet.deleteRule(toDelete[i]);
  470. }
  471. }
  472. export function isHTMLElement(o) {
  473. if (typeof HTMLElement === 'object') {
  474. return o instanceof HTMLElement;
  475. }
  476. return o && typeof o === 'object' && o.nodeType === 1 && typeof o.nodeName === 'string';
  477. }
  478. export const EventType = {
  479. // Mouse
  480. CLICK: 'click',
  481. AUXCLICK: 'auxclick',
  482. DBLCLICK: 'dblclick',
  483. MOUSE_UP: 'mouseup',
  484. MOUSE_DOWN: 'mousedown',
  485. MOUSE_OVER: 'mouseover',
  486. MOUSE_MOVE: 'mousemove',
  487. MOUSE_OUT: 'mouseout',
  488. MOUSE_ENTER: 'mouseenter',
  489. MOUSE_LEAVE: 'mouseleave',
  490. MOUSE_WHEEL: 'wheel',
  491. POINTER_UP: 'pointerup',
  492. POINTER_DOWN: 'pointerdown',
  493. POINTER_MOVE: 'pointermove',
  494. POINTER_LEAVE: 'pointerleave',
  495. CONTEXT_MENU: 'contextmenu',
  496. WHEEL: 'wheel',
  497. // Keyboard
  498. KEY_DOWN: 'keydown',
  499. KEY_PRESS: 'keypress',
  500. KEY_UP: 'keyup',
  501. // HTML Document
  502. LOAD: 'load',
  503. BEFORE_UNLOAD: 'beforeunload',
  504. UNLOAD: 'unload',
  505. PAGE_SHOW: 'pageshow',
  506. PAGE_HIDE: 'pagehide',
  507. ABORT: 'abort',
  508. ERROR: 'error',
  509. RESIZE: 'resize',
  510. SCROLL: 'scroll',
  511. FULLSCREEN_CHANGE: 'fullscreenchange',
  512. WK_FULLSCREEN_CHANGE: 'webkitfullscreenchange',
  513. // Form
  514. SELECT: 'select',
  515. CHANGE: 'change',
  516. SUBMIT: 'submit',
  517. RESET: 'reset',
  518. FOCUS: 'focus',
  519. FOCUS_IN: 'focusin',
  520. FOCUS_OUT: 'focusout',
  521. BLUR: 'blur',
  522. INPUT: 'input',
  523. // Local Storage
  524. STORAGE: 'storage',
  525. // Drag
  526. DRAG_START: 'dragstart',
  527. DRAG: 'drag',
  528. DRAG_ENTER: 'dragenter',
  529. DRAG_LEAVE: 'dragleave',
  530. DRAG_OVER: 'dragover',
  531. DROP: 'drop',
  532. DRAG_END: 'dragend',
  533. // Animation
  534. ANIMATION_START: browser.isWebKit ? 'webkitAnimationStart' : 'animationstart',
  535. ANIMATION_END: browser.isWebKit ? 'webkitAnimationEnd' : 'animationend',
  536. ANIMATION_ITERATION: browser.isWebKit ? 'webkitAnimationIteration' : 'animationiteration'
  537. };
  538. export function isEventLike(obj) {
  539. const candidate = obj;
  540. return !!(candidate && typeof candidate.preventDefault === 'function' && typeof candidate.stopPropagation === 'function');
  541. }
  542. export const EventHelper = {
  543. stop: (e, cancelBubble) => {
  544. e.preventDefault();
  545. if (cancelBubble) {
  546. e.stopPropagation();
  547. }
  548. return e;
  549. }
  550. };
  551. export function saveParentsScrollTop(node) {
  552. const r = [];
  553. for (let i = 0; node && node.nodeType === node.ELEMENT_NODE; i++) {
  554. r[i] = node.scrollTop;
  555. node = node.parentNode;
  556. }
  557. return r;
  558. }
  559. export function restoreParentsScrollTop(node, state) {
  560. for (let i = 0; node && node.nodeType === node.ELEMENT_NODE; i++) {
  561. if (node.scrollTop !== state[i]) {
  562. node.scrollTop = state[i];
  563. }
  564. node = node.parentNode;
  565. }
  566. }
  567. class FocusTracker extends Disposable {
  568. static hasFocusWithin(element) {
  569. const shadowRoot = getShadowRoot(element);
  570. const activeElement = (shadowRoot ? shadowRoot.activeElement : document.activeElement);
  571. return isAncestor(activeElement, element);
  572. }
  573. constructor(element) {
  574. super();
  575. this._onDidFocus = this._register(new event.Emitter());
  576. this.onDidFocus = this._onDidFocus.event;
  577. this._onDidBlur = this._register(new event.Emitter());
  578. this.onDidBlur = this._onDidBlur.event;
  579. let hasFocus = FocusTracker.hasFocusWithin(element);
  580. let loosingFocus = false;
  581. const onFocus = () => {
  582. loosingFocus = false;
  583. if (!hasFocus) {
  584. hasFocus = true;
  585. this._onDidFocus.fire();
  586. }
  587. };
  588. const onBlur = () => {
  589. if (hasFocus) {
  590. loosingFocus = true;
  591. window.setTimeout(() => {
  592. if (loosingFocus) {
  593. loosingFocus = false;
  594. hasFocus = false;
  595. this._onDidBlur.fire();
  596. }
  597. }, 0);
  598. }
  599. };
  600. this._refreshStateHandler = () => {
  601. const currentNodeHasFocus = FocusTracker.hasFocusWithin(element);
  602. if (currentNodeHasFocus !== hasFocus) {
  603. if (hasFocus) {
  604. onBlur();
  605. }
  606. else {
  607. onFocus();
  608. }
  609. }
  610. };
  611. this._register(addDisposableListener(element, EventType.FOCUS, onFocus, true));
  612. this._register(addDisposableListener(element, EventType.BLUR, onBlur, true));
  613. this._register(addDisposableListener(element, EventType.FOCUS_IN, () => this._refreshStateHandler()));
  614. this._register(addDisposableListener(element, EventType.FOCUS_OUT, () => this._refreshStateHandler()));
  615. }
  616. }
  617. /**
  618. * Creates a new `IFocusTracker` instance that tracks focus changes on the given `element` and its descendants.
  619. *
  620. * @param element The `HTMLElement` or `Window` to track focus changes on.
  621. * @returns An `IFocusTracker` instance.
  622. */
  623. export function trackFocus(element) {
  624. return new FocusTracker(element);
  625. }
  626. export function append(parent, ...children) {
  627. parent.append(...children);
  628. if (children.length === 1 && typeof children[0] !== 'string') {
  629. return children[0];
  630. }
  631. }
  632. export function prepend(parent, child) {
  633. parent.insertBefore(child, parent.firstChild);
  634. return child;
  635. }
  636. /**
  637. * Removes all children from `parent` and appends `children`
  638. */
  639. export function reset(parent, ...children) {
  640. parent.innerText = '';
  641. append(parent, ...children);
  642. }
  643. const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/;
  644. export var Namespace;
  645. (function (Namespace) {
  646. Namespace["HTML"] = "http://www.w3.org/1999/xhtml";
  647. Namespace["SVG"] = "http://www.w3.org/2000/svg";
  648. })(Namespace || (Namespace = {}));
  649. function _$(namespace, description, attrs, ...children) {
  650. const match = SELECTOR_REGEX.exec(description);
  651. if (!match) {
  652. throw new Error('Bad use of emmet');
  653. }
  654. const tagName = match[1] || 'div';
  655. let result;
  656. if (namespace !== Namespace.HTML) {
  657. result = document.createElementNS(namespace, tagName);
  658. }
  659. else {
  660. result = document.createElement(tagName);
  661. }
  662. if (match[3]) {
  663. result.id = match[3];
  664. }
  665. if (match[4]) {
  666. result.className = match[4].replace(/\./g, ' ').trim();
  667. }
  668. if (attrs) {
  669. Object.entries(attrs).forEach(([name, value]) => {
  670. if (typeof value === 'undefined') {
  671. return;
  672. }
  673. if (/^on\w+$/.test(name)) {
  674. result[name] = value;
  675. }
  676. else if (name === 'selected') {
  677. if (value) {
  678. result.setAttribute(name, 'true');
  679. }
  680. }
  681. else {
  682. result.setAttribute(name, value);
  683. }
  684. });
  685. }
  686. result.append(...children);
  687. return result;
  688. }
  689. export function $(description, attrs, ...children) {
  690. return _$(Namespace.HTML, description, attrs, ...children);
  691. }
  692. $.SVG = function (description, attrs, ...children) {
  693. return _$(Namespace.SVG, description, attrs, ...children);
  694. };
  695. export function setVisibility(visible, ...elements) {
  696. if (visible) {
  697. show(...elements);
  698. }
  699. else {
  700. hide(...elements);
  701. }
  702. }
  703. export function show(...elements) {
  704. for (const element of elements) {
  705. element.style.display = '';
  706. element.removeAttribute('aria-hidden');
  707. }
  708. }
  709. export function hide(...elements) {
  710. for (const element of elements) {
  711. element.style.display = 'none';
  712. element.setAttribute('aria-hidden', 'true');
  713. }
  714. }
  715. /**
  716. * Find a value usable for a dom node size such that the likelihood that it would be
  717. * displayed with constant screen pixels size is as high as possible.
  718. *
  719. * e.g. We would desire for the cursors to be 2px (CSS px) wide. Under a devicePixelRatio
  720. * of 1.25, the cursor will be 2.5 screen pixels wide. Depending on how the dom node aligns/"snaps"
  721. * with the screen pixels, it will sometimes be rendered with 2 screen pixels, and sometimes with 3 screen pixels.
  722. */
  723. export function computeScreenAwareSize(cssPx) {
  724. const screenPx = window.devicePixelRatio * cssPx;
  725. return Math.max(1, Math.floor(screenPx)) / window.devicePixelRatio;
  726. }
  727. /**
  728. * Open safely a new window. This is the best way to do so, but you cannot tell
  729. * if the window was opened or if it was blocked by the browser's popup blocker.
  730. * If you want to tell if the browser blocked the new window, use {@link windowOpenWithSuccess}.
  731. *
  732. * See https://github.com/microsoft/monaco-editor/issues/601
  733. * To protect against malicious code in the linked site, particularly phishing attempts,
  734. * the window.opener should be set to null to prevent the linked site from having access
  735. * to change the location of the current page.
  736. * See https://mathiasbynens.github.io/rel-noopener/
  737. */
  738. export function windowOpenNoOpener(url) {
  739. // By using 'noopener' in the `windowFeatures` argument, the newly created window will
  740. // not be able to use `window.opener` to reach back to the current page.
  741. // See https://stackoverflow.com/a/46958731
  742. // See https://developer.mozilla.org/en-US/docs/Web/API/Window/open#noopener
  743. // However, this also doesn't allow us to realize if the browser blocked
  744. // the creation of the window.
  745. window.open(url, '_blank', 'noopener');
  746. }
  747. export function animate(fn) {
  748. const step = () => {
  749. fn();
  750. stepDisposable = scheduleAtNextAnimationFrame(step);
  751. };
  752. let stepDisposable = scheduleAtNextAnimationFrame(step);
  753. return toDisposable(() => stepDisposable.dispose());
  754. }
  755. RemoteAuthorities.setPreferredWebSchema(/^https:/.test(window.location.href) ? 'https' : 'http');
  756. /**
  757. * returns url('...')
  758. */
  759. export function asCSSUrl(uri) {
  760. if (!uri) {
  761. return `url('')`;
  762. }
  763. return `url('${FileAccess.uriToBrowserUri(uri).toString(true).replace(/'/g, '%27')}')`;
  764. }
  765. export function asCSSPropertyValue(value) {
  766. return `'${value.replace(/'/g, '%27')}'`;
  767. }
  768. export function asCssValueWithDefault(cssPropertyValue, dflt) {
  769. if (cssPropertyValue !== undefined) {
  770. const variableMatch = cssPropertyValue.match(/^\s*var\((.+)\)$/);
  771. if (variableMatch) {
  772. const varArguments = variableMatch[1].split(',', 2);
  773. if (varArguments.length === 2) {
  774. dflt = asCssValueWithDefault(varArguments[1].trim(), dflt);
  775. }
  776. return `var(${varArguments[0]}, ${dflt})`;
  777. }
  778. return cssPropertyValue;
  779. }
  780. return dflt;
  781. }
  782. // -- sanitize and trusted html
  783. /**
  784. * Hooks dompurify using `afterSanitizeAttributes` to check that all `href` and `src`
  785. * attributes are valid.
  786. */
  787. export function hookDomPurifyHrefAndSrcSanitizer(allowedProtocols, allowDataImages = false) {
  788. // https://github.com/cure53/DOMPurify/blob/main/demos/hooks-scheme-allowlist.html
  789. // build an anchor to map URLs to
  790. const anchor = document.createElement('a');
  791. dompurify.addHook('afterSanitizeAttributes', (node) => {
  792. // check all href/src attributes for validity
  793. for (const attr of ['href', 'src']) {
  794. if (node.hasAttribute(attr)) {
  795. const attrValue = node.getAttribute(attr);
  796. if (attr === 'href' && attrValue.startsWith('#')) {
  797. // Allow fragment links
  798. continue;
  799. }
  800. anchor.href = attrValue;
  801. if (!allowedProtocols.includes(anchor.protocol.replace(/:$/, ''))) {
  802. if (allowDataImages && attr === 'src' && anchor.href.startsWith('data:')) {
  803. continue;
  804. }
  805. node.removeAttribute(attr);
  806. }
  807. }
  808. }
  809. });
  810. return toDisposable(() => {
  811. dompurify.removeHook('afterSanitizeAttributes');
  812. });
  813. }
  814. /**
  815. * List of safe, non-input html tags.
  816. */
  817. export const basicMarkupHtmlTags = Object.freeze([
  818. 'a',
  819. 'abbr',
  820. 'b',
  821. 'bdo',
  822. 'blockquote',
  823. 'br',
  824. 'caption',
  825. 'cite',
  826. 'code',
  827. 'col',
  828. 'colgroup',
  829. 'dd',
  830. 'del',
  831. 'details',
  832. 'dfn',
  833. 'div',
  834. 'dl',
  835. 'dt',
  836. 'em',
  837. 'figcaption',
  838. 'figure',
  839. 'h1',
  840. 'h2',
  841. 'h3',
  842. 'h4',
  843. 'h5',
  844. 'h6',
  845. 'hr',
  846. 'i',
  847. 'img',
  848. 'ins',
  849. 'kbd',
  850. 'label',
  851. 'li',
  852. 'mark',
  853. 'ol',
  854. 'p',
  855. 'pre',
  856. 'q',
  857. 'rp',
  858. 'rt',
  859. 'ruby',
  860. 'samp',
  861. 'small',
  862. 'small',
  863. 'source',
  864. 'span',
  865. 'strike',
  866. 'strong',
  867. 'sub',
  868. 'summary',
  869. 'sup',
  870. 'table',
  871. 'tbody',
  872. 'td',
  873. 'tfoot',
  874. 'th',
  875. 'thead',
  876. 'time',
  877. 'tr',
  878. 'tt',
  879. 'u',
  880. 'ul',
  881. 'var',
  882. 'video',
  883. 'wbr',
  884. ]);
  885. const defaultDomPurifyConfig = Object.freeze({
  886. ALLOWED_TAGS: ['a', 'button', 'blockquote', 'code', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'input', 'label', 'li', 'p', 'pre', 'select', 'small', 'span', 'strong', 'textarea', 'ul', 'ol'],
  887. ALLOWED_ATTR: ['href', 'data-href', 'data-command', 'target', 'title', 'name', 'src', 'alt', 'class', 'id', 'role', 'tabindex', 'style', 'data-code', 'width', 'height', 'align', 'x-dispatch', 'required', 'checked', 'placeholder', 'type', 'start'],
  888. RETURN_DOM: false,
  889. RETURN_DOM_FRAGMENT: false,
  890. RETURN_TRUSTED_TYPE: true
  891. });
  892. export class ModifierKeyEmitter extends event.Emitter {
  893. constructor() {
  894. super();
  895. this._subscriptions = new DisposableStore();
  896. this._keyStatus = {
  897. altKey: false,
  898. shiftKey: false,
  899. ctrlKey: false,
  900. metaKey: false
  901. };
  902. this._subscriptions.add(addDisposableListener(window, 'keydown', e => {
  903. if (e.defaultPrevented) {
  904. return;
  905. }
  906. const event = new StandardKeyboardEvent(e);
  907. // If Alt-key keydown event is repeated, ignore it #112347
  908. // Only known to be necessary for Alt-Key at the moment #115810
  909. if (event.keyCode === 6 /* KeyCode.Alt */ && e.repeat) {
  910. return;
  911. }
  912. if (e.altKey && !this._keyStatus.altKey) {
  913. this._keyStatus.lastKeyPressed = 'alt';
  914. }
  915. else if (e.ctrlKey && !this._keyStatus.ctrlKey) {
  916. this._keyStatus.lastKeyPressed = 'ctrl';
  917. }
  918. else if (e.metaKey && !this._keyStatus.metaKey) {
  919. this._keyStatus.lastKeyPressed = 'meta';
  920. }
  921. else if (e.shiftKey && !this._keyStatus.shiftKey) {
  922. this._keyStatus.lastKeyPressed = 'shift';
  923. }
  924. else if (event.keyCode !== 6 /* KeyCode.Alt */) {
  925. this._keyStatus.lastKeyPressed = undefined;
  926. }
  927. else {
  928. return;
  929. }
  930. this._keyStatus.altKey = e.altKey;
  931. this._keyStatus.ctrlKey = e.ctrlKey;
  932. this._keyStatus.metaKey = e.metaKey;
  933. this._keyStatus.shiftKey = e.shiftKey;
  934. if (this._keyStatus.lastKeyPressed) {
  935. this._keyStatus.event = e;
  936. this.fire(this._keyStatus);
  937. }
  938. }, true));
  939. this._subscriptions.add(addDisposableListener(window, 'keyup', e => {
  940. if (e.defaultPrevented) {
  941. return;
  942. }
  943. if (!e.altKey && this._keyStatus.altKey) {
  944. this._keyStatus.lastKeyReleased = 'alt';
  945. }
  946. else if (!e.ctrlKey && this._keyStatus.ctrlKey) {
  947. this._keyStatus.lastKeyReleased = 'ctrl';
  948. }
  949. else if (!e.metaKey && this._keyStatus.metaKey) {
  950. this._keyStatus.lastKeyReleased = 'meta';
  951. }
  952. else if (!e.shiftKey && this._keyStatus.shiftKey) {
  953. this._keyStatus.lastKeyReleased = 'shift';
  954. }
  955. else {
  956. this._keyStatus.lastKeyReleased = undefined;
  957. }
  958. if (this._keyStatus.lastKeyPressed !== this._keyStatus.lastKeyReleased) {
  959. this._keyStatus.lastKeyPressed = undefined;
  960. }
  961. this._keyStatus.altKey = e.altKey;
  962. this._keyStatus.ctrlKey = e.ctrlKey;
  963. this._keyStatus.metaKey = e.metaKey;
  964. this._keyStatus.shiftKey = e.shiftKey;
  965. if (this._keyStatus.lastKeyReleased) {
  966. this._keyStatus.event = e;
  967. this.fire(this._keyStatus);
  968. }
  969. }, true));
  970. this._subscriptions.add(addDisposableListener(document.body, 'mousedown', () => {
  971. this._keyStatus.lastKeyPressed = undefined;
  972. }, true));
  973. this._subscriptions.add(addDisposableListener(document.body, 'mouseup', () => {
  974. this._keyStatus.lastKeyPressed = undefined;
  975. }, true));
  976. this._subscriptions.add(addDisposableListener(document.body, 'mousemove', e => {
  977. if (e.buttons) {
  978. this._keyStatus.lastKeyPressed = undefined;
  979. }
  980. }, true));
  981. this._subscriptions.add(addDisposableListener(window, 'blur', () => {
  982. this.resetKeyStatus();
  983. }));
  984. }
  985. get keyStatus() {
  986. return this._keyStatus;
  987. }
  988. /**
  989. * Allows to explicitly reset the key status based on more knowledge (#109062)
  990. */
  991. resetKeyStatus() {
  992. this.doResetKeyStatus();
  993. this.fire(this._keyStatus);
  994. }
  995. doResetKeyStatus() {
  996. this._keyStatus = {
  997. altKey: false,
  998. shiftKey: false,
  999. ctrlKey: false,
  1000. metaKey: false
  1001. };
  1002. }
  1003. static getInstance() {
  1004. if (!ModifierKeyEmitter.instance) {
  1005. ModifierKeyEmitter.instance = new ModifierKeyEmitter();
  1006. }
  1007. return ModifierKeyEmitter.instance;
  1008. }
  1009. dispose() {
  1010. super.dispose();
  1011. this._subscriptions.dispose();
  1012. }
  1013. }
  1014. export class DragAndDropObserver extends Disposable {
  1015. constructor(element, callbacks) {
  1016. super();
  1017. this.element = element;
  1018. this.callbacks = callbacks;
  1019. // A helper to fix issues with repeated DRAG_ENTER / DRAG_LEAVE
  1020. // calls see https://github.com/microsoft/vscode/issues/14470
  1021. // when the element has child elements where the events are fired
  1022. // repeadedly.
  1023. this.counter = 0;
  1024. // Allows to measure the duration of the drag operation.
  1025. this.dragStartTime = 0;
  1026. this.registerListeners();
  1027. }
  1028. registerListeners() {
  1029. this._register(addDisposableListener(this.element, EventType.DRAG_ENTER, (e) => {
  1030. this.counter++;
  1031. this.dragStartTime = e.timeStamp;
  1032. this.callbacks.onDragEnter(e);
  1033. }));
  1034. this._register(addDisposableListener(this.element, EventType.DRAG_OVER, (e) => {
  1035. var _a, _b;
  1036. e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
  1037. (_b = (_a = this.callbacks).onDragOver) === null || _b === void 0 ? void 0 : _b.call(_a, e, e.timeStamp - this.dragStartTime);
  1038. }));
  1039. this._register(addDisposableListener(this.element, EventType.DRAG_LEAVE, (e) => {
  1040. this.counter--;
  1041. if (this.counter === 0) {
  1042. this.dragStartTime = 0;
  1043. this.callbacks.onDragLeave(e);
  1044. }
  1045. }));
  1046. this._register(addDisposableListener(this.element, EventType.DRAG_END, (e) => {
  1047. this.counter = 0;
  1048. this.dragStartTime = 0;
  1049. this.callbacks.onDragEnd(e);
  1050. }));
  1051. this._register(addDisposableListener(this.element, EventType.DROP, (e) => {
  1052. this.counter = 0;
  1053. this.dragStartTime = 0;
  1054. this.callbacks.onDrop(e);
  1055. }));
  1056. }
  1057. }
  1058. const H_REGEX = /(?<tag>[\w\-]+)?(?:#(?<id>[\w\-]+))?(?<class>(?:\.(?:[\w\-]+))*)(?:@(?<name>(?:[\w\_])+))?/;
  1059. export function h(tag, ...args) {
  1060. let attributes;
  1061. let children;
  1062. if (Array.isArray(args[0])) {
  1063. attributes = {};
  1064. children = args[0];
  1065. }
  1066. else {
  1067. attributes = args[0] || {};
  1068. children = args[1];
  1069. }
  1070. const match = H_REGEX.exec(tag);
  1071. if (!match || !match.groups) {
  1072. throw new Error('Bad use of h');
  1073. }
  1074. const tagName = match.groups['tag'] || 'div';
  1075. const el = document.createElement(tagName);
  1076. if (match.groups['id']) {
  1077. el.id = match.groups['id'];
  1078. }
  1079. const classNames = [];
  1080. if (match.groups['class']) {
  1081. for (const className of match.groups['class'].split('.')) {
  1082. if (className !== '') {
  1083. classNames.push(className);
  1084. }
  1085. }
  1086. }
  1087. if (attributes.className !== undefined) {
  1088. for (const className of attributes.className.split('.')) {
  1089. if (className !== '') {
  1090. classNames.push(className);
  1091. }
  1092. }
  1093. }
  1094. if (classNames.length > 0) {
  1095. el.className = classNames.join(' ');
  1096. }
  1097. const result = {};
  1098. if (match.groups['name']) {
  1099. result[match.groups['name']] = el;
  1100. }
  1101. if (children) {
  1102. for (const c of children) {
  1103. if (c instanceof HTMLElement) {
  1104. el.appendChild(c);
  1105. }
  1106. else if (typeof c === 'string') {
  1107. el.append(c);
  1108. }
  1109. else {
  1110. Object.assign(result, c);
  1111. el.appendChild(c.root);
  1112. }
  1113. }
  1114. }
  1115. for (const [key, value] of Object.entries(attributes)) {
  1116. if (key === 'className') {
  1117. continue;
  1118. }
  1119. else if (key === 'style') {
  1120. for (const [cssKey, cssValue] of Object.entries(value)) {
  1121. el.style.setProperty(camelCaseToHyphenCase(cssKey), typeof cssValue === 'number' ? cssValue + 'px' : '' + cssValue);
  1122. }
  1123. }
  1124. else if (key === 'tabIndex') {
  1125. el.tabIndex = value;
  1126. }
  1127. else {
  1128. el.setAttribute(camelCaseToHyphenCase(key), value.toString());
  1129. }
  1130. }
  1131. result['root'] = el;
  1132. return result;
  1133. }
  1134. function camelCaseToHyphenCase(str) {
  1135. return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
  1136. }