diff --git a/packages/devextreme/js/__internal/core/utils/browser.ts b/packages/devextreme/js/__internal/core/utils/browser.ts new file mode 100644 index 000000000000..bb4720a1f979 --- /dev/null +++ b/packages/devextreme/js/__internal/core/utils/browser.ts @@ -0,0 +1,54 @@ +import type { BrowserInfo } from '@js/core/utils/browser'; +import { extend } from '@js/core/utils/extend'; +import { getNavigator } from '@js/core/utils/window'; + +export type BrowserName = Exclude; + +export type Browser = BrowserInfo & { + _fromUA: (userAgent: string) => BrowserInfo; +}; + +const navigator = getNavigator(); + +const webkitRegExp = /(webkit)[ /]([\w.]+)/; +const mozillaRegExp = /(mozilla)(?:.*? rv:([\w.]+))/; + +const browserFromUA = (userAgent: string): BrowserInfo => { + const ua = userAgent.toLowerCase(); + + const result: BrowserInfo = {}; + const matches = webkitRegExp.exec(ua) + ?? (!ua.includes('compatible') ? mozillaRegExp.exec(ua) : null) + ?? []; + let browserName = matches[1] as BrowserName | undefined; + let browserVersion = matches[2] as string | undefined; + + if (browserName === 'webkit') { + result.webkit = true; + + if (ua.includes('chrome') || ua.includes('crios')) { + browserName = 'chrome'; + browserVersion = /(?:chrome|crios)\/(\d+\.\d+)/.exec(ua)?.[1]; + } else if (ua.includes('fxios')) { + browserName = 'mozilla'; + browserVersion = /fxios\/(\d+\.\d+)/.exec(ua)?.[1]; + } else if (ua.includes('safari') && /version|phantomjs/.test(ua)) { + browserName = 'safari'; + browserVersion = /(?:version|phantomjs)\/([0-9.]+)/.exec(ua)?.[1]; + } else { + browserName = 'unknown'; + browserVersion = /applewebkit\/([0-9.]+)/.exec(ua)?.[1]; + } + } + + if (browserName) { + result[browserName] = true; + result.version = browserVersion; + } + + return result; +}; + +const browser: Browser = extend({ _fromUA: browserFromUA }, browserFromUA(navigator.userAgent)); + +export { browser }; diff --git a/packages/devextreme/js/__internal/core/utils/m_browser.ts b/packages/devextreme/js/__internal/core/utils/m_browser.ts deleted file mode 100644 index 185f2d95c47e..000000000000 --- a/packages/devextreme/js/__internal/core/utils/m_browser.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { extend } from '@js/core/utils/extend'; -import { getNavigator } from '@js/core/utils/window'; - -const navigator = getNavigator(); - -const webkitRegExp = /(webkit)[ /]([\w.]+)/; -const mozillaRegExp = /(mozilla)(?:.*? rv:([\w.]+))/; - -const browserFromUA = (ua) => { - ua = ua.toLowerCase(); - - const result: any = {}; - const matches = webkitRegExp.exec(ua) - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - || ua.indexOf('compatible') < 0 && mozillaRegExp.exec(ua) - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - || []; - let browserName = matches[1]; - let browserVersion: any = matches[2]; - - if (browserName === 'webkit') { - result.webkit = true; - - if (ua.indexOf('chrome') >= 0 || ua.indexOf('crios') >= 0) { - browserName = 'chrome'; - browserVersion = /(?:chrome|crios)\/(\d+\.\d+)/.exec(ua); - browserVersion = browserVersion && browserVersion[1]; - } else if (ua.indexOf('fxios') >= 0) { - browserName = 'mozilla'; - browserVersion = /fxios\/(\d+\.\d+)/.exec(ua); - browserVersion = browserVersion && browserVersion[1]; - } else if (ua.indexOf('safari') >= 0 && /version|phantomjs/.test(ua)) { - browserName = 'safari'; - browserVersion = /(?:version|phantomjs)\/([0-9.]+)/.exec(ua); - browserVersion = browserVersion && browserVersion[1]; - } else { - browserName = 'unknown'; - browserVersion = /applewebkit\/([0-9.]+)/.exec(ua); - browserVersion = browserVersion && browserVersion[1]; - } - } - - if (browserName) { - result[browserName] = true; - result.version = browserVersion; - } - - return result; -}; -const browser = extend({ _fromUA: browserFromUA }, browserFromUA(navigator.userAgent)); -export { browser }; diff --git a/packages/devextreme/js/__internal/core/utils/m_version.ts b/packages/devextreme/js/__internal/core/utils/m_version.ts deleted file mode 100644 index df53961d1a65..000000000000 --- a/packages/devextreme/js/__internal/core/utils/m_version.ts +++ /dev/null @@ -1,34 +0,0 @@ -export function compare(x, y, maxLevel?) { - function normalizeArg(value) { - if (typeof value === 'string') { - return value.split('.'); - } - if (typeof value === 'number') { - return [value]; - } - return value; - } - - x = normalizeArg(x); - y = normalizeArg(y); - - let length = Math.max(x.length, y.length); - - if (isFinite(maxLevel)) { - length = Math.min(length, maxLevel); - } - - for (let i = 0; i < length; i++) { - const xItem = parseInt(x[i] || 0, 10); - const yItem = parseInt(y[i] || 0, 10); - - if (xItem < yItem) { - return -1; - } - if (xItem > yItem) { - return 1; - } - } - - return 0; -} diff --git a/packages/devextreme/js/__internal/core/utils/version.ts b/packages/devextreme/js/__internal/core/utils/version.ts new file mode 100644 index 000000000000..886e13eece07 --- /dev/null +++ b/packages/devextreme/js/__internal/core/utils/version.ts @@ -0,0 +1,37 @@ +export type ComparableVersion = string | number | (string | number)[]; + +function toParts(value: ComparableVersion): number[] { + const source = typeof value === 'number' ? [value] : value; + const parts = typeof source === 'string' ? source.split('.') : source; + + return parts.map((part) => parseInt(String(part || 0), 10)); +} + +export function compare( + x: ComparableVersion, + y: ComparableVersion, + maxLevel?: number, +): number { + const xParts = toParts(x); + const yParts = toParts(y); + + let length = Math.max(xParts.length, yParts.length); + + if (maxLevel !== undefined && isFinite(maxLevel)) { + length = Math.min(length, maxLevel); + } + + for (let i = 0; i < length; i += 1) { + const xItem = xParts[i] ?? 0; + const yItem = yParts[i] ?? 0; + + if (xItem < yItem) { + return -1; + } + if (xItem > yItem) { + return 1; + } + } + + return 0; +} diff --git a/packages/devextreme/js/__internal/core/widget/widget.ts b/packages/devextreme/js/__internal/core/widget/widget.ts index 577eeb992f38..24fff81b1743 100644 --- a/packages/devextreme/js/__internal/core/widget/widget.ts +++ b/packages/devextreme/js/__internal/core/widget/widget.ts @@ -151,7 +151,7 @@ class Widget< const device = devices.real(); const { platform } = device; const { version } = device; - return platform === 'ios' && compareVersions(version, '13.3') <= 0; + return platform === 'ios' && compareVersions(version ?? [], '13.3') <= 0; }, options: { useResizeObserver: false, diff --git a/packages/devextreme/js/__internal/integration/jquery.ts b/packages/devextreme/js/__internal/integration/jquery.ts index 95588c2628c5..d43ff3399ed7 100644 --- a/packages/devextreme/js/__internal/integration/jquery.ts +++ b/packages/devextreme/js/__internal/integration/jquery.ts @@ -1,6 +1,6 @@ /* eslint-disable import/first */ import errors from '@ts/core/utils/m_error'; -import { compare as compareVersions } from '@ts/core/utils/m_version'; +import { compare as compareVersions } from '@ts/core/utils/version'; // eslint-disable-next-line import/no-extraneous-dependencies import jQuery from 'jquery'; diff --git a/packages/devextreme/js/__internal/integration/jquery/deferred.ts b/packages/devextreme/js/__internal/integration/jquery/deferred.ts index b0e3d85322fa..b9f0feceac21 100644 --- a/packages/devextreme/js/__internal/integration/jquery/deferred.ts +++ b/packages/devextreme/js/__internal/integration/jquery/deferred.ts @@ -1,6 +1,6 @@ import type { DeferredObj } from '@js/core/utils/deferred'; import { setStrategy } from '@ts/core/utils/m_deferred'; -import { compare as compareVersion } from '@ts/core/utils/m_version'; +import { compare as compareVersion } from '@ts/core/utils/version'; // eslint-disable-next-line import/no-extraneous-dependencies import jQuery from 'jquery'; diff --git a/packages/devextreme/js/__internal/integration/jquery/hooks.ts b/packages/devextreme/js/__internal/integration/jquery/hooks.ts index d5333716262a..e593a8dc74f0 100644 --- a/packages/devextreme/js/__internal/integration/jquery/hooks.ts +++ b/packages/devextreme/js/__internal/integration/jquery/hooks.ts @@ -1,6 +1,6 @@ import { each } from '@ts/core/utils/m_iterator'; import { isNumeric } from '@ts/core/utils/m_type'; -import { compare as compareVersion } from '@ts/core/utils/m_version'; +import { compare as compareVersion } from '@ts/core/utils/version'; import registerEvent from '@ts/events/core/event_registrator'; import hookTouchProps from '@ts/events/core/hook_touch_props'; import { setEventFixMethod } from '@ts/events/utils/index'; diff --git a/packages/devextreme/js/__internal/integration/knockout.ts b/packages/devextreme/js/__internal/integration/knockout.ts index dc751718986e..4d1f3d3ea267 100644 --- a/packages/devextreme/js/__internal/integration/knockout.ts +++ b/packages/devextreme/js/__internal/integration/knockout.ts @@ -1,6 +1,6 @@ /* eslint-disable import/first */ import errors from '@ts/core/utils/m_error'; -import { compare as compareVersion } from '@ts/core/utils/m_version'; +import { compare as compareVersion } from '@ts/core/utils/version'; // eslint-disable-next-line import/no-extraneous-dependencies import ko from 'knockout'; diff --git a/packages/devextreme/js/__internal/integration/knockout/clean_node.ts b/packages/devextreme/js/__internal/integration/knockout/clean_node.ts index ad94487dcdcd..7daf29a23376 100644 --- a/packages/devextreme/js/__internal/integration/knockout/clean_node.ts +++ b/packages/devextreme/js/__internal/integration/knockout/clean_node.ts @@ -1,5 +1,5 @@ import { afterCleanData, cleanData, strategyChanging } from '@ts/core/element_data'; -import { compare as compareVersion } from '@ts/core/utils/m_version'; +import { compare as compareVersion } from '@ts/core/utils/version'; // eslint-disable-next-line import/no-extraneous-dependencies import ko from 'knockout'; diff --git a/packages/devextreme/js/__internal/integration/knockout/clean_node_old.ts b/packages/devextreme/js/__internal/integration/knockout/clean_node_old.ts index 6f59702285ec..69ece80af84e 100644 --- a/packages/devextreme/js/__internal/integration/knockout/clean_node_old.ts +++ b/packages/devextreme/js/__internal/integration/knockout/clean_node_old.ts @@ -1,6 +1,6 @@ /* eslint-disable func-names */ import { strategyChanging } from '@ts/core/element_data'; -import { compare as compareVersion } from '@ts/core/utils/m_version'; +import { compare as compareVersion } from '@ts/core/utils/version'; // eslint-disable-next-line import/no-extraneous-dependencies import ko from 'knockout'; diff --git a/packages/devextreme/js/core/utils/browser.js b/packages/devextreme/js/core/utils/browser.js index f61e0a712a42..ce2b6d020d17 100644 --- a/packages/devextreme/js/core/utils/browser.js +++ b/packages/devextreme/js/core/utils/browser.js @@ -1,3 +1,3 @@ // deprecated -import { browser } from '../../__internal/core/utils/m_browser'; +import { browser } from '../../__internal/core/utils/browser'; export default browser; diff --git a/packages/devextreme/js/core/utils/version.js b/packages/devextreme/js/core/utils/version.js index 491cc6781004..9ed24fdd9598 100644 --- a/packages/devextreme/js/core/utils/version.js +++ b/packages/devextreme/js/core/utils/version.js @@ -1,2 +1,2 @@ // deprecated -export { compare } from '../../__internal/core/utils/m_version'; +export { compare } from '../../__internal/core/utils/version'; diff --git a/packages/devextreme/testing/tests/DevExpress.core/utils.browser.tests.js b/packages/devextreme/testing/tests/DevExpress.core/utils.browser.tests.js index 73f57f1e2e9e..3a6d86bf9c6a 100644 --- a/packages/devextreme/testing/tests/DevExpress.core/utils.browser.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.core/utils.browser.tests.js @@ -9,7 +9,8 @@ const userAgents = { chrome_ios: 'Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/74.0.3729.157 Mobile/13B143 Safari/601.1.46', mozilla_ios: 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/18.2b15817 Mobile/15E148 Safari/605.1.15', phantom: 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1', - google_app_ios: 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/88.0.281793270 Mobile/15E148 Safari/604.1' + google_app_ios: 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/88.0.281793270 Mobile/15E148 Safari/604.1', + chrome_without_version: 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome Safari/537.36' }; QUnit.module('browser'); @@ -69,6 +70,13 @@ QUnit.test('browser is mozilla (mobile)', function(assert) { assert.equal(browserObject.version, '18.2', 'version was detect correctly'); }); +QUnit.test('version is undefined when the inner regexp does not match', function(assert) { + const browserObject = browser._fromUA(userAgents.chrome_without_version); + + assert.ok(browserObject.chrome, 'chrome detected'); + assert.strictEqual(browserObject.version, undefined, 'version is undefined, as browser.d.ts declares it optional'); +}); + QUnit.test('google app is chrome (mobile)', function(assert) { const browserObject = browser._fromUA(userAgents.google_app_ios); diff --git a/packages/devextreme/testing/tests/DevExpress.core/utils.version.tests.js b/packages/devextreme/testing/tests/DevExpress.core/utils.version.tests.js index 7d92557fef71..d70c2c7a117c 100644 --- a/packages/devextreme/testing/tests/DevExpress.core/utils.version.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.core/utils.version.tests.js @@ -18,3 +18,14 @@ QUnit.test('compareVersions', function(assert) { assert.equal(compare('1.10.0', [1, 10]), 0); assert.equal(compare('1.11.3', [1, 10]), 1); }); + +QUnit.test('compareVersions truncates a fractional number argument', function(assert) { + assert.equal(compare(13.3, [13.3]), 0, 'the same version written two ways is equal'); + assert.equal(compare(13.3, 13), 0); + assert.equal(compare(13, 13.3), 0); + + assert.equal(compare(2.5, '2.0.1'), -1); + assert.equal(compare([2, 0], 2.5), 0); + + assert.equal(compare(0.5, 0), 0); +});