UNPKG

8.64 kBPlain TextView Raw
1// *****************************************************************************
2// Copyright (C) 2017 TypeFox and others.
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License v. 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0.
7//
8// This Source Code may also be made available under the following Secondary
9// Licenses when the conditions for such availability set forth in the Eclipse
10// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11// with the GNU Classpath Exception which is available at
12// https://www.gnu.org/software/classpath/license.html.
13//
14// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15// *****************************************************************************
16/*---------------------------------------------------------------------------------------------
17 * Copyright (c) Microsoft Corporation. All rights reserved.
18 * Licensed under the MIT License. See License.txt in the project root for license information.
19 *--------------------------------------------------------------------------------------------*/
20
21const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : '';
22
23export const isIE = (userAgent.indexOf('Trident') >= 0);
24export const isEdge = (userAgent.indexOf('Edge/') >= 0);
25export const isEdgeOrIE = isIE || isEdge;
26
27export const isOpera = (userAgent.indexOf('Opera') >= 0);
28export const isFirefox = (userAgent.indexOf('Firefox') >= 0);
29export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0);
30export const isChrome = (userAgent.indexOf('Chrome') >= 0);
31export const isSafari = (userAgent.indexOf('Chrome') === -1) && (userAgent.indexOf('Safari') >= 0);
32export const isIPad = (userAgent.indexOf('iPad') >= 0);
33// eslint-disable-next-line @typescript-eslint/no-explicit-any
34export const isNative = typeof (window as any).process !== 'undefined';
35// eslint-disable-next-line @typescript-eslint/no-explicit-any
36export const isBasicWasmSupported = typeof (window as any).WebAssembly !== 'undefined';
37
38/**
39 * Resolves after the next animation frame if no parameter is given,
40 * or after the given number of animation frames.
41 */
42export function animationFrame(n: number = 1): Promise<void> {
43 return new Promise(resolve => {
44 function frameFunc(): void {
45 if (n <= 0) {
46 resolve();
47 } else {
48 n--;
49 requestAnimationFrame(frameFunc);
50 }
51 }
52 frameFunc();
53 });
54}
55
56/**
57 * Parse a magnitude value (e.g. width, height, left, top) from a CSS attribute value.
58 * Returns the given default value (or undefined) if the value cannot be determined,
59 * e.g. because it is a relative value like `50%` or `auto`.
60 */
61export function parseCssMagnitude(value: string | null, defaultValue: number): number;
62export function parseCssMagnitude(value: string | null, defaultValue?: number): number | undefined {
63 if (value) {
64 let parsed: number;
65 if (value.endsWith('px')) {
66 parsed = parseFloat(value.substring(0, value.length - 2));
67 } else {
68 parsed = parseFloat(value);
69 }
70 if (!isNaN(parsed)) {
71 return parsed;
72 }
73 }
74 return defaultValue;
75}
76
77/**
78 * Parse the number of milliseconds from a CSS time value.
79 * Returns the given default value (or undefined) if the value cannot be determined.
80 */
81export function parseCssTime(time: string | null, defaultValue: number): number;
82export function parseCssTime(time: string | null, defaultValue?: number): number | undefined {
83 if (time) {
84 let parsed: number;
85 if (time.endsWith('ms')) {
86 parsed = parseFloat(time.substring(0, time.length - 2));
87 } else if (time.endsWith('s')) {
88 parsed = parseFloat(time.substring(0, time.length - 1)) * 1000;
89 } else {
90 parsed = parseFloat(time);
91 }
92 if (!isNaN(parsed)) {
93 return parsed;
94 }
95 }
96 return defaultValue;
97}
98
99interface ElementScroll {
100 left: number
101 top: number
102 maxLeft: number
103 maxTop: number
104}
105
106function getMonacoEditorScroll(elem: HTMLElement): ElementScroll | undefined {
107 const linesContent = elem.querySelector('.lines-content') as HTMLElement;
108 const viewLines = elem.querySelector('.view-lines') as HTMLElement;
109 // eslint-disable-next-line no-null/no-null
110 if (linesContent === null || viewLines === null) {
111 return undefined;
112 }
113 const linesContentStyle = linesContent.style;
114 const elemStyle = elem.style;
115 const viewLinesStyle = viewLines.style;
116 return {
117 left: -parseCssMagnitude(linesContentStyle.left, 0),
118 top: -parseCssMagnitude(linesContentStyle.top, 0),
119 maxLeft: parseCssMagnitude(viewLinesStyle.width, 0) - parseCssMagnitude(elemStyle.width, 0),
120 maxTop: parseCssMagnitude(viewLinesStyle.height, 0) - parseCssMagnitude(elemStyle.height, 0)
121 };
122}
123
124/**
125 * Prevent browser back/forward navigation of a mouse wheel event.
126 */
127export function preventNavigation(event: WheelEvent): void {
128 const { currentTarget, deltaX, deltaY } = event;
129 const absDeltaX = Math.abs(deltaX);
130 const absDeltaY = Math.abs(deltaY);
131 let elem = event.target as Element | null;
132
133 while (elem && elem !== currentTarget) {
134 let scroll: ElementScroll | undefined;
135 if (elem.classList.contains('monaco-scrollable-element')) {
136 scroll = getMonacoEditorScroll(elem as HTMLElement);
137 } else {
138 scroll = {
139 left: elem.scrollLeft,
140 top: elem.scrollTop,
141 maxLeft: elem.scrollWidth - elem.clientWidth,
142 maxTop: elem.scrollHeight - elem.clientHeight
143 };
144 }
145 if (scroll) {
146 const scrollH = scroll.maxLeft > 0 && (deltaX < 0 && scroll.left > 0 || deltaX > 0 && scroll.left < scroll.maxLeft);
147 const scrollV = scroll.maxTop > 0 && (deltaY < 0 && scroll.top > 0 || deltaY > 0 && scroll.top < scroll.maxTop);
148 if (scrollH && scrollV || scrollH && absDeltaX > absDeltaY || scrollV && absDeltaY > absDeltaX) {
149 // The event is consumed by the scrollable child element
150 return;
151 }
152 }
153 elem = elem.parentElement;
154 }
155
156 event.preventDefault();
157 event.stopPropagation();
158}
159
160export type PartialCSSStyle = Omit<Partial<CSSStyleDeclaration>,
161 'visibility' |
162 'display' |
163 'parentRule' |
164 'getPropertyPriority' |
165 'getPropertyValue' |
166 'item' |
167 'removeProperty' |
168 'setProperty'>;
169
170export function measureTextWidth(text: string | string[], style?: PartialCSSStyle): number {
171 const measureElement = getMeasurementElement(style);
172 text = Array.isArray(text) ? text : [text];
173 let width = 0;
174 for (const item of text) {
175 measureElement.textContent = item;
176 width = Math.max(measureElement.getBoundingClientRect().width, width);
177 }
178 return width;
179}
180
181export function measureTextHeight(text: string | string[], style?: PartialCSSStyle): number {
182 const measureElement = getMeasurementElement(style);
183 text = Array.isArray(text) ? text : [text];
184 let height = 0;
185 for (const item of text) {
186 measureElement.textContent = item;
187 height = Math.max(measureElement.getBoundingClientRect().height, height);
188 }
189 return height;
190}
191
192const defaultStyle = document.createElement('div').style;
193defaultStyle.fontFamily = 'var(--theia-ui-font-family)';
194defaultStyle.fontSize = 'var(--theia-ui-font-size1)';
195defaultStyle.visibility = 'hidden';
196
197function getMeasurementElement(style?: PartialCSSStyle): HTMLElement {
198 let measureElement = document.getElementById('measure');
199 if (!measureElement) {
200 measureElement = document.createElement('span');
201 measureElement.id = 'measure';
202 measureElement.style.fontFamily = defaultStyle.fontFamily;
203 measureElement.style.fontSize = defaultStyle.fontSize;
204 measureElement.style.visibility = defaultStyle.visibility;
205 document.body.appendChild(measureElement);
206 }
207 const measureStyle = measureElement.style;
208 // Reset styling first
209 for (let i = 0; i < measureStyle.length; i++) {
210 const property = measureStyle[i];
211 measureStyle.setProperty(property, defaultStyle.getPropertyValue(property));
212 }
213 // Apply new styling
214 if (style) {
215 for (const [key, value] of Object.entries(style)) {
216 measureStyle.setProperty(key, value as string);
217 }
218 }
219 return measureElement;
220}